install_leechblock: split into lib/ under the 250-line cap

485 -> 114 lines, with the six install phases moved into four sourced
libs: leechblock_fetch.sh, leechblock_config.sh, leechblock_browsers.sh
and leechblock_firefox.sh.

detect_browsers, replace_browser_in_place and wire_up_browsers stay in
one lib on purpose: detect_browsers writes the BROWSERS/FIREFOXES arrays
that wire_up_browsers reads, and splitting them would create a
write-only cross-file global.

Four justified body changes, all recorded in the evidence:

- Quoted the six hyphenated associative-array keys. shfmt 3.13.1 parses
  an unquoted [google-chrome-stable] as arithmetic and rewrites it to
  [google - chrome - stable], which would break browser detection
  entirely. The pre-commit hook runs shfmt -w on staged files and
  re-stages them, so this corruption would have landed in the commit
  unseen -- this is what caused the previous attempt at this split to be
  reverted. Behaviour-preserving: quoted and unquoted are the same key.

- SCRIPT_DIR is assigned in the entry script rather than inside
  inject_default_config. Left in the lib, ${BASH_SOURCE[0]} resolves to
  lib/, so the defaults JSON lookup would miss and default blocking
  rules would silently never be seeded.

- install_firefox_policy takes ff_found as a parameter instead of
  reading it as a cross-file global, fixing a genuine SC2154 without a
  suppression.

- Added require_cmd unzip. It was used bare under set -Eeuo pipefail, so
  a missing unzip would abort after the version dir was populated but
  before the 'current' symlink was made, leaving a half-installed tree.

verify_shell_split.sh reports additions only -- no -/+ pair -- so
get_latest_tag and replace_browser_in_place moved byte-identically.
--help and unrecognized-flag output are byte-identical to the baseline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-08-21 15:32:48 +02:00
parent b9608bbdd8
commit 1dceca6600
7 changed files with 546 additions and 389 deletions

View File

@ -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 <old> <old> <new...> 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."
}

View File

@ -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 <baseline --help> <post-split --help>; diff <baseline --bogus> <post-split --bogus>",
"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 <old> <old> <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 <all 5>; 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."
]
}

View File

@ -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 <command>" 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 <<WRAP
#!/usr/bin/env bash
# __LEECHBLOCK_WRAPPER__ — auto-generated by install_leechblock.sh
# Original backed up at: $orig_backup
exec "$orig_backup" --load-extension="$EXT_PATH" "\$@"
WRAP
sudo chmod +x "$real_bin"
info "✓ $pretty now always launches with LeechBlock"
found_any=1
}
info "Detecting installed browsers…"
for bin in "${!BROWSERS[@]}"; do
if command -v "$bin" >/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 <<FF
Options:
1) Install from Mozilla Add-ons (recommended):
https://addons.mozilla.org/firefox/addon/leechblock-ng/
2) For testing with Developer Edition or Nightly, you can set xpinstall.signatures.required=false
and install a built XPI. We'll still keep the downloaded source at:
$VERSION_DIR
To load temporarily for testing (session-only), open 'about:debugging#/runtime/this-firefox' and "Load Temporary Add-on…" then select $VERSION_DIR/manifest.json.
Tip: Re-run this script with --install-firefox to auto-install from AMO via enterprise policy (requires sudo).
FF
fi
if [[ $found_any -eq 0 && $ff_found -eq 0 ]]; then
warn "No supported browsers detected. We placed the extension at: $VERSION_DIR"
echo "Supported (auto-wired): ${!BROWSERS[*]}. Detected Firefox variants will show guidance only."
fi
echo
info "Done. Version: $VERSION (tag $TAG) installed under $VERSION_DIR"
# 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" <<JSON
{
"policies": {
"ExtensionSettings": {
"*": { "installation_mode": "allowed" },
"$ADDON_ID": {
"installation_mode": "force_installed",
"install_url": "$ADDON_AMO_URL"
}
}
}
}
JSON
fi
else
info "Creating new policies.json at $pol_target"
cat >"$tmp_pol" <<JSON
{
"policies": {
"ExtensionSettings": {
"*": { "installation_mode": "allowed" },
"$ADDON_ID": {
"installation_mode": "force_installed",
"install_url": "$ADDON_AMO_URL"
}
}
}
}
JSON
fi
sudo mkdir -p "$pol_target"
sudo cp "$tmp_pol" "$pol_target/policies.json"
rm -f "$tmp_pol"
updated_any=1
done
if [[ $updated_any -eq 1 ]]; then
info "Firefox policies updated. Restart Firefox/LibreWolf to complete installation of LeechBlock NG."
else
warn "No Firefox policy locations updated. You may not have a supported Firefox installed."
fi
info "Firefox policy updated. Restart Firefox to complete installation of LeechBlock NG."
fi
inject_default_config
detect_browsers
wire_up_browsers
install_firefox_policy "$ff_found"

