diff --git a/.github/workflows/open-link.yml b/.github/workflows/open-link.yml new file mode 100644 index 00000000..f909367f --- /dev/null +++ b/.github/workflows/open-link.yml @@ -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 diff --git a/hugo-site/content/build/manual/share-links.md b/hugo-site/content/build/manual/share-links.md index 91c42e7f..d9da9ace 100644 --- a/hugo-site/content/build/manual/share-links.md +++ b/hugo-site/content/build/manual/share-links.md @@ -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:///...`. 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:/...`, 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://` 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 diff --git a/hugo-site/static/uninstall.sh b/hugo-site/static/uninstall.sh index b523b6bf..434070c4 100755 --- a/hugo-site/static/uninstall.sh +++ b/hugo-site/static/uninstall.sh @@ -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 ------------ @@ -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." diff --git a/hugo-site/tests/open-link-vectors.test.mjs b/hugo-site/tests/open-link-vectors.test.mjs new file mode 100644 index 00000000..781f943b --- /dev/null +++ b/hugo-site/tests/open-link-vectors.test.mjs @@ -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(/