MediaWiki:Common.js: Difference between revisions

From B-Wiki
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($content) {
 
  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:520px;margin:1.5em auto;padding:1.25em 1.5em;border:1px solid #a2a9b1;background:#f8f9fa;border-radius:8px;font-family:sans-serif;";
     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("pre");
     var out = document.createElement("div");
     out.id = "family-trip-out";
     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";
    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)
         );
         );
         out.textContent = new TextDecoder().decode(pt);
         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", function () { mount(document); });
     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 === */