From c88781a4061564ddba73ba829fb584d2782e16eb Mon Sep 17 00:00:00 2001 From: Glenn Goossens <48956933+glgoose@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:15:06 +0200 Subject: [PATCH] publisher-uaproxy-banner: red bar and Connect button on publisher pages when the UA proxy is down Co-Authored-By: Claude Sonnet 5.5 --- scripts/publisher-uaproxy-banner/README.md | 31 +++++ .../publisher-uaproxy-banner.user.js | 129 ++++++++++++++++++ scripts/publisher-uaproxy-banner/sync-matches | 32 +++++ 3 files changed, 192 insertions(+) create mode 100644 scripts/publisher-uaproxy-banner/README.md create mode 100644 scripts/publisher-uaproxy-banner/publisher-uaproxy-banner.user.js create mode 100755 scripts/publisher-uaproxy-banner/sync-matches diff --git a/scripts/publisher-uaproxy-banner/README.md b/scripts/publisher-uaproxy-banner/README.md new file mode 100644 index 0000000..989c5bc --- /dev/null +++ b/scripts/publisher-uaproxy-banner/README.md @@ -0,0 +1,31 @@ +# publisher-uaproxy-banner + +Rode balk bovenaan uitgeverspagina's als de UA-proxy uit staat, met een **Connect**-knop. Zonder +proxy laadt de pagina zonder UA-toegang en geeft een Zotero-save geen PDF. + +## Installeren + +- Open `http://127.0.0.1:18088/ua-banner.user.js` in Chrome (`uaproxyd` moet draaien). De daemon + zet het token in de kopie, dus er hoeft niets ingevuld te worden. +- Dev-spoor: `bin/dev-stub publisher-uaproxy-banner`. Het token zit niet in de repo, dus de + eerste klik op Connect vraagt ernaar (`cat ~/.local/share/uaproxyd/token`) en onthoudt het in + Tampermonkey. + +## Hoe het werkt + +- Het script vraagt `/status` aan `uaproxyd` (geen verkeer door de tunnel, dus de idle-stop van + `uavpn` blijft werken) en toont de balk zolang de state niet `up` is. Tijdens het verbinden + staat het MFA-nummer in de balk. +- **Connect** doet een `POST /connect` met het token in `X-UA-Token`. +- De balk heeft een vaste hoogte en de knop een vaste breedte, dus een nieuwe state verschuift + niets. + +## De `@match`-lijst + +De domeinen komen uit `~/dotfiles/lib/uaproxy/publishers.txt`. Na een wijziging van die lijst: +`./sync-matches` herschrijft de regels, `./sync-matches --check` faalt bij verschil. Plak de +stub daarna opnieuw in Tampermonkey, want de metadata komt uit de stub. + +## Licentie + +[GPL-3.0-or-later](../../LICENSE). diff --git a/scripts/publisher-uaproxy-banner/publisher-uaproxy-banner.user.js b/scripts/publisher-uaproxy-banner/publisher-uaproxy-banner.user.js new file mode 100644 index 0000000..b661c2f --- /dev/null +++ b/scripts/publisher-uaproxy-banner/publisher-uaproxy-banner.user.js @@ -0,0 +1,129 @@ +// ==UserScript== +// @name Algemeen: UA-proxy waarschuwing op uitgeverspagina's +// @namespace https://github.com/glgoose/ua-userscripts +// @version 1.0.0 +// @description Rode balk op uitgeverspagina's als de UA-proxy uit staat, met een knop om te verbinden. +// @author glgoose +// @license GPL-3.0-or-later +// @homepageURL https://github.com/glgoose/ua-userscripts/tree/main/scripts/publisher-uaproxy-banner +// @supportURL https://github.com/glgoose/ua-userscripts/issues +// @match *://*.tandfonline.com/* +// @match *://*.taylorfrancis.com/* +// @match *://*.sagepub.com/* +// @match *://*.wiley.com/* +// @match *://*.oup.com/* +// @match *://*.sciencedirect.com/* +// @match *://*.elsevier.com/* +// @match *://*.cambridge.org/* +// @match *://*.jstor.org/* +// @match *://*.springer.com/* +// @match *://*.springernature.com/* +// @match *://*.nature.com/* +// @match *://*.degruyter.com/* +// @match *://*.degruyterbrill.com/* +// @match *://*.brill.com/* +// @match *://*.muse.jhu.edu/* +// @match *://*.journals.uchicago.edu/* +// @match *://*.annualreviews.org/* +// @match *://*.emerald.com/* +// @match *://*.direct.mit.edu/* +// @match *://*.psycnet.apa.org/* +// @match *://*.ingentaconnect.com/* +// @match *://*.bloomsburycollections.com/* +// @run-at document-idle +// @connect 127.0.0.1 +// @grant GM_xmlhttpRequest +// @grant GM_getValue +// @grant GM_setValue +// ==/UserScript== +(function () { + 'use strict'; + + var BASE = 'http://127.0.0.1:18088'; + var POLL_MS = 3000; + // uaproxyd fills in {{TOKEN}} when it serves this file; the dev-stub route has no token, so + // the first click on Connect asks for it once and keeps it in Tampermonkey storage. + var BAKED = '{{TOKEN}}'; + var token = BAKED.charAt(0) === '{' ? GM_getValue('token', '') : BAKED; + + var bar = null, text = null, btn = null, timer = null; + + function request(method, path, cb) { + GM_xmlhttpRequest({ + method: method, + url: BASE + path, + headers: method === 'POST' ? { 'X-UA-Token': token } : {}, + timeout: 4000, + onload: function (r) { + var j = null; + try { j = JSON.parse(r.responseText); } catch (e) { /* not JSON */ } + cb(r.status, j); + }, + onerror: function () { cb(0, null); }, + ontimeout: function () { cb(0, null); }, + }); + } + + // The bar is built once at its final size and never changes height or button width, so + // state changes only swap text (single line, truncated). + function build() { + bar = document.createElement('div'); + text = document.createElement('span'); + btn = document.createElement('button'); + btn.textContent = 'Connect'; + btn.style.cssText = + 'margin-left:12px;min-width:96px;padding:2px 12px;border:1px solid #fff;border-radius:4px;' + + 'background:transparent;color:#fff;font:inherit;font-weight:600;cursor:pointer'; + btn.addEventListener('click', connect); + bar.append(text, btn); + bar.style.cssText = + 'position:fixed;top:0;left:0;right:0;z-index:2147483647;padding:8px 16px;' + + 'background:#b3261e;color:#fff;font:14px/1.4 -apple-system,system-ui,sans-serif;' + + 'text-align:center;box-shadow:0 1px 4px rgba(0,0,0,.3);white-space:nowrap;overflow:hidden;' + + 'text-overflow:ellipsis'; + document.documentElement.appendChild(bar); + } + + function render(s) { + var state = s && s.state; + if (state === 'up') { hide(); return; } + if (!bar) build(); + var base = 'UA proxy down: this page loaded without UA access, so a Zotero save gets no PDF.'; + if (!s) { + text.textContent = 'uaproxyd is not running: this page loaded without UA access.'; + btn.disabled = true; + } else if (state === 'connecting') { + text.textContent = s.mfa ? 'Enter ' + s.mfa + ' in the Authenticator app.' : 'Connecting to the UA proxy.'; + btn.disabled = true; + } else { + text.textContent = s.error ? base + ' ' + s.error : base; + btn.disabled = false; + } + btn.textContent = state === 'connecting' ? 'Connecting' : 'Connect'; + } + + function hide() { + if (bar) { bar.remove(); bar = null; } + if (timer) { clearInterval(timer); timer = null; } + } + + function poll() { + request('GET', '/status', function (code, j) { render(code === 200 ? j : null); }); + } + + function connect() { + if (!token) { + token = (window.prompt('uaproxyd token (cat ~/.local/share/uaproxyd/token):') || '').trim(); + if (!token) return; + GM_setValue('token', token); + } + btn.disabled = true; + request('POST', '/connect', function (code, j) { + if (code === 403) { token = ''; GM_setValue('token', ''); } + render(code === 202 ? j : null); + }); + } + + poll(); + timer = setInterval(poll, POLL_MS); +})(); diff --git a/scripts/publisher-uaproxy-banner/sync-matches b/scripts/publisher-uaproxy-banner/sync-matches new file mode 100755 index 0000000..ffdcdac --- /dev/null +++ b/scripts/publisher-uaproxy-banner/sync-matches @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Rewrites the @match lines of publisher-uaproxy-banner.user.js from the one publisher list +# (dotfiles lib/uaproxy/publishers.txt), so the list stays the single source. +# Usage: sync-matches [--check|-c] [--help|-h] +# --check, -c change nothing, exit 1 when the @match lines differ from the list +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +SCRIPT="$HERE/publisher-uaproxy-banner.user.js" +LIST="${PUBLISHERS:-$HOME/dotfiles/lib/uaproxy/publishers.txt}" +CHECK=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --check|-c) CHECK=true; shift ;; + --help|-h) sed -n '2,5p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) echo "Error: unknown option: $1" >&2; exit 1 ;; + esac +done + +[[ -f "$LIST" ]] || { echo "Error: publisher list not found: $LIST" >&2; exit 1; } + +matches=$(sed 's/#.*//; s/[[:space:]]//g; /^$/d' "$LIST" | awk '{ printf "// @match *://*.%s/*\n", $0 }') +new=$(MATCHES="$matches" awk ' + /^\/\/ @match / { if (!done) { print ENVIRON["MATCHES"]; done = 1 } ; next } + { print }' "$SCRIPT") + +if $CHECK; then + [[ "$new" == "$(cat "$SCRIPT")" ]] || { echo "Error: @match lines differ from $LIST; run sync-matches" >&2; exit 1; } +else + printf '%s\n' "$new" > "$SCRIPT" +fi