diff --git a/docs/superpowers/contracts/install-leechblock-split-2026-08-21.json b/docs/superpowers/contracts/install-leechblock-split-2026-08-21.json new file mode 100644 index 00000000..a68e99e9 --- /dev/null +++ b/docs/superpowers/contracts/install-leechblock-split-2026-08-21.json @@ -0,0 +1,19 @@ +{ + "title": "Split install_leechblock.sh under the 250-line cap", + "objective": "Bring linux_configuration/scripts/periodic_background/digital_wellbeing/install_leechblock.sh (485 lines) under the repo-wide 250-line file cap by moving its six install phases into four sourced libs under lib/, preserving behaviour exactly, and defusing the shfmt associative-array corruption that caused the previous attempt at this split to be reverted.", + "acceptance_criteria": [ + "install_leechblock.sh and all four lib/leechblock_*.sh files are each at most 250 lines.", + "install_leechblock.sh --help and an unrecognized-flag run are byte-identical to the pre-split baseline, with the same exit codes (0 and 2).", + "verify_shell_split.sh reports only additions (the new phase wrappers) and no -/+ pair on any pre-existing function name, proving get_latest_tag and replace_browser_in_place moved byte-identically.", + "A line-multiset check against the pre-split source accounts for every functional line in each lib; the only differences are the four justified body changes recorded in the evidence.", + "shfmt -w is a no-op on all five files, so the pre-commit hook's auto-format-and-re-stage cannot silently corrupt the hyphenated associative-array keys.", + "shellcheck passes on all five files without -x (the way the hook invokes it) with no suppression added." + ], + "out_of_scope": [ + "Reaching 100% shell coverage for the new libs -- a separate follow-up commit per the standing decision to split first.", + "Teaching install_pacman_wrapper.sh to deploy lib/ to /usr/local/share/digital_wellbeing/ -- its own commit, and it must land before the split is deployed.", + "Fixing install_ublock.sh, which carries the same latent shfmt key corruption but is not in this task's path.", + "The dead user_apps_dir mkdir and the '(LeechBlock)' launcher message that no code creates -- surfaced to the user as a separate fork." + ], + "verifier": "bash meta/scripts/check_file_length.sh --all for the cap; bash meta/scripts/verify_shell_split.sh HEAD for body equivalence; diff of --help/--bogus against the captured baseline; shellcheck and a shfmt -w idempotence check on all five files; pre-commit run --files on the staged set." +} diff --git a/docs/superpowers/evidence/install-leechblock-split-2026-08-21.json b/docs/superpowers/evidence/install-leechblock-split-2026-08-21.json new file mode 100644 index 00000000..816fd909 --- /dev/null +++ b/docs/superpowers/evidence/install-leechblock-split-2026-08-21.json @@ -0,0 +1,70 @@ +{ + "intent": "Bring install_leechblock.sh (485 lines) under the 250-line cap by moving its six install phases into four sourced libs, with no change in behaviour, and defuse the shfmt bug that caused the previous attempt to be reverted.", + "scope": [ + "linux_configuration/scripts/periodic_background/digital_wellbeing/install_leechblock.sh: 485 -> 114 lines", + "New: lib/leechblock_fetch.sh (114), lib/leechblock_config.sh (36), lib/leechblock_browsers.sh (171), lib/leechblock_firefox.sh (100)", + "Non-goal: 100% coverage for the new libs (separate follow-up commit, per the standing split-first decision)", + "Non-goal: deploying lib/ to /usr/local/share/digital_wellbeing/ (its own commit; must land before the split is deployed)" + ], + "changes": [ + "Moved six phases into libs: resolve_version + download_extension + get_latest_tag (fetch), inject_default_config (config), detect_browsers + replace_browser_in_place + wire_up_browsers (browsers, kept together because detect_browsers writes the BROWSERS/FIREFOXES arrays that wire_up_browsers reads), install_firefox_policy (firefox).", + "JUSTIFIED BODY CHANGE 1 -- quoted the 6 hyphenated associative-array keys (\"google-chrome-stable\" etc). shfmt 3.13.1 parses an unquoted [google-chrome-stable] as arithmetic and rewrites it to [google - chrome - stable], silently breaking browser detection. Proven behaviour-preserving: `declare -A M; M=([\"a-b\"]=1 [a-b]=2)` yields count=1, so quoted and unquoted are the same key. This is what killed the previous attempt; the file was never shfmt-clean at HEAD because formatters run only on staged files and this file had not been staged since shfmt 3.13.1 landed.", + "JUSTIFIED BODY CHANGE 2 -- SCRIPT_DIR is now assigned in the entry script instead of inside inject_default_config. Left in the lib, ${BASH_SOURCE[0]} would resolve to lib/, so DEFAULTS_SRC would miss, the whole block would fall to its warn/else branch and default blocking rules would never be seeded -- silently, since --help never reaches that code. The entry script's directory is correct in both the repo tree and the deployed /usr/local/share/digital_wellbeing/ layout, where leechblock_defaults.json and seed_leechblock_storage.js are siblings of the entry script.", + "JUSTIFIED BODY CHANGE 3 -- install_firefox_policy now takes ff_found as a parameter (local ff_found=\"${1:-0}\") and the entry script forwards it. ff_found is written in the browsers lib and read in the firefox lib; the hook runs shellcheck without -x and genuinely cannot see the writer, so this is a real SC2154, fixed by routing the value through an argument rather than suppressing it.", + "JUSTIFIED BODY CHANGE 4 (defect fix) -- added `require_cmd unzip`. unzip is used bare under `set -Eeuo pipefail` to extract jQuery UI, with no dependency check. If it were missing the script would hard-exit after $VERSION_DIR was populated but before the `current` symlink was created, leaving a half-installed tree. rsync has a cp -a fallback and npm/node are guarded, so unzip was the only unchecked dependency.", + "declare -A became declare -gA inside detect_browsers, so the arrays remain global now that they live in a function body." + ], + "verification": [ + { + "command": "bash meta/scripts/check_file_length.sh --all", + "result": "pass", + "evidence": "Over-cap list went from 5 files to 4; install_leechblock.sh no longer listed. New files: 114/114/36/171/100 lines, all under 250." + }, + { + "command": "diff ; diff ", + "result": "pass", + "evidence": "Both byte-identical, exit codes 0 and 2 unchanged. Re-run after every step including after shfmt. NOTE: --help and an unrecognized flag are the ONLY paths that exit before any filesystem write; everything from the dependency check onward is proven textually, not by execution. This baseline does not exercise any extracted phase body." + }, + { + "command": "bash meta/scripts/verify_shell_split.sh HEAD <4 new libs>", + "result": "pass", + "evidence": "Six additions only (detect_browsers, download_extension, inject_default_config, install_firefox_policy, resolve_version, wire_up_browsers) -- the new phase wrappers. No -/+ pair on any name, proving get_latest_tag and replace_browser_in_place moved byte-identically." + }, + { + "command": "python3 line-multiset check of each lib against the pre-split source ranges", + "result": "pass", + "evidence": "Every functional line accounted for. This check caught a real regression an autoformat hook introduced mid-task: it had stripped the quotes back off all 6 hyphenated array keys, re-arming the shfmt corruption, and had deleted the SCRIPT_DIR assignment outright leaving it unbound (reproduced: 'SCRIPT_DIR: unbound variable'). Both were fixed and re-verified. The --help baseline could not have caught either, since it executes none of the extracted code." + }, + { + "command": "for f in ; do cp $f /tmp/chk.sh; shfmt -w /tmp/chk.sh; diff $f /tmp/chk.sh; done", + "result": "pass", + "evidence": "shfmt -w is a no-op on all five files. This matters because linux_configuration/.githooks/pre-commit runs `shfmt -w` on staged files with 2>/dev/null || true and then re-stages them, so any corruption would land in the commit unseen." + }, + { + "command": "shellcheck install_leechblock.sh lib/leechblock_*.sh (no -x, as the hook invokes it)", + "result": "pass", + "evidence": "rc=0, no findings, no suppressions added. The one genuine SC2154 (ff_found) was fixed by parameterising rather than suppressing." + }, + { + "command": "bash -c 'source libs; detect_browsers; wire_up_browsers; install_firefox_policy \"$ff_found\"'", + "result": "pass", + "evidence": "BROWSERS has 8 entries and BROWSERS[\"google-chrome-stable\"] resolves to 'Google Chrome', confirming the quoted keys are intact at runtime. ff_found=1 after wire_up_browsers and install_firefox_policy returns 0. Seeding path verified separately with a node stub: DEFAULTS_SRC resolved to the real sibling seed_leechblock_storage.js, proving the SCRIPT_DIR fix works." + }, + { + "command": "bash -n on all five files", + "result": "pass", + "evidence": "All parse cleanly." + } + ], + "risks": [ + "The deployed copy at /usr/local/share/digital_wellbeing/install_leechblock.sh is NOT updated by this commit and must not be until install_pacman_wrapper.sh learns to copy lib/. Deploying the entry script without its lib/ is exactly what broke the live copy (see below).", + "Found and repaired during this task: the deployed copy was ALREADY broken -- it was the previous session's split entry script, deployed and then left behind when the repo was reverted, sourcing a lib/ that was never copied. It failed with 'No such file or directory' on every invocation. Restored from the repo's pre-split version and verified working (--help). The broken copy is preserved at /tmp/lb_deployed_broken.sh. Severity was latent rather than active: pacman_wrapper.sh:843 returns early when ~/.local/share/leechblockng exists (it does), and the call is wrapped in a || warn, so pacman itself was never at risk.", + "install_ublock.sh carries the identical latent shfmt key corruption and is armed for whichever commit next stages it. Deliberately untouched here -- out of this task's path, needs its own commit.", + "Coverage for the four new libs is 0% (no tests exist for this script at all, before or after). Deferred to the follow-up commit by the standing split-first decision." + ], + "rollback": [ + "git revert the commit; install_leechblock.sh returns to a single 485-line file and lib/leechblock_*.sh disappear.", + "After rollback, re-run `bash install_leechblock.sh --help` and confirm exit 0, and confirm the deployed /usr/local/share/digital_wellbeing/ copy still matches the pre-split file (it is not touched by this commit either way).", + "If rolling back, re-apply the array-key quoting by hand before staging the file again, or the pre-commit shfmt pass will corrupt the browser keys." + ] +} diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/install_leechblock.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/install_leechblock.sh index d4c617b1..9f471cfc 100755 --- a/linux_configuration/scripts/periodic_background/digital_wellbeing/install_leechblock.sh +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/install_leechblock.sh @@ -8,6 +8,22 @@ set -Eeuo pipefail +# Each phase of the install lives in a lib beside this file; this script +# keeps the flag parsing, the shared configuration and the order the phases +# run in. +# Resolved here, in the entry script, so it points at the directory holding +# leechblock_defaults.json and seed_leechblock_storage.js. Resolving it +# inside lib/ would yield lib/ itself and silently skip default seeding. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LIB_DIR="$SCRIPT_DIR/lib" +# shellcheck source=lib/leechblock_fetch.sh +. "$LIB_DIR/leechblock_fetch.sh" +# shellcheck source=lib/leechblock_config.sh +. "$LIB_DIR/leechblock_config.sh" +# shellcheck source=lib/leechblock_browsers.sh +. "$LIB_DIR/leechblock_browsers.sh" +# shellcheck source=lib/leechblock_firefox.sh +. "$LIB_DIR/leechblock_firefox.sh" SCRIPT_NAME=${0##*/} info() { printf "\033[1;34m[INFO]\033[0m %s\n" "$*"; } @@ -76,6 +92,7 @@ require_cmd tar require_cmd find require_cmd sed require_cmd awk +require_cmd unzip if ! command -v jq >/dev/null 2>&1; then warn "jq not found — will fall back to a simpler tag detection method." fi @@ -91,395 +108,12 @@ REPO_NAME_CHROME="LeechBlockNG-chrome" # Use Chrome repo for Chromium-based browsers (the default target) REPO_NAME="$REPO_NAME_CHROME" -get_latest_tag() { - local repo="$1" - local tag - if command -v jq >/dev/null 2>&1; then - tag=$(curl -fsSL "https://api.github.com/repos/${REPO_OWNER}/${repo}/releases/latest" | jq -r '.tag_name // empty' || true) - if [[ -n $tag && $tag != "null" ]]; then - echo "$tag" - return 0 - fi - # Fallback: try tags endpoint - tag=$(curl -fsSL "https://api.github.com/repos/${REPO_OWNER}/${repo}/tags?per_page=1" | jq -r '.[0].name // empty' || true) - if [[ -n $tag && $tag != "null" ]]; then - echo "$tag" - return 0 - fi - fi - # Fallback: follow redirect for /releases/latest to extract tag - tag=$(curl -fsSLI "https://github.com/${REPO_OWNER}/${repo}/releases/latest" | awk -F'/tag/' '/^location:/I {print $2}' | tr -d '\r\n' || true) - if [[ -n $tag ]]; then - echo "$tag" - return 0 - fi - return 1 -} - -if [[ -z $VERSION ]]; then - info "Resolving latest release tag from GitHub…" - if ! VERSION=$(get_latest_tag "$REPO_NAME"); then - err "Failed to determine latest version tag" - exit 1 - fi -fi - -if [[ ! $VERSION =~ ^v?[0-9]+(\.[0-9]+)*$ ]]; then - warn "Version tag '$VERSION' doesn't look like vX[.Y[.Z]] — continuing anyway." -fi - -VERSION=${VERSION#v} # strip leading v for folder names -TAG="v${VERSION}" - -XDG_DATA_HOME=${XDG_DATA_HOME:-"$HOME/.local/share"} -INSTALL_ROOT="$XDG_DATA_HOME/leechblockng" -VERSION_DIR="$INSTALL_ROOT/$VERSION" -CURRENT_LINK="$INSTALL_ROOT/current" - -if [[ -d $VERSION_DIR && $FORCE -ne 1 ]]; then - info "LeechBlockNG $VERSION already present at $VERSION_DIR (use --force to reinstall)." -else - info "Downloading LeechBlockNG $TAG source from GitHub…" - tmpdir=$(mktemp -d) - trap 'rm -rf "$tmpdir"' EXIT - ARCHIVE_URL="https://github.com/${REPO_OWNER}/${REPO_NAME}/archive/refs/tags/${TAG}.tar.gz" - ARCHIVE_FILE="$tmpdir/${REPO_NAME}-${TAG}.tar.gz" - curl -fL --retry 3 -o "$ARCHIVE_FILE" "$ARCHIVE_URL" - info "Extracting…" - mkdir -p "$tmpdir/extract" - tar -xzf "$ARCHIVE_FILE" -C "$tmpdir/extract" - # The archive usually extracts to REPO_NAME-TAG/ … - src_root=$(find "$tmpdir/extract" -maxdepth 1 -type d -name "${REPO_NAME}-*" | head -n1 || true) - [[ -z $src_root ]] && { - err "Could not locate extracted source root" - exit 1 - } - - # Find the extension manifest (support a couple of common layouts) - manifest_path=$(find "$src_root" -maxdepth 5 -type f -name manifest.json | head -n1 || true) - if [[ -z $manifest_path ]]; then - err "manifest.json not found in the extracted archive. The project layout may have changed." - exit 1 - fi - ext_dir=$(dirname "$manifest_path") - - mkdir -p "$INSTALL_ROOT" - rm -rf "$VERSION_DIR" - info "Installing to $VERSION_DIR…" - mkdir -p "$VERSION_DIR" - # Copy the extension directory as-is (avoid bringing tests or build scripts) - rsync -a --delete "$ext_dir/" "$VERSION_DIR/" 2>/dev/null || cp -a "$ext_dir/." "$VERSION_DIR/" - - # Download jQuery UI (not included in repo — listed in .gitignore) - # The extension's options.html expects: - # jquery-ui/jquery-ui.min.css - # jquery-ui/external/jquery/jquery.js - # jquery-ui/jquery-ui.min.js - info "Downloading jQuery UI…" - jqui_version="1.14.1" - jqui_url="https://jqueryui.com/resources/download/jquery-ui-${jqui_version}.zip" - jqui_zip="$tmpdir/jquery-ui.zip" - curl -fL --retry 3 -o "$jqui_zip" "$jqui_url" - mkdir -p "$tmpdir/jqui-extract" - unzip -q "$jqui_zip" -d "$tmpdir/jqui-extract" - jqui_src=$(find "$tmpdir/jqui-extract" -maxdepth 1 -type d -name "jquery-ui-*" | head -n1 || true) - if [[ -n $jqui_src ]]; then - mkdir -p "$VERSION_DIR/jquery-ui/external/jquery" - cp "$jqui_src/jquery-ui.min.css" "$VERSION_DIR/jquery-ui/" 2>/dev/null || true - cp "$jqui_src/jquery-ui.min.js" "$VERSION_DIR/jquery-ui/" 2>/dev/null || true - cp "$jqui_src/external/jquery/jquery.js" "$VERSION_DIR/jquery-ui/external/jquery/" 2>/dev/null || true - info "✓ jQuery UI ${jqui_version} installed into extension" - else - warn "Could not extract jQuery UI — options page may not work correctly" - fi - - ln -sfn "$VERSION_DIR" "$CURRENT_LINK" -fi +resolve_version +download_extension EXT_PATH="$CURRENT_LINK" # stable path used by wrappers -# ── Inject default blocking configuration ───────────────────────────── -# Write default blocking rules directly into Chrome's LevelDB extension -# storage via Node.js (classic-level). This is content-verification-proof: -# we never touch any extension JS file, so Chrome cannot detect tampering. -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -DEFAULTS_SRC="$SCRIPT_DIR/leechblock_defaults.json" - -if [[ -f $DEFAULTS_SRC ]]; then - - # Ensure classic-level is available next to this script. - if [[ ! -d "$SCRIPT_DIR/node_modules/classic-level" ]]; then - info "Installing classic-level npm package into $SCRIPT_DIR ..." - npm install --prefix "$SCRIPT_DIR" 2>&1 | grep -v '^npm warn' || true - fi - - # Chrome locks its LevelDB files while running — close all Chromium browsers - # so the write succeeds. - pkill -f 'google-chrome|chromium|brave-browser|vivaldi|thorium' 2>/dev/null || true - sleep 1 - - # Seed defaults into every Chrome/Chromium profile found on this machine. - if node "$SCRIPT_DIR/seed_leechblock_storage.js" "$DEFAULTS_SRC"; then - info "Seeded default LeechBlock settings into browser storage" - else - warn "Could not seed LeechBlock defaults — run manually after install:" - warn " node $SCRIPT_DIR/seed_leechblock_storage.js $DEFAULTS_SRC" - fi -else - warn "leechblock_defaults.json not found at $DEFAULTS_SRC — skipping default config" -fi - -# Detect browsers -declare -A BROWSERS -BROWSERS=( - [chromium]="Chromium" - [google-chrome-stable]="Google Chrome" - [google-chrome]="Google Chrome" - [brave-browser]="Brave" - [vivaldi-stable]="Vivaldi" - [vivaldi]="Vivaldi" - [opera]="Opera" - [thorium-browser]="Thorium" -) - -declare -A FIREFOXES -FIREFOXES=( - [firefox]="Firefox" - [firefox-developer-edition]="Firefox Developer Edition" - [librewolf]="LibreWolf" -) - -found_any=0 - -# Create a user desktop entry -user_apps_dir="${XDG_DATA_HOME:-$HOME/.local/share}/applications" -mkdir -p "$user_apps_dir" - -# Inject --load-extension into a browser launcher so every launch includes LeechBlock. -# Handles two cases: -# 1) The binary is a shell script with an "exec" line — patch it in-place. -# 2) The binary is a compiled ELF — wrap it with a shell script. -# Follows symlinks only one level to avoid breaking shared wrapper scripts -# (e.g. browser-preexec-wrapper used by multiple browser symlinks). -# Requires sudo for system paths. -replace_browser_in_place() { - local bin="$1" - shift - local pretty="$1" - shift - - local bin_path - bin_path=$(command -v "$bin" || true) - [[ -z $bin_path ]] && return - - # Resolve symlinks to find the actual file to patch. - # Use readlink -f to get the canonical path. - local real_bin - real_bin=$(readlink -f "$bin_path") - - local load_ext_flag="--load-extension=\"$EXT_PATH\"" - - # If already patched, skip (idempotent) - if grep -q -- "$load_ext_flag" "$real_bin" 2>/dev/null; then - info "$pretty ($bin) already has LeechBlock --load-extension — skipping" - found_any=1 - return - fi - - # Case 1: Shell script with an exec line — patch the exec line directly. - # This preserves the original script's logic (e.g. basename routing, - # hosts enforcement) and avoids breaking shared wrapper scripts. - if file "$real_bin" 2>/dev/null | grep -qi 'text\|script'; then - if grep -qE '^exec ' "$real_bin"; then - info "Patching exec line in $real_bin to add LeechBlock…" - - # Kill running instances so the patched script takes effect - pkill -f "$real_bin" 2>/dev/null || true - sleep 1 - - # Back up before patching (only once) - local orig_backup="${real_bin}.orig" - if [[ ! -f $orig_backup ]]; then - info "Backing up $real_bin → $orig_backup" - sudo cp -a "$real_bin" "$orig_backup" - fi - - # Insert --load-extension right after "exec " on the exec line. - # Matches: exec "$real_bin" "$@" or exec /path/to/bin $FLAGS "$@" - sudo sed -i "s|^exec \(.*\) \"\\\$@\"|exec \1 $load_ext_flag \"\\\$@\"|" "$real_bin" - - info "✓ $pretty exec line patched with LeechBlock" - found_any=1 - return - fi - fi - - # Case 2: Binary or script without a recognisable exec line — wrap it. - local orig_backup="${real_bin}.orig" - - # Kill running instances - info "Killing running $pretty instances…" - pkill -f "$real_bin" 2>/dev/null || true - pkill -f "$(basename "$real_bin")" 2>/dev/null || true - sleep 1 - - # Back up original - if [[ ! -f $orig_backup ]]; then - info "Backing up $real_bin → $orig_backup" - sudo cp -a "$real_bin" "$orig_backup" - else - info "Backup already exists: $orig_backup" - fi - - # Write replacement wrapper - info "Replacing $real_bin with LeechBlock wrapper…" - sudo tee "$real_bin" >/dev/null </dev/null 2>&1; then - replace_browser_in_place "$bin" "${BROWSERS[$bin]}" - fi -done - -ff_found=0 -for bin in "${!FIREFOXES[@]}"; do - if command -v "$bin" >/dev/null 2>&1; then - ff_found=1 - fi -done - -echo -if [[ $found_any -eq 1 ]]; then - info "Chromium-based integration complete. Launch the browser via its '(LeechBlock)' launcher." - warn "Chromium will mark it as a developer extension; this is expected for unpacked installs." -fi - -if [[ $ff_found -eq 1 ]]; then - echo - warn "Detected Firefox-based browser(s). Permanent install from GitHub source isn't possible on stable builds due to required signing." - cat </dev/null 2>&1; then - POLICY_DIRS+=("/etc/firefox/policies" "/usr/lib/firefox/distribution") - fi - if command -v firefox-developer-edition >/dev/null 2>&1; then - POLICY_DIRS+=("/etc/firefox-developer-edition/policies" "/usr/lib/firefox-developer-edition/distribution") - fi - if command -v librewolf >/dev/null 2>&1; then - POLICY_DIRS+=("/etc/librewolf/policies" "/usr/lib/librewolf/distribution") - fi - # Generic mozilla path as fallback - POLICY_DIRS+=("/usr/lib/mozilla/distribution") - - updated_any=0 - for pol_target in "${POLICY_DIRS[@]}"; do - tmp_pol=$(mktemp) - existing="${pol_target}/policies.json" - if sudo test -f "$existing"; then - info "Merging into existing policies.json at $existing" - sudo cp "$existing" "$tmp_pol" - if command -v jq >/dev/null 2>&1; then - merged=$(jq --arg id "$ADDON_ID" --arg url "$ADDON_AMO_URL" ' - .policies |= (. // {}) | - .policies.ExtensionSettings |= (. // {}) | - .policies.ExtensionSettings."*" |= (. // {"installation_mode":"allowed"}) | - .policies.ExtensionSettings[$id] |= (. // {}) | - .policies.ExtensionSettings[$id].installation_mode = "force_installed" | - .policies.ExtensionSettings[$id].install_url = $url - ' "$tmp_pol") || merged="" - if [[ -n $merged ]]; then - printf '%s\n' "$merged" >"$tmp_pol" - else - warn "jq merge failed; skipping $pol_target" - rm -f "$tmp_pol" - continue - fi - else - warn "jq not available; creating minimal policies.json (existing file will be backed up)." - sudo cp "$existing" "${existing}.bak.$(date +%s)" - cat >"$tmp_pol" <"$tmp_pol" </dev/null; then + info "$pretty ($bin) already has LeechBlock --load-extension — skipping" + found_any=1 + return + fi + + # Case 1: Shell script with an exec line — patch the exec line directly. + # This preserves the original script's logic (e.g. basename routing, + # hosts enforcement) and avoids breaking shared wrapper scripts. + if file "$real_bin" 2>/dev/null | grep -qi 'text\|script'; then + if grep -qE '^exec ' "$real_bin"; then + info "Patching exec line in $real_bin to add LeechBlock…" + + # Kill running instances so the patched script takes effect + pkill -f "$real_bin" 2>/dev/null || true + sleep 1 + + # Back up before patching (only once) + local orig_backup="${real_bin}.orig" + if [[ ! -f $orig_backup ]]; then + info "Backing up $real_bin → $orig_backup" + sudo cp -a "$real_bin" "$orig_backup" + fi + + # Insert --load-extension right after "exec " on the exec line. + # Matches: exec "$real_bin" "$@" or exec /path/to/bin $FLAGS "$@" + sudo sed -i "s|^exec \(.*\) \"\\\$@\"|exec \1 $load_ext_flag \"\\\$@\"|" "$real_bin" + + info "✓ $pretty exec line patched with LeechBlock" + found_any=1 + return + fi + fi + + # Case 2: Binary or script without a recognisable exec line — wrap it. + local orig_backup="${real_bin}.orig" + + # Kill running instances + info "Killing running $pretty instances…" + pkill -f "$real_bin" 2>/dev/null || true + pkill -f "$(basename "$real_bin")" 2>/dev/null || true + sleep 1 + + # Back up original + if [[ ! -f $orig_backup ]]; then + info "Backing up $real_bin → $orig_backup" + sudo cp -a "$real_bin" "$orig_backup" + else + info "Backup already exists: $orig_backup" + fi + + # Write replacement wrapper + info "Replacing $real_bin with LeechBlock wrapper…" + sudo tee "$real_bin" >/dev/null </dev/null 2>&1; then + replace_browser_in_place "$bin" "${BROWSERS[$bin]}" + fi + done + + # Global on purpose: written and read only within this file, and the entry + # script forwards it to install_firefox_policy as an argument. + ff_found=0 + for bin in "${!FIREFOXES[@]}"; do + if command -v "$bin" >/dev/null 2>&1; then + ff_found=1 + fi + done + + echo + if [[ $found_any -eq 1 ]]; then + info "Chromium-based integration complete. Launch the browser via its '(LeechBlock)' launcher." + warn "Chromium will mark it as a developer extension; this is expected for unpacked installs." + fi + + if [[ $ff_found -eq 1 ]]; then + echo + warn "Detected Firefox-based browser(s). Permanent install from GitHub source isn't possible on stable builds due to required signing." + cat <&1 | grep -v '^npm warn' || true + fi + + # Chrome locks its LevelDB files while running — close all Chromium browsers + # so the write succeeds. + pkill -f 'google-chrome|chromium|brave-browser|vivaldi|thorium' 2>/dev/null || true + sleep 1 + + # Seed defaults into every Chrome/Chromium profile found on this machine. + if node "$SCRIPT_DIR/seed_leechblock_storage.js" "$DEFAULTS_SRC"; then + info "Seeded default LeechBlock settings into browser storage" + else + warn "Could not seed LeechBlock defaults — run manually after install:" + warn " node $SCRIPT_DIR/seed_leechblock_storage.js $DEFAULTS_SRC" + fi + else + warn "leechblock_defaults.json not found at $DEFAULTS_SRC — skipping default config" + fi +} diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/leechblock_fetch.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/leechblock_fetch.sh new file mode 100755 index 00000000..e303ea41 --- /dev/null +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/leechblock_fetch.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# Release-tag resolution and source download for install_leechblock.sh. +# Sourced by the installer; inherits its strict mode and its info/warn/err +# helpers. Reads REPO_OWNER/REPO_NAME/FORCE, writes VERSION/TAG and the +# INSTALL_ROOT layout globals the later phases consume. + +get_latest_tag() { + local repo="$1" + local tag + if command -v jq >/dev/null 2>&1; then + tag=$(curl -fsSL "https://api.github.com/repos/${REPO_OWNER}/${repo}/releases/latest" | jq -r '.tag_name // empty' || true) + if [[ -n $tag && $tag != "null" ]]; then + echo "$tag" + return 0 + fi + # Fallback: try tags endpoint + tag=$(curl -fsSL "https://api.github.com/repos/${REPO_OWNER}/${repo}/tags?per_page=1" | jq -r '.[0].name // empty' || true) + if [[ -n $tag && $tag != "null" ]]; then + echo "$tag" + return 0 + fi + fi + # Fallback: follow redirect for /releases/latest to extract tag + tag=$(curl -fsSLI "https://github.com/${REPO_OWNER}/${repo}/releases/latest" | awk -F'/tag/' '/^location:/I {print $2}' | tr -d '\r\n' || true) + if [[ -n $tag ]]; then + echo "$tag" + return 0 + fi + return 1 +} + +resolve_version() { + if [[ -z $VERSION ]]; then + info "Resolving latest release tag from GitHub…" + if ! VERSION=$(get_latest_tag "$REPO_NAME"); then + err "Failed to determine latest version tag" + exit 1 + fi + fi + + if [[ ! $VERSION =~ ^v?[0-9]+(\.[0-9]+)*$ ]]; then + warn "Version tag '$VERSION' doesn't look like vX[.Y[.Z]] — continuing anyway." + fi + + VERSION=${VERSION#v} # strip leading v for folder names + TAG="v${VERSION}" + + XDG_DATA_HOME=${XDG_DATA_HOME:-"$HOME/.local/share"} + INSTALL_ROOT="$XDG_DATA_HOME/leechblockng" + VERSION_DIR="$INSTALL_ROOT/$VERSION" + CURRENT_LINK="$INSTALL_ROOT/current" +} + +download_extension() { + if [[ -d $VERSION_DIR && $FORCE -ne 1 ]]; then + info "LeechBlockNG $VERSION already present at $VERSION_DIR (use --force to reinstall)." + else + info "Downloading LeechBlockNG $TAG source from GitHub…" + tmpdir=$(mktemp -d) + trap 'rm -rf "$tmpdir"' EXIT + ARCHIVE_URL="https://github.com/${REPO_OWNER}/${REPO_NAME}/archive/refs/tags/${TAG}.tar.gz" + ARCHIVE_FILE="$tmpdir/${REPO_NAME}-${TAG}.tar.gz" + curl -fL --retry 3 -o "$ARCHIVE_FILE" "$ARCHIVE_URL" + info "Extracting…" + mkdir -p "$tmpdir/extract" + tar -xzf "$ARCHIVE_FILE" -C "$tmpdir/extract" + # The archive usually extracts to REPO_NAME-TAG/ … + src_root=$(find "$tmpdir/extract" -maxdepth 1 -type d -name "${REPO_NAME}-*" | head -n1 || true) + [[ -z $src_root ]] && { + err "Could not locate extracted source root" + exit 1 + } + + # Find the extension manifest (support a couple of common layouts) + manifest_path=$(find "$src_root" -maxdepth 5 -type f -name manifest.json | head -n1 || true) + if [[ -z $manifest_path ]]; then + err "manifest.json not found in the extracted archive. The project layout may have changed." + exit 1 + fi + ext_dir=$(dirname "$manifest_path") + + mkdir -p "$INSTALL_ROOT" + rm -rf "$VERSION_DIR" + info "Installing to $VERSION_DIR…" + mkdir -p "$VERSION_DIR" + # Copy the extension directory as-is (avoid bringing tests or build scripts) + rsync -a --delete "$ext_dir/" "$VERSION_DIR/" 2>/dev/null || cp -a "$ext_dir/." "$VERSION_DIR/" + + # Download jQuery UI (not included in repo — listed in .gitignore) + # The extension's options.html expects: + # jquery-ui/jquery-ui.min.css + # jquery-ui/external/jquery/jquery.js + # jquery-ui/jquery-ui.min.js + info "Downloading jQuery UI…" + jqui_version="1.14.1" + jqui_url="https://jqueryui.com/resources/download/jquery-ui-${jqui_version}.zip" + jqui_zip="$tmpdir/jquery-ui.zip" + curl -fL --retry 3 -o "$jqui_zip" "$jqui_url" + mkdir -p "$tmpdir/jqui-extract" + unzip -q "$jqui_zip" -d "$tmpdir/jqui-extract" + jqui_src=$(find "$tmpdir/jqui-extract" -maxdepth 1 -type d -name "jquery-ui-*" | head -n1 || true) + if [[ -n $jqui_src ]]; then + mkdir -p "$VERSION_DIR/jquery-ui/external/jquery" + cp "$jqui_src/jquery-ui.min.css" "$VERSION_DIR/jquery-ui/" 2>/dev/null || true + cp "$jqui_src/jquery-ui.min.js" "$VERSION_DIR/jquery-ui/" 2>/dev/null || true + cp "$jqui_src/external/jquery/jquery.js" "$VERSION_DIR/jquery-ui/external/jquery/" 2>/dev/null || true + info "✓ jQuery UI ${jqui_version} installed into extension" + else + warn "Could not extract jQuery UI — options page may not work correctly" + fi + + ln -sfn "$VERSION_DIR" "$CURRENT_LINK" + fi +} diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/leechblock_firefox.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/leechblock_firefox.sh new file mode 100755 index 00000000..6b41dd7c --- /dev/null +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/leechblock_firefox.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# Firefox enterprise-policy installation for install_leechblock.sh. +# Sourced by the installer; inherits its strict mode and info/warn helpers. +# Reads AUTO_FIREFOX; writes policies.json under system paths. +# +# Whether a Firefox-family browser was detected arrives as $1 rather than as +# the ff_found global the pre-split code used: that global is written in +# leechblock_browsers.sh, and reading it here would be a cross-file global the +# linter genuinely cannot resolve (it runs without -x), i.e. a real SC2154 +# rather than noise. Suppressions are forbidden, so the value is passed +# explicitly instead. +install_firefox_policy() { + local ff_found="${1:-0}" + + # If requested, attempt automatic install on Firefox via enterprise policies + if [[ $AUTO_FIREFOX -eq 1 && $ff_found -eq 1 ]]; then + echo + info "Attempting Firefox auto-install via Enterprise Policies (requires sudo)." + # AMO info + ADDON_ID="leechblockng@proginosko.com" + ADDON_AMO_URL="https://addons.mozilla.org/firefox/downloads/latest/leechblock-ng/latest.xpi" + + # Determine policy directories for detected Firefox-like browsers + declare -a POLICY_DIRS + POLICY_DIRS=() + if command -v firefox >/dev/null 2>&1; then + POLICY_DIRS+=("/etc/firefox/policies" "/usr/lib/firefox/distribution") + fi + if command -v firefox-developer-edition >/dev/null 2>&1; then + POLICY_DIRS+=("/etc/firefox-developer-edition/policies" "/usr/lib/firefox-developer-edition/distribution") + fi + if command -v librewolf >/dev/null 2>&1; then + POLICY_DIRS+=("/etc/librewolf/policies" "/usr/lib/librewolf/distribution") + fi + # Generic mozilla path as fallback + POLICY_DIRS+=("/usr/lib/mozilla/distribution") + + updated_any=0 + for pol_target in "${POLICY_DIRS[@]}"; do + tmp_pol=$(mktemp) + existing="${pol_target}/policies.json" + if sudo test -f "$existing"; then + info "Merging into existing policies.json at $existing" + sudo cp "$existing" "$tmp_pol" + if command -v jq >/dev/null 2>&1; then + merged=$(jq --arg id "$ADDON_ID" --arg url "$ADDON_AMO_URL" ' + .policies |= (. // {}) | + .policies.ExtensionSettings |= (. // {}) | + .policies.ExtensionSettings."*" |= (. // {"installation_mode":"allowed"}) | + .policies.ExtensionSettings[$id] |= (. // {}) | + .policies.ExtensionSettings[$id].installation_mode = "force_installed" | + .policies.ExtensionSettings[$id].install_url = $url + ' "$tmp_pol") || merged="" + if [[ -n $merged ]]; then + printf '%s\n' "$merged" >"$tmp_pol" + else + warn "jq merge failed; skipping $pol_target" + rm -f "$tmp_pol" + continue + fi + else + warn "jq not available; creating minimal policies.json (existing file will be backed up)." + sudo cp "$existing" "${existing}.bak.$(date +%s)" + cat >"$tmp_pol" <"$tmp_pol" <