Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/open-link.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: /open link validation

# The /open page and freenet-core's freenet:// handler must accept exactly the
# same links. Both are tested against one vector file, which lives in
# freenet-core (crates/core/tests/data/share-link-vectors.json); this repo
# keeps a copy. This workflow runs the page's own JS over the copy and fails
# if the copy has drifted from freenet-core main.

on:
pull_request:
paths:
- 'hugo-site/themes/freenet/layouts/shortcodes/open-link.html'
- 'hugo-site/tests/**'
- '.github/workflows/open-link.yml'
push:
branches: [main]
paths:
- 'hugo-site/themes/freenet/layouts/shortcodes/open-link.html'
- 'hugo-site/tests/**'
- '.github/workflows/open-link.yml'
schedule:
# Catch a rule change on the freenet-core side, which no change here
# would trigger.
- cron: '41 6 * * *'
workflow_dispatch:

jobs:
vectors:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Page JS against the shared vectors
run: node hugo-site/tests/open-link-vectors.test.mjs
- name: Vector copy matches freenet-core main
run: |
curl -fsSL https://raw.githubusercontent.com/freenet/freenet-core/main/crates/core/tests/data/share-link-vectors.json \
-o /tmp/upstream-vectors.json
if ! diff -u /tmp/upstream-vectors.json hugo-site/tests/share-link-vectors.json; then
echo "::error::hugo-site/tests/share-link-vectors.json differs from freenet-core main. Copy it over, and update open-link.html if the rules changed."
exit 1
fi
11 changes: 6 additions & 5 deletions hugo-site/content/build/manual/share-links.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,12 @@ https://freenet.org/open#6FzSeAUKcqJrveKyU8RJgGKc5jRB1Z2juvxXtwTA4Em9/#store=Ab3
who already has Freenet installed and running.
- **Use in your browser** -- the same target on `try.freenet.org`, a peer we host, for anyone who
wants to look without installing anything.
- **Open in Freenet** -- `freenet://<contract-id>/<path>...`. Works once a visitor's Freenet install
has registered the `freenet://` link type. That handler is not shipped yet
([freenet-core#5726](https://github.com/freenet/freenet-core/issues/5726) tracks it), so this
button is styled and ordered as a secondary option for now -- swap it back to primary and first
once installers register the scheme.
- **Open in Freenet** -- `freenet:<contract-id>/<path>...`, which the visitor's own Freenet opens
on their local peer. The handler ([freenet-core#5726](https://github.com/freenet/freenet-core/issues/5726))
ships in the first release after 0.2.139, so until peers have updated this button is styled and
ordered as a secondary option. The link has no `//`: in `freenet://<contract-id>` the
case-sensitive contract id would be the URL's host, which some desktops lowercase before the
handler sees it. (The handler accepts both forms.)
- **Get Freenet** -- the [install guide](/quickstart/).

An invalid or truncated fragment shows a "this link looks broken" message instead of guessing at
Expand Down
89 changes: 81 additions & 8 deletions hugo-site/static/uninstall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,87 @@ if [ "$OS" = "linux" ]; then
(or: sudo systemctl disable --now freenet && sudo rm /etc/systemd/system/freenet.service)"
fi
elif [ "$OS" = "macos" ]; then
PLIST="${HOME}/Library/LaunchAgents/org.freenet.node.plist"
if [ -f "$PLIST" ]; then
info "Unloading launchd agent..."
launchctl unload "$PLIST" >/dev/null 2>&1 || true
rm -f "$PLIST"
info "Removed ${PLIST}"
removed_service="launchd"
# Legacy install.sh / `freenet service install` plist (pre-DMG):
LEGACY_PLIST="${HOME}/Library/LaunchAgents/org.freenet.node.plist"
# DMG-installed Freenet.app's Launch-at-Login plist:
DMG_PLIST="${HOME}/Library/LaunchAgents/org.freenet.Freenet.plist"
UID_VALUE="$(id -u 2>/dev/null || echo 501)"
for PLIST in "$LEGACY_PLIST" "$DMG_PLIST"; do
if [ -f "$PLIST" ]; then
info "Unloading launchd agent at $PLIST"
# Try modern bootout first (macOS 11+), fall back to legacy
# unload for older systems. Either may fail non-fatally if
# the agent was never actually loaded in this session.
launchctl bootout "gui/${UID_VALUE}" "$PLIST" >/dev/null 2>&1 || \
launchctl unload "$PLIST" >/dev/null 2>&1 || true
rm -f "$PLIST"
info "Removed ${PLIST}"
removed_service="launchd"
fi
done
fi

# --- Step 1b: remove the freenet:// link handler (Linux) -------------------
#
# `freenet service install` registers a desktop entry for freenet:// links
# (crates/core/src/bin/commands/url_handler.rs). Remove it only if it carries
# Freenet's marker line, and remove only Freenet's own association line from
# mimeapps.list, leaving every other entry in that file untouched.

removed_handler="0"
if [ "$OS" = "linux" ]; then
# Relative XDG_* values are invalid per the spec and ignored, as the Rust
# side ignores them.
case "${XDG_DATA_HOME:-}" in /*) data_home="$XDG_DATA_HOME" ;; *) data_home="${HOME}/.local/share" ;; esac
case "${XDG_CONFIG_HOME:-}" in /*) config_home="$XDG_CONFIG_HOME" ;; *) config_home="${HOME}/.config" ;; esac
APPS_DIR="${data_home}/applications"
DESKTOP_ENTRY="${APPS_DIR}/freenet-url-handler.desktop"
handler_is_ours="1"
if [ -f "$DESKTOP_ENTRY" ]; then
if grep -qx 'X-Freenet-Managed=true' "$DESKTOP_ENTRY"; then
rm -f "$DESKTOP_ENTRY"
info "Removed ${DESKTOP_ENTRY}"
removed_handler="1"
if has_cmd update-desktop-database; then
update-desktop-database -q "$APPS_DIR" >/dev/null 2>&1 || true
fi
else
# Someone else's file at our path: it, and the association
# naming it, are not ours to remove.
handler_is_ours="0"
fi
fi
# Surrounding whitespace and a CR are tolerated, as in the Rust copy.
HANDLER_LINE='^[[:space:]]*x-scheme-handler/freenet=freenet-url-handler\.desktop;\{0,1\}[[:space:]]*$'
# The current XDG location, and the one older xdg-utils wrote to.
for MIMEAPPS in "${config_home}/mimeapps.list" "${APPS_DIR}/mimeapps.list"; do
if [ "$handler_is_ours" = "1" ] && [ -f "$MIMEAPPS" ] && grep -q "$HANDLER_LINE" "$MIMEAPPS"; then
# Rewrite the real file (following a dotfile-manager symlink)
# through a temp file in ITS directory, then rename it into place:
# atomic, so a failure never leaves a truncated list, and the
# symlink itself is untouched.
target="$(readlink -f "$MIMEAPPS" 2>/dev/null || echo "$MIMEAPPS")"
if ! tmp_list="$(mktemp "${target}.freenet-XXXXXX" 2>/dev/null)"; then
warn "Could not create a temp file; left the freenet:// line in ${MIMEAPPS}"
continue
fi
# grep -v exits 1 when it prints nothing (the file held only our
# line); that is still success. Exit 2 is a read error: keep the
# user's file.
grep_status=0
grep -v "$HANDLER_LINE" "$MIMEAPPS" > "$tmp_list" || grep_status=$?
# Keep the file's mode (best effort: busybox chmod has no
# --reference, and then the list simply becomes 0600).
chmod --reference="$target" "$tmp_list" 2>/dev/null || true
if [ "$grep_status" -le 1 ] && mv -f "$tmp_list" "$target"; then
info "Removed the freenet:// association from ${MIMEAPPS}"
removed_handler="1"
else
warn "Could not rewrite ${MIMEAPPS}; left it unchanged"
fi
rm -f "$tmp_list"
fi
done
fi

# --- Step 2: remove binaries from every known install location ------------
Expand Down Expand Up @@ -294,7 +367,7 @@ fi

# --- Summary --------------------------------------------------------------

if [ -z "$removed_service" ] && [ "$removed_binaries" = "0" ]; then
if [ -z "$removed_service" ] && [ "$removed_binaries" = "0" ] && [ "$removed_handler" = "0" ]; then
info "Nothing to uninstall - Freenet does not appear to be installed for this user."
else
success "Freenet uninstalled."
Expand Down
78 changes: 78 additions & 0 deletions hugo-site/tests/open-link-vectors.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Runs the /open page's own validation code against the share-link vectors
// that freenet-core's freenet:// handler is also tested against
// (crates/core/tests/data/share-link-vectors.json there; a copy here, kept
// identical by the drift check in .github/workflows/open-link.yml). A rule
// changed on one side only fails a test.
//
// Usage: node hugo-site/tests/open-link-vectors.test.mjs

import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import vm from "node:vm";

const here = dirname(fileURLToPath(import.meta.url));
const shortcode = readFileSync(
join(here, "../themes/freenet/layouts/shortcodes/open-link.html"),
"utf8",
);
const match = shortcode.match(/<script>([\s\S]*?)<\/script>/);
if (!match) throw new Error("no <script> block in open-link.html");

// Run the page script with a stub DOM, then drive processFragment() the way
// the browser does: set location.hash, fire DOMContentLoaded, read the state
// and the button hrefs it produced.
function runPage(hash) {
const elements = {};
const el = (id) =>
(elements[id] ??= { id, style: {}, textContent: "", href: "#" });
const listeners = {};
const document = {
getElementById: el,
addEventListener: (ev, fn) => (listeners[ev] = fn),
};
const window = {
location: { hash },
addEventListener: () => {},
};
vm.runInNewContext(match[1], { document, window });
listeners.DOMContentLoaded();
const shown = ["open-link-missing", "open-link-invalid", "open-link-valid"].find(
(id) => el(id).style.display === "",
);
return { shown, el };
}

const { vectors } = JSON.parse(
readFileSync(join(here, "share-link-vectors.json"), "utf8"),
);
let failures = 0;
for (const v of vectors) {
const { shown, el } = runPage("#" + v.raw);
const valid = shown === "open-link-valid";
let problem = null;
if (valid !== v.valid) {
problem = `expected valid=${v.valid}, page showed ${shown}`;
} else if (valid) {
const local = el("open-link-local").href;
const tryIt = el("open-link-try").href;
const scheme = el("open-link-scheme").href;
const want = "http://127.0.0.1:7509" + v.local_path;
const wantTry = "https://try.freenet.org" + v.local_path;
const rest = v.local_path.slice("/v1/contract/web/".length);
if (local !== want) problem = `local button ${local} != ${want}`;
else if (tryIt !== wantTry) problem = `try button ${tryIt} != ${wantTry}`;
else if (scheme !== "freenet:" + rest)
problem = `scheme button ${scheme} != freenet:${rest}`;
}
if (problem) {
failures++;
console.error(`FAIL ${JSON.stringify(v.raw.slice(0, 80))} (${v.note}): ${problem}`);
}
}
console.log(`${vectors.length} vectors, ${failures} failures`);
if (vectors.length < 40) {
console.error("vector file looks truncated");
process.exit(1);
}
process.exit(failures ? 1 : 0);
Loading
Loading