summaryrefslogtreecommitdiff
path: root/data/extensions/jsr@javascriptrestrictor/nscl/content/patchWindow.js
diff options
context:
space:
mode:
Diffstat (limited to 'data/extensions/jsr@javascriptrestrictor/nscl/content/patchWindow.js')
-rw-r--r--data/extensions/jsr@javascriptrestrictor/nscl/content/patchWindow.js488
1 files changed, 0 insertions, 488 deletions
diff --git a/data/extensions/jsr@javascriptrestrictor/nscl/content/patchWindow.js b/data/extensions/jsr@javascriptrestrictor/nscl/content/patchWindow.js
deleted file mode 100644
index 976470b..0000000
--- a/data/extensions/jsr@javascriptrestrictor/nscl/content/patchWindow.js
+++ /dev/null
@@ -1,488 +0,0 @@
-/*
- * NoScript Commons Library
- * Reusable building blocks for cross-browser security/privacy WebExtensions.
- * Copyright (C) 2020-2024 Giorgio Maone <https://maone.net>
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify it under
- * the terms of the GNU General Public License as published by the Free Software
- * Foundation, either version 3 of the License, or (at your option) any later
- * version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
- * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-// depends on /nscl/common/uuid.js
-
-"use strict";
-/**
- * Injects code into page context in a cross-browser way, providing it
- * with tools to wrap/patch the DOM and the JavaScript environment
- * and propagating the changes to child windows created on the fly in order
- * to prevent the modifications to be cancelled by hostile code.
- *
- * @param {function} patchingCallback
- * the (semi)privileged wrapping code to be injected.
- * Warning: this is not to be considered a closure, since Chromium
- * injection needs it to be reparsed out of context.
- * Use the env argument to propagate parameters.
- * It will be called as patchingCallback(unwrappedWindow, env).
- * @param {object} env
- * a JSON-serializable object to made available to patchingCallback as
- * its second argument. It gets augmented by two additional properties:
- * 1. a Port (port: {postMessage(), onMessage()}) object
- * allowing the injected script to communicate with
- * the privileged content script by calling port.postMessage(msg, [event])
- * and/or by listening to a port.onMessage(msg, event) user-defined callback.
- * 2. A "xray" object property to help handling
- * Firefox's XRAY wrappers.
- * xray: {
- * enabled: true, // false on Chromium
- * unwrap(obj), // returns the XPC-wrapped object - or just obj on Chromium
- * wrap(obj), // returns the XPC wrapper around the object - or just obj on Chromium
- * forPage(obj), // returns cloneInto(obj) including functions and DOM objects - or just obj on Chromium
- * window, // the XPC-wrapped version of unwrappedWindow, or unwrappedWindow itself on Chromium
- * }
- * @returns {object} port
- * A Port object allowing the privileged content script to communicate
- * with the injected script on the page by calling port.postMessage(msg, [event])
- * and/or by listening to a port.onMessage(msg, event) user-defined callback.
- */
-
-function patchWindow(patchingCallback, env = {}) {
- const forcedPortId = patchingCallback.portId;
- const justPort = forcedPortId && !patchingCallback.code;
- const portId = forcedPortId ||
- this && this.portId ||
- `windowPatchMessages:${uuid()}`;
-
- const { dispatchEvent, addEventListener } = self;
-
- function Port(from, to) {
- if (!self.document) {
- // ServiceWorker scope, dummy port, won't be used.
- this.postMessage = () => {};
- return;
- }
- // we need a double dispatching dance and maintaining a stack of
- // return values / thrown errors because Chromium seals the detail object
- // (on Firefox we could just append further properties to it...)
- let retStack = [];
-
- function fire(e, detail, target = window) {
- dispatchEvent.call(target, new CustomEvent(`${portId}:${e}`, {detail, composed: true}));
- }
- this.postMessage = function(msg, target = window) {
- retStack.push({});
- let detail = {msg};
- fire(to, detail, target);
- let ret = retStack.pop();
- if (ret.error) throw ret.error;
- return ret.value;
- };
- addEventListener.call(window, `${portId}:${from}`, event => {
- if (typeof this.onMessage === "function" && event.detail) {
- let ret = {};
- try {
- ret.value = this.onMessage(event.detail.msg, event);
- } catch (error) {
- ret.error = error;
- }
- fire(`return:${to}`, ret);
- }
- }, true);
- addEventListener.call(window, `${portId}:return:${from}`, event => {
- let {detail} = event;
- if (detail && retStack.length) {
- retStack[retStack.length -1] = detail;
- }
- }, true);
- this.onMessage = null;
- }
- let port = new Port("extension", "page");
- if (patchWindow.disabled) {
- return port;
- }
- if (justPort) {
- return port;
- } else if (patchingCallback.code) {
- patchingCallback = patchingCallback.code;
- }
-
- const nativeExport = typeof exportFunction == "function";
- if (typeof patchingCallback !== "function") {
- patchingCallback =
- nativeExport ? new Function("unwrappedWindow", "env", patchingCallback)
- : `function (unwrappedWindow, env) {\n${patchingCallback}\n}`;
- }
- if (!(nativeExport || this && this.exportFunction)) {
- // Chromium
- let exportFunction = (func, targetObject, {defineAs, original} = {}) => {
- try {
- let [propDef, getOrSet, propName] = defineAs && /^([gs]et)(?:\s+(\w+))$/.exec(defineAs) || [null, null, defineAs];
- let propDes = propName && Object.getOwnPropertyDescriptor(targetObject, propName);
- if (getOrSet && !propDes) { // escalate through prototype chain
- for (let proto = Object.getPrototypeOf(targetObject); proto; proto = Object.getPrototypeOf(proto)) {
- propDes = Object.getOwnPropertyDescriptor(proto, propName);
- if (propDes) {
- targetObject = proto;
- break;
- }
- }
- }
-
- let toString = Function.prototype.toString;
- let strVal;
- if (!original) {
- original = propDef && propDes ? propDes[getOrSet] : defineAs && targetObject[defineAs];
- }
- if (!original) {
- // It seems to be a brand new function, rather than a replacement.
- // Let's ensure it appears as a native one with little hack: we proxy a Promise callback ;)
- Promise.resolve(new Promise(resolve => original = resolve));
- let name = propDef && propDes ? `${getOrSet} ${propName}` : defineAs;
- if (name) {
- let nameDef = Reflect.getOwnPropertyDescriptor(original, "name");
- nameDef.value = name;
- Reflect.defineProperty(original, "name", nameDef);
- strVal = toString.call(original).replace(/^function \(\)/, `function ${name}()`)
- }
- }
-
- strVal = strVal || toString.call(original);
-
- let proxy = new Proxy(original, {
- apply(target, thisArg, args) {
- return func.apply(thisArg, args);
- }
- });
-
- if (!exportFunction._toStringMap) {
- let map = new WeakMap();
- exportFunction._toStringMap = map;
- let toStringProxy = new Proxy(toString, {
- apply(target, thisArg, args) {
- return map.has(thisArg) ? map.get(thisArg) : Reflect.apply(target, thisArg, args);
- }
- });
- map.set(toStringProxy, toString.apply(toString));
- Function.prototype.toString = toStringProxy;
- }
- exportFunction._toStringMap.set(proxy, strVal);
-
- if (propName) {
- if (!propDes) {
- targetObject[propName] = proxy;
- } else {
- if (getOrSet) {
- propDes[getOrSet] = proxy;
- } else {
- if ("value" in propDes) {
- propDes.value = proxy;
- } else {
- return exportFunction(() => proxy, targetObject, `get ${propName}`);
- }
- }
- Object.defineProperty(targetObject, propName, propDes);
- }
- }
- return proxy;
- } catch (e) {
- console.error(e, `setting ${targetObject}.${defineAs || original}`, func);
- }
- return null;
- };
- let cloneInto = (obj, targetObject) => {
- return obj; // dummy for assignment
- };
-
- const code = `
- (() => {
- let patchWindow = ${patchWindow};
- let cloneInto = ${cloneInto};
- let exportFunction = ${exportFunction};
- let env = ${JSON.stringify(env)};
- let portId = ${JSON.stringify(portId)};
- const console = Object.fromEntries(Object.entries(self.console).map(([n, v]) => v.bind ? [n, v.bind(self.console)] : [n,v]));
-
- env.port = new (${Port})("page", "extension");
- ({
- patchWindow,
- exportFunction,
- cloneInto,
- portId,
- }).patchWindow(${patchingCallback}, env);
- })();
- `;
- if (!self.document) {
- // we're doing it with userScripts on mv3
- return {portId, code};
- }
- let script = document.createElement("script");
- script.text = code;
- try {
- document.documentElement.insertBefore(script, document.documentElement.firstChild);
- } catch(e) {
- console.error(e, code);
- }
- script.remove();
- return port;
- }
-
- env.port = new Port("page", "extension");
-
- const {xrayEnabled} = patchWindow;
- const zombieDanger = xrayEnabled && document.readyState === "complete";
- const isZombieException = e => e.message.includes("dead object");
-
- const getSafeMethod = zombieDanger
- ? (obj, method, wrappedObj) => {
- let actualTarget = obj[method];
- return XPCNativeWrapper.unwrap(new window.Proxy(actualTarget, cloneInto({
- apply(targetFunc, thisArg, args) {
- try {
- return actualTarget.apply(thisArg, args);
- } catch (e) {
- if (isZombieException(e)) {
- return (actualTarget = (wrappedObj || XPCNativeWrapper(obj))[method]).apply(thisArg, args);
- }
- throw e;
- }
- },
- }, window, {cloneFunctions: true, wrapReflectors: true}
- )));
-
- } : (obj, method) => obj[method];
-
- const getSafeDescriptor = (proto, prop, accessor) => {
- const des = Reflect.getOwnPropertyDescriptor(proto, prop);
- if (zombieDanger) {
- const wrappedDescriptor = Reflect.getOwnPropertyDescriptor(xray.wrap(proto), prop);
- des[accessor] = getSafeMethod(des, accessor, wrappedDescriptor);
- }
- return des;
- }
-
- let xrayMake = (enabled, wrap, unwrap = wrap, forPage = wrap) => ({
- enabled, wrap, unwrap, forPage,
- getSafeMethod, getSafeDescriptor
- });
-
- let xray = !xrayEnabled
- ? xrayMake(false, o => o)
- : xrayMake(true, o => XPCNativeWrapper(o), o => XPCNativeWrapper.unwrap(o),
- function(obj, win = this.window || window) {
- return cloneInto(obj, win, {cloneFunctions: true, wrapReflectors: true});
- });
-
- const patchedWindows = new WeakSet(); // track them to avoid indirect recursion
-
- // win: window object to modify.
- function modifyWindow(win) {
- try {
- win = xray.unwrap(win);
- env.xray = Object.assign({window: xray.wrap(win)}, xray);
-
- if (patchedWindows.has(win)) return;
- patchedWindows.add(win);
- patchingCallback(win, env);
- modifyWindowOpenMethod(win);
- modifyFramingElements(win);
- // we don't need to modify win.opener, read skriptimaahinen notes
- // at https://forums.informaction.com/viewtopic.php?p=103754#p103754
- } catch (e) {
- if (e instanceof DOMException && e.name === "SecurityError") {
- // In case someone tries to access SOP restricted window.
- // We can just ignore this.
- } else throw e;
- }
- }
-
- function modifyWindowOpenMethod(win) {
- let windowOpen = win.open;
- exportFunction(function(...args) {
- let newWin = windowOpen.call(this, ...args);
- if (newWin) modifyWindow(newWin);
- return newWin;
- }, win, {defineAs: "open"});
- }
-
- function modifyFramingElements(win) {
- for (let property of ["contentWindow", "contentDocument"]) {
- for (let iface of ["Frame", "IFrame", "Object"]) {
- let proto = win[`HTML${iface}Element`].prototype;
- modifyContentProperties(proto, property)
- }
- }
- // auto-trigger window patching whenever new elements are added to the DOM
- let patchAll = () => {
- if (patchWindow.disabled) {
- observer.disconnect();
- }
- for (let j = 0; j in window; j++) {
- try {
- modifyWindow(window[j]);
- } catch (e) {
- console.error(e, `Patching frames[${j}]`);
- }
- }
- };
-
- let xrayWin = xray.wrap(win);
- let observer = new MutationObserver(patchAll);
- observer.observe(win.document, { subtree: true, childList: true });
- let patchHandler = {
- apply(target, thisArg, args) {
- let ret = Reflect.apply(target, thisArg, args);
- const wrapped = thisArg && xray.wrap(thisArg);
- if (wrapped) {
- try {
- if ((wrapped.ownerDocument || wrapped) === xrayWin.document) {
- patchAll();
- }
- } catch (e) {
- console.error("Can't propagate patches (likely SOP violation).", e, thisArg, wrapped, location); // DEV_ONLY
- }
- }
- try {
- return ret ? xray.forPage(ret, win) : ret;
- } catch (e) {
- console.error("Can't wrap return value.", e, thisArg, target, args, ret, location); // DEV_ONLY
- }
- return ret;
- }
- };
-
- let domChangers = {
- Element: [
- "set innerHTML", "set outerHTML",
- "after", "append", "appendChild",
- "before",
- "insertAdjacentElement", "insertAdjacentHTML", "insertBefore",
- "prepend",
- "replaceChildren", "replaceWith", "replaceChild",
- "setHTML",
- ],
- Document: [
- "append", "prepend", "replaceChildren",
- "write", "writeln",
- ]
- };
-
- function patch(proto, method) {
- let accessor;
- if (method.startsWith("set ")) {
- accessor = "set";
- method = method.replace("set ", "");
- } else {
- accessor = "value";
- }
- if (!(method in proto)) return;
- while (!proto.hasOwnProperty(method)) {
- proto = Object.getPrototypeOf(proto);
- if (!proto) {
- console.error(`Couldn't find property ${method} on the prototype chain!`);
- return;
- }
- }
- let des = getSafeDescriptor(proto, method, accessor);
- des[accessor] = exportFunction(new Proxy(des[accessor], patchHandler), proto, {defineAs: `${accessor} ${method}`});;
- Reflect.defineProperty(xray.unwrap(proto), method, des);
- }
-
- for (let [obj, methods] of Object.entries(domChangers)) {
- let proto = win[obj].prototype;
- for (let method of methods) {
- patch(proto, method);
- }
- }
- if (patchWindow.onObject) patchWindow.onObject.add(patchAll);
- }
-
- function modifyContentProperties(proto, property) {
- let descriptor = getSafeDescriptor(proto, property, "get");
- let origGetter = descriptor.get;
- let replacements = {
- contentWindow() {
- let win = origGetter.call(this);
- if (win) modifyWindow(win);
- return win;
- },
- contentDocument() {
- let document = origGetter.call(this);
- if (document && document.defaultView) modifyWindow(document.defaultView);
- return document;
- }
- };
-
- descriptor.get = exportFunction(replacements[property], proto, {defineAs: `get ${property}`});
- Reflect.defineProperty(proto, property, descriptor);
- }
-
- modifyWindow(window);
- return port;
-}
-
-patchWindow.xrayEnabled = typeof XPCNativeWrapper !== "undefined";
-if (patchWindow.xrayEnabled) {
- // make up for object element initialization inconsistencies on Firefox
- let callbacks = new Set();
- patchWindow.onObject = {
- add(callback) {
- callbacks.add(callback);
- },
- fire() {
- for (let callback of [...callbacks]) {
- callback();
- }
- }
- };
-
- const eventId = "__nscl_patchWindow_onObject__";
- const intercepted = new WeakSet();
- addEventListener(eventId, e => {
- let {target} = e;
- if (target instanceof HTMLObjectElement &&
- target.contentWindow &&
- !intercepted.has(target.contentWindow)) {
- intercepted.add(target.contentWindow);
- e.stopImmediatePropagation();
- patchWindow.onObject.fire();
- }
- }, true);
-
- if (frameElement instanceof HTMLObjectElement) {
- frameElement.dispatchEvent(new CustomEvent(eventId));
- }
-}
-
-Object.defineProperty(patchWindow, "disabled", {
- get() {
- if (typeof ns === "object" && ns) {
- if (ns.allows && ns.policy) {
- const value = !ns.allows("script");
- Object.defineProperty(patchWindow, "disabled", { value, configurable: true });
- return value;
- }
- if (typeof ns.on === "function") {
- ns.on("capabilities", () => {
- if (ns.allows) {
- this.disabled;
- }
- });
- }
- }
- return false;
- },
- set(value) {
- Object.defineProperty(patchWindow, "disabled", { value, configurable: true });
- return value;
- },
- configurable: true,
-}); \ No newline at end of file