MediaWiki:Common.js: Difference between revisions
Jump to navigation
Jump to search
m Add family trip PIN unlock gate for Trip_to_Berlin_2026 |
m PIN gate: render decrypted wikitext as HTML via action=parse |
||
| 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 === */ | /* === FAMILY_TRIP_PIN_GATE_START === */ | ||
(function () { | (function () { | ||
| Line 9: | Line 8: | ||
return out; | return out; | ||
} | } | ||
async function deriveKey(pin, salt) { | async function deriveKey(pin, salt) { | ||
var enc = new TextEncoder(); | var enc = new TextEncoder(); | ||
var baseKey = await crypto.subtle.importKey("raw", enc.encode(pin), "PBKDF2", false, ["deriveKey"]); | var baseKey = await crypto.subtle.importKey( | ||
"raw", | |||
enc.encode(pin), | |||
"PBKDF2", | |||
false, | |||
["deriveKey"] | |||
); | |||
return crypto.subtle.deriveKey( | return crypto.subtle.deriveKey( | ||
{ name: "PBKDF2", salt: salt, iterations: 200000, hash: "SHA-256" }, | { name: "PBKDF2", salt: salt, iterations: 200000, hash: "SHA-256" }, | ||
| Line 20: | Line 26: | ||
); | ); | ||
} | } | ||
function mount( | |||
async function parseWikitext(wikitext) { | |||
// Prefer mw.Api when available (same-origin, handles large POST bodies). | |||
if (window.mw && mw.Api) { | |||
var api = new mw.Api(); | |||
var data = await api.post({ | |||
action: "parse", | |||
formatversion: 2, | |||
text: wikitext, | |||
contentmodel: "wikitext", | |||
prop: "text", | |||
disablelimitreport: 1, | |||
disableeditsection: 1, | |||
wrapoutputclass: "mw-parser-output" | |||
}); | |||
if (data && data.parse && data.parse.text) { | |||
return data.parse.text; | |||
} | |||
throw new Error("parse returned no text"); | |||
} | |||
var body = new URLSearchParams(); | |||
body.set("action", "parse"); | |||
body.set("format", "json"); | |||
body.set("formatversion", "2"); | |||
body.set("text", wikitext); | |||
body.set("contentmodel", "wikitext"); | |||
body.set("prop", "text"); | |||
body.set("disablelimitreport", "1"); | |||
body.set("disableeditsection", "1"); | |||
body.set("wrapoutputclass", "mw-parser-output"); | |||
var resp = await fetch("/api.php", { | |||
method: "POST", | |||
headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" }, | |||
body: body.toString(), | |||
credentials: "same-origin" | |||
}); | |||
if (!resp.ok) throw new Error("parse HTTP " + resp.status); | |||
var json = await resp.json(); | |||
if (json.error) throw new Error(json.error.info || json.error.code); | |||
return json.parse.text; | |||
} | |||
function mount() { | |||
var el = document.getElementById("family-trip-cipher"); | var el = document.getElementById("family-trip-cipher"); | ||
if (!el || el.getAttribute("data-ready") === "1") return; | if (!el || el.getAttribute("data-ready") === "1") return; | ||
el.setAttribute("data-ready", "1"); | el.setAttribute("data-ready", "1"); | ||
var pack; | var pack; | ||
try { pack = JSON.parse(el.textContent); } catch (e) { return; } | try { | ||
pack = JSON.parse(el.textContent); | |||
} catch (e) { | |||
return; | |||
} | |||
var gate = document.createElement("div"); | var gate = document.createElement("div"); | ||
gate.id = "family-trip-gate"; | gate.id = "family-trip-gate"; | ||
gate.style.cssText = "max-width: | gate.style.cssText = | ||
gate.innerHTML = "<h2 style=\"margin-top:0\">Family trip — PIN required</h2>" + | "max-width:560px;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>" + | "<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>" + | "<label for=\"family-trip-pin\">PIN</label><br>" + | ||
| Line 38: | Line 94: | ||
"<div id=\"family-trip-err\" style=\"color:#b32424;margin-top:8px;min-height:1.2em\"></div>"; | "<div id=\"family-trip-err\" style=\"color:#b32424;margin-top:8px;min-height:1.2em\"></div>"; | ||
var out = document.createElement(" | var out = document.createElement("div"); | ||
out.id = "family-trip-out"; | out.id = "family-trip-out"; | ||
out.style.cssText = "display:none; | out.className = "mw-parser-output"; | ||
out.style.cssText = "display:none;margin-top:1em;"; | |||
el.style.display = "none"; | el.style.display = "none"; | ||
| Line 48: | Line 105: | ||
async function unlock() { | async function unlock() { | ||
var err = document.getElementById("family-trip-err"); | var err = document.getElementById("family-trip-err"); | ||
var btn = document.getElementById("family-trip-unlock"); | |||
err.textContent = ""; | err.textContent = ""; | ||
var pin = document.getElementById("family-trip-pin").value.trim(); | var pin = document.getElementById("family-trip-pin").value.trim(); | ||
if (pin.length < 4) { err.textContent = "PIN too short."; return; } | if (pin.length < 4) { | ||
err.textContent = "PIN too short."; | |||
return; | |||
} | |||
btn.disabled = true; | |||
err.textContent = "Decrypting…"; | |||
try { | try { | ||
var key = await deriveKey(pin, b64ToBytes(pack.salt)); | var key = await deriveKey(pin, b64ToBytes(pack.salt)); | ||
| Line 58: | Line 121: | ||
b64ToBytes(pack.ciphertext) | b64ToBytes(pack.ciphertext) | ||
); | ); | ||
var wikitext = new TextDecoder().decode(pt); | |||
err.textContent = "Rendering…"; | |||
var html = await parseWikitext(wikitext); | |||
out.innerHTML = html; | |||
out.style.display = "block"; | out.style.display = "block"; | ||
gate.style.display = "none"; | gate.style.display = "none"; | ||
// Re-run MediaWiki content hooks for collapsibles / makeCollapsible etc. | |||
if (window.mw && mw.hook) { | |||
mw.hook("wikipage.content").fire($(out)); | |||
} | |||
} catch (e) { | } catch (e) { | ||
err.textContent = "Wrong PIN."; | err.textContent = "Wrong PIN or render failed. Try again."; | ||
btn.disabled = false; | |||
} | } | ||
} | } | ||
document.getElementById("family-trip-unlock").addEventListener("click", unlock); | document.getElementById("family-trip-unlock").addEventListener("click", unlock); | ||
document.getElementById("family-trip-pin").addEventListener("keydown", function (ev) { | document.getElementById("family-trip-pin").addEventListener("keydown", function (ev) { | ||
| Line 70: | Line 143: | ||
}); | }); | ||
} | } | ||
if (window.mw && mw.hook) { | if (window.mw && mw.hook) { | ||
mw.hook("wikipage.content").add(mount); | mw.hook("wikipage.content").add(function () { | ||
mount(); | |||
}); | |||
} else { | } else { | ||
document.addEventListener("DOMContentLoaded", | document.addEventListener("DOMContentLoaded", mount); | ||
} | } | ||
})(); | })(); | ||
/* === FAMILY_TRIP_PIN_GATE_END === */ | /* === FAMILY_TRIP_PIN_GATE_END === */ | ||
Revision as of 23:20, 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"]
);
}
async function parseWikitext(wikitext) {
// Prefer mw.Api when available (same-origin, handles large POST bodies).
if (window.mw && mw.Api) {
var api = new mw.Api();
var data = await api.post({
action: "parse",
formatversion: 2,
text: wikitext,
contentmodel: "wikitext",
prop: "text",
disablelimitreport: 1,
disableeditsection: 1,
wrapoutputclass: "mw-parser-output"
});
if (data && data.parse && data.parse.text) {
return data.parse.text;
}
throw new Error("parse returned no text");
}
var body = new URLSearchParams();
body.set("action", "parse");
body.set("format", "json");
body.set("formatversion", "2");
body.set("text", wikitext);
body.set("contentmodel", "wikitext");
body.set("prop", "text");
body.set("disablelimitreport", "1");
body.set("disableeditsection", "1");
body.set("wrapoutputclass", "mw-parser-output");
var resp = await fetch("/api.php", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
body: body.toString(),
credentials: "same-origin"
});
if (!resp.ok) throw new Error("parse HTTP " + resp.status);
var json = await resp.json();
if (json.error) throw new Error(json.error.info || json.error.code);
return json.parse.text;
}
function mount() {
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:560px;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("div");
out.id = "family-trip-out";
out.className = "mw-parser-output";
out.style.cssText = "display:none;margin-top:1em;";
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");
var btn = document.getElementById("family-trip-unlock");
err.textContent = "";
var pin = document.getElementById("family-trip-pin").value.trim();
if (pin.length < 4) {
err.textContent = "PIN too short.";
return;
}
btn.disabled = true;
err.textContent = "Decrypting…";
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)
);
var wikitext = new TextDecoder().decode(pt);
err.textContent = "Rendering…";
var html = await parseWikitext(wikitext);
out.innerHTML = html;
out.style.display = "block";
gate.style.display = "none";
// Re-run MediaWiki content hooks for collapsibles / makeCollapsible etc.
if (window.mw && mw.hook) {
mw.hook("wikipage.content").fire($(out));
}
} catch (e) {
err.textContent = "Wrong PIN or render failed. Try again.";
btn.disabled = false;
}
}
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(function () {
mount();
});
} else {
document.addEventListener("DOMContentLoaded", mount);
}
})();
/* === FAMILY_TRIP_PIN_GATE_END === */