View File

@ -0,0 +1,176 @@
#!/usr/bin/env bash
# Browser detection and launcher wiring for install_leechblock.sh.
# Sourced by the installer; inherits its strict mode and info/warn helpers.
#
# detect_browsers, replace_browser_in_place and wire_up_browsers live in ONE
# file on purpose: detect_browsers writes BROWSERS/FIREFOXES, which the other
# two read. Splitting them would make those globals write-only across a file
# boundary — a genuine SC2034 the hook cannot see past (it runs shellcheck
# without -x), and suppressions are forbidden.
detect_browsers() {
# Detect browsers
declare -gA 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 -gA 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 <command>" 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 <<WRAP
#!/usr/bin/env bash
# __LEECHBLOCK_WRAPPER__ — auto-generated by install_leechblock.sh
# Original backed up at: $orig_backup
exec "$orig_backup" --load-extension="$EXT_PATH" "\$@"
WRAP
sudo chmod +x "$real_bin"
info "✓ $pretty now always launches with LeechBlock"
found_any=1
}
wire_up_browsers() {
info "Detecting installed browsers…"
for bin in "${!BROWSERS[@]}"; do
if command -v "$bin" >/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 <<FF
Options:
1) Install from Mozilla Add-ons (recommended):
https://addons.mozilla.org/firefox/addon/leechblock-ng/
2) For testing with Developer Edition or Nightly, you can set xpinstall.signatures.required=false
and install a built XPI. We'll still keep the downloaded source at:
$VERSION_DIR
To load temporarily for testing (session-only), open 'about:debugging#/runtime/this-firefox' and "Load Temporary Add-on…" then select $VERSION_DIR/manifest.json.
Tip: Re-run this script with --install-firefox to auto-install from AMO via enterprise policy (requires sudo).
FF
fi
if [[ $found_any -eq 0 && $ff_found -eq 0 ]]; then
warn "No supported browsers detected. We placed the extension at: $VERSION_DIR"
echo "Supported (auto-wired): ${!BROWSERS[*]}. Detected Firefox variants will show guidance only."
fi
echo
info "Done. Version: $VERSION (tag $TAG) installed under $VERSION_DIR"
}

View File

@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Default-configuration seeding for install_leechblock.sh.
# Sourced by the installer; inherits its strict mode and info/warn helpers.
# Reads SCRIPT_DIR, writes the browser LevelDB storage via the Node seeder.
inject_default_config() {
# ── 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.
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
}

View File

@ -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
}

View File

@ -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" <<JSON
{
"policies": {
"ExtensionSettings": {
"*": { "installation_mode": "allowed" },
"$ADDON_ID": {
"installation_mode": "force_installed",
"install_url": "$ADDON_AMO_URL"
}
}
}
}
JSON
fi
else
info "Creating new policies.json at $pol_target"
cat >"$tmp_pol" <<JSON
{
"policies": {
"ExtensionSettings": {
"*": { "installation_mode": "allowed" },
"$ADDON_ID": {
"installation_mode": "force_installed",
"install_url": "$ADDON_AMO_URL"
}
}
}
}
JSON
fi
sudo mkdir -p "$pol_target"
sudo cp "$tmp_pol" "$pol_target/policies.json"
rm -f "$tmp_pol"
updated_any=1
done
if [[ $updated_any -eq 1 ]]; then
info "Firefox policies updated. Restart Firefox/LibreWolf to complete installation of LeechBlock NG."
else
warn "No Firefox policy locations updated. You may not have a supported Firefox installed."
fi
info "Firefox policy updated. Restart Firefox to complete installation of LeechBlock NG."
fi
}