Restructure inclusion of file/ftp protocol-specific content scripts.

This commit is contained in:
hackademix 2024-11-19 23:16:13 +01:00
parent fd33d7b3ce
commit 06cd1c1e4e
No known key found for this signature in database
GPG Key ID: 231A83AFDA9C2434
5 changed files with 206 additions and 207 deletions

View File

@ -46,8 +46,9 @@ if (MANIFEST_VER.includes(3)) {
/^(?:<all_urls>|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

View File

@ -18,7 +18,7 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
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;

View File

@ -18,8 +18,9 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
'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);
}
};
}

View File

@ -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 <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) {
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 <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) {
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();
}

View File

@ -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"
]
}
],