From 8518443e3d7f1343d7b90f6c7042e99b1d83c616 Mon Sep 17 00:00:00 2001 From: hackademix Date: Thu, 3 Sep 2020 13:19:08 +0200 Subject: [PATCH] Document freezing to handle SVG and other XML documents impervious to CSP on Mozilla. --- build.sh | 4 +- src/content/DocumentCSP.js | 11 +-- src/content/syncFetchPolicy.js | 154 +++++++++++++-------------------- src/lib/DocumentFreezer.js | 95 ++++++++++++++++++++ src/manifest.json | 1 + 5 files changed, 160 insertions(+), 105 deletions(-) create mode 100644 src/lib/DocumentFreezer.js diff --git a/build.sh b/build.sh index 2fa02d8..553f25e 100644 --- a/build.sh +++ b/build.sh @@ -71,8 +71,8 @@ fi if ./html5_events/html5_events.pl; then # update full event list as an array in src/content/syncFetchPolicy.js - EVENTS=$(egrep '^on[a-z]+$' html5_events/html5_events_archive.txt | sed "s/^on//;s/.*/'&'/;H;1h;"'$!d;x;s/\n/, /g'); - perl -pi -e 's/(\blet eventTypes\s*=\s*)\[.*?\]/$1['"$EVENTS"']/' src/content/syncFetchPolicy.js + EVENTS=$(grep '^on[a-z]\+$' html5_events/html5_events_archive.txt | sed "s/^on//;s/.*/'&'/;H;1h;"'$!d;x;s/\n/, /g'); + perl -pi -e 's/(\bconst eventTypes\s*=\s*)\[.*?\]/$1['"$EVENTS"']/' src/lib/DocumentFreezer.js fi rm -rf "$BUILD" "$XPI" diff --git a/src/content/DocumentCSP.js b/src/content/DocumentCSP.js index df354c2..47358f3 100644 --- a/src/content/DocumentCSP.js +++ b/src/content/DocumentCSP.js @@ -3,7 +3,6 @@ class DocumentCSP { constructor(document) { this.document = document; this.builder = new CapsCSP(); - this.root = document.documentElement; } apply(capabilities, embedding = CSP.isEmbedType(this.document.contentType)) { @@ -17,10 +16,7 @@ class DocumentCSP { debug("Fallback beforexecutescript listener blocked ", e.target); }, true); } - if (!(document instanceof HTMLDocument)) { - // this is not HTML, hence we cannot inject a CSP - return false; - } + let csp = this.builder; let blocker = csp.buildFromCapabilities(capabilities, embedding); if (!blocker) return true; @@ -35,10 +31,7 @@ class DocumentCSP { let root = document.documentElement; let {head} = document; - let parent = head || - (root instanceof HTMLElement - ? document.documentElement.appendChild(createHTMLElement("head")) - : root); + let parent = head || document.documentElement.appendChild(createHTMLElement("head")) try { parent.insertBefore(meta, parent.firstElementChild); diff --git a/src/content/syncFetchPolicy.js b/src/content/syncFetchPolicy.js index c8d5435..dca0a85 100644 --- a/src/content/syncFetchPolicy.js +++ b/src/content/syncFetchPolicy.js @@ -18,113 +18,92 @@ if (UA.isMozilla) { // Mozilla has already parsed the element, we must take extra steps... - let softReloading = true; - let suppressedScripts = 0; - debug("Early parsing: preemptively suppressing events and script execution."); - try { - - if (document.body && document.body.onload) { - // special treatment for body[onload], which could not be suppressed otherwise - document.body._onload = document.body.getAttribute("onload"); - document.body.removeAttribute("onload"); - document.body.onload = null; - } - - // List updated by build.sh from https://hg.mozilla.org/mozilla-central/raw-file/tip/xpcom/ds/StaticAtoms.py - // whenever html5_events/html5_events.pl retrieves something new. - let eventTypes = ['abort', 'mozaccesskeynotfound', 'activate', 'afterprint', 'afterscriptexecute', 'animationcancel', 'animationend', 'animationiteration', 'animationstart', 'audioprocess', 'auxclick', 'beforecopy', 'beforecut', 'beforeinput', 'beforepaste', 'beforeprint', 'beforescriptexecute', 'beforeunload', 'blocked', 'blur', 'bounce', 'boundschange', 'broadcast', 'bufferedamountlow', 'cached', 'cancel', 'change', 'chargingchange', 'chargingtimechange', 'checking', 'click', 'close', 'command', 'commandupdate', 'complete', 'compositionend', 'compositionstart', 'compositionupdate', 'connect', 'connectionavailable', 'contextmenu', 'copy', 'cut', 'dblclick', 'dischargingtimechange', 'downloading', 'data', 'drag', 'dragdrop', 'dragend', 'dragenter', 'dragexit', 'dragleave', 'dragover', 'dragstart', 'drain', 'drop', 'error', 'finish', 'focus', 'focusin', 'focusout', 'fullscreenchange', 'fullscreenerror', 'get', 'hashchange', 'input', 'inputsourceschange', 'install', 'invalid', 'keydown', 'keypress', 'keyup', 'languagechange', 'levelchange', 'load', 'loading', 'loadingdone', 'loadingerror', 'popstate', 'merchantvalidation', 'message', 'messageerror', 'midimessage', 'mousedown', 'mouseenter', 'mouseleave', 'mouselongtap', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'mozfullscreenchange', 'mozfullscreenerror', 'mozkeydownonplugin', 'mozkeyuponplugin', 'mozpointerlockchange', 'mozpointerlockerror', 'mute', 'notificationclick', 'notificationclose', 'noupdate', 'obsolete', 'online', 'offline', 'open', 'orientationchange', 'overflow', 'pagehide', 'pageshow', 'paste', 'payerdetailchange', 'paymentmethodchange', 'pointerlockchange', 'pointerlockerror', 'popuphidden', 'popuphiding', 'popuppositioned', 'popupshowing', 'popupshown', 'processorerror', 'push', 'pushsubscriptionchange', 'readystatechange', 'rejectionhandled', 'remove', 'requestprogress', 'resourcetimingbufferfull', 'responseprogress', 'reset', 'resize', 'scroll', 'select', 'selectionchange', 'selectend', 'selectstart', 'set', 'shippingaddresschange', 'shippingoptionchange', 'show', 'squeeze', 'squeezeend', 'squeezestart', 'statechange', 'storage', 'submit', 'success', 'typechange', 'terminate', 'text', 'toggle', 'tonechange', 'touchstart', 'touchend', 'touchmove', 'touchcancel', 'transitioncancel', 'transitionend', 'transitionrun', 'transitionstart', 'underflow', 'unhandledrejection', 'unload', 'unmute', 'updatefound', 'updateready', 'upgradeneeded', 'versionchange', 'visibilitychange', 'voiceschanged', 'vrdisplayactivate', 'vrdisplayconnect', 'vrdisplaydeactivate', 'vrdisplaydisconnect', 'vrdisplaypresentchange', 'webkitanimationend', 'webkitanimationiteration', 'webkitanimationstart', 'webkittransitionend', 'wheel', 'zoom', 'begin', 'end', 'repeat', 'pointerdown', 'pointermove', 'pointerup', 'pointercancel', 'pointerover', 'pointerout', 'pointerenter', 'pointerleave', 'gotpointercapture', 'lostpointercapture', 'devicemotion', 'deviceorientation', 'absolutedeviceorientation', 'deviceproximity', 'mozorientationchange', 'userproximity', 'devicelight', 'devicechange', 'mozvisualresize', 'mozvisualscroll', 'mozshowdropdown', 'scrollend', 'loadend', 'loadstart', 'progress', 'suspend', 'emptied', 'stalled', 'play', 'pause', 'loadedmetadata', 'loadeddata', 'waiting', 'playing', 'canplay', 'canplaythrough', 'seeking', 'seeked', 'timeout', 'timeupdate', 'ended', 'formdata', 'ratechange', 'durationchange', 'volumechange', 'addtrack', 'controllerchange', 'cuechange', 'enter', 'exit', 'encrypted', 'waitingforkey', 'keystatuseschange', 'removetrack', 'dataavailable', 'warning', 'start', 'stop', 'photo', 'gamepadbuttondown', 'gamepadbuttonup', 'gamepadaxismove', 'gamepadconnected', 'gamepaddisconnected', 'fetch', 'audiostart', 'audioend', 'soundstart', 'soundend', 'speechstart', 'speechend', 'result', 'nomatch', 'resume', 'mark', 'boundary', 'activated', 'deactivated', 'metadatachange', 'playbackstatechange', 'positionstatechange', 'supportedkeyschange', 'sourceopen', 'sourceended', 'sourceclosed', 'updatestart', 'update', 'updateend', 'addsourcebuffer', 'removesourcebuffer', 'appinstalled', 'activestatechanged', 'adapteradded', 'adapterremoved', 'alerting', 'antennaavailablechange', 'attributechanged', 'attributereadreq', 'attributewritereq', 'beforeevicted', 'busy', 'callschanged', 'cardstatechange', 'cfstatechange', 'characteristicchanged', 'clirmodechange', 'connected', 'connecting', 'connectionstatechanged', 'currentchannelchanged', 'currentsourcechanged', 'datachange', 'dataerror', 'deleted', 'deliveryerror', 'deliverysuccess', 'devicefound', 'devicepaired', 'deviceunpaired', 'dialing', 'disabled', 'disconnect', 'disconnected', 'disconnecting', 'displaypasskeyreq', 'draggesture', 'eitbroadcasted', 'emergencycbmodechange', 'enabled', 'enterpincodereq', 'evicted', 'failed', 'frequencychange', 'groupchange', 'headphoneschange', 'held', 'hfpstatuschanged', 'hidstatuschanged', 'holding', 'iccchange', 'iccdetected', 'iccinfochange', 'iccundetected', 'incoming', 'mapfolderlistingreq', 'mapgetmessagereq', 'mapmessageslistingreq', 'mapmessageupdatereq', 'mapsendmessagereq', 'mapsetmessagestatusreq', 'mousewheel', 'mozbrowserafterkeydown', 'mozbrowserafterkeyup', 'mozbrowserbeforekeydown', 'mozbrowserbeforekeyup', 'mozinterruptbegin', 'mozinterruptend', 'moznetworkdownload', 'moznetworkupload', 'moztimechange', 'newrdsgroup', 'obexpasswordreq', 'otastatuschange', 'overflowchanged', 'paint', 'pairingaborted', 'pairingconfirmationreq', 'pairingconsentreq', 'pendingchange', 'pichange', 'pschange', 'ptychange', 'pullphonebookreq', 'pullvcardentryreq', 'pullvcardlistingreq', 'radiostatechange', 'rdsdisabled', 'rdsenabled', 'readerror', 'readsuccess', 'ready', 'received', 'reloadpage', 'remoteheld', 'remoteresumed', 'requestmediaplaystatus', 'resuming', 'retrieving', 'rtchange', 'scanningstatechanged', 'scostatuschanged', 'sending', 'sent', 'speakerforcedchange', 'statuschanged', 'stkcommand', 'stksessionend', 'storageareachanged', 'ussdreceived', 'voicechange', 'websocket']; - let eventSuppressor = e => { - try { - debug("Event suppressor called for ", e.type, e.target); // DEV_ONLY - - if (softReloading) { - e.stopPropagation(); - debug(`Suppressing ${e.type} on `, e.target); // DEV_ONLY - } else { - debug("Stopping event suppression"); - for (let et of eventTypes) document.removeEventListener(et, eventSuppressor, true); - } - } catch (e) { - error(e); - } - } - debug("Starting event suppression"); - for (let et of eventTypes) document.addEventListener(et, eventSuppressor, true); + DocumentFreezer.freeze(); ns.on("capabilities", () => { - if (document.body && document.body._onload) { - document.body.setAttribute("onload", document.body._onload); - } let {readyState} = document; - debug("Readystate: %s, %suppressedScripts %s, canScript = %s", readyState, suppressedScripts, ns.canScript); + debug("Readystate: %s, suppressedScripts = %s, canScript = %s", readyState, DocumentFreezer.suppressedScripts, ns.canScript); + // CSP works on HTML documents only on Mozilla: we'll keep frozen elsewhere + let honorsCSP = document instanceof HTMLDocument; + if (!ns.canScript) { - for (let node of document.querySelectorAll("*")) { - let evAttrs = [...node.attributes].filter(a => a.name.toLowerCase().startsWith("on")); - for (let a of evAttrs) { - debug("Reparsing event attribute", a, node); - node.removeAttributeNode(a); - node.setAttributeNodeNS(a); - } + if (honorsCSP) { + DocumentFreezer.unfreeze(); } - softReloading = false; return; } - if (suppressedScripts === 0 && readyState === "loading") { + 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 - softReloading = false; + DocumentFreezer.unfreeze(); return; } let softReload = ev => { - let html = document.documentElement.outerHTML; try { - debug("Soft reload", ev, html); - softReloading = false; + //let html = document.documentElement.outerHTML; + debug("Soft reload", ev); // DEV_ONLY try { let doc = window.wrappedJSObject.document; removeEventListener("DOMContentLoaded", softReload, true); doc.open(); - doc.write(html); - doc.close(); - debug("Written", html) + console.debug("Opened", doc.documentElement); + DocumentFreezer.unfreeze(); + (async () => { + let html = await ((await fetch(document.URL)).text()); + doc.write(html); + doc.close(); + debug("Written", html) + })(); } catch (e) { debug("Can't use document.write(), XML document?"); try { - Promise.all([...document.querySelectorAll("script")].map(s => { - let clone = document.createElement("script"); - for (let a of s.attributes) { - clone.setAttribute(a.name, a.value); - } - clone.textContent = s.textContent; - let doneEvents = ["afterscriptexecute", "load", "error"]; - return 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); + 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(); }); - })).then(r => { - debug("All scripts done", r); - document.dispatchEvent(new Event("readystatechange")); - document.dispatchEvent(new Event("DOMContentLoaded", { - bubbles: true, - cancelable: true - })); - if (document.readyState === "complete") { - window.dispatchEvent(new Event("load")); + } + if (deferred.length) scripts.push(...deferred); + let doneEvents = ["afterscriptexecute", "load", "error"]; + (async () => { + for (let s of scripts) { + let clone = document.createElement("script"); + for (let a of s.attributes) { + clone.setAttribute(a.name, a.value); } - }); + clone.text = s.text; + 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")); + document.dispatchEvent(new Event("DOMContentLoaded", { + bubbles: true, + cancelable: true + })); + if (document.readyState === "complete") { + window.dispatchEvent(new Event("load")); + } + })(); } catch (e) { error(e); } @@ -145,19 +124,6 @@ } catch (e) { error(e); } - - let scriptSuppressor = e => { - if (!e.isTrusted) return; - debug(e.type, e.target, softReloading); // DEV_ONLY - if (softReloading) { - e.preventDefault(); - ++suppressedScripts; - debug(`Suppressed early script #${suppressedScripts}`, e.target); - } else { - removeEventListener(e.type, scriptSuppressor); - } - }; - addEventListener("beforescriptexecute", scriptSuppressor, true); } let setup = policy => { diff --git a/src/lib/DocumentFreezer.js b/src/lib/DocumentFreezer.js new file mode 100644 index 0000000..df5bf81 --- /dev/null +++ b/src/lib/DocumentFreezer.js @@ -0,0 +1,95 @@ +'use strict' +var DocumentFreezer = (() => { + + const loaderAttributes = ["href", "src", "data"]; + const jsOrDataUrlRx = /^(?:data:(?:[^,;]*ml|unknown-content-type)|javascript:)/i; + + // List updated by build.sh from https://hg.mozilla.org/mozilla-central/raw-file/tip/xpcom/ds/StaticAtoms.py + // whenever html5_events/html5_events.pl retrieves something new. + const eventTypes = ['abort', 'mozaccesskeynotfound', 'activate', 'afterprint', 'afterscriptexecute', 'animationcancel', 'animationend', 'animationiteration', 'animationstart', 'audioprocess', 'auxclick', 'beforecopy', 'beforecut', 'beforeinput', 'beforepaste', 'beforeprint', 'beforescriptexecute', 'beforeunload', 'blocked', 'blur', 'bounce', 'boundschange', 'broadcast', 'bufferedamountlow', 'cached', 'cancel', 'change', 'chargingchange', 'chargingtimechange', 'checking', 'click', 'close', 'command', 'commandupdate', 'complete', 'compositionend', 'compositionstart', 'compositionupdate', 'connect', 'connectionavailable', 'contextmenu', 'copy', 'cut', 'dblclick', 'dischargingtimechange', 'downloading', 'data', 'drag', 'dragdrop', 'dragend', 'dragenter', 'dragexit', 'dragleave', 'dragover', 'dragstart', 'drain', 'drop', 'error', 'finish', 'focus', 'focusin', 'focusout', 'fullscreenchange', 'fullscreenerror', 'get', 'hashchange', 'input', 'inputsourceschange', 'install', 'invalid', 'keydown', 'keypress', 'keyup', 'languagechange', 'levelchange', 'load', 'loading', 'loadingdone', 'loadingerror', 'popstate', 'merchantvalidation', 'message', 'messageerror', 'midimessage', 'mousedown', 'mouseenter', 'mouseleave', 'mouselongtap', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'mozfullscreenchange', 'mozfullscreenerror', 'mozkeydownonplugin', 'mozkeyuponplugin', 'mozpointerlockchange', 'mozpointerlockerror', 'mute', 'notificationclick', 'notificationclose', 'noupdate', 'obsolete', 'online', 'offline', 'open', 'orientationchange', 'overflow', 'pagehide', 'pageshow', 'paste', 'payerdetailchange', 'paymentmethodchange', 'pointerlockchange', 'pointerlockerror', 'popuphidden', 'popuphiding', 'popuppositioned', 'popupshowing', 'popupshown', 'processorerror', 'push', 'pushsubscriptionchange', 'readystatechange', 'rejectionhandled', 'remove', 'requestprogress', 'resourcetimingbufferfull', 'responseprogress', 'reset', 'resize', 'scroll', 'select', 'selectionchange', 'selectend', 'selectstart', 'set', 'shippingaddresschange', 'shippingoptionchange', 'show', 'squeeze', 'squeezeend', 'squeezestart', 'statechange', 'storage', 'submit', 'success', 'typechange', 'terminate', 'text', 'toggle', 'tonechange', 'touchstart', 'touchend', 'touchmove', 'touchcancel', 'transitioncancel', 'transitionend', 'transitionrun', 'transitionstart', 'underflow', 'unhandledrejection', 'unload', 'unmute', 'updatefound', 'updateready', 'upgradeneeded', 'versionchange', 'visibilitychange', 'voiceschanged', 'vrdisplayactivate', 'vrdisplayconnect', 'vrdisplaydeactivate', 'vrdisplaydisconnect', 'vrdisplaypresentchange', 'webkitanimationend', 'webkitanimationiteration', 'webkitanimationstart', 'webkittransitionend', 'wheel', 'zoom', 'begin', 'end', 'repeat', 'pointerdown', 'pointermove', 'pointerup', 'pointercancel', 'pointerover', 'pointerout', 'pointerenter', 'pointerleave', 'gotpointercapture', 'lostpointercapture', 'devicemotion', 'deviceorientation', 'absolutedeviceorientation', 'deviceproximity', 'mozorientationchange', 'userproximity', 'devicelight', 'devicechange', 'mozvisualresize', 'mozvisualscroll', 'mozshowdropdown', 'scrollend', 'loadend', 'loadstart', 'progress', 'suspend', 'emptied', 'stalled', 'play', 'pause', 'loadedmetadata', 'loadeddata', 'waiting', 'playing', 'canplay', 'canplaythrough', 'seeking', 'seeked', 'timeout', 'timeupdate', 'ended', 'formdata', 'ratechange', 'durationchange', 'volumechange', 'addtrack', 'controllerchange', 'cuechange', 'enter', 'exit', 'encrypted', 'waitingforkey', 'keystatuseschange', 'removetrack', 'dataavailable', 'warning', 'start', 'stop', 'photo', 'gamepadbuttondown', 'gamepadbuttonup', 'gamepadaxismove', 'gamepadconnected', 'gamepaddisconnected', 'fetch', 'audiostart', 'audioend', 'soundstart', 'soundend', 'speechstart', 'speechend', 'result', 'nomatch', 'resume', 'mark', 'boundary', 'activated', 'deactivated', 'metadatachange', 'playbackstatechange', 'positionstatechange', 'supportedkeyschange', 'sourceopen', 'sourceended', 'sourceclosed', 'updatestart', 'update', 'updateend', 'addsourcebuffer', 'removesourcebuffer', 'appinstalled', 'activestatechanged', 'adapteradded', 'adapterremoved', 'alerting', 'antennaavailablechange', 'attributechanged', 'attributereadreq', 'attributewritereq', 'beforeevicted', 'busy', 'callschanged', 'cardstatechange', 'cfstatechange', 'characteristicchanged', 'clirmodechange', 'connected', 'connecting', 'connectionstatechanged', 'currentchannelchanged', 'currentsourcechanged', 'datachange', 'dataerror', 'deleted', 'deliveryerror', 'deliverysuccess', 'devicefound', 'devicepaired', 'deviceunpaired', 'dialing', 'disabled', 'disconnect', 'disconnected', 'disconnecting', 'displaypasskeyreq', 'draggesture', 'eitbroadcasted', 'emergencycbmodechange', 'enabled', 'enterpincodereq', 'evicted', 'failed', 'frequencychange', 'groupchange', 'headphoneschange', 'held', 'hfpstatuschanged', 'hidstatuschanged', 'holding', 'iccchange', 'iccdetected', 'iccinfochange', 'iccundetected', 'incoming', 'mapfolderlistingreq', 'mapgetmessagereq', 'mapmessageslistingreq', 'mapmessageupdatereq', 'mapsendmessagereq', 'mapsetmessagestatusreq', 'mousewheel', 'mozbrowserafterkeydown', 'mozbrowserafterkeyup', 'mozbrowserbeforekeydown', 'mozbrowserbeforekeyup', 'mozinterruptbegin', 'mozinterruptend', 'moznetworkdownload', 'moznetworkupload', 'moztimechange', 'newrdsgroup', 'obexpasswordreq', 'otastatuschange', 'overflowchanged', 'paint', 'pairingaborted', 'pairingconfirmationreq', 'pairingconsentreq', 'pendingchange', 'pichange', 'pschange', 'ptychange', 'pullphonebookreq', 'pullvcardentryreq', 'pullvcardlistingreq', 'radiostatechange', 'rdsdisabled', 'rdsenabled', 'readerror', 'readsuccess', 'ready', 'received', 'reloadpage', 'remoteheld', 'remoteresumed', 'requestmediaplaystatus', 'resuming', 'retrieving', 'rtchange', 'scanningstatechanged', 'scostatuschanged', 'sending', 'sent', 'speakerforcedchange', 'statuschanged', 'stkcommand', 'stksessionend', 'storageareachanged', 'ussdreceived', 'voicechange', 'websocket']; + + function suppressEvents(e) { + e.stopPropagation(); + console.debug(`Suppressing ${e.type} on `, e.target); // DEV_ONLY + } + + function freezeAttributes() { + try { + for (let element of document.querySelectorAll("*")) { + if (element._frozenAttributes) continue; + let fa = []; + let loaders = []; + for (let a of element.attributes) { + let name = a.localName.toLowerCase(); + if (loaderAttributes.includes(name)) { + if (jsOrDataUrlRx.test(a.value)) { + loaders.push(a); + } + } else if (name.startsWith("on")) { + console.debug("Removing", a, element.outerHTML); + fa.push(a.cloneNode()); + a.value = ""; + element[name] = null; + } + } + if (loaders.length) { + for (let a of loaders) { + fa.push(a.cloneNode()); + a.value = "javascript://frozen"; + } + if ("contentWindow" in element) { + element.replaceWith(element = element.cloneNode(true)); + } + } + element._frozenAttributes = fa; + } + } catch(e) { console.error(e )} + } + + function unfreezeAttributes() { + for (let element of document.querySelectorAll("*")) { + if (!element._frozenAttributes) continue; + for (let a of element._frozenAttributes) { + element.setAttributeNodeNS(a); + } + } + } + + let domFreezer = new MutationObserver(records => { + console.debug("domFreezer on", document.documentElement.outerHTML); + freezeAttributes(); + }); + + let suppressedScripts = 0; + let scriptSuppressor = e => { + if (!e.isTrusted) return; + e.preventDefault(); + ++suppressedScripts; + console.debug(`Suppressed script #${suppressedScripts}`, e.target); + }; + + return { + freeze() { + if (document._frozen) return false; + console.debug("Freezing", document.URL); + document._frozen = true; + for (let et of eventTypes) document.addEventListener(et, suppressEvents, true); + freezeAttributes(); + domFreezer.observe(document, {childList: true, subtree: true}); + suppressedScripts = 0; + addEventListener("beforescriptexecute", scriptSuppressor, true); + return true; + }, + unfreeze() { + if (!document._frozen) return false; + console.debug("Unfreezing", document.URL); + domFreezer.disconnect(); + unfreezeAttributes(); + removeEventListener("beforescriptexecute", scriptSuppressor, true); + for (let et of eventTypes) document.removeEventListener(et, suppressEvents, true); + document._frozen = false; + return true; + }, + get suppressedScripts() { return suppressedScripts; }, + }; +})() \ No newline at end of file diff --git a/src/manifest.json b/src/manifest.json index 97ca6ee..67864be 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -77,6 +77,7 @@ "run_at": "document_start", "matches": ["file://*/*", "ftp://*/*"], "js": [ + "lib/DocumentFreezer.js", "content/syncFetchPolicy.js" ] },