MediaWiki:Common.js: Difference between revisions
Jump to navigation
Jump to search
m Replaced content with "→Any JavaScript here will be loaded for all users on every page load.: " Tag: Replaced |
m Add family trip PIN unlock gate for Trip_to_Berlin_2026 |
||
| Line 1: | Line 1: | ||
/* Any JavaScript here will be loaded for all users on every page load. */ | /* Any JavaScript here will be loaded for all users on every page load. */ | ||
/* === FAMILY_TRIP_PIN_GATE_START === */ | |||
(function () { | |||
function b64ToBytes(b64) { | |||
var bin = atob(b64); | |||
var out = new Uint8Array(bin.length); | |||
for (var i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); | |||
return out; | |||
} | |||
async function deriveKey(pin, salt) { | |||
var enc = new TextEncoder(); | |||
var baseKey = await crypto.subtle.importKey("raw", enc.encode(pin), "PBKDF2", false, ["deriveKey"]); | |||
return crypto.subtle.deriveKey( | |||
{ name: "PBKDF2", salt: salt, iterations: 200000, hash: "SHA-256" }, | |||
baseKey, | |||
{ name: "AES-GCM", length: 256 }, | |||
false, | |||
["decrypt"] | |||
); | |||
} | |||
function mount($content) { | |||
var el = document.getElementById("family-trip-cipher"); | |||
if (!el || el.getAttribute("data-ready") === "1") return; | |||
el.setAttribute("data-ready", "1"); | |||
var pack; | |||
try { pack = JSON.parse(el.textContent); } catch (e) { return; } | |||
var gate = document.createElement("div"); | |||
gate.id = "family-trip-gate"; | |||
gate.style.cssText = "max-width:520px;margin:1.5em auto;padding:1.25em 1.5em;border:1px solid #a2a9b1;background:#f8f9fa;border-radius:8px;font-family:sans-serif;"; | |||
gate.innerHTML = "<h2 style=\"margin-top:0\">Family trip — PIN required</h2>" + | |||
"<p>This page is locked for family members. Enter the family PIN to view the itinerary.</p>" + | |||
"<label for=\"family-trip-pin\">PIN</label><br>" + | |||
"<input id=\"family-trip-pin\" type=\"password\" inputmode=\"numeric\" autocomplete=\"off\" " + | |||
"style=\"font-size:1.2em;letter-spacing:0.2em;padding:8px;width:10em;margin:8px 0\" /> " + | |||
"<button id=\"family-trip-unlock\" type=\"button\" style=\"padding:8px 14px\">Unlock</button>" + | |||
"<div id=\"family-trip-err\" style=\"color:#b32424;margin-top:8px;min-height:1.2em\"></div>"; | |||
var out = document.createElement("pre"); | |||
out.id = "family-trip-out"; | |||
out.style.cssText = "display:none;white-space:pre-wrap;word-break:break-word;background:#fff;border:1px solid #a2a9b1;padding:1em;max-height:70vh;overflow:auto"; | |||
el.style.display = "none"; | |||
el.parentNode.insertBefore(gate, el); | |||
el.parentNode.insertBefore(out, el.nextSibling); | |||
async function unlock() { | |||
var err = document.getElementById("family-trip-err"); | |||
err.textContent = ""; | |||
var pin = document.getElementById("family-trip-pin").value.trim(); | |||
if (pin.length < 4) { err.textContent = "PIN too short."; return; } | |||
try { | |||
var key = await deriveKey(pin, b64ToBytes(pack.salt)); | |||
var pt = await crypto.subtle.decrypt( | |||
{ name: "AES-GCM", iv: b64ToBytes(pack.nonce), tagLength: 128 }, | |||
key, | |||
b64ToBytes(pack.ciphertext) | |||
); | |||
out.textContent = new TextDecoder().decode(pt); | |||
out.style.display = "block"; | |||
gate.style.display = "none"; | |||
} catch (e) { | |||
err.textContent = "Wrong PIN."; | |||
} | |||
} | |||
document.getElementById("family-trip-unlock").addEventListener("click", unlock); | |||
document.getElementById("family-trip-pin").addEventListener("keydown", function (ev) { | |||
if (ev.key === "Enter") unlock(); | |||
}); | |||
} | |||
if (window.mw && mw.hook) { | |||
mw.hook("wikipage.content").add(mount); | |||
} else { | |||
document.addEventListener("DOMContentLoaded", function () { mount(document); }); | |||
} | |||
})(); | |||
/* === FAMILY_TRIP_PIN_GATE_END === */ | |||
Revision as of 23:01, 14 August 2026
/* Any JavaScript here will be loaded for all users on every page load. */
/* === FAMILY_TRIP_PIN_GATE_START === */
(function () {
function b64ToBytes(b64) {
var bin = atob(b64);
var out = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
return out;
}
async function deriveKey(pin, salt) {
var enc = new TextEncoder();
var baseKey = await crypto.subtle.importKey("raw", enc.encode(pin), "PBKDF2", false, ["deriveKey"]);
return crypto.subtle.deriveKey(
{ name: "PBKDF2", salt: salt, iterations: 200000, hash: "SHA-256" },
baseKey,
{ name: "AES-GCM", length: 256 },
false,
["decrypt"]
);
}
function mount($content) {
var el = document.getElementById("family-trip-cipher");
if (!el || el.getAttribute("data-ready") === "1") return;
el.setAttribute("data-ready", "1");
var pack;
try { pack = JSON.parse(el.textContent); } catch (e) { return; }
var gate = document.createElement("div");
gate.id = "family-trip-gate";
gate.style.cssText = "max-width:520px;margin:1.5em auto;padding:1.25em 1.5em;border:1px solid #a2a9b1;background:#f8f9fa;border-radius:8px;font-family:sans-serif;";
gate.innerHTML = "<h2 style=\"margin-top:0\">Family trip — PIN required</h2>" +
"<p>This page is locked for family members. Enter the family PIN to view the itinerary.</p>" +
"<label for=\"family-trip-pin\">PIN</label><br>" +
"<input id=\"family-trip-pin\" type=\"password\" inputmode=\"numeric\" autocomplete=\"off\" " +
"style=\"font-size:1.2em;letter-spacing:0.2em;padding:8px;width:10em;margin:8px 0\" /> " +
"<button id=\"family-trip-unlock\" type=\"button\" style=\"padding:8px 14px\">Unlock</button>" +
"<div id=\"family-trip-err\" style=\"color:#b32424;margin-top:8px;min-height:1.2em\"></div>";
var out = document.createElement("pre");
out.id = "family-trip-out";
out.style.cssText = "display:none;white-space:pre-wrap;word-break:break-word;background:#fff;border:1px solid #a2a9b1;padding:1em;max-height:70vh;overflow:auto";
el.style.display = "none";
el.parentNode.insertBefore(gate, el);
el.parentNode.insertBefore(out, el.nextSibling);
async function unlock() {
var err = document.getElementById("family-trip-err");
err.textContent = "";
var pin = document.getElementById("family-trip-pin").value.trim();
if (pin.length < 4) { err.textContent = "PIN too short."; return; }
try {
var key = await deriveKey(pin, b64ToBytes(pack.salt));
var pt = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: b64ToBytes(pack.nonce), tagLength: 128 },
key,
b64ToBytes(pack.ciphertext)
);
out.textContent = new TextDecoder().decode(pt);
out.style.display = "block";
gate.style.display = "none";
} catch (e) {
err.textContent = "Wrong PIN.";
}
}
document.getElementById("family-trip-unlock").addEventListener("click", unlock);
document.getElementById("family-trip-pin").addEventListener("keydown", function (ev) {
if (ev.key === "Enter") unlock();
});
}
if (window.mw && mw.hook) {
mw.hook("wikipage.content").add(mount);
} else {
document.addEventListener("DOMContentLoaded", function () { mount(document); });
}
})();
/* === FAMILY_TRIP_PIN_GATE_END === */