diff --git a/manifest.js b/manifest.js index ae88ff6..a4f0292 100644 --- a/manifest.js +++ b/manifest.js @@ -46,8 +46,9 @@ if (MANIFEST_VER.includes(3)) { /^(?:|webRequestBlocking)$/ .test(p) ); + const excludedScriptsRx = /\bcontent\/(?:embeddingDocument|dirindex)\.js$/; for (const cs of json.content_scripts) { - cs.js = cs.js.filter(js => !js.includes("content/embeddingDocument.js")) + cs.js = cs.js.filter(path => !excludedScriptsRx.test(path)); } delete json.browser_action; delete json.commands._execute_browser_action diff --git a/src/content/ftp.js b/src/content/dirindex.js similarity index 98% rename from src/content/ftp.js rename to src/content/dirindex.js index 6ed1a59..322477a 100644 --- a/src/content/ftp.js +++ b/src/content/dirindex.js @@ -18,7 +18,7 @@ * this program. If not, see . */ -if (UA.isMozilla) (() => { +if (FILE_OR_FTP && UA.isMozilla) (() => { // see https://searchfox.org/mozilla-central/rev/76c1ff5f0de23366fe952ab228610ee695a56e68/netwerk/streamconv/converters/nsIndexedToHTML.cpp#334 'use strict'; var gTable, gOrderBy, gTBody, gRows, gUI_showHidden; diff --git a/src/content/staticNS.js b/src/content/staticNS.js index fcb10eb..2acc83c 100644 --- a/src/content/staticNS.js +++ b/src/content/staticNS.js @@ -18,8 +18,9 @@ * this program. If not, see . */ +'use strict'; +const FILE_OR_FTP = /^(?:file|ftp):$/.test(location.protocol); { - 'use strict'; let listenersMap = new Map(); let backlog = new Set(); @@ -155,7 +156,7 @@ if (!(UA.isMozilla || perms.capabilities.includes("script")) && /^file:\/\/\/(?:[^#?]+\/)?$/.test(document.URL)) { // Allow Chromium browser UI scripts for directory navigation - // (for Firefox we rely on emulation in content/ftp.js). + // (for Firefox we rely on emulation in content/dirindex.js). perms.capabilities.push("script"); } this.capabilities = new Set(perms.capabilities); @@ -177,6 +178,10 @@ globalThis.ns_setupCallBack = ns.domPolicy ? () => {} : ({domPolicy}) => { - + ns.domPolicy = domPolicy; + if (ns.setup) { + if (ns.syncSetup) ns.syncSetup(domPolicy); + else ns.setup(domPolicy); + } }; } diff --git a/src/content/syncFetchPolicy.js b/src/content/syncFetchPolicy.js index 351e7b9..2ed790e 100644 --- a/src/content/syncFetchPolicy.js +++ b/src/content/syncFetchPolicy.js @@ -22,205 +22,207 @@ "use strict"; -(window.ns || (window.ns = {})).syncFetchPolicy = function() { +if (FILE_OR_FTP) { + (globalThis.ns ||= {}).syncFetchPolicy = function() { - ns.pendingSyncFetchPolicy = false; - ns.syncFetchPolicy = () => {}; + ns.pendingSyncFetchPolicy = false; + ns.syncFetchPolicy = () => {}; - let url = document.URL; + 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); + // 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); + 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 document state", document.readyState, document.documentElement, document.head, document.body); // DEV_ONLY - - let mustFreeze = document.head && UA.isMozilla - && (!/^(?:image|video|audio)/.test(document.contentType) || document instanceof XMLDocument) - && document.readyState !== "complete"; - - if (mustFreeze) { - // Mozilla has already parsed the 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) { - queueMicrotask(() => DocumentFreezer.unfreeze()); - 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 { - debug("Soft reload", ev); // DEV_ONLY - try { - 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; - DocRewriter.rewrite(html); - 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.getElementsByTagName("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); - } - } + 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); } }; - - if (DocumentFreezer.firedDOMContentLoaded || document.readyState !== "loading") { - softReload(); - } else { - debug("Deferring softReload to DOMContentLoaded..."); - addEventListener("DOMContentLoaded", softReload, true); + } 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) { - error(e); + } catch (e) { + // cross-origin access violation, ignore + } } + if (ns.domPolicy) { + syncSetup(ns.domPolicy); + return; + } + + debug("Initial document state", document.readyState, document.documentElement, document.head, document.body); // DEV_ONLY + + let mustFreeze = document.head && UA.isMozilla + && (!/^(?:image|video|audio)/.test(document.contentType) || document instanceof XMLDocument) + && document.readyState !== "complete"; + + if (mustFreeze) { + // Mozilla has already parsed the 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) { + queueMicrotask(() => DocumentFreezer.unfreeze()); + 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 { + debug("Soft reload", ev); // DEV_ONLY + try { + 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; + DocRewriter.rewrite(html); + 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.getElementsByTagName("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(); } - - ns.fetchLikeNoTomorrow(url, syncSetup); -}; - -if (ns.pendingSyncFetchPolicy) { - ns.syncFetchPolicy(); } \ No newline at end of file diff --git a/src/manifest.json b/src/manifest.json index 1ca0aa9..a839578 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -116,7 +116,10 @@ "/nscl/content/WebGLHook.js", "/nscl/content/promptHook.js", "content/embeddingDocument.js", - "content/content.js" + "content/content.js", + "content/dirindex.js", + "/nscl/content/DocumentFreezer.js", + "content/syncFetchPolicy.js" ] }, { @@ -134,18 +137,6 @@ "/nscl/main/WebGLHook.main.js", "/nscl/main/prefetchCSSResources.main.js" ] - }, - { - "run_at": "document_start", - "matches": ["file://*/*", "ftp://*/*"], - "match_about_blank": true, - "match_origin_as_fallback": true, - "all_frames": true, - "js": [ - "content/ftp.js", - "/nscl/content/DocumentFreezer.js", - "content/syncFetchPolicy.js" - ] } ],