MediaWiki:Common.js: Difference between revisions
Jump to navigation
Jump to search
m PIN gate: inject pre-rendered HTML after decrypt (no client parse) |
m PIN gate v3: render unlocked trip in iframe srcdoc (fixes HTML source view) |
||
| Line 2: | Line 2: | ||
/* === FAMILY_TRIP_PIN_GATE_START === */ | /* === FAMILY_TRIP_PIN_GATE_START === */ | ||
(function () { | (function () { | ||
var CIPHER_ID = "family-trip-cipher-v3"; | |||
function b64ToBytes(b64) { | function b64ToBytes(b64) { | ||
var bin = atob(b64); | var bin = atob(b64); | ||
| Line 25: | Line 27: | ||
["decrypt"] | ["decrypt"] | ||
); | ); | ||
} | |||
function renderHtml(out, html) { | |||
// Clear any previous content | |||
while (out.firstChild) out.removeChild(out.firstChild); | |||
// Prefer iframe so tags always render as a document, never as visible source | |||
var frame = document.createElement("iframe"); | |||
frame.id = "family-trip-frame"; | |||
frame.title = "Trip to Berlin 2026"; | |||
frame.style.cssText = "width:100%;min-height:75vh;border:1px solid #a2a9b1;border-radius:8px;background:#fff;"; | |||
frame.setAttribute("sandbox", "allow-same-origin allow-popups allow-popups-to-escape-sandbox"); | |||
var doc = | |||
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">" + | |||
"<base href=\"https://bogza.ro/\">" + | |||
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">" + | |||
"<link rel=\"stylesheet\" href=\"https://bogza.ro/load.php?lang=en&modules=site.styles%7Cskins.vector.styles.legacy&only=styles&skin=vector\">" + | |||
"<style>body{margin:16px;background:#fff;color:#202122;font-family:sans-serif;} table{border-collapse:collapse;} td,th{border:1px solid #a2a9b1;padding:4px 8px;} .wikitable{background:#f8f9fa;} img{max-width:100%;height:auto;}</style>" + | |||
"</head><body class=\"mediawiki ltr sitedir-ltr\">" + | |||
html + | |||
"</body></html>"; | |||
frame.srcdoc = doc; | |||
out.appendChild(frame); | |||
frame.addEventListener("load", function () { | |||
try { | |||
var h = frame.contentDocument.documentElement.scrollHeight; | |||
if (h && h > 400) frame.style.height = Math.min(h + 40, 12000) + "px"; | |||
} catch (e) {} | |||
}); | |||
} | } | ||
function mount() { | function mount() { | ||
var el = document.getElementById( | var el = document.getElementById(CIPHER_ID); | ||
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"); | ||
| Line 40: | Line 74: | ||
var gate = document.createElement("div"); | var gate = document.createElement("div"); | ||
gate.id = "family-trip-gate"; | gate.id = "family-trip-gate-v3"; | ||
gate.style.cssText = | 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;"; | "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 = | gate.innerHTML = | ||
"<h2 style=\"margin-top:0\">Family trip — PIN required</h2>" + | "<h2 style=\"margin-top:0\">Family trip — PIN required</h2>" + | ||
"<p> | "<p>Enter the family PIN to open the formatted itinerary.</p>" + | ||
"<label for=\"family-trip-pin\">PIN</label><br>" + | "<label for=\"family-trip-pin-v3\">PIN</label><br>" + | ||
"<input id=\"family-trip-pin\" type=\"password\" inputmode=\"numeric\" autocomplete=\"off\" " + | "<input id=\"family-trip-pin-v3\" type=\"password\" inputmode=\"numeric\" autocomplete=\"off\" " + | ||
"style=\"font-size:1.2em;letter-spacing:0.2em;padding:8px;width:10em;margin:8px 0\" /> " + | "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>" + | "<button id=\"family-trip-unlock-v3\" type=\"button\" style=\"padding:8px 14px\">Unlock</button>" + | ||
"<div id=\"family-trip-err\" style=\"color:#b32424;margin-top:8px;min-height:1.2em\"></div>"; | "<div id=\"family-trip-err-v3\" style=\"color:#b32424;margin-top:8px;min-height:1.2em\"></div>"; | ||
var out = document.createElement("div"); | var out = document.createElement("div"); | ||
out.id = "family-trip-out | out.id = "family-trip-out-v3"; | ||
out.style.cssText = "display:none;margin-top:1em;"; | out.style.cssText = "display:none;margin-top:1em;"; | ||
| Line 62: | Line 95: | ||
async function unlock() { | async function unlock() { | ||
var err = document.getElementById("family-trip-err"); | var err = document.getElementById("family-trip-err-v3"); | ||
var btn = document.getElementById("family-trip-unlock"); | var btn = document.getElementById("family-trip-unlock-v3"); | ||
err.textContent = ""; | err.textContent = ""; | ||
var pin = document.getElementById("family-trip-pin").value.trim(); | var pin = document.getElementById("family-trip-pin-v3").value.trim(); | ||
if (pin.length < 4) { | if (pin.length < 4) { | ||
err.textContent = "PIN too short."; | err.textContent = "PIN too short."; | ||
| Line 80: | Line 113: | ||
); | ); | ||
var html = new TextDecoder().decode(pt); | var html = new TextDecoder().decode(pt); | ||
if (pack.fmt !== "html") { | |||
if (pack.fmt | err.textContent = "Unexpected package format. Ask admin to republish."; | ||
err.textContent = " | |||
btn.disabled = false; | btn.disabled = false; | ||
return; | return; | ||
} | } | ||
// If somehow escaped markup was stored, unescape once | |||
if (html.indexOf("<table") !== -1 && html.indexOf("<table") === -1) { | |||
var ta = document.createElement("textarea"); | |||
ta.innerHTML = html; | |||
html = ta.value; | |||
} | |||
renderHtml(out, html); | |||
out.style.display = "block"; | out.style.display = "block"; | ||
gate.style.display = "none"; | gate.style.display = "none"; | ||
} catch (e) { | } catch (e) { | ||
err.textContent = "Wrong PIN."; | err.textContent = "Wrong PIN."; | ||
| Line 98: | Line 133: | ||
} | } | ||
document.getElementById("family-trip-unlock").addEventListener("click", unlock); | document.getElementById("family-trip-unlock-v3").addEventListener("click", unlock); | ||
document.getElementById("family-trip-pin").addEventListener("keydown", function (ev) { | document.getElementById("family-trip-pin-v3").addEventListener("keydown", function (ev) { | ||
if (ev.key === "Enter") unlock(); | if (ev.key === "Enter") unlock(); | ||
}); | }); | ||
Revision as of 23:24, 14 August 2026
/* Any JavaScript here will be loaded for all users on every page load. */
/* === FAMILY_TRIP_PIN_GATE_START === */
(function () {
var CIPHER_ID = "family-trip-cipher-v3";
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 renderHtml(out, html) {
// Clear any previous content
while (out.firstChild) out.removeChild(out.firstChild);
// Prefer iframe so tags always render as a document, never as visible source
var frame = document.createElement("iframe");
frame.id = "family-trip-frame";
frame.title = "Trip to Berlin 2026";
frame.style.cssText = "width:100%;min-height:75vh;border:1px solid #a2a9b1;border-radius:8px;background:#fff;";
frame.setAttribute("sandbox", "allow-same-origin allow-popups allow-popups-to-escape-sandbox");
var doc =
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">" +
"<base href=\"https://bogza.ro/\">" +
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">" +
"<link rel=\"stylesheet\" href=\"https://bogza.ro/load.php?lang=en&modules=site.styles%7Cskins.vector.styles.legacy&only=styles&skin=vector\">" +
"<style>body{margin:16px;background:#fff;color:#202122;font-family:sans-serif;} table{border-collapse:collapse;} td,th{border:1px solid #a2a9b1;padding:4px 8px;} .wikitable{background:#f8f9fa;} img{max-width:100%;height:auto;}</style>" +
"</head><body class=\"mediawiki ltr sitedir-ltr\">" +
html +
"</body></html>";
frame.srcdoc = doc;
out.appendChild(frame);
frame.addEventListener("load", function () {
try {
var h = frame.contentDocument.documentElement.scrollHeight;
if (h && h > 400) frame.style.height = Math.min(h + 40, 12000) + "px";
} catch (e) {}
});
}
function mount() {
var el = document.getElementById(CIPHER_ID);
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-v3";
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>Enter the family PIN to open the formatted itinerary.</p>" +
"<label for=\"family-trip-pin-v3\">PIN</label><br>" +
"<input id=\"family-trip-pin-v3\" 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-v3\" type=\"button\" style=\"padding:8px 14px\">Unlock</button>" +
"<div id=\"family-trip-err-v3\" style=\"color:#b32424;margin-top:8px;min-height:1.2em\"></div>";
var out = document.createElement("div");
out.id = "family-trip-out-v3";
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-v3");
var btn = document.getElementById("family-trip-unlock-v3");
err.textContent = "";
var pin = document.getElementById("family-trip-pin-v3").value.trim();
if (pin.length < 4) {
err.textContent = "PIN too short.";
return;
}
btn.disabled = true;
err.textContent = "Unlocking…";
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 html = new TextDecoder().decode(pt);
if (pack.fmt !== "html") {
err.textContent = "Unexpected package format. Ask admin to republish.";
btn.disabled = false;
return;
}
// If somehow escaped markup was stored, unescape once
if (html.indexOf("<table") !== -1 && html.indexOf("<table") === -1) {
var ta = document.createElement("textarea");
ta.innerHTML = html;
html = ta.value;
}
renderHtml(out, html);
out.style.display = "block";
gate.style.display = "none";
} catch (e) {
err.textContent = "Wrong PIN.";
btn.disabled = false;
}
}
document.getElementById("family-trip-unlock-v3").addEventListener("click", unlock);
document.getElementById("family-trip-pin-v3").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 === */