MediaWiki:Common.js: Difference between revisions

From B-Wiki
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("family-trip-cipher");
     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>This page is locked for family members. Enter the family PIN to view the itinerary.</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.className = "mw-parser-output";
     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);
        // pack.fmt === "html" means pre-rendered MediaWiki HTML
         if (pack.fmt !== "html") {
         if (pack.fmt === "wikitext") {
           err.textContent = "Unexpected package format. Ask admin to republish.";
           err.textContent = "Old package format. Please ask admin to republish.";
           btn.disabled = false;
           btn.disabled = false;
           return;
           return;
         }
         }
         out.innerHTML = html;
         // If somehow escaped markup was stored, unescape once
        if (html.indexOf("&lt;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";
        if (window.mw && mw.hook && window.jQuery) {
          mw.hook("wikipage.content").fire(jQuery(out));
        }
       } 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("&lt;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 === */