publisher-uaproxy-banner: red bar and Connect button on publisher pages when the UA proxy is down
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
abdcb59a3a
commit
c88781a406
@@ -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).
|
||||||
@@ -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);
|
||||||
|
})();
|
||||||
+32
@@ -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
|
||||||
Reference in New Issue
Block a user