229 lines
No EOL
8.4 KiB
JavaScript
229 lines
No EOL
8.4 KiB
JavaScript
/*
|
|
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
|
|
*
|
|
* Copyright (C) 2005-2021 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/>.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
(window.ns || (window.ns = {})).syncFetchPolicy = function() {
|
|
|
|
ns.pendingSyncFetchPolicy = false;
|
|
ns.syncFetchPolicy = () => {};
|
|
|
|
let url = document.URL;
|
|
|
|
// Here we've got no CSP header yet (file: or ftp: URL), we need one
|
|
// injected in the DOM as soon as possible.
|
|
debug("No CSP yet for non-HTTP document load: fetching policy synchronously...", ns);
|
|
|
|
let syncSetup = ns.setup.bind(ns);
|
|
|
|
if (window.wrappedJSObject) {
|
|
if (top === window) {
|
|
let persistentPolicy = null;
|
|
syncSetup = policy => {
|
|
if (persistentPolicy) return;
|
|
ns.setup(policy);
|
|
persistentPolicy = JSON.stringify(policy);
|
|
Object.freeze(persistentPolicy);
|
|
try {
|
|
Object.defineProperty(window.wrappedJSObject, "_noScriptPolicy", {value: cloneInto(persistentPolicy, window)});
|
|
} catch(e) {
|
|
error(e);
|
|
}
|
|
};
|
|
} else try {
|
|
if (top.wrappedJSObject._noScriptPolicy) {
|
|
debug("Policy set in parent frame found!")
|
|
try {
|
|
ns.setup(JSON.parse(top.wrappedJSObject._noScriptPolicy));
|
|
return;
|
|
} catch(e) {
|
|
error(e);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// cross-origin access violation, ignore
|
|
}
|
|
}
|
|
if (ns.domPolicy) {
|
|
syncSetup(ns.domPolicy);
|
|
return;
|
|
}
|
|
|
|
debug("Initial readyState and body", document.readyState, document.body);
|
|
|
|
let mustFreeze = UA.isMozilla
|
|
&& (!/^(?:image|video|audio)/.test(document.contentType) || document instanceof XMLDocument)
|
|
&& document.readyState !== "complete";
|
|
|
|
if (mustFreeze) {
|
|
// Mozilla has already parsed the <head> element, we must take extra steps...
|
|
|
|
try {
|
|
DocumentFreezer.freeze();
|
|
|
|
ns.on("capabilities", () => {
|
|
|
|
let {readyState} = document;
|
|
|
|
debug("Readystate: %s, suppressedScripts = %s, canScript = %s", readyState, DocumentFreezer.suppressedScripts, ns.canScript);
|
|
|
|
if (!ns.canScript) {
|
|
setTimeout(() => DocumentFreezer.unfreeze(), 0);
|
|
let normalizeDir = e => {
|
|
// Chromium does this automatically. We need it to understand we're a directory earlier and allow browser UI scripts.
|
|
if (document.baseURI === document.URL + "/") {
|
|
if (e) {
|
|
document.removeEventListener(e.type, normalizeDir);
|
|
e.stopImmediatePropagation();
|
|
}
|
|
window.stop();
|
|
location.replace(document.baseURI);
|
|
}
|
|
}
|
|
if (DocumentFreezer.firedDOMContentLoaded) {
|
|
normalizeDir();
|
|
} else {
|
|
document.addEventListener("readystatechange", normalizeDir);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (DocumentFreezer.suppressedScripts === 0 && readyState === "loading") {
|
|
// we don't care reloading, if no script has been suppressed
|
|
// and no readyState change has been fired yet
|
|
DocumentFreezer.unfreeze();
|
|
return;
|
|
}
|
|
|
|
let softReload = ev => {
|
|
removeEventListener("DOMContentLoaded", softReload, true);
|
|
try {
|
|
|
|
try {
|
|
let doc = window.wrappedJSObject.document;
|
|
let isDir = document.querySelector("link[rel=stylesheet][href^='chrome:']")
|
|
&& document.querySelector(`base[href^="${url}"]`);
|
|
if (isDir || document.contentType !== "text/html") {
|
|
throw new Error(`Can't document.write() on ${isDir ? "directory listings" : document.contentType}`)
|
|
}
|
|
|
|
DocumentFreezer.unfreeze();
|
|
|
|
let html = document.documentElement.outerHTML;
|
|
let sx = window.scrollX, sy = window.scrollY;
|
|
doc.open();
|
|
console.debug("Opened", doc.documentElement);
|
|
doc.write(html);
|
|
doc.close();
|
|
debug("Written", html);
|
|
// Work-around this rendering bug: https://forums.informaction.com/viewtopic.php?p=103105#p103050
|
|
debug("Scrolling back to", sx, sy);
|
|
window.scrollTo(sx, sy);
|
|
} catch (e) {
|
|
debug("Can't use document.write(), XML document?", e);
|
|
try {
|
|
let eventSuppressor = ev => {
|
|
if (ev.isTrusted) {
|
|
debug("Suppressing natural event", ev);
|
|
ev.preventDefault();
|
|
ev.stopImmediatePropagation();
|
|
ev.currentTarget.removeEventListener(ev.type, eventSuppressor, true);
|
|
}
|
|
};
|
|
let svg = document.documentElement instanceof SVGElement;
|
|
if (svg) {
|
|
document.addEventListener("SVGLoad", eventSuppressor, true);
|
|
}
|
|
document.addEventListener("DOMContentLoaded", eventSuppressor, true);
|
|
if (ev) eventSuppressor(ev);
|
|
DocumentFreezer.unfreeze();
|
|
let scripts = [], deferred = [];
|
|
// push deferred scripts, if any, to the end
|
|
for (let s of [...document.querySelectorAll("script")]) {
|
|
(s.defer && !s.text ? deferred : scripts).push(s);
|
|
s.addEventListener("beforescriptexecute", e => {
|
|
console.debug("Suppressing", script);
|
|
e.preventDefault();
|
|
});
|
|
}
|
|
if (deferred.length) scripts.push(...deferred);
|
|
let doneEvents = ["afterscriptexecute", "load", "error"];
|
|
(async () => {
|
|
for (let s of scripts) {
|
|
let clone = document.createElementNS(s.namespaceURI, "script");
|
|
for (let a of s.attributes) {
|
|
clone.setAttributeNS(a.namespaceURI, a.name, a.value);
|
|
}
|
|
clone.innerHTML = s.innerHTML;
|
|
await new Promise(resolve => {
|
|
let listener = ev => {
|
|
if (ev.target !== clone) return;
|
|
debug("Resolving on ", ev.type, ev.target);
|
|
resolve(ev.target);
|
|
for (let et of doneEvents) removeEventListener(et, listener, true);
|
|
};
|
|
for (let et of doneEvents) {
|
|
addEventListener(et, listener, true);
|
|
}
|
|
s.replaceWith(clone);
|
|
debug("Replaced", clone);
|
|
});
|
|
}
|
|
debug("All scripts done, firing completion events.");
|
|
document.dispatchEvent(new Event("readystatechange"));
|
|
if (svg) {
|
|
document.documentElement.dispatchEvent(new Event("SVGLoad"));
|
|
}
|
|
document.dispatchEvent(new Event("DOMContentLoaded", {
|
|
bubbles: true,
|
|
cancelable: false
|
|
}));
|
|
if (document.readyState === "complete") {
|
|
window.dispatchEvent(new Event("load"));
|
|
}
|
|
})();
|
|
} catch (e) {
|
|
error(e);
|
|
}
|
|
}
|
|
} catch(e) {
|
|
error(e);
|
|
}
|
|
};
|
|
|
|
if (DocumentFreezer.firedDOMContentLoaded || document.readyState !== "loading") {
|
|
softReload();
|
|
} else {
|
|
debug("Deferring softReload to DOMContentLoaded...");
|
|
addEventListener("DOMContentLoaded", softReload, true);
|
|
}
|
|
|
|
});
|
|
} catch (e) {
|
|
error(e);
|
|
}
|
|
}
|
|
|
|
ns.fetchLikeNoTomorrow(url, syncSetup);
|
|
};
|
|
|
|
if (ns.pendingSyncFetchPolicy) {
|
|
ns.syncFetchPolicy();
|
|
} |