Free the Copy URL shortcut from ghost extension registrations (#6821)
* Rebind ghost Copy URL shortcut registrations to the pinned id Chromium never hands a suggested shortcut to one extension while another — even a long-gone one — still holds the registration. Profiles that first loaded Copy URL before its id was pinned registered Alt+Shift+L under an id derived from the extension's load path at the time, so the pinned extension never receives the shortcut and the keypress does nothing (#6816). The quattro upgrade tried to repair this against one hardcoded path-derived id, which only ever matched a single home directory. The historical ids are unknowable in general — they hash long-gone absolute paths through whatever symlinks existed then — but the registration itself names the command, so a migration now rebinds any copy-url command that points away from the pinned id, unless that id belongs to an extension that is actually installed or the pinned extension already holds a binding of its own. Browsers rewrite Preferences on exit, which reverts any repair made while one runs, so the migration asks for this user's browser windows to be closed first — failing and staying pending when there is no terminal to ask in or the prompt is declined. The backup a repair leaves behind marks it as attempted but unverified: until a browser-free run confirms the registration stayed repaired, the migration keeps itself pending rather than trusting a disk state an open browser may still overwrite. The upgrade-time repair is dropped: the upgrade already runs migrations, so the migration is the single implementation. Fixes #6816 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Pin the WhatsApp Slim extension id Keyless unpacked extensions get path-derived ids, which go stale if the load path or packaging ever changes — the same class of bug that broke the Copy URL shortcut for pre-package installs. Pin the id with a manifest key like the other bundled extensions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
30f8f191c0
commit
de854d3f0c
@@ -104,36 +104,3 @@ native_reply=$(bash -c '
|
||||
[[ $native_reply == "0f0000007b22636f70696564223a747275657d" ]] ||
|
||||
fail "copy-url native host returns a framed success response" "$native_reply"
|
||||
pass "copy-url native host returns a framed success response"
|
||||
|
||||
preferences="$TMPDIR/Preferences"
|
||||
backup="$TMPDIR/Preferences.bak"
|
||||
patch_script="$TMPDIR/repair-shortcuts.py"
|
||||
|
||||
awk '
|
||||
/<<'\''CHROMIUM_SHORTCUTS_PATCH_PY'\''/ { copying = 1; next }
|
||||
copying && $0 == "CHROMIUM_SHORTCUTS_PATCH_PY" { exit }
|
||||
copying { print }
|
||||
' "$ROOT/bin/omarchy-upgrade-to-quattro" >"$patch_script"
|
||||
|
||||
cat >"$preferences" <<'JSON'
|
||||
{"extensions":{"commands":{"linux:Alt+Shift+L":{"command_name":"copy-url","extension":"bocglpkldciamkbmlphanhkfnhpmnbma","global":false},"linux:Alt+Shift+D":{"command_name":"download-video","extension":"dedjgknigfeelejglamclffonmophnfl","global":false}},"settings":{"bocglpkldciamkbmlphanhkfnhpmnbma":{"commands":{"copy-url":{"suggested_key":"Alt+Shift+L","was_assigned":true}}},"bgpiichlckmfanooecilcjemknkcpngb":{"commands":{"copy-url":{"suggested_key":"Alt+Shift+L"}}}}}}
|
||||
JSON
|
||||
|
||||
python3 "$patch_script" "$preferences" "$backup"
|
||||
|
||||
jq -e '
|
||||
.extensions.commands["linux:Alt+Shift+L"].extension == "bgpiichlckmfanooecilcjemknkcpngb" and
|
||||
.extensions.commands["linux:Alt+Shift+D"].extension == "dedjgknigfeelejglamclffonmophnfl" and
|
||||
(.extensions.settings.bocglpkldciamkbmlphanhkfnhpmnbma.commands["copy-url"] | has("was_assigned") | not) and
|
||||
.extensions.settings.bgpiichlckmfanooecilcjemknkcpngb.commands["copy-url"].was_assigned == true
|
||||
' "$preferences" >/dev/null || fail "quattro upgrade moves the Copy URL shortcut to the stable extension id"
|
||||
cmp -s "$backup" <(printf '%s\n' '{"extensions":{"commands":{"linux:Alt+Shift+L":{"command_name":"copy-url","extension":"bocglpkldciamkbmlphanhkfnhpmnbma","global":false},"linux:Alt+Shift+D":{"command_name":"download-video","extension":"dedjgknigfeelejglamclffonmophnfl","global":false}},"settings":{"bocglpkldciamkbmlphanhkfnhpmnbma":{"commands":{"copy-url":{"suggested_key":"Alt+Shift+L","was_assigned":true}}},"bgpiichlckmfanooecilcjemknkcpngb":{"commands":{"copy-url":{"suggested_key":"Alt+Shift+L"}}}}}}') ||
|
||||
fail "quattro upgrade backs up Chromium preferences before shortcut repair"
|
||||
pass "quattro upgrade repairs and backs up the Copy URL shortcut"
|
||||
|
||||
unchanged_hash=$(sha256sum "$preferences" | cut -d' ' -f1)
|
||||
rm "$backup"
|
||||
python3 "$patch_script" "$preferences" "$backup"
|
||||
[[ $(sha256sum "$preferences" | cut -d' ' -f1) == "$unchanged_hash" && ! -e $backup ]] ||
|
||||
fail "Copy URL shortcut repair is idempotent"
|
||||
pass "Copy URL shortcut repair is idempotent"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
|
||||
|
||||
require_command node
|
||||
|
||||
# A keyless unpacked extension gets a path-derived id, so its settings — and
|
||||
# any shortcut registrations it may grow — would go stale if its load path or
|
||||
# packaging ever changed. The pinned key keeps the id stable everywhere.
|
||||
whatsapp_slim_id=$(node - <<'JS' "$ROOT/default/chromium/extensions/whatsapp-slim/manifest.json"
|
||||
const crypto = require('crypto')
|
||||
const fs = require('fs')
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'))
|
||||
const hash = crypto.createHash('sha256').update(Buffer.from(manifest.key, 'base64')).digest()
|
||||
const alphabet = 'abcdefghijklmnop'
|
||||
let id = ''
|
||||
|
||||
for (const byte of hash.subarray(0, 16)) {
|
||||
id += alphabet[byte >> 4]
|
||||
id += alphabet[byte & 0x0f]
|
||||
}
|
||||
|
||||
process.stdout.write(id)
|
||||
JS
|
||||
)
|
||||
|
||||
[[ $whatsapp_slim_id == "amhpgjbcfakkmkeojmoaoiochhifohdi" ]] ||
|
||||
fail "whatsapp-slim extension manifest has the stable id" "$whatsapp_slim_id"
|
||||
pass "whatsapp-slim extension manifest has the stable id"
|
||||
@@ -0,0 +1,181 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
|
||||
|
||||
require_command jq
|
||||
require_command python3
|
||||
|
||||
migration="$ROOT/migrations/1786643346.sh"
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
|
||||
home="$test_dir/home"
|
||||
preferences="$home/.config/chromium/Default/Preferences"
|
||||
mkdir -p "$(dirname "$preferences")"
|
||||
|
||||
# Any id Chromium once derived from the extension's keyless load path; the
|
||||
# repair keys off the registered command name, not the id.
|
||||
ghost_id="ikkebdkaanlebnifjnbeiaklodhbjcci"
|
||||
pinned_id="bgpiichlckmfanooecilcjemknkcpngb"
|
||||
|
||||
write_stale_preferences() {
|
||||
jq -n --arg ghost "$ghost_id" --arg pinned "$pinned_id" '{extensions: {commands: {"linux:Alt+Shift+L": {command_name: "copy-url", extension: $ghost, global: false}}, settings: {($ghost): {commands: {"copy-url": {suggested_key: "Alt+Shift+L", was_assigned: true}}}, ($pinned): {commands: {"copy-url": {suggested_key: "Alt+Shift+L"}}}}}}' >"$preferences"
|
||||
}
|
||||
|
||||
stub_bin="$test_dir/bin"
|
||||
mkdir -p "$stub_bin"
|
||||
|
||||
run_migration() {
|
||||
HOME="$home" PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# A running browser prompts for the windows to be closed; declining (or having
|
||||
# no terminal to ask in) defers the repair so a rewrite-on-exit cannot revert
|
||||
# it.
|
||||
printf '#!/bin/bash\nexit 0\n' >"$stub_bin/pgrep"
|
||||
printf '#!/bin/bash\nexit 1\n' >"$stub_bin/gum"
|
||||
chmod +x "$stub_bin/pgrep" "$stub_bin/gum"
|
||||
write_stale_preferences
|
||||
before_hash=$(sha256sum "$preferences" | cut -d' ' -f1)
|
||||
|
||||
run_migration && fail "migration defers while a browser is running"
|
||||
[[ $(sha256sum "$preferences" | cut -d' ' -f1) == "$before_hash" ]] ||
|
||||
fail "migration leaves preferences alone while a browser is running"
|
||||
pass "migration defers the repair while a browser is running"
|
||||
|
||||
# Confirming the prompt after closing the browser lets the repair proceed.
|
||||
cat >"$stub_bin/gum" <<'STUB'
|
||||
#!/bin/bash
|
||||
touch "${GUM_CALLED:?}"
|
||||
exit 0
|
||||
STUB
|
||||
cat >"$stub_bin/pgrep" <<'STUB'
|
||||
#!/bin/bash
|
||||
count_file="${PGREP_COUNT_FILE:?}"
|
||||
count=$(( $(cat "$count_file" 2>/dev/null || echo 0) + 1 ))
|
||||
printf '%s\n' "$count" >"$count_file"
|
||||
(( count == 1 )) && exit 0 || exit 1
|
||||
STUB
|
||||
rm -f "$test_dir/pgrep-count"
|
||||
GUM_CALLED="$test_dir/gum-called" PGREP_COUNT_FILE="$test_dir/pgrep-count" \
|
||||
HOME="$home" PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" >/dev/null 2>&1 ||
|
||||
fail "migration proceeds once the browser prompt is confirmed"
|
||||
[[ -e $test_dir/gum-called ]] || fail "migration asks before repairing under a running browser"
|
||||
jq -e --arg pinned "$pinned_id" '.extensions.commands["linux:Alt+Shift+L"].extension == $pinned' "$preferences" >/dev/null ||
|
||||
fail "migration repairs after the browser prompt is confirmed"
|
||||
pass "migration asks to close the browser and repairs on confirmation"
|
||||
rm -f "$preferences.omarchy-copy-url-repair.bak"
|
||||
printf '#!/bin/bash\nexit 1\n' >"$stub_bin/gum"
|
||||
printf '#!/bin/bash\nexit 1\n' >"$stub_bin/pgrep"
|
||||
write_stale_preferences
|
||||
|
||||
# With browsers closed the ghost registration moves to the pinned id.
|
||||
printf '#!/bin/bash\nexit 1\n' >"$stub_bin/pgrep"
|
||||
run_migration || fail "migration repairs the shortcut when no browser is running"
|
||||
|
||||
jq -e --arg ghost "$ghost_id" --arg pinned "$pinned_id" '
|
||||
.extensions.commands["linux:Alt+Shift+L"].extension == $pinned and
|
||||
(.extensions.settings | has($ghost) | not) and
|
||||
.extensions.settings[$pinned].commands["copy-url"].was_assigned == true
|
||||
' "$preferences" >/dev/null || fail "migration rebinds the Copy URL shortcut to the pinned extension id"
|
||||
[[ -f $preferences.omarchy-copy-url-repair.bak ]] ||
|
||||
fail "migration backs up preferences before the repair"
|
||||
pass "migration rebinds the Copy URL shortcut to the pinned extension id"
|
||||
|
||||
# A repaired profile has no ghost registration left, so nothing is pending —
|
||||
# even while a browser is running.
|
||||
rm "$preferences.omarchy-copy-url-repair.bak"
|
||||
repaired_hash=$(sha256sum "$preferences" | cut -d' ' -f1)
|
||||
printf '#!/bin/bash\nexit 0\n' >"$stub_bin/pgrep"
|
||||
run_migration || fail "migration reruns cleanly after the repair"
|
||||
[[ $(sha256sum "$preferences" | cut -d' ' -f1) == "$repaired_hash" && ! -e $preferences.omarchy-copy-url-repair.bak ]] ||
|
||||
fail "migration is idempotent after the repair"
|
||||
pass "migration is idempotent after the repair"
|
||||
printf '#!/bin/bash\nexit 1\n' >"$stub_bin/pgrep"
|
||||
|
||||
# A remapped shortcut keeps the user's chosen key while moving to the pinned id.
|
||||
jq -n --arg ghost "$ghost_id" '{extensions: {commands: {"linux:Ctrl+Alt+P": {command_name: "copy-url", extension: $ghost, global: false}}, settings: {}}}' >"$preferences"
|
||||
run_migration || fail "migration repairs remapped shortcuts"
|
||||
jq -e --arg pinned "$pinned_id" '.extensions.commands["linux:Ctrl+Alt+P"].extension == $pinned' "$preferences" >/dev/null ||
|
||||
fail "migration keeps the remapped key while rebinding to the pinned id"
|
||||
pass "migration keeps remapped shortcut keys"
|
||||
|
||||
# When the pinned extension already holds a copy-url binding (the user fixed
|
||||
# it by hand), the ghost is dropped rather than doubled into a second binding.
|
||||
jq -n --arg ghost "$ghost_id" --arg pinned "$pinned_id" '{extensions: {commands: {"linux:Ctrl+Alt+P": {command_name: "copy-url", extension: $pinned, global: false}, "linux:Alt+Shift+L": {command_name: "copy-url", extension: $ghost, global: false}}, settings: {}}}' >"$preferences"
|
||||
run_migration || fail "migration cleans ghosts alongside a manual repair"
|
||||
jq -e --arg pinned "$pinned_id" '
|
||||
(.extensions.commands | has("linux:Alt+Shift+L") | not) and
|
||||
.extensions.commands["linux:Ctrl+Alt+P"].extension == $pinned
|
||||
' "$preferences" >/dev/null || fail "migration drops the ghost instead of double-binding the pinned extension"
|
||||
pass "migration never double-binds the pinned extension"
|
||||
|
||||
# A browser starting mid-repair may write stale Preferences back on exit, so
|
||||
# the migration must stay pending for a later browser-free run to verify.
|
||||
write_stale_preferences
|
||||
cat >"$stub_bin/pgrep" <<'STUB'
|
||||
#!/bin/bash
|
||||
count_file="${PGREP_COUNT_FILE:?}"
|
||||
count=$(( $(cat "$count_file" 2>/dev/null || echo 0) + 1 ))
|
||||
printf '%s\n' "$count" >"$count_file"
|
||||
(( count >= 2 )) && exit 0 || exit 1
|
||||
STUB
|
||||
rm -f "$test_dir/pgrep-count"
|
||||
if PGREP_COUNT_FILE="$test_dir/pgrep-count" HOME="$home" PATH="$stub_bin:$PATH" \
|
||||
bash -euo pipefail "$migration" >/dev/null 2>&1; then
|
||||
fail "migration stays pending when a browser starts mid-repair"
|
||||
fi
|
||||
jq -e --arg pinned "$pinned_id" '.extensions.commands["linux:Alt+Shift+L"].extension == $pinned' "$preferences" >/dev/null ||
|
||||
fail "migration still repairs preferences before deferring on a late browser"
|
||||
pass "migration stays pending when a browser starts mid-repair"
|
||||
rm -f "$preferences.omarchy-copy-url-repair.bak"
|
||||
|
||||
# A browser that started and exited mid-repair restores stale Preferences
|
||||
# before the final process check; the post-repair file verification catches it.
|
||||
write_stale_preferences
|
||||
cp "$preferences" "$test_dir/stale-preferences"
|
||||
cat >"$stub_bin/pgrep" <<'STUB'
|
||||
#!/bin/bash
|
||||
count_file="${PGREP_COUNT_FILE:?}"
|
||||
count=$(( $(cat "$count_file" 2>/dev/null || echo 0) + 1 ))
|
||||
printf '%s\n' "$count" >"$count_file"
|
||||
(( count == 2 )) && cp "$STALE_PREFERENCES" "$REPAIRED_PREFERENCES"
|
||||
exit 1
|
||||
STUB
|
||||
rm -f "$test_dir/pgrep-count"
|
||||
if PGREP_COUNT_FILE="$test_dir/pgrep-count" STALE_PREFERENCES="$test_dir/stale-preferences" \
|
||||
REPAIRED_PREFERENCES="$preferences" HOME="$home" PATH="$stub_bin:$PATH" \
|
||||
bash -euo pipefail "$migration" >/dev/null 2>&1; then
|
||||
fail "migration stays pending when a briefly-lived browser undoes the repair"
|
||||
fi
|
||||
pass "migration stays pending when a briefly-lived browser undoes the repair"
|
||||
printf '#!/bin/bash\nexit 1\n' >"$stub_bin/pgrep"
|
||||
write_stale_preferences
|
||||
run_migration || fail "migration recovers after a reverted repair"
|
||||
rm -f "$preferences.omarchy-copy-url-repair.bak"
|
||||
|
||||
# A repair attempted while a browser was open leaves its backup behind. A
|
||||
# rerun that sees a clean disk while that browser still runs must stay
|
||||
# pending — the browser can restore the ghost on exit — and only a
|
||||
# browser-free rerun verifies the repair and completes.
|
||||
write_stale_preferences
|
||||
run_migration || fail "repair run before the verification scenario"
|
||||
[[ -f $preferences.omarchy-copy-url-repair.bak ]] || fail "verification scenario has a repair backup"
|
||||
printf '#!/bin/bash\nexit 0\n' >"$stub_bin/pgrep"
|
||||
run_migration && fail "migration must not complete an unverified repair while a browser runs"
|
||||
pass "migration keeps an unverified repair pending while a browser runs"
|
||||
printf '#!/bin/bash\nexit 1\n' >"$stub_bin/pgrep"
|
||||
run_migration || fail "migration completes once the repair is verified with browsers closed"
|
||||
pass "migration verifies an attempted repair on a browser-free rerun"
|
||||
rm -f "$preferences.omarchy-copy-url-repair.bak"
|
||||
|
||||
# An installed third-party extension with a command that happens to be named
|
||||
# copy-url keeps its own registration.
|
||||
jq -n '{extensions: {commands: {"linux:Alt+Shift+L": {command_name: "copy-url", extension: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", global: false}}, settings: {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: {path: "/home/user/.config/some-extension", commands: {}}}}}' >"$preferences"
|
||||
untouched_hash=$(sha256sum "$preferences" | cut -d' ' -f1)
|
||||
run_migration || fail "migration leaves installed third-party extensions alone"
|
||||
[[ $(sha256sum "$preferences" | cut -d' ' -f1) == "$untouched_hash" ]] ||
|
||||
fail "migration does not steal a third-party copy-url command registration"
|
||||
pass "migration leaves installed third-party extensions alone"
|
||||
Reference in New Issue
Block a user