summaryrefslogtreecommitdiffstats
path: root/src/js/scriptlets
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 05:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 05:47:55 +0000
commit31d6ff6f931696850c348007241195ab3b2eddc7 (patch)
tree615cb1c57ce9f6611bad93326b9105098f379609 /src/js/scriptlets
parentInitial commit. (diff)
downloadublock-origin-31d6ff6f931696850c348007241195ab3b2eddc7.tar.xz
ublock-origin-31d6ff6f931696850c348007241195ab3b2eddc7.zip
Adding upstream version 1.55.0+dfsg.upstream/1.55.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/js/scriptlets')
-rw-r--r--src/js/scriptlets/cosmetic-logger.js365
-rw-r--r--src/js/scriptlets/cosmetic-off.js48
-rw-r--r--src/js/scriptlets/cosmetic-on.js48
-rw-r--r--src/js/scriptlets/cosmetic-report.js142
-rw-r--r--src/js/scriptlets/dom-inspector.js924
-rw-r--r--src/js/scriptlets/dom-survey-elements.js72
-rw-r--r--src/js/scriptlets/dom-survey-scripts.js126
-rw-r--r--src/js/scriptlets/epicker.js1356
-rw-r--r--src/js/scriptlets/load-3p-css.js67
-rw-r--r--src/js/scriptlets/load-large-media-all.js62
-rw-r--r--src/js/scriptlets/load-large-media-interactive.js299
-rw-r--r--src/js/scriptlets/noscript-spoof.js89
-rw-r--r--src/js/scriptlets/should-inject-contentscript.js40
-rw-r--r--src/js/scriptlets/subscriber.js113
-rw-r--r--src/js/scriptlets/updater.js118
15 files changed, 3869 insertions, 0 deletions
diff --git a/src/js/scriptlets/cosmetic-logger.js b/src/js/scriptlets/cosmetic-logger.js
new file mode 100644
index 0000000..5d1f1b9
--- /dev/null
+++ b/src/js/scriptlets/cosmetic-logger.js
@@ -0,0 +1,365 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2015-present Raymond Hill
+
+ 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 {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+/* globals browser */
+
+'use strict';
+
+/******************************************************************************/
+
+(( ) => {
+// >>>>>>>> start of private namespace
+
+/******************************************************************************/
+
+if ( typeof vAPI !== 'object' ) { return; }
+if ( vAPI.domWatcher instanceof Object === false ) { return; }
+
+const reHasCSSCombinators = /[ >+~]/;
+const simpleDeclarativeSet = new Set();
+let simpleDeclarativeStr;
+const complexDeclarativeSet = new Set();
+let complexDeclarativeStr;
+const proceduralDict = new Map();
+const exceptionDict = new Map();
+let exceptionStr;
+const proceduralExceptionDict = new Map();
+const nodesToProcess = new Set();
+const loggedSelectors = new Set();
+
+/******************************************************************************/
+
+const rePseudoElements = /:(?::?after|:?before|:[a-z-]+)$/;
+
+function hasSelector(selector, context = document) {
+ try {
+ return context.querySelector(selector) !== null;
+ }
+ catch(ex) {
+ }
+ return false;
+}
+
+function safeMatchSelector(selector, context) {
+ const safeSelector = rePseudoElements.test(selector)
+ ? selector.replace(rePseudoElements, '')
+ : selector;
+ try {
+ return context.matches(safeSelector);
+ }
+ catch(ex) {
+ }
+ return false;
+}
+
+function safeQuerySelector(selector, context = document) {
+ const safeSelector = rePseudoElements.test(selector)
+ ? selector.replace(rePseudoElements, '')
+ : selector;
+ try {
+ return context.querySelector(safeSelector);
+ }
+ catch(ex) {
+ }
+ return null;
+}
+
+function safeGroupSelectors(selectors) {
+ const arr = Array.isArray(selectors)
+ ? selectors
+ : Array.from(selectors);
+ return arr.map(s => {
+ return rePseudoElements.test(s)
+ ? s.replace(rePseudoElements, '')
+ : s;
+ }).join(',\n');
+}
+
+/******************************************************************************/
+
+function processDeclarativeSimple(node, out) {
+ if ( simpleDeclarativeSet.size === 0 ) { return; }
+ if ( simpleDeclarativeStr === undefined ) {
+ simpleDeclarativeStr = safeGroupSelectors(simpleDeclarativeSet);
+ }
+ if (
+ (node === document || node.matches(simpleDeclarativeStr) === false) &&
+ (hasSelector(simpleDeclarativeStr, node) === false)
+ ) {
+ return;
+ }
+ for ( const selector of simpleDeclarativeSet ) {
+ if (
+ (node === document || safeMatchSelector(selector, node) === false) &&
+ (safeQuerySelector(selector, node) === null)
+ ) {
+ continue;
+ }
+ out.push(`##${selector}`);
+ simpleDeclarativeSet.delete(selector);
+ simpleDeclarativeStr = undefined;
+ loggedSelectors.add(selector);
+ }
+}
+
+/******************************************************************************/
+
+function processDeclarativeComplex(out) {
+ if ( complexDeclarativeSet.size === 0 ) { return; }
+ if ( complexDeclarativeStr === undefined ) {
+ complexDeclarativeStr = safeGroupSelectors(complexDeclarativeSet);
+ }
+ if ( hasSelector(complexDeclarativeStr) === false ) { return; }
+ for ( const selector of complexDeclarativeSet ) {
+ if ( safeQuerySelector(selector) === null ) { continue; }
+ out.push(`##${selector}`);
+ complexDeclarativeSet.delete(selector);
+ complexDeclarativeStr = undefined;
+ loggedSelectors.add(selector);
+ }
+}
+
+/******************************************************************************/
+
+function processProcedural(out) {
+ if ( proceduralDict.size === 0 ) { return; }
+ for ( const [ raw, pselector ] of proceduralDict ) {
+ if ( pselector.converted ) {
+ if ( safeQuerySelector(pselector.selector) === null ) { continue; }
+ } else if ( pselector.hit === false && pselector.exec().length === 0 ) {
+ continue;
+ }
+ out.push(`##${raw}`);
+ proceduralDict.delete(raw);
+ }
+}
+
+/******************************************************************************/
+
+function processExceptions(out) {
+ if ( exceptionDict.size === 0 ) { return; }
+ if ( exceptionStr === undefined ) {
+ exceptionStr = safeGroupSelectors(exceptionDict.keys());
+ }
+ if ( hasSelector(exceptionStr) === false ) { return; }
+ for ( const [ selector, raw ] of exceptionDict ) {
+ if ( safeQuerySelector(selector) === null ) { continue; }
+ out.push(`#@#${raw}`);
+ exceptionDict.delete(selector);
+ exceptionStr = undefined;
+ loggedSelectors.add(raw);
+ }
+}
+
+/******************************************************************************/
+
+function processProceduralExceptions(out) {
+ if ( proceduralExceptionDict.size === 0 ) { return; }
+ for ( const exception of proceduralExceptionDict.values() ) {
+ if ( exception.test() === false ) { continue; }
+ out.push(`#@#${exception.raw}`);
+ proceduralExceptionDict.delete(exception.raw);
+ }
+}
+
+/******************************************************************************/
+
+const processTimer = new vAPI.SafeAnimationFrame(( ) => {
+ //console.time('dom logger/scanning for matches');
+ processTimer.clear();
+ if ( nodesToProcess.size === 0 ) { return; }
+
+ if ( nodesToProcess.size !== 1 && nodesToProcess.has(document) ) {
+ nodesToProcess.clear();
+ nodesToProcess.add(document);
+ }
+
+ const toLog = [];
+ if ( simpleDeclarativeSet.size !== 0 ) {
+ for ( const node of nodesToProcess ) {
+ processDeclarativeSimple(node, toLog);
+ }
+ }
+
+ processDeclarativeComplex(toLog);
+ processProcedural(toLog);
+ processExceptions(toLog);
+ processProceduralExceptions(toLog);
+
+ nodesToProcess.clear();
+
+ if ( toLog.length === 0 ) { return; }
+
+ const location = vAPI.effectiveSelf.location;
+
+ vAPI.messaging.send('scriptlets', {
+ what: 'logCosmeticFilteringData',
+ frameURL: location.href,
+ frameHostname: location.hostname,
+ matchedSelectors: toLog,
+ });
+ //console.timeEnd('dom logger/scanning for matches');
+});
+
+/******************************************************************************/
+
+const attributeObserver = new MutationObserver(mutations => {
+ if ( nodesToProcess.has(document) ) { return; }
+ for ( const mutation of mutations ) {
+ const node = mutation.target;
+ if ( node.nodeType !== 1 ) { continue; }
+ nodesToProcess.add(node);
+ }
+ if ( nodesToProcess.size !== 0 ) {
+ processTimer.start(100);
+ }
+});
+
+/******************************************************************************/
+
+const handlers = {
+ onFiltersetChanged: function(changes) {
+ //console.time('dom logger/filterset changed');
+ for ( const block of (changes.declarative || []) ) {
+ for ( const selector of block.split(',\n') ) {
+ if ( loggedSelectors.has(selector) ) { continue; }
+ if ( reHasCSSCombinators.test(selector) ) {
+ complexDeclarativeSet.add(selector);
+ complexDeclarativeStr = undefined;
+ } else {
+ simpleDeclarativeSet.add(selector);
+ simpleDeclarativeStr = undefined;
+ }
+ }
+ }
+ if (
+ Array.isArray(changes.procedural) &&
+ changes.procedural.length !== 0
+ ) {
+ for ( const selector of changes.procedural ) {
+ proceduralDict.set(selector.raw, selector);
+ }
+ }
+ if ( Array.isArray(changes.exceptions) ) {
+ for ( const selector of changes.exceptions ) {
+ if ( loggedSelectors.has(selector) ) { continue; }
+ if ( selector.charCodeAt(0) !== 0x7B /* '{' */ ) {
+ exceptionDict.set(selector, selector);
+ continue;
+ }
+ const details = JSON.parse(selector);
+ if (
+ details.action !== undefined &&
+ details.tasks === undefined &&
+ details.action[0] === 'style'
+ ) {
+ exceptionDict.set(details.selector, details.raw);
+ continue;
+ }
+ proceduralExceptionDict.set(
+ details.raw,
+ vAPI.domFilterer.createProceduralFilter(details)
+ );
+ }
+ exceptionStr = undefined;
+ }
+ nodesToProcess.clear();
+ nodesToProcess.add(document);
+ processTimer.start(1);
+ //console.timeEnd('dom logger/filterset changed');
+ },
+
+ onDOMCreated: function() {
+ if ( vAPI.domFilterer instanceof Object === false ) {
+ return shutdown();
+ }
+ handlers.onFiltersetChanged(vAPI.domFilterer.getAllSelectors());
+ vAPI.domFilterer.addListener(handlers);
+ attributeObserver.observe(document.body, {
+ attributes: true,
+ subtree: true
+ });
+ },
+
+ onDOMChanged: function(addedNodes) {
+ if ( nodesToProcess.has(document) ) { return; }
+ for ( const node of addedNodes ) {
+ if ( node.parentNode === null ) { continue; }
+ nodesToProcess.add(node);
+ }
+ if ( nodesToProcess.size !== 0 ) {
+ processTimer.start(100);
+ }
+ }
+};
+
+vAPI.domWatcher.addListener(handlers);
+
+/******************************************************************************/
+
+const broadcastHandler = msg => {
+ if ( msg.what === 'loggerDisabled' ) {
+ shutdown();
+ }
+};
+
+browser.runtime.onMessage.addListener(broadcastHandler);
+
+/******************************************************************************/
+
+function shutdown() {
+ browser.runtime.onMessage.removeListener(broadcastHandler);
+ processTimer.clear();
+ attributeObserver.disconnect();
+ if ( typeof vAPI !== 'object' ) { return; }
+ if ( vAPI.domFilterer instanceof Object ) {
+ vAPI.domFilterer.removeListener(handlers);
+ }
+ if ( vAPI.domWatcher instanceof Object ) {
+ vAPI.domWatcher.removeListener(handlers);
+ }
+}
+
+/******************************************************************************/
+
+// <<<<<<<< end of private namespace
+})();
+
+
+
+
+
+
+
+
+/*******************************************************************************
+
+ DO NOT:
+ - Remove the following code
+ - Add code beyond the following code
+ Reason:
+ - https://github.com/gorhill/uBlock/pull/3721
+ - uBO never uses the return value from injected content scripts
+
+**/
+
+void 0;
+
diff --git a/src/js/scriptlets/cosmetic-off.js b/src/js/scriptlets/cosmetic-off.js
new file mode 100644
index 0000000..f1301e2
--- /dev/null
+++ b/src/js/scriptlets/cosmetic-off.js
@@ -0,0 +1,48 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2015-2018 Raymond Hill
+
+ 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 {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+'use strict';
+
+/******************************************************************************/
+
+if ( typeof vAPI === 'object' && vAPI.domFilterer ) {
+ vAPI.domFilterer.toggle(false);
+}
+
+
+
+
+
+
+
+
+/*******************************************************************************
+
+ DO NOT:
+ - Remove the following code
+ - Add code beyond the following code
+ Reason:
+ - https://github.com/gorhill/uBlock/pull/3721
+ - uBO never uses the return value from injected content scripts
+
+**/
+
+void 0;
diff --git a/src/js/scriptlets/cosmetic-on.js b/src/js/scriptlets/cosmetic-on.js
new file mode 100644
index 0000000..7b30976
--- /dev/null
+++ b/src/js/scriptlets/cosmetic-on.js
@@ -0,0 +1,48 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2015-2018 Raymond Hill
+
+ 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 {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+'use strict';
+
+/******************************************************************************/
+
+if ( typeof vAPI === 'object' && vAPI.domFilterer ) {
+ vAPI.domFilterer.toggle(true);
+}
+
+
+
+
+
+
+
+
+/*******************************************************************************
+
+ DO NOT:
+ - Remove the following code
+ - Add code beyond the following code
+ Reason:
+ - https://github.com/gorhill/uBlock/pull/3721
+ - uBO never uses the return value from injected content scripts
+
+**/
+
+void 0;
diff --git a/src/js/scriptlets/cosmetic-report.js b/src/js/scriptlets/cosmetic-report.js
new file mode 100644
index 0000000..a968d4d
--- /dev/null
+++ b/src/js/scriptlets/cosmetic-report.js
@@ -0,0 +1,142 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2015-present Raymond Hill
+
+ 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 {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+'use strict';
+
+/******************************************************************************/
+
+(( ) => {
+// >>>>>>>> start of private namespace
+
+/******************************************************************************/
+
+if ( typeof vAPI !== 'object' ) { return; }
+if ( typeof vAPI.domFilterer !== 'object' ) { return; }
+if ( vAPI.domFilterer === null ) { return; }
+
+/******************************************************************************/
+
+const rePseudoElements = /:(?::?after|:?before|:[a-z-]+)$/;
+
+const hasSelector = selector => {
+ try {
+ return document.querySelector(selector) !== null;
+ }
+ catch(ex) {
+ }
+ return false;
+};
+
+const safeQuerySelector = selector => {
+ const safeSelector = rePseudoElements.test(selector)
+ ? selector.replace(rePseudoElements, '')
+ : selector;
+ try {
+ return document.querySelector(safeSelector);
+ }
+ catch(ex) {
+ }
+ return null;
+};
+
+const safeGroupSelectors = selectors => {
+ const arr = Array.isArray(selectors)
+ ? selectors
+ : Array.from(selectors);
+ return arr.map(s => {
+ return rePseudoElements.test(s)
+ ? s.replace(rePseudoElements, '')
+ : s;
+ }).join(',\n');
+};
+
+const allSelectors = vAPI.domFilterer.getAllSelectors();
+const matchedSelectors = [];
+
+if ( Array.isArray(allSelectors.declarative) ) {
+ const declarativeSet = new Set();
+ for ( const block of allSelectors.declarative ) {
+ for ( const selector of block.split(',\n') ) {
+ declarativeSet.add(selector);
+ }
+ }
+ if ( hasSelector(safeGroupSelectors(declarativeSet)) ) {
+ for ( const selector of declarativeSet ) {
+ if ( safeQuerySelector(selector) === null ) { continue; }
+ matchedSelectors.push(`##${selector}`);
+ }
+ }
+}
+
+if (
+ Array.isArray(allSelectors.procedural) &&
+ allSelectors.procedural.length !== 0
+) {
+ for ( const pselector of allSelectors.procedural ) {
+ if ( pselector.hit === false && pselector.exec().length === 0 ) { continue; }
+ matchedSelectors.push(`##${pselector.raw}`);
+ }
+}
+
+if ( Array.isArray(allSelectors.exceptions) ) {
+ const exceptionDict = new Map();
+ for ( const selector of allSelectors.exceptions ) {
+ if ( selector.charCodeAt(0) !== 0x7B /* '{' */ ) {
+ exceptionDict.set(selector, selector);
+ continue;
+ }
+ const details = JSON.parse(selector);
+ if (
+ details.action !== undefined &&
+ details.tasks === undefined &&
+ details.action[0] === 'style'
+ ) {
+ exceptionDict.set(details.selector, details.raw);
+ continue;
+ }
+ const pselector = vAPI.domFilterer.createProceduralFilter(details);
+ if ( pselector.test() === false ) { continue; }
+ matchedSelectors.push(`#@#${pselector.raw}`);
+ }
+ if (
+ exceptionDict.size !== 0 &&
+ hasSelector(safeGroupSelectors(exceptionDict.keys()))
+ ) {
+ for ( const [ selector, raw ] of exceptionDict ) {
+ if ( safeQuerySelector(selector) === null ) { continue; }
+ matchedSelectors.push(`#@#${raw}`);
+ }
+ }
+}
+
+if ( typeof self.uBO_scriptletsInjected === 'string' ) {
+ matchedSelectors.push(...self.uBO_scriptletsInjected.split('\n'));
+}
+
+if ( matchedSelectors.length === 0 ) { return; }
+
+return matchedSelectors;
+
+/******************************************************************************/
+
+// <<<<<<<< end of private namespace
+})();
+
diff --git a/src/js/scriptlets/dom-inspector.js b/src/js/scriptlets/dom-inspector.js
new file mode 100644
index 0000000..b5317d5
--- /dev/null
+++ b/src/js/scriptlets/dom-inspector.js
@@ -0,0 +1,924 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2015-present Raymond Hill
+
+ 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 {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+/* globals browser */
+
+'use strict';
+
+/******************************************************************************/
+/******************************************************************************/
+
+(async ( ) => {
+
+/******************************************************************************/
+
+if ( typeof vAPI !== 'object' ) { return; }
+if ( typeof vAPI === null ) { return; }
+if ( vAPI.domFilterer instanceof Object === false ) { return; }
+
+if ( vAPI.inspectorFrame ) { return; }
+vAPI.inspectorFrame = true;
+
+const inspectorUniqueId = vAPI.randomToken();
+
+const nodeToIdMap = new WeakMap(); // No need to iterate
+
+let blueNodes = [];
+const roRedNodes = new Map(); // node => current cosmetic filter
+const rwRedNodes = new Set(); // node => new cosmetic filter (toggle node)
+const rwGreenNodes = new Set(); // node => new exception cosmetic filter (toggle filter)
+//const roGreenNodes = new Map(); // node => current exception cosmetic filter (can't toggle)
+
+const reHasCSSCombinators = /[ >+~]/;
+
+/******************************************************************************/
+
+const domLayout = (( ) => {
+ const skipTagNames = new Set([
+ 'br', 'head', 'link', 'meta', 'script', 'style', 'title'
+ ]);
+ const resourceAttrNames = new Map([
+ [ 'a', 'href' ],
+ [ 'iframe', 'src' ],
+ [ 'img', 'src' ],
+ [ 'object', 'data' ]
+ ]);
+
+ let idGenerator = 1;
+
+ // This will be used to uniquely identify nodes across process.
+
+ const newNodeId = node => {
+ const nid = `n${(idGenerator++).toString(36)}`;
+ nodeToIdMap.set(node, nid);
+ return nid;
+ };
+
+ const selectorFromNode = node => {
+ const tag = node.localName;
+ let selector = CSS.escape(tag);
+ // Id
+ if ( typeof node.id === 'string' ) {
+ let str = node.id.trim();
+ if ( str !== '' ) {
+ selector += `#${CSS.escape(str)}`;
+ }
+ }
+ // Class
+ const cl = node.classList;
+ if ( cl ) {
+ for ( let i = 0; i < cl.length; i++ ) {
+ selector += `.${CSS.escape(cl[i])}`;
+ }
+ }
+ // Tag-specific attributes
+ const attr = resourceAttrNames.get(tag);
+ if ( attr !== undefined ) {
+ let str = node.getAttribute(attr) || '';
+ str = str.trim();
+ const pos = str.startsWith('data:') ? 5 : str.search(/[#?]/);
+ let sw = '';
+ if ( pos !== -1 ) {
+ str = str.slice(0, pos);
+ sw = '^';
+ }
+ if ( str !== '' ) {
+ selector += `[${attr}${sw}="${CSS.escape(str, true)}"]`;
+ }
+ }
+ return selector;
+ };
+
+ function DomRoot() {
+ this.nid = newNodeId(document.body);
+ this.lvl = 0;
+ this.sel = 'body';
+ this.cnt = 0;
+ this.filter = roRedNodes.get(document.body);
+ }
+
+ function DomNode(node, level) {
+ this.nid = newNodeId(node);
+ this.lvl = level;
+ this.sel = selectorFromNode(node);
+ this.cnt = 0;
+ this.filter = roRedNodes.get(node);
+ }
+
+ const domNodeFactory = (level, node) => {
+ const localName = node.localName;
+ if ( skipTagNames.has(localName) ) { return null; }
+ // skip uBlock's own nodes
+ if ( node === inspectorFrame ) { return null; }
+ if ( level === 0 && localName === 'body' ) {
+ return new DomRoot();
+ }
+ return new DomNode(node, level);
+ };
+
+ // Collect layout data
+
+ const getLayoutData = ( ) => {
+ const layout = [];
+ const stack = [];
+ let lvl = 0;
+ let node = document.documentElement;
+ if ( node === null ) { return layout; }
+
+ for (;;) {
+ const domNode = domNodeFactory(lvl, node);
+ if ( domNode !== null ) {
+ layout.push(domNode);
+ }
+ // children
+ if ( domNode !== null && node.firstElementChild !== null ) {
+ stack.push(node);
+ lvl += 1;
+ node = node.firstElementChild;
+ continue;
+ }
+ // sibling
+ if ( node instanceof Element ) {
+ if ( node.nextElementSibling === null ) {
+ do {
+ node = stack.pop();
+ if ( !node ) { break; }
+ lvl -= 1;
+ } while ( node.nextElementSibling === null );
+ if ( !node ) { break; }
+ }
+ node = node.nextElementSibling;
+ }
+ }
+
+ return layout;
+ };
+
+ // Descendant count for each node.
+
+ const patchLayoutData = layout => {
+ const stack = [];
+ let ptr;
+ let lvl = 0;
+ let i = layout.length;
+
+ while ( i-- ) {
+ const domNode = layout[i];
+ if ( domNode.lvl === lvl ) {
+ stack[ptr] += 1;
+ continue;
+ }
+ if ( domNode.lvl > lvl ) {
+ while ( lvl < domNode.lvl ) {
+ stack.push(0);
+ lvl += 1;
+ }
+ ptr = lvl - 1;
+ stack[ptr] += 1;
+ continue;
+ }
+ // domNode.lvl < lvl
+ const cnt = stack.pop();
+ domNode.cnt = cnt;
+ lvl -= 1;
+ ptr = lvl - 1;
+ stack[ptr] += cnt + 1;
+ }
+ return layout;
+ };
+
+ // Track and report mutations of the DOM
+
+ let mutationObserver = null;
+ let mutationTimer;
+ let addedNodelists = [];
+ let removedNodelist = [];
+
+ const previousElementSiblingId = node => {
+ let sibling = node;
+ for (;;) {
+ sibling = sibling.previousElementSibling;
+ if ( sibling === null ) { return null; }
+ if ( skipTagNames.has(sibling.localName) ) { continue; }
+ return nodeToIdMap.get(sibling);
+ }
+ };
+
+ const journalFromBranch = (root, newNodes, newNodeToIdMap) => {
+ let node = root.firstElementChild;
+ while ( node !== null ) {
+ const domNode = domNodeFactory(undefined, node);
+ if ( domNode !== null ) {
+ newNodeToIdMap.set(domNode.nid, domNode);
+ newNodes.push(node);
+ }
+ // down
+ if ( node.firstElementChild !== null ) {
+ node = node.firstElementChild;
+ continue;
+ }
+ // right
+ if ( node.nextElementSibling !== null ) {
+ node = node.nextElementSibling;
+ continue;
+ }
+ // up then right
+ for (;;) {
+ if ( node.parentElement === root ) { return; }
+ node = node.parentElement;
+ if ( node.nextElementSibling !== null ) {
+ node = node.nextElementSibling;
+ break;
+ }
+ }
+ }
+ };
+
+ const journalFromMutations = ( ) => {
+ mutationTimer = undefined;
+
+ // This is used to temporarily hold all added nodes, before resolving
+ // their node id and relative position.
+ const newNodes = [];
+ const journalEntries = [];
+ const newNodeToIdMap = new Map();
+
+ for ( const nodelist of addedNodelists ) {
+ for ( const node of nodelist ) {
+ if ( node.nodeType !== 1 ) { continue; }
+ if ( node.parentElement === null ) { continue; }
+ cosmeticFilterMapper.incremental(node);
+ const domNode = domNodeFactory(undefined, node);
+ if ( domNode !== null ) {
+ newNodeToIdMap.set(domNode.nid, domNode);
+ newNodes.push(node);
+ }
+ journalFromBranch(node, newNodes, newNodeToIdMap);
+ }
+ }
+ addedNodelists = [];
+ for ( const nodelist of removedNodelist ) {
+ for ( const node of nodelist ) {
+ if ( node.nodeType !== 1 ) { continue; }
+ const nid = nodeToIdMap.get(node);
+ if ( nid === undefined ) { continue; }
+ journalEntries.push({ what: -1, nid });
+ }
+ }
+ removedNodelist = [];
+ for ( const node of newNodes ) {
+ journalEntries.push({
+ what: 1,
+ nid: nodeToIdMap.get(node),
+ u: nodeToIdMap.get(node.parentElement),
+ l: previousElementSiblingId(node)
+ });
+ }
+
+ if ( journalEntries.length === 0 ) { return; }
+
+ contentInspectorChannel.toLogger({
+ what: 'domLayoutIncremental',
+ url: window.location.href,
+ hostname: window.location.hostname,
+ journal: journalEntries,
+ nodes: Array.from(newNodeToIdMap)
+ });
+ };
+
+ const onMutationObserved = mutationRecords => {
+ for ( const record of mutationRecords ) {
+ if ( record.addedNodes.length !== 0 ) {
+ addedNodelists.push(record.addedNodes);
+ }
+ if ( record.removedNodes.length !== 0 ) {
+ removedNodelist.push(record.removedNodes);
+ }
+ }
+ if ( mutationTimer === undefined ) {
+ mutationTimer = vAPI.setTimeout(journalFromMutations, 1000);
+ }
+ };
+
+ // API
+
+ const getLayout = ( ) => {
+ cosmeticFilterMapper.reset();
+ mutationObserver = new MutationObserver(onMutationObserved);
+ mutationObserver.observe(document.body, {
+ childList: true,
+ subtree: true
+ });
+
+ return {
+ what: 'domLayoutFull',
+ url: window.location.href,
+ hostname: window.location.hostname,
+ layout: patchLayoutData(getLayoutData())
+ };
+ };
+
+ const reset = ( ) => {
+ shutdown();
+ };
+
+ const shutdown = ( ) => {
+ if ( mutationTimer !== undefined ) {
+ clearTimeout(mutationTimer);
+ mutationTimer = undefined;
+ }
+ if ( mutationObserver !== null ) {
+ mutationObserver.disconnect();
+ mutationObserver = null;
+ }
+ addedNodelists = [];
+ removedNodelist = [];
+ };
+
+ return {
+ get: getLayout,
+ reset,
+ shutdown,
+ };
+})();
+
+/******************************************************************************/
+/******************************************************************************/
+
+const cosmeticFilterMapper = (( ) => {
+ const nodesFromStyleTag = rootNode => {
+ const filterMap = roRedNodes;
+ const details = vAPI.domFilterer.getAllSelectors();
+
+ // Declarative selectors.
+ for ( const block of (details.declarative || []) ) {
+ for ( const selector of block.split(',\n') ) {
+ let nodes;
+ if ( reHasCSSCombinators.test(selector) ) {
+ nodes = document.querySelectorAll(selector);
+ } else {
+ if (
+ filterMap.has(rootNode) === false &&
+ rootNode.matches(selector)
+ ) {
+ filterMap.set(rootNode, selector);
+ }
+ nodes = rootNode.querySelectorAll(selector);
+ }
+ for ( const node of nodes ) {
+ if ( filterMap.has(node) ) { continue; }
+ filterMap.set(node, selector);
+ }
+ }
+ }
+
+ // Procedural selectors.
+ for ( const entry of (details.procedural || []) ) {
+ const nodes = entry.exec();
+ for ( const node of nodes ) {
+ // Upgrade declarative selector to procedural one
+ filterMap.set(node, entry.raw);
+ }
+ }
+ };
+
+ const incremental = rootNode => {
+ nodesFromStyleTag(rootNode);
+ };
+
+ const reset = ( ) => {
+ roRedNodes.clear();
+ if ( document.documentElement !== null ) {
+ incremental(document.documentElement);
+ }
+ };
+
+ const shutdown = ( ) => {
+ vAPI.domFilterer.toggle(true);
+ };
+
+ return {
+ incremental,
+ reset,
+ shutdown,
+ };
+})();
+
+/******************************************************************************/
+
+const elementsFromSelector = function(selector, context) {
+ if ( !context ) {
+ context = document;
+ }
+ if ( selector.indexOf(':') !== -1 ) {
+ const out = elementsFromSpecialSelector(selector);
+ if ( out !== undefined ) { return out; }
+ }
+ // plain CSS selector
+ try {
+ return context.querySelectorAll(selector);
+ } catch (ex) {
+ }
+ return [];
+};
+
+const elementsFromSpecialSelector = function(selector) {
+ const out = [];
+ let matches = /^(.+?):has\((.+?)\)$/.exec(selector);
+ if ( matches !== null ) {
+ let nodes;
+ try {
+ nodes = document.querySelectorAll(matches[1]);
+ } catch(ex) {
+ nodes = [];
+ }
+ for ( const node of nodes ) {
+ if ( node.querySelector(matches[2]) === null ) { continue; }
+ out.push(node);
+ }
+ return out;
+ }
+
+ matches = /^:xpath\((.+?)\)$/.exec(selector);
+ if ( matches === null ) { return; }
+ const xpr = document.evaluate(
+ matches[1],
+ document,
+ null,
+ XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
+ null
+ );
+ let i = xpr.snapshotLength;
+ while ( i-- ) {
+ out.push(xpr.snapshotItem(i));
+ }
+ return out;
+};
+
+/******************************************************************************/
+
+const highlightElements = ( ) => {
+ const paths = [];
+
+ const path = [];
+ for ( const elem of rwRedNodes.keys() ) {
+ if ( elem === inspectorFrame ) { continue; }
+ if ( rwGreenNodes.has(elem) ) { continue; }
+ if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; }
+ const rect = elem.getBoundingClientRect();
+ const xl = rect.left;
+ const w = rect.width;
+ const yt = rect.top;
+ const h = rect.height;
+ const ws = w.toFixed(1);
+ const poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) +
+ 'h' + ws +
+ 'v' + h.toFixed(1) +
+ 'h-' + ws +
+ 'z';
+ path.push(poly);
+ }
+ paths.push(path.join('') || 'M0 0');
+
+ path.length = 0;
+ for ( const elem of rwGreenNodes ) {
+ if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; }
+ const rect = elem.getBoundingClientRect();
+ const xl = rect.left;
+ const w = rect.width;
+ const yt = rect.top;
+ const h = rect.height;
+ const ws = w.toFixed(1);
+ const poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) +
+ 'h' + ws +
+ 'v' + h.toFixed(1) +
+ 'h-' + ws +
+ 'z';
+ path.push(poly);
+ }
+ paths.push(path.join('') || 'M0 0');
+
+ path.length = 0;
+ for ( const elem of roRedNodes.keys() ) {
+ if ( elem === inspectorFrame ) { continue; }
+ if ( rwGreenNodes.has(elem) ) { continue; }
+ if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; }
+ const rect = elem.getBoundingClientRect();
+ const xl = rect.left;
+ const w = rect.width;
+ const yt = rect.top;
+ const h = rect.height;
+ const ws = w.toFixed(1);
+ const poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) +
+ 'h' + ws +
+ 'v' + h.toFixed(1) +
+ 'h-' + ws +
+ 'z';
+ path.push(poly);
+ }
+ paths.push(path.join('') || 'M0 0');
+
+ path.length = 0;
+ for ( const elem of blueNodes ) {
+ if ( elem === inspectorFrame ) { continue; }
+ if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; }
+ const rect = elem.getBoundingClientRect();
+ const xl = rect.left;
+ const w = rect.width;
+ const yt = rect.top;
+ const h = rect.height;
+ const ws = w.toFixed(1);
+ const poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) +
+ 'h' + ws +
+ 'v' + h.toFixed(1) +
+ 'h-' + ws +
+ 'z';
+ path.push(poly);
+ }
+ paths.push(path.join('') || 'M0 0');
+
+ contentInspectorChannel.toFrame({
+ what: 'svgPaths',
+ paths,
+ });
+};
+
+/******************************************************************************/
+
+const onScrolled = (( ) => {
+ let timer;
+ return ( ) => {
+ if ( timer ) { return; }
+ timer = window.requestAnimationFrame(( ) => {
+ timer = undefined;
+ highlightElements();
+ });
+ };
+})();
+
+const onMouseOver = ( ) => {
+ if ( blueNodes.length === 0 ) { return; }
+ blueNodes = [];
+ highlightElements();
+};
+
+/******************************************************************************/
+
+const selectNodes = (selector, nid) => {
+ const nodes = elementsFromSelector(selector);
+ if ( nid === '' ) { return nodes; }
+ for ( const node of nodes ) {
+ if ( nodeToIdMap.get(node) === nid ) {
+ return [ node ];
+ }
+ }
+ return [];
+};
+
+/******************************************************************************/
+
+const nodesFromFilter = selector => {
+ const out = [];
+ for ( const entry of roRedNodes ) {
+ if ( entry[1] === selector ) {
+ out.push(entry[0]);
+ }
+ }
+ return out;
+};
+
+/******************************************************************************/
+
+const toggleExceptions = (nodes, targetState) => {
+ for ( const node of nodes ) {
+ if ( targetState ) {
+ rwGreenNodes.add(node);
+ } else {
+ rwGreenNodes.delete(node);
+ }
+ }
+};
+
+const toggleFilter = (nodes, targetState) => {
+ for ( const node of nodes ) {
+ if ( targetState ) {
+ rwRedNodes.delete(node);
+ } else {
+ rwRedNodes.add(node);
+ }
+ }
+};
+
+const resetToggledNodes = ( ) => {
+ rwGreenNodes.clear();
+ rwRedNodes.clear();
+};
+
+/******************************************************************************/
+
+const startInspector = ( ) => {
+ const onReady = ( ) => {
+ window.addEventListener('scroll', onScrolled, {
+ capture: true,
+ passive: true,
+ });
+ window.addEventListener('mouseover', onMouseOver, {
+ capture: true,
+ passive: true,
+ });
+ contentInspectorChannel.toLogger(domLayout.get());
+ vAPI.domFilterer.toggle(false, highlightElements);
+ };
+ if ( document.readyState === 'loading' ) {
+ document.addEventListener('DOMContentLoaded', onReady, { once: true });
+ } else {
+ onReady();
+ }
+};
+
+/******************************************************************************/
+
+const shutdownInspector = ( ) => {
+ cosmeticFilterMapper.shutdown();
+ domLayout.shutdown();
+ window.removeEventListener('scroll', onScrolled, {
+ capture: true,
+ passive: true,
+ });
+ window.removeEventListener('mouseover', onMouseOver, {
+ capture: true,
+ passive: true,
+ });
+ contentInspectorChannel.shutdown();
+ if ( inspectorFrame ) {
+ inspectorFrame.remove();
+ inspectorFrame = null;
+ }
+ vAPI.userStylesheet.remove(inspectorCSS);
+ vAPI.userStylesheet.apply();
+ vAPI.inspectorFrame = false;
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+const onMessage = request => {
+ switch ( request.what ) {
+ case 'startInspector':
+ startInspector();
+ break;
+
+ case 'quitInspector':
+ shutdownInspector();
+ break;
+
+ case 'commitFilters':
+ highlightElements();
+ break;
+
+ case 'domLayout':
+ domLayout.get();
+ highlightElements();
+ break;
+
+ case 'highlightMode':
+ break;
+
+ case 'highlightOne':
+ blueNodes = selectNodes(request.selector, request.nid);
+ if ( blueNodes.length !== 0 ) {
+ blueNodes[0].scrollIntoView({
+ behavior: 'smooth',
+ block: 'nearest',
+ inline: 'nearest',
+ });
+ }
+ highlightElements();
+ break;
+
+ case 'resetToggledNodes':
+ resetToggledNodes();
+ highlightElements();
+ break;
+
+ case 'showCommitted':
+ blueNodes = [];
+ // TODO: show only the new filters and exceptions.
+ highlightElements();
+ break;
+
+ case 'showInteractive':
+ blueNodes = [];
+ highlightElements();
+ break;
+
+ case 'toggleFilter': {
+ const nodes = selectNodes(request.selector, request.nid);
+ if ( nodes.length !== 0 ) {
+ nodes[0].scrollIntoView({
+ behavior: 'smooth',
+ block: 'nearest',
+ inline: 'nearest',
+ });
+ }
+ toggleExceptions(nodesFromFilter(request.filter), request.target);
+ highlightElements();
+ break;
+ }
+ case 'toggleNodes': {
+ const nodes = selectNodes(request.selector, request.nid);
+ if ( nodes.length !== 0 ) {
+ nodes[0].scrollIntoView({
+ behavior: 'smooth',
+ block: 'nearest',
+ inline: 'nearest',
+ });
+ }
+ toggleFilter(nodes, request.target);
+ highlightElements();
+ break;
+ }
+ default:
+ break;
+ }
+};
+
+/*******************************************************************************
+ *
+ * Establish two-way communication with logger/inspector window and
+ * inspector frame
+ *
+ * */
+
+const contentInspectorChannel = (( ) => {
+ let toLoggerPort;
+ let toFramePort;
+
+ const toLogger = msg => {
+ if ( toLoggerPort === undefined ) { return; }
+ try {
+ toLoggerPort.postMessage(msg);
+ } catch(_) {
+ shutdownInspector();
+ }
+ };
+
+ const onLoggerMessage = msg => {
+ onMessage(msg);
+ };
+
+ const onLoggerDisconnect = ( ) => {
+ shutdownInspector();
+ };
+
+ const onLoggerConnect = port => {
+ browser.runtime.onConnect.removeListener(onLoggerConnect);
+ toLoggerPort = port;
+ port.onMessage.addListener(onLoggerMessage);
+ port.onDisconnect.addListener(onLoggerDisconnect);
+ };
+
+ const toFrame = msg => {
+ if ( toFramePort === undefined ) { return; }
+ toFramePort.postMessage(msg);
+ };
+
+ const shutdown = ( ) => {
+ if ( toFramePort !== undefined ) {
+ toFrame({ what: 'quitInspector' });
+ toFramePort.onmessage = null;
+ toFramePort.close();
+ toFramePort = undefined;
+ }
+ if ( toLoggerPort !== undefined ) {
+ toLoggerPort.onMessage.removeListener(onLoggerMessage);
+ toLoggerPort.onDisconnect.removeListener(onLoggerDisconnect);
+ toLoggerPort.disconnect();
+ toLoggerPort = undefined;
+ }
+ browser.runtime.onConnect.removeListener(onLoggerConnect);
+ };
+
+ const start = async ( ) => {
+ browser.runtime.onConnect.addListener(onLoggerConnect);
+ const inspectorArgs = await vAPI.messaging.send('domInspectorContent', {
+ what: 'getInspectorArgs',
+ });
+ if ( typeof inspectorArgs !== 'object' ) { return; }
+ if ( inspectorArgs === null ) { return; }
+ return new Promise(resolve => {
+ const iframe = document.createElement('iframe');
+ iframe.setAttribute(inspectorUniqueId, '');
+ document.documentElement.append(iframe);
+ iframe.addEventListener('load', ( ) => {
+ iframe.setAttribute(`${inspectorUniqueId}-loaded`, '');
+ const channel = new MessageChannel();
+ toFramePort = channel.port1;
+ toFramePort.onmessage = ev => {
+ const msg = ev.data || {};
+ if ( msg.what !== 'startInspector' ) { return; }
+ };
+ iframe.contentWindow.postMessage(
+ { what: 'startInspector' },
+ inspectorArgs.inspectorURL,
+ [ channel.port2 ]
+ );
+ resolve(iframe);
+ }, { once: true });
+ iframe.contentWindow.location = inspectorArgs.inspectorURL;
+ });
+ };
+
+ return { start, toLogger, toFrame, shutdown };
+})();
+
+
+// Install DOM inspector widget
+const inspectorCSSStyle = [
+ 'background: transparent',
+ 'border: 0',
+ 'border-radius: 0',
+ 'box-shadow: none',
+ 'color-scheme: light dark',
+ 'display: block',
+ 'filter: none',
+ 'height: 100%',
+ 'left: 0',
+ 'margin: 0',
+ 'max-height: none',
+ 'max-width: none',
+ 'min-height: unset',
+ 'min-width: unset',
+ 'opacity: 1',
+ 'outline: 0',
+ 'padding: 0',
+ 'pointer-events: none',
+ 'position: fixed',
+ 'top: 0',
+ 'transform: none',
+ 'visibility: hidden',
+ 'width: 100%',
+ 'z-index: 2147483647',
+ ''
+].join(' !important;\n');
+
+const inspectorCSS = `
+:root > [${inspectorUniqueId}] {
+ ${inspectorCSSStyle}
+}
+:root > [${inspectorUniqueId}-loaded] {
+ visibility: visible !important;
+}
+`;
+
+vAPI.userStylesheet.add(inspectorCSS);
+vAPI.userStylesheet.apply();
+
+let inspectorFrame = await contentInspectorChannel.start();
+if ( inspectorFrame instanceof HTMLIFrameElement === false ) {
+ return shutdownInspector();
+}
+
+startInspector();
+
+/******************************************************************************/
+
+})();
+
+
+
+
+
+
+
+
+/*******************************************************************************
+
+ DO NOT:
+ - Remove the following code
+ - Add code beyond the following code
+ Reason:
+ - https://github.com/gorhill/uBlock/pull/3721
+ - uBO never uses the return value from injected content scripts
+
+**/
+
+void 0;
diff --git a/src/js/scriptlets/dom-survey-elements.js b/src/js/scriptlets/dom-survey-elements.js
new file mode 100644
index 0000000..1582596
--- /dev/null
+++ b/src/js/scriptlets/dom-survey-elements.js
@@ -0,0 +1,72 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2015-present Raymond Hill
+
+ 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 {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+'use strict';
+
+/******************************************************************************/
+
+// https://github.com/uBlockOrigin/uBlock-issues/issues/756
+// Keep in mind CPU usage with large DOM and/or filterset.
+
+(( ) => {
+ if ( typeof vAPI !== 'object' ) { return; }
+
+ const t0 = Date.now();
+
+ if ( vAPI.domSurveyElements instanceof Object === false ) {
+ vAPI.domSurveyElements = {
+ busy: false,
+ hiddenElementCount: Number.NaN,
+ surveyTime: t0,
+ };
+ }
+ const surveyResults = vAPI.domSurveyElements;
+
+ if ( surveyResults.busy ) { return; }
+ surveyResults.busy = true;
+
+ if ( surveyResults.surveyTime < vAPI.domMutationTime ) {
+ surveyResults.hiddenElementCount = Number.NaN;
+ }
+ surveyResults.surveyTime = t0;
+
+ if ( isNaN(surveyResults.hiddenElementCount) ) {
+ surveyResults.hiddenElementCount = (( ) => {
+ if ( vAPI.domFilterer instanceof Object === false ) { return 0; }
+ const details = vAPI.domFilterer.getAllSelectors(0b11);
+ if (
+ Array.isArray(details.declarative) === false ||
+ details.declarative.length === 0
+ ) {
+ return 0;
+ }
+ return document.querySelectorAll(
+ details.declarative.join(',\n')
+ ).length;
+ })();
+ }
+
+ surveyResults.busy = false;
+
+ // IMPORTANT: This is returned to the injector, so this MUST be
+ // the last statement.
+ return surveyResults.hiddenElementCount;
+})();
diff --git a/src/js/scriptlets/dom-survey-scripts.js b/src/js/scriptlets/dom-survey-scripts.js
new file mode 100644
index 0000000..e5300ff
--- /dev/null
+++ b/src/js/scriptlets/dom-survey-scripts.js
@@ -0,0 +1,126 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2015-present Raymond Hill
+
+ 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 {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+'use strict';
+
+/******************************************************************************/
+
+// Scriptlets to count the number of script tags in a document.
+
+(( ) => {
+ if ( typeof vAPI !== 'object' ) { return; }
+
+ const t0 = Date.now();
+
+ if ( vAPI.domSurveyScripts instanceof Object === false ) {
+ vAPI.domSurveyScripts = {
+ busy: false,
+ scriptCount: -1,
+ surveyTime: t0,
+ };
+ }
+ const surveyResults = vAPI.domSurveyScripts;
+
+ if ( surveyResults.busy ) { return; }
+ surveyResults.busy = true;
+
+ if ( surveyResults.surveyTime < vAPI.domMutationTime ) {
+ surveyResults.scriptCount = -1;
+ }
+ surveyResults.surveyTime = t0;
+
+ if ( surveyResults.scriptCount === -1 ) {
+ const reInlineScript = /^(data:|blob:|$)/;
+ let inlineScriptCount = 0;
+ let scriptCount = 0;
+ for ( const script of document.scripts ) {
+ if ( reInlineScript.test(script.src) ) {
+ inlineScriptCount = 1;
+ continue;
+ }
+ scriptCount += 1;
+ if ( scriptCount === 99 ) { break; }
+ }
+ scriptCount += inlineScriptCount;
+ if ( scriptCount !== 0 ) {
+ surveyResults.scriptCount = scriptCount;
+ }
+ }
+
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/756
+ // Keep trying to find inline script-like instances but only if we
+ // have the time-budget to do so.
+ if ( surveyResults.scriptCount === -1 ) {
+ if ( document.querySelector('a[href^="javascript:"]') !== null ) {
+ surveyResults.scriptCount = 1;
+ }
+ }
+
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/1756
+ // Mind that there might be no body element.
+ if ( surveyResults.scriptCount === -1 && document.body !== null ) {
+ surveyResults.scriptCount = 0;
+ const onHandlers = new Set([
+ 'onabort', 'onblur', 'oncancel', 'oncanplay',
+ 'oncanplaythrough', 'onchange', 'onclick', 'onclose',
+ 'oncontextmenu', 'oncuechange', 'ondblclick', 'ondrag',
+ 'ondragend', 'ondragenter', 'ondragexit', 'ondragleave',
+ 'ondragover', 'ondragstart', 'ondrop', 'ondurationchange',
+ 'onemptied', 'onended', 'onerror', 'onfocus',
+ 'oninput', 'oninvalid', 'onkeydown', 'onkeypress',
+ 'onkeyup', 'onload', 'onloadeddata', 'onloadedmetadata',
+ 'onloadstart', 'onmousedown', 'onmouseenter', 'onmouseleave',
+ 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup',
+ 'onwheel', 'onpause', 'onplay', 'onplaying',
+ 'onprogress', 'onratechange', 'onreset', 'onresize',
+ 'onscroll', 'onseeked', 'onseeking', 'onselect',
+ 'onshow', 'onstalled', 'onsubmit', 'onsuspend',
+ 'ontimeupdate', 'ontoggle', 'onvolumechange', 'onwaiting',
+ 'onafterprint', 'onbeforeprint', 'onbeforeunload', 'onhashchange',
+ 'onlanguagechange', 'onmessage', 'onoffline', 'ononline',
+ 'onpagehide', 'onpageshow', 'onrejectionhandled', 'onpopstate',
+ 'onstorage', 'onunhandledrejection', 'onunload',
+ 'oncopy', 'oncut', 'onpaste'
+ ]);
+ const nodeIter = document.createNodeIterator(
+ document.body,
+ NodeFilter.SHOW_ELEMENT
+ );
+ for (;;) {
+ const node = nodeIter.nextNode();
+ if ( node === null ) { break; }
+ if ( node.hasAttributes() === false ) { continue; }
+ for ( const attr of node.getAttributeNames() ) {
+ if ( onHandlers.has(attr) === false ) { continue; }
+ surveyResults.scriptCount = 1;
+ break;
+ }
+ }
+ }
+
+ surveyResults.busy = false;
+
+ // IMPORTANT: This is returned to the injector, so this MUST be
+ // the last statement.
+ if ( surveyResults.scriptCount !== -1 ) {
+ return surveyResults.scriptCount;
+ }
+})();
diff --git a/src/js/scriptlets/epicker.js b/src/js/scriptlets/epicker.js
new file mode 100644
index 0000000..80489e8
--- /dev/null
+++ b/src/js/scriptlets/epicker.js
@@ -0,0 +1,1356 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2014-present Raymond Hill
+
+ 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 {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+/* global CSS */
+
+'use strict';
+
+/******************************************************************************/
+/******************************************************************************/
+
+(async ( ) => {
+
+/******************************************************************************/
+
+if ( typeof vAPI !== 'object' ) { return; }
+if ( typeof vAPI === null ) { return; }
+
+if ( vAPI.pickerFrame ) { return; }
+vAPI.pickerFrame = true;
+
+const pickerUniqueId = vAPI.randomToken();
+
+const reCosmeticAnchor = /^#(\$|\?|\$\?)?#/;
+
+const netFilterCandidates = [];
+const cosmeticFilterCandidates = [];
+
+let targetElements = [];
+let candidateElements = [];
+let bestCandidateFilter = null;
+
+const lastNetFilterSession = window.location.host + window.location.pathname;
+let lastNetFilterHostname = '';
+let lastNetFilterUnion = '';
+
+const hideBackgroundStyle = 'background-image:none!important;';
+
+/******************************************************************************/
+
+const safeQuerySelectorAll = function(node, selector) {
+ if ( node !== null ) {
+ try {
+ return node.querySelectorAll(selector);
+ } catch (e) {
+ }
+ }
+ return [];
+};
+
+/******************************************************************************/
+
+const getElementBoundingClientRect = function(elem) {
+ let rect = typeof elem.getBoundingClientRect === 'function'
+ ? elem.getBoundingClientRect()
+ : { height: 0, left: 0, top: 0, width: 0 };
+
+ // https://github.com/gorhill/uBlock/issues/1024
+ // Try not returning an empty bounding rect.
+ if ( rect.width !== 0 && rect.height !== 0 ) {
+ return rect;
+ }
+ if ( elem.shadowRoot instanceof DocumentFragment ) {
+ return getElementBoundingClientRect(elem.shadowRoot);
+ }
+
+ let left = rect.left,
+ right = left + rect.width,
+ top = rect.top,
+ bottom = top + rect.height;
+
+ for ( const child of elem.children ) {
+ rect = getElementBoundingClientRect(child);
+ if ( rect.width === 0 || rect.height === 0 ) { continue; }
+ if ( rect.left < left ) { left = rect.left; }
+ if ( rect.right > right ) { right = rect.right; }
+ if ( rect.top < top ) { top = rect.top; }
+ if ( rect.bottom > bottom ) { bottom = rect.bottom; }
+ }
+
+ return {
+ bottom,
+ height: bottom - top,
+ left,
+ right,
+ top,
+ width: right - left
+ };
+};
+
+/******************************************************************************/
+
+const highlightElements = function(elems, force) {
+ // To make mouse move handler more efficient
+ if (
+ (force !== true) &&
+ (elems.length === targetElements.length) &&
+ (elems.length === 0 || elems[0] === targetElements[0])
+ ) {
+ return;
+ }
+ targetElements = [];
+
+ const ow = self.innerWidth;
+ const oh = self.innerHeight;
+ const islands = [];
+
+ for ( const elem of elems ) {
+ if ( elem === pickerFrame ) { continue; }
+ targetElements.push(elem);
+ const rect = getElementBoundingClientRect(elem);
+ // Ignore offscreen areas
+ if (
+ rect.left > ow || rect.top > oh ||
+ rect.left + rect.width < 0 || rect.top + rect.height < 0
+ ) {
+ continue;
+ }
+ islands.push(
+ `M${rect.left} ${rect.top}h${rect.width}v${rect.height}h-${rect.width}z`
+ );
+ }
+
+ pickerFramePort.postMessage({
+ what: 'svgPaths',
+ ocean: `M0 0h${ow}v${oh}h-${ow}z`,
+ islands: islands.join(''),
+ });
+};
+
+/******************************************************************************/
+
+const mergeStrings = function(urls) {
+ if ( urls.length === 0 ) { return ''; }
+ if (
+ urls.length === 1 ||
+ self.diff_match_patch instanceof Function === false
+ ) {
+ return urls[0];
+ }
+ const differ = new self.diff_match_patch();
+ let merged = urls[0];
+ for ( let i = 1; i < urls.length; i++ ) {
+ // The differ works at line granularity: we insert a linefeed after
+ // each character to trick the differ to work at character granularity.
+ const diffs = differ.diff_main(
+ urls[i].split('').join('\n'),
+ merged.split('').join('\n')
+ );
+ const result = [];
+ for ( const diff of diffs ) {
+ if ( diff[0] !== 0 ) {
+ result.push('*');
+ } else {
+ result.push(diff[1].replace(/\n+/g, ''));
+ }
+ merged = result.join('');
+ }
+ }
+ // Keep usage of wildcards to a sane level, too many of them can cause
+ // high overhead filters
+ merged = merged.replace(/^\*+$/, '')
+ .replace(/\*{2,}/g, '*')
+ .replace(/([^*]{1,3}\*)(?:[^*]{1,3}\*)+/g, '$1');
+
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/1494
+ let pos = merged.indexOf('/');
+ if ( pos === -1 ) { pos = merged.length; }
+ return merged.slice(0, pos).includes('*') ? urls[0] : merged;
+};
+
+/******************************************************************************/
+
+// Remove fragment part from a URL.
+
+const trimFragmentFromURL = function(url) {
+ const pos = url.indexOf('#');
+ return pos !== -1 ? url.slice(0, pos) : url;
+};
+
+/******************************************************************************/
+
+// https://github.com/gorhill/uBlock/issues/1897
+// Ignore `data:` URI, they can't be handled by an HTTP observer.
+
+const backgroundImageURLFromElement = function(elem) {
+ const style = window.getComputedStyle(elem);
+ const bgImg = style.backgroundImage || '';
+ const matches = /^url\((["']?)([^"']+)\1\)$/.exec(bgImg);
+ const url = matches !== null && matches.length === 3 ? matches[2] : '';
+ return url.lastIndexOf('data:', 0) === -1
+ ? trimFragmentFromURL(url.slice(0, 1024))
+ : '';
+};
+
+/******************************************************************************/
+
+// https://github.com/gorhill/uBlock/issues/1725#issuecomment-226479197
+// Limit returned string to 1024 characters.
+// Also, return only URLs which will be seen by an HTTP observer.
+// https://github.com/uBlockOrigin/uBlock-issues/issues/2260
+// Maybe get to the actual URL indirectly.
+const resourceURLsFromElement = function(elem) {
+ const urls = [];
+ const tagName = elem.localName;
+ const prop = netFilter1stSources[tagName];
+ if ( prop === undefined ) {
+ const url = backgroundImageURLFromElement(elem);
+ if ( url !== '' ) { urls.push(url); }
+ return urls;
+ }
+ let s = elem[prop];
+ if ( s instanceof SVGAnimatedString ) {
+ s = s.baseVal;
+ }
+ if ( typeof s === 'string' && /^https?:\/\//.test(s) ) {
+ urls.push(trimFragmentFromURL(s.slice(0, 1024)));
+ }
+ resourceURLsFromSrcset(elem, urls);
+ resourceURLsFromPicture(elem, urls);
+ return urls;
+};
+
+// https://html.spec.whatwg.org/multipage/images.html#parsing-a-srcset-attribute
+// https://github.com/uBlockOrigin/uBlock-issues/issues/1071
+const resourceURLsFromSrcset = function(elem, out) {
+ let srcset = elem.srcset;
+ if ( typeof srcset !== 'string' || srcset === '' ) { return; }
+ for(;;) {
+ // trim whitespace
+ srcset = srcset.trim();
+ if ( srcset.length === 0 ) { break; }
+ // abort in case of leading comma
+ if ( /^,/.test(srcset) ) { break; }
+ // collect and consume all non-whitespace characters
+ let match = /^\S+/.exec(srcset);
+ if ( match === null ) { break; }
+ srcset = srcset.slice(match.index + match[0].length);
+ let url = match[0];
+ // consume descriptor, if any
+ if ( /,$/.test(url) ) {
+ url = url.replace(/,$/, '');
+ if ( /,$/.test(url) ) { break; }
+ } else {
+ match = /^[^,]*(?:\(.+?\))?[^,]*(?:,|$)/.exec(srcset);
+ if ( match === null ) { break; }
+ srcset = srcset.slice(match.index + match[0].length);
+ }
+ const parsedURL = new URL(url, document.baseURI);
+ if ( parsedURL.pathname.length === 0 ) { continue; }
+ out.push(trimFragmentFromURL(parsedURL.href));
+ }
+};
+
+// https://github.com/uBlockOrigin/uBlock-issues/issues/2069#issuecomment-1080600661
+// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture
+const resourceURLsFromPicture = function(elem, out) {
+ if ( elem.localName === 'source' ) { return; }
+ const picture = elem.parentElement;
+ if ( picture === null || picture.localName !== 'picture' ) { return; }
+ const sources = picture.querySelectorAll(':scope > source');
+ for ( const source of sources ) {
+ const urls = resourceURLsFromElement(source);
+ if ( urls.length === 0 ) { continue; }
+ out.push(...urls);
+ }
+};
+
+/******************************************************************************/
+
+const netFilterFromUnion = function(patternIn, out) {
+ // Reset reference filter when dealing with unrelated URLs
+ const currentHostname = self.location.hostname;
+ if (
+ lastNetFilterUnion === '' ||
+ currentHostname === '' ||
+ currentHostname !== lastNetFilterHostname
+ ) {
+ lastNetFilterHostname = currentHostname;
+ lastNetFilterUnion = patternIn;
+ vAPI.messaging.send('elementPicker', {
+ what: 'elementPickerEprom',
+ lastNetFilterSession,
+ lastNetFilterHostname,
+ lastNetFilterUnion,
+ });
+ return;
+ }
+
+ // Related URLs
+ lastNetFilterHostname = currentHostname;
+ let patternOut = mergeStrings([ patternIn, lastNetFilterUnion ]);
+ if ( patternOut !== '/*' && patternOut !== patternIn ) {
+ const filter = `||${patternOut}`;
+ if ( out.indexOf(filter) === -1 ) {
+ out.push(filter);
+ }
+ lastNetFilterUnion = patternOut;
+ }
+
+ // Remember across element picker sessions
+ vAPI.messaging.send('elementPicker', {
+ what: 'elementPickerEprom',
+ lastNetFilterSession,
+ lastNetFilterHostname,
+ lastNetFilterUnion,
+ });
+};
+
+/******************************************************************************/
+
+// Extract the best possible net filter, i.e. as specific as possible.
+
+const netFilterFromElement = function(elem) {
+ if ( elem === null ) { return 0; }
+ if ( elem.nodeType !== 1 ) { return 0; }
+ const urls = resourceURLsFromElement(elem);
+ if ( urls.length === 0 ) { return 0; }
+
+ if ( candidateElements.indexOf(elem) === -1 ) {
+ candidateElements.push(elem);
+ }
+
+ const candidates = netFilterCandidates;
+ const len = candidates.length;
+
+ for ( let i = 0; i < urls.length; i++ ) {
+ urls[i] = urls[i].replace(/^https?:\/\//, '');
+ }
+ const pattern = mergeStrings(urls);
+
+
+ if ( bestCandidateFilter === null && elem.matches('html,body') === false ) {
+ bestCandidateFilter = {
+ type: 'net',
+ filters: candidates,
+ slot: candidates.length
+ };
+ }
+
+ candidates.push(`||${pattern}`);
+
+ // Suggest a less narrow filter if possible
+ const pos = pattern.indexOf('?');
+ if ( pos !== -1 ) {
+ candidates.push(`||${pattern.slice(0, pos)}`);
+ }
+
+ // Suggest a filter which is a result of combining more than one URL.
+ netFilterFromUnion(pattern, candidates);
+
+ return candidates.length - len;
+};
+
+const netFilter1stSources = {
+ 'audio': 'src',
+ 'embed': 'src',
+ 'iframe': 'src',
+ 'img': 'src',
+ 'image': 'href',
+ 'object': 'data',
+ 'source': 'src',
+ 'video': 'src'
+};
+
+const filterTypes = {
+ 'audio': 'media',
+ 'embed': 'object',
+ 'iframe': 'subdocument',
+ 'img': 'image',
+ 'object': 'object',
+ 'video': 'media',
+};
+
+/******************************************************************************/
+
+// Extract the best possible cosmetic filter, i.e. as specific as possible.
+
+// https://github.com/gorhill/uBlock/issues/1725
+// Also take into account the `src` attribute for `img` elements -- and limit
+// the value to the 1024 first characters.
+
+const cosmeticFilterFromElement = function(elem) {
+ if ( elem === null ) { return 0; }
+ if ( elem.nodeType !== 1 ) { return 0; }
+ if ( noCosmeticFiltering ) { return 0; }
+
+ if ( candidateElements.indexOf(elem) === -1 ) {
+ candidateElements.push(elem);
+ }
+
+ let selector = '';
+
+ // Id
+ let v = typeof elem.id === 'string' && CSS.escape(elem.id);
+ if ( v ) {
+ selector = '#' + v;
+ }
+
+ // Class(es)
+ v = elem.classList;
+ if ( v ) {
+ let i = v.length || 0;
+ while ( i-- ) {
+ selector += '.' + CSS.escape(v.item(i));
+ }
+ }
+
+ // Tag name
+ const tagName = CSS.escape(elem.localName);
+
+ // Use attributes if still no selector found.
+ // https://github.com/gorhill/uBlock/issues/1901
+ // Trim attribute value, this may help in case of malformed HTML.
+ //
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/1923
+ // Escape unescaped `"` in attribute values
+ if ( selector === '' ) {
+ let attributes = [], attr;
+ switch ( tagName ) {
+ case 'a':
+ v = elem.getAttribute('href');
+ if ( v ) {
+ v = v.trim().replace(/\?.*$/, '');
+ if ( v.length ) {
+ attributes.push({ k: 'href', v: v });
+ }
+ }
+ break;
+ case 'iframe':
+ case 'img':
+ v = elem.getAttribute('src');
+ if ( v && v.length !== 0 ) {
+ v = v.trim();
+ if ( v.startsWith('data:') ) {
+ let pos = v.indexOf(',');
+ if ( pos !== -1 ) {
+ v = v.slice(0, pos + 1);
+ }
+ } else if ( v.startsWith('blob:') ) {
+ v = new URL(v.slice(5));
+ v.pathname = '';
+ v = 'blob:' + v.href;
+ }
+ attributes.push({ k: 'src', v: v.slice(0, 256) });
+ break;
+ }
+ v = elem.getAttribute('alt');
+ if ( v && v.length !== 0 ) {
+ attributes.push({ k: 'alt', v: v });
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+ while ( (attr = attributes.pop()) ) {
+ if ( attr.v.length === 0 ) { continue; }
+ const w = attr.v.replace(/([^\\])"/g, '$1\\"');
+ v = elem.getAttribute(attr.k);
+ if ( attr.v === v ) {
+ selector += `[${attr.k}="${w}"]`;
+ } else if ( v.startsWith(attr.v) ) {
+ selector += `[${attr.k}^="${w}"]`;
+ } else {
+ selector += `[${attr.k}*="${w}"]`;
+ }
+ }
+ }
+
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/17
+ // If selector is ambiguous at this point, add the element name to
+ // further narrow it down.
+ const parentNode = elem.parentNode;
+ if (
+ selector === '' ||
+ safeQuerySelectorAll(parentNode, `:scope > ${selector}`).length > 1
+ ) {
+ selector = tagName + selector;
+ }
+
+ // https://github.com/chrisaljoudi/uBlock/issues/637
+ // If the selector is still ambiguous at this point, further narrow using
+ // `nth-of-type`. It is preferable to use `nth-of-type` as opposed to
+ // `nth-child`, as `nth-of-type` is less volatile.
+ if ( safeQuerySelectorAll(parentNode, `:scope > ${selector}`).length > 1 ) {
+ let i = 1;
+ while ( elem.previousSibling !== null ) {
+ elem = elem.previousSibling;
+ if (
+ typeof elem.localName === 'string' &&
+ elem.localName === tagName
+ ) {
+ i++;
+ }
+ }
+ selector += `:nth-of-type(${i})`;
+ }
+
+ if ( bestCandidateFilter === null ) {
+ bestCandidateFilter = {
+ type: 'cosmetic',
+ filters: cosmeticFilterCandidates,
+ slot: cosmeticFilterCandidates.length
+ };
+ }
+
+ cosmeticFilterCandidates.push(`##${selector}`);
+
+ return 1;
+};
+
+/******************************************************************************/
+
+const filtersFrom = function(x, y) {
+ bestCandidateFilter = null;
+ netFilterCandidates.length = 0;
+ cosmeticFilterCandidates.length = 0;
+ candidateElements.length = 0;
+
+ // We need at least one element.
+ let first = null;
+ if ( typeof x === 'number' ) {
+ first = elementFromPoint(x, y);
+ } else if ( x instanceof HTMLElement ) {
+ first = x;
+ x = undefined;
+ }
+
+ // https://github.com/gorhill/uBlock/issues/1545
+ // Network filter candidates from all other elements found at [x,y].
+ // https://www.reddit.com/r/uBlockOrigin/comments/qmjk36/
+ // Extract network candidates first.
+ if ( typeof x === 'number' ) {
+ const magicAttr = `${pickerUniqueId}-clickblind`;
+ pickerFrame.setAttribute(magicAttr, '');
+ const elems = document.elementsFromPoint(x, y);
+ pickerFrame.removeAttribute(magicAttr);
+ for ( const elem of elems ) {
+ netFilterFromElement(elem);
+ }
+ } else if ( first !== null ) {
+ netFilterFromElement(first);
+ }
+
+ // Cosmetic filter candidates from ancestors.
+ // https://github.com/gorhill/uBlock/issues/2519
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/17
+ // Prepend `body` if full selector is ambiguous.
+ let elem = first;
+ while ( elem && elem !== document.body ) {
+ cosmeticFilterFromElement(elem);
+ elem = elem.parentNode;
+ }
+ // The body tag is needed as anchor only when the immediate child
+ // uses `nth-of-type`.
+ let i = cosmeticFilterCandidates.length;
+ if ( i !== 0 ) {
+ const selector = cosmeticFilterCandidates[i-1].slice(2);
+ if ( safeQuerySelectorAll(document.body, selector).length > 1 ) {
+ cosmeticFilterCandidates.push('##body');
+ }
+ }
+
+ // https://github.com/gorhill/uBlock/commit/ebaa8a8bb28aef043a68c99965fe6c128a3fe5e4#commitcomment-63818019
+ // If still no best candidate, just use whatever is available in network
+ // filter candidates -- which may have been previously skipped in favor
+ // of cosmetic filters.
+ if ( bestCandidateFilter === null && netFilterCandidates.length !== 0 ) {
+ bestCandidateFilter = {
+ type: 'net',
+ filters: netFilterCandidates,
+ slot: 0
+ };
+ }
+
+ return netFilterCandidates.length + cosmeticFilterCandidates.length;
+};
+
+/*******************************************************************************
+
+ filterToDOMInterface.queryAll
+ @desc Look-up all the HTML elements matching the filter passed in
+ argument.
+ @param string, a cosmetic or network filter.
+ @param function, called once all items matching the filter have been
+ collected.
+ @return array, or undefined if the filter is invalid.
+
+ filterToDOMInterface.preview
+ @desc Apply/unapply filter to the DOM.
+ @param string, a cosmetic of network filter, or literal false to remove
+ the effects of the filter on the DOM.
+ @return undefined.
+
+ TODO: need to be revised once I implement chained cosmetic operators.
+
+*/
+
+const filterToDOMInterface = (( ) => {
+ const reHnAnchorPrefix = '^[\\w-]+://(?:[^/?#]+\\.)?';
+ const reCaret = '(?:[^%.0-9a-z_-]|$)';
+ const rePseudoElements = /:(?::?after|:?before|:[a-z-]+)$/;
+
+ // Net filters: we need to lookup manually -- translating into a foolproof
+ // CSS selector is just not possible.
+ //
+ // https://github.com/chrisaljoudi/uBlock/issues/945
+ // Transform into a regular expression, this allows the user to
+ // edit and insert wildcard(s) into the proposed filter.
+ // https://www.reddit.com/r/uBlockOrigin/comments/c5do7w/
+ // Better handling of pure hostname filters. Also, discard single
+ // alphanumeric character filters.
+ const fromNetworkFilter = function(filter) {
+ const out = [];
+ if ( /^[0-9a-z]$/i.test(filter) ) { return out; }
+ let reStr = '';
+ if (
+ filter.length > 2 &&
+ filter.startsWith('/') &&
+ filter.endsWith('/')
+ ) {
+ reStr = filter.slice(1, -1);
+ } else if ( /^\w[\w.-]*[a-z]$/i.test(filter) ) {
+ reStr = reHnAnchorPrefix +
+ filter.toLowerCase().replace(/\./g, '\\.') +
+ reCaret;
+ } else {
+ let rePrefix = '', reSuffix = '';
+ if ( filter.startsWith('||') ) {
+ rePrefix = reHnAnchorPrefix;
+ filter = filter.slice(2);
+ } else if ( filter.startsWith('|') ) {
+ rePrefix = '^';
+ filter = filter.slice(1);
+ }
+ if ( filter.endsWith('|') ) {
+ reSuffix = '$';
+ filter = filter.slice(0, -1);
+ }
+ reStr = rePrefix +
+ filter.replace(/[.+?${}()|[\]\\]/g, '\\$&')
+ .replace(/\*+/g, '.*')
+ .replace(/\^/g, reCaret) +
+ reSuffix;
+ }
+ let reFilter = null;
+ try {
+ reFilter = new RegExp(reStr, 'i');
+ }
+ catch (e) {
+ return out;
+ }
+
+ // Lookup by tag names.
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/2260
+ // Maybe get to the actual URL indirectly.
+ const elems = document.querySelectorAll(
+ Object.keys(netFilter1stSources).join()
+ );
+ for ( const elem of elems ) {
+ const srcProp = netFilter1stSources[elem.localName];
+ let src = elem[srcProp];
+ if ( src instanceof SVGAnimatedString ) {
+ src = src.baseVal;
+ }
+ if (
+ typeof src === 'string' &&
+ reFilter.test(src) ||
+ typeof elem.currentSrc === 'string' &&
+ reFilter.test(elem.currentSrc)
+ ) {
+ out.push({
+ elem,
+ src: srcProp,
+ opt: filterTypes[elem.localName],
+ style: vAPI.hideStyle,
+ });
+ }
+ }
+
+ // Find matching background image in current set of candidate elements.
+ for ( const elem of candidateElements ) {
+ if ( reFilter.test(backgroundImageURLFromElement(elem)) ) {
+ out.push({
+ elem,
+ bg: true,
+ opt: 'image',
+ style: hideBackgroundStyle,
+ });
+ }
+ }
+
+ return out;
+ };
+
+ // Cosmetic filters: these are straight CSS selectors.
+ //
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/389
+ // Test filter using comma-separated list to better detect invalid CSS
+ // selectors.
+ //
+ // https://github.com/gorhill/uBlock/issues/2515
+ // Remove trailing pseudo-element when querying.
+ const fromPlainCosmeticFilter = function(raw) {
+ let elems;
+ try {
+ document.documentElement.matches(`${raw},\na`);
+ elems = document.querySelectorAll(
+ raw.replace(rePseudoElements, '')
+ );
+ }
+ catch (e) {
+ return;
+ }
+ const out = [];
+ for ( const elem of elems ) {
+ if ( elem === pickerFrame ) { continue; }
+ out.push({ elem, raw, style: vAPI.hideStyle });
+ }
+ return out;
+ };
+
+ // https://github.com/gorhill/uBlock/issues/1772
+ // Handle procedural cosmetic filters.
+ //
+ // https://github.com/gorhill/uBlock/issues/2515
+ // Remove trailing pseudo-element when querying.
+ const fromCompiledCosmeticFilter = function(raw) {
+ if ( noCosmeticFiltering ) { return; }
+ if ( typeof raw !== 'string' ) { return; }
+ let elems, style;
+ try {
+ const o = JSON.parse(raw);
+ elems = vAPI.domFilterer.createProceduralFilter(o).exec();
+ switch ( o.action && o.action[0] || '' ) {
+ case '':
+ case 'remove':
+ style = vAPI.hideStyle;
+ break;
+ case 'style':
+ style = o.action[1];
+ break;
+ default:
+ break;
+ }
+ } catch(ex) {
+ return;
+ }
+ if ( !elems ) { return; }
+ const out = [];
+ for ( const elem of elems ) {
+ out.push({ elem, raw, style });
+ }
+ return out;
+ };
+
+ vAPI.epickerStyleProxies = vAPI.epickerStyleProxies || new Map();
+
+ let lastFilter;
+ let lastResultset;
+ let previewing = false;
+
+ const queryAll = function(details) {
+ let { filter, compiled } = details;
+ filter = filter.trim();
+ if ( filter === lastFilter ) { return lastResultset; }
+ unapply();
+ if ( filter === '' || filter === '!' ) {
+ lastFilter = '';
+ lastResultset = undefined;
+ return;
+ }
+ lastFilter = filter;
+ if ( reCosmeticAnchor.test(filter) === false ) {
+ lastResultset = fromNetworkFilter(filter);
+ if ( previewing ) { apply(); }
+ return lastResultset;
+ }
+ lastResultset = fromPlainCosmeticFilter(compiled);
+ if ( lastResultset ) {
+ if ( previewing ) { apply(); }
+ return lastResultset;
+ }
+ // Procedural cosmetic filter
+ lastResultset = fromCompiledCosmeticFilter(compiled);
+ if ( previewing ) { apply(); }
+ return lastResultset;
+ };
+
+ const apply = function() {
+ unapply();
+ if ( Array.isArray(lastResultset) === false ) { return; }
+ const rootElem = document.documentElement;
+ for ( const { elem, style } of lastResultset ) {
+ if ( elem === pickerFrame ) { continue; }
+ if ( style === undefined ) { continue; }
+ if ( elem === rootElem && style === vAPI.hideStyle ) { continue; }
+ let styleToken = vAPI.epickerStyleProxies.get(style);
+ if ( styleToken === undefined ) {
+ styleToken = vAPI.randomToken();
+ vAPI.epickerStyleProxies.set(style, styleToken);
+ vAPI.userStylesheet.add(`[${styleToken}]\n{${style}}`, true);
+ }
+ elem.setAttribute(styleToken, '');
+ }
+ };
+
+ const unapply = function() {
+ for ( const styleToken of vAPI.epickerStyleProxies.values() ) {
+ for ( const elem of document.querySelectorAll(`[${styleToken}]`) ) {
+ elem.removeAttribute(styleToken);
+ }
+ }
+ };
+
+ // https://www.reddit.com/r/uBlockOrigin/comments/c62irc/
+ // Support injecting the cosmetic filters into the DOM filterer
+ // immediately rather than wait for the next page load.
+ const preview = function(state, permanent = false) {
+ previewing = state !== false;
+ if ( previewing === false ) {
+ return unapply();
+ }
+ if ( Array.isArray(lastResultset) === false ) { return; }
+ if ( permanent === false || reCosmeticAnchor.test(lastFilter) === false ) {
+ return apply();
+ }
+ if ( noCosmeticFiltering ) { return; }
+ const cssSelectors = new Set();
+ const proceduralSelectors = new Set();
+ for ( const { raw } of lastResultset ) {
+ if ( raw.startsWith('{') ) {
+ proceduralSelectors.add(raw);
+ } else {
+ cssSelectors.add(raw);
+ }
+ }
+ if ( cssSelectors.size !== 0 ) {
+ vAPI.domFilterer.addCSS(
+ `${Array.from(cssSelectors).join('\n')}\n{${vAPI.hideStyle}}`,
+ { mustInject: true }
+ );
+ }
+ if ( proceduralSelectors.size !== 0 ) {
+ vAPI.domFilterer.addProceduralSelectors(
+ Array.from(proceduralSelectors)
+ );
+ }
+ };
+
+ return { preview, queryAll };
+})();
+
+/******************************************************************************/
+
+const onOptimizeCandidates = function(details) {
+ const { candidates } = details;
+ const results = [];
+ for ( const paths of candidates ) {
+ let count = Number.MAX_SAFE_INTEGER;
+ let selector = '';
+ for ( let i = 0, n = paths.length; i < n; i++ ) {
+ const s = paths.slice(n - i - 1).join('');
+ const elems = document.querySelectorAll(s);
+ if ( elems.length < count ) {
+ selector = s;
+ count = elems.length;
+ }
+ }
+ results.push({ selector: `##${selector}`, count });
+ }
+ // Sort by most match count and shortest selector to least match count and
+ // longest selector.
+ results.sort((a, b) => {
+ const r = b.count - a.count;
+ if ( r !== 0 ) { return r; }
+ return a.selector.length - b.selector.length;
+ });
+ pickerFramePort.postMessage({
+ what: 'candidatesOptimized',
+ candidates: results.map(a => a.selector),
+ slot: details.slot,
+ });
+};
+
+/******************************************************************************/
+
+const showDialog = function(options) {
+ pickerFramePort.postMessage({
+ what: 'showDialog',
+ url: self.location.href,
+ netFilters: netFilterCandidates,
+ cosmeticFilters: cosmeticFilterCandidates,
+ filter: bestCandidateFilter,
+ options,
+ });
+};
+
+/******************************************************************************/
+
+const elementFromPoint = (( ) => {
+ let lastX, lastY;
+
+ return (x, y) => {
+ if ( x !== undefined ) {
+ lastX = x; lastY = y;
+ } else if ( lastX !== undefined ) {
+ x = lastX; y = lastY;
+ } else {
+ return null;
+ }
+ if ( !pickerFrame ) { return null; }
+ const magicAttr = `${pickerUniqueId}-clickblind`;
+ pickerFrame.setAttribute(magicAttr, '');
+ let elem = document.elementFromPoint(x, y);
+ if (
+ elem === null || /* to skip following tests */
+ elem === document.body ||
+ elem === document.documentElement || (
+ pickerBootArgs.zap !== true &&
+ noCosmeticFiltering &&
+ resourceURLsFromElement(elem).length === 0
+ )
+ ) {
+ elem = null;
+ }
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/380
+ pickerFrame.removeAttribute(magicAttr);
+ return elem;
+ };
+})();
+
+/******************************************************************************/
+
+const highlightElementAtPoint = function(mx, my) {
+ const elem = elementFromPoint(mx, my);
+ highlightElements(elem ? [ elem ] : []);
+};
+
+/******************************************************************************/
+
+const filterElementAtPoint = function(mx, my, broad) {
+ if ( filtersFrom(mx, my) === 0 ) { return; }
+ showDialog({ broad });
+};
+
+/******************************************************************************/
+
+// https://www.reddit.com/r/uBlockOrigin/comments/bktxtb/scrolling_doesnt_work/emn901o
+// Override 'fixed' position property on body element if present.
+
+// With touch-driven devices, first highlight the element and remove only
+// when tapping again the highlighted area.
+
+const zapElementAtPoint = function(mx, my, options) {
+ if ( options.highlight ) {
+ const elem = elementFromPoint(mx, my);
+ if ( elem ) {
+ highlightElements([ elem ]);
+ }
+ return;
+ }
+
+ let elemToRemove = targetElements.length !== 0 && targetElements[0] || null;
+ if ( elemToRemove === null && mx !== undefined ) {
+ elemToRemove = elementFromPoint(mx, my);
+ }
+
+ if ( elemToRemove instanceof Element === false ) { return; }
+
+ const getStyleValue = (elem, prop) => {
+ const style = window.getComputedStyle(elem);
+ return style ? style[prop] : '';
+ };
+
+ // Heuristic to detect scroll-locking: remove such lock when detected.
+ let maybeScrollLocked = elemToRemove.shadowRoot instanceof DocumentFragment;
+ if ( maybeScrollLocked === false ) {
+ let elem = elemToRemove;
+ do {
+ maybeScrollLocked =
+ parseInt(getStyleValue(elem, 'zIndex'), 10) >= 1000 ||
+ getStyleValue(elem, 'position') === 'fixed';
+ elem = elem.parentElement;
+ } while ( elem !== null && maybeScrollLocked === false );
+ }
+ if ( maybeScrollLocked ) {
+ const doc = document;
+ if ( getStyleValue(doc.body, 'overflowY') === 'hidden' ) {
+ doc.body.style.setProperty('overflow', 'auto', 'important');
+ }
+ if ( getStyleValue(doc.body, 'position') === 'fixed' ) {
+ doc.body.style.setProperty('position', 'initial', 'important');
+ }
+ if ( getStyleValue(doc.documentElement, 'position') === 'fixed' ) {
+ doc.documentElement.style.setProperty('position', 'initial', 'important');
+ }
+ if ( getStyleValue(doc.documentElement, 'overflowY') === 'hidden' ) {
+ doc.documentElement.style.setProperty('overflow', 'auto', 'important');
+ }
+ }
+ elemToRemove.remove();
+ highlightElementAtPoint(mx, my);
+};
+
+/******************************************************************************/
+
+const onKeyPressed = function(ev) {
+ // Delete
+ if (
+ (ev.key === 'Delete' || ev.key === 'Backspace') &&
+ pickerBootArgs.zap
+ ) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ zapElementAtPoint();
+ return;
+ }
+ // Esc
+ if ( ev.key === 'Escape' || ev.which === 27 ) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ filterToDOMInterface.preview(false);
+ quitPicker();
+ return;
+ }
+};
+
+/******************************************************************************/
+
+// https://github.com/chrisaljoudi/uBlock/issues/190
+// May need to dynamically adjust the height of the overlay + new position
+// of highlighted elements.
+
+const onViewportChanged = function() {
+ highlightElements(targetElements, true);
+};
+
+/******************************************************************************/
+
+// Auto-select a specific target, if any, and if possible
+
+const startPicker = function() {
+ pickerFrame.focus();
+
+ self.addEventListener('scroll', onViewportChanged, { passive: true });
+ self.addEventListener('resize', onViewportChanged, { passive: true });
+ self.addEventListener('keydown', onKeyPressed, true);
+
+ // Try using mouse position
+ if (
+ pickerBootArgs.mouse &&
+ vAPI.mouseClick instanceof Object &&
+ typeof vAPI.mouseClick.x === 'number' &&
+ vAPI.mouseClick.x > 0
+ ) {
+ if ( filtersFrom(vAPI.mouseClick.x, vAPI.mouseClick.y) !== 0 ) {
+ return showDialog();
+ }
+ }
+
+ // No mouse position available, use suggested target
+ const target = pickerBootArgs.target || '';
+ const pos = target.indexOf('\t');
+ if ( pos === -1 ) { return; }
+
+ const srcAttrMap = {
+ 'a': 'href',
+ 'audio': 'src',
+ 'embed': 'src',
+ 'iframe': 'src',
+ 'img': 'src',
+ 'video': 'src',
+ };
+ const tagName = target.slice(0, pos);
+ const url = target.slice(pos + 1);
+ const attr = srcAttrMap[tagName];
+ if ( attr === undefined ) { return; }
+ const elems = document.getElementsByTagName(tagName);
+ for ( const elem of elems ) {
+ if ( elem === pickerFrame ) { continue; }
+ const srcs = resourceURLsFromElement(elem);
+ if (
+ (srcs.length !== 0 && srcs.includes(url) === false) ||
+ (srcs.length === 0 && url !== 'about:blank')
+ ) {
+ continue;
+ }
+ filtersFrom(elem);
+ if (
+ netFilterCandidates.length !== 0 ||
+ cosmeticFilterCandidates.length !== 0
+ ) {
+ if ( pickerBootArgs.mouse !== true ) {
+ elem.scrollIntoView({
+ behavior: 'smooth',
+ block: 'center',
+ inline: 'center'
+ });
+ }
+ showDialog({ broad: true });
+ }
+ return;
+ }
+
+ // A target was specified, but it wasn't found: abort.
+ quitPicker();
+};
+
+/******************************************************************************/
+
+// Let's have the element picker code flushed from memory when no longer
+// in use: to ensure this, release all local references.
+
+const quitPicker = function() {
+ self.removeEventListener('scroll', onViewportChanged, { passive: true });
+ self.removeEventListener('resize', onViewportChanged, { passive: true });
+ self.removeEventListener('keydown', onKeyPressed, true);
+ vAPI.shutdown.remove(quitPicker);
+ if ( pickerFramePort ) {
+ pickerFramePort.close();
+ pickerFramePort = null;
+ }
+ if ( pickerFrame ) {
+ pickerFrame.remove();
+ pickerFrame = null;
+ }
+ vAPI.userStylesheet.remove(pickerCSS);
+ vAPI.userStylesheet.apply();
+ vAPI.pickerFrame = false;
+ self.focus();
+};
+
+vAPI.shutdown.add(quitPicker);
+
+/******************************************************************************/
+
+const onDialogMessage = function(msg) {
+ switch ( msg.what ) {
+ case 'start':
+ startPicker();
+ if ( pickerFramePort === null ) { break; }
+ if ( targetElements.length === 0 ) {
+ highlightElements([], true);
+ }
+ break;
+ case 'optimizeCandidates':
+ onOptimizeCandidates(msg);
+ break;
+ case 'dialogCreate':
+ filterToDOMInterface.queryAll(msg);
+ filterToDOMInterface.preview(true, true);
+ quitPicker();
+ break;
+ case 'dialogSetFilter': {
+ const resultset = filterToDOMInterface.queryAll(msg) || [];
+ highlightElements(resultset.map(a => a.elem), true);
+ if ( msg.filter === '!' ) { break; }
+ pickerFramePort.postMessage({
+ what: 'resultsetDetails',
+ count: resultset.length,
+ opt: resultset.length !== 0 ? resultset[0].opt : undefined,
+ });
+ break;
+ }
+ case 'quitPicker':
+ filterToDOMInterface.preview(false);
+ quitPicker();
+ break;
+ case 'highlightElementAtPoint':
+ highlightElementAtPoint(msg.mx, msg.my);
+ break;
+ case 'unhighlight':
+ highlightElements([]);
+ break;
+ case 'filterElementAtPoint':
+ filterElementAtPoint(msg.mx, msg.my, msg.broad);
+ break;
+ case 'zapElementAtPoint':
+ zapElementAtPoint(msg.mx, msg.my, msg.options);
+ if ( msg.options.highlight !== true && msg.options.stay !== true ) {
+ quitPicker();
+ }
+ break;
+ case 'togglePreview':
+ filterToDOMInterface.preview(msg.state);
+ if ( msg.state === false ) {
+ highlightElements(targetElements, true);
+ }
+ break;
+ default:
+ break;
+ }
+};
+
+/******************************************************************************/
+
+// epicker-ui.html will be injected in the page through an iframe, and
+// is a sandboxed so as to prevent the page from interfering with its
+// content and behavior.
+//
+// The purpose of epicker.js is to:
+// - Install the element picker UI, and wait for the component to establish
+// a direct communication channel.
+// - Lookup candidate filters from elements at a specific position.
+// - Highlight element(s) at a specific position or according to whether
+// they match candidate filters;
+// - Preview the result of applying a candidate filter;
+//
+// When the element picker is installed on a page, the only change the page
+// sees is an iframe with a random attribute. The page can't see the content
+// of the iframe, and cannot interfere with its style properties. However the
+// page can remove the iframe.
+
+// The DOM filterer will not be present when cosmetic filtering is disabled.
+const noCosmeticFiltering =
+ vAPI.domFilterer instanceof Object === false ||
+ vAPI.noSpecificCosmeticFiltering === true;
+
+// https://github.com/gorhill/uBlock/issues/1529
+// In addition to inline styles, harden the element picker styles by using
+// dedicated CSS rules.
+const pickerCSSStyle = [
+ 'background: transparent',
+ 'border: 0',
+ 'border-radius: 0',
+ 'box-shadow: none',
+ 'color-scheme: light dark',
+ 'display: block',
+ 'filter: none',
+ 'height: 100vh',
+ 'left: 0',
+ 'margin: 0',
+ 'max-height: none',
+ 'max-width: none',
+ 'min-height: unset',
+ 'min-width: unset',
+ 'opacity: 1',
+ 'outline: 0',
+ 'padding: 0',
+ 'pointer-events: auto',
+ 'position: fixed',
+ 'top: 0',
+ 'transform: none',
+ 'visibility: hidden',
+ 'width: 100%',
+ 'z-index: 2147483647',
+ ''
+].join(' !important;\n');
+
+
+const pickerCSS = `
+:root > [${pickerUniqueId}] {
+ ${pickerCSSStyle}
+}
+:root > [${pickerUniqueId}-loaded] {
+ visibility: visible !important;
+}
+:root [${pickerUniqueId}-clickblind] {
+ pointer-events: none !important;
+}
+`;
+
+vAPI.userStylesheet.add(pickerCSS);
+vAPI.userStylesheet.apply();
+
+let pickerBootArgs;
+let pickerFramePort = null;
+
+const bootstrap = async ( ) => {
+ pickerBootArgs = await vAPI.messaging.send('elementPicker', {
+ what: 'elementPickerArguments',
+ });
+ if ( typeof pickerBootArgs !== 'object' ) { return; }
+ if ( pickerBootArgs === null ) { return; }
+ // Restore net filter union data if origin is the same.
+ const eprom = pickerBootArgs.eprom || null;
+ if ( eprom !== null && eprom.lastNetFilterSession === lastNetFilterSession ) {
+ lastNetFilterHostname = eprom.lastNetFilterHostname || '';
+ lastNetFilterUnion = eprom.lastNetFilterUnion || '';
+ }
+ const url = new URL(pickerBootArgs.pickerURL);
+ if ( pickerBootArgs.zap ) {
+ url.searchParams.set('zap', '1');
+ }
+ return new Promise(resolve => {
+ const iframe = document.createElement('iframe');
+ iframe.setAttribute(pickerUniqueId, '');
+ document.documentElement.append(iframe);
+ iframe.addEventListener('load', ( ) => {
+ iframe.setAttribute(`${pickerUniqueId}-loaded`, '');
+ const channel = new MessageChannel();
+ pickerFramePort = channel.port1;
+ pickerFramePort.onmessage = ev => {
+ onDialogMessage(ev.data || {});
+ };
+ pickerFramePort.onmessageerror = ( ) => {
+ quitPicker();
+ };
+ iframe.contentWindow.postMessage(
+ { what: 'epickerStart' },
+ url.href,
+ [ channel.port2 ]
+ );
+ resolve(iframe);
+ }, { once: true });
+ iframe.contentWindow.location = url.href;
+ });
+};
+
+let pickerFrame = await bootstrap();
+if ( Boolean(pickerFrame) === false ) {
+ quitPicker();
+}
+
+/******************************************************************************/
+
+})();
+
+
+
+
+
+
+
+
+/*******************************************************************************
+
+ DO NOT:
+ - Remove the following code
+ - Add code beyond the following code
+ Reason:
+ - https://github.com/gorhill/uBlock/pull/3721
+ - uBO never uses the return value from injected content scripts
+
+**/
+
+void 0;
diff --git a/src/js/scriptlets/load-3p-css.js b/src/js/scriptlets/load-3p-css.js
new file mode 100644
index 0000000..bb7d542
--- /dev/null
+++ b/src/js/scriptlets/load-3p-css.js
@@ -0,0 +1,67 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2020-present Raymond Hill
+
+ 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 {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+'use strict';
+
+/******************************************************************************/
+
+(( ) => {
+ if ( typeof vAPI !== 'object' ) { return; }
+
+ if ( vAPI.dynamicReloadToken === undefined ) {
+ vAPI.dynamicReloadToken = vAPI.randomToken();
+ }
+
+ for ( const sheet of Array.from(document.styleSheets) ) {
+ let loaded = false;
+ try {
+ loaded = sheet.rules.length !== 0;
+ } catch(ex) {
+ }
+ if ( loaded ) { continue; }
+ const link = sheet.ownerNode || null;
+ if ( link === null || link.localName !== 'link' ) { continue; }
+ if ( link.hasAttribute(vAPI.dynamicReloadToken) ) { continue; }
+ const clone = link.cloneNode(true);
+ clone.setAttribute(vAPI.dynamicReloadToken, '');
+ link.replaceWith(clone);
+ }
+})();
+
+
+
+
+
+
+
+
+/*******************************************************************************
+
+ DO NOT:
+ - Remove the following code
+ - Add code beyond the following code
+ Reason:
+ - https://github.com/gorhill/uBlock/pull/3721
+ - uBO never uses the return value from injected content scripts
+
+**/
+
+void 0;
diff --git a/src/js/scriptlets/load-large-media-all.js b/src/js/scriptlets/load-large-media-all.js
new file mode 100644
index 0000000..a44539e
--- /dev/null
+++ b/src/js/scriptlets/load-large-media-all.js
@@ -0,0 +1,62 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2015-2018 Raymond Hill
+
+ 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 {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+'use strict';
+
+/******************************************************************************/
+
+(( ) => {
+
+/******************************************************************************/
+
+if (
+ typeof vAPI !== 'object' ||
+ vAPI.loadAllLargeMedia instanceof Function === false
+) {
+ return;
+}
+
+vAPI.loadAllLargeMedia();
+vAPI.loadAllLargeMedia = undefined;
+
+/******************************************************************************/
+
+})();
+
+
+
+
+
+
+
+
+/*******************************************************************************
+
+ DO NOT:
+ - Remove the following code
+ - Add code beyond the following code
+ Reason:
+ - https://github.com/gorhill/uBlock/pull/3721
+ - uBO never uses the return value from injected content scripts
+
+**/
+
+void 0;
diff --git a/src/js/scriptlets/load-large-media-interactive.js b/src/js/scriptlets/load-large-media-interactive.js
new file mode 100644
index 0000000..57198e4
--- /dev/null
+++ b/src/js/scriptlets/load-large-media-interactive.js
@@ -0,0 +1,299 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2015-present Raymond Hill
+
+ 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 {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+'use strict';
+
+/******************************************************************************/
+
+(( ) => {
+
+/******************************************************************************/
+
+// This can happen
+if ( typeof vAPI !== 'object' || vAPI.loadAllLargeMedia instanceof Function ) {
+ return;
+}
+
+/******************************************************************************/
+
+const largeMediaElementAttribute = 'data-' + vAPI.sessionId;
+const largeMediaElementSelector =
+ ':root audio[' + largeMediaElementAttribute + '],\n' +
+ ':root img[' + largeMediaElementAttribute + '],\n' +
+ ':root picture[' + largeMediaElementAttribute + '],\n' +
+ ':root video[' + largeMediaElementAttribute + ']';
+
+/******************************************************************************/
+
+const isMediaElement = function(elem) {
+ return /^(?:audio|img|picture|video)$/.test(elem.localName);
+};
+
+/******************************************************************************/
+
+const mediaNotLoaded = function(elem) {
+ switch ( elem.localName ) {
+ case 'audio':
+ case 'video': {
+ const src = elem.src || '';
+ if ( src.startsWith('blob:') ) {
+ elem.autoplay = false;
+ elem.pause();
+ }
+ return elem.readyState === 0 || elem.error !== null;
+ }
+ case 'img': {
+ if ( elem.naturalWidth !== 0 || elem.naturalHeight !== 0 ) {
+ break;
+ }
+ const style = window.getComputedStyle(elem);
+ // For some reason, style can be null with Pale Moon.
+ return style !== null ?
+ style.getPropertyValue('display') !== 'none' :
+ elem.offsetHeight !== 0 && elem.offsetWidth !== 0;
+ }
+ default:
+ break;
+ }
+ return false;
+};
+
+/******************************************************************************/
+
+// For all media resources which have failed to load, trigger a reload.
+
+// <audio> and <video> elements.
+// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement
+
+const surveyMissingMediaElements = function() {
+ let largeMediaElementCount = 0;
+ for ( const elem of document.querySelectorAll('audio,img,video') ) {
+ if ( mediaNotLoaded(elem) === false ) { continue; }
+ elem.setAttribute(largeMediaElementAttribute, '');
+ largeMediaElementCount += 1;
+ switch ( elem.localName ) {
+ case 'img': {
+ const picture = elem.closest('picture');
+ if ( picture !== null ) {
+ picture.setAttribute(largeMediaElementAttribute, '');
+ }
+ } break;
+ default:
+ break;
+ }
+ }
+ return largeMediaElementCount;
+};
+
+if ( surveyMissingMediaElements() === 0 ) { return; }
+
+// Insert CSS to highlight blocked media elements.
+if ( vAPI.largeMediaElementStyleSheet === undefined ) {
+ vAPI.largeMediaElementStyleSheet = [
+ largeMediaElementSelector + ' {',
+ 'border: 2px dotted red !important;',
+ 'box-sizing: border-box !important;',
+ 'cursor: zoom-in !important;',
+ 'display: inline-block;',
+ 'filter: none !important;',
+ 'font-size: 1rem !important;',
+ 'min-height: 1em !important;',
+ 'min-width: 1em !important;',
+ 'opacity: 1 !important;',
+ 'outline: none !important;',
+ 'transform: none !important;',
+ 'visibility: visible !important;',
+ 'z-index: 2147483647',
+ '}',
+ ].join('\n');
+ vAPI.userStylesheet.add(vAPI.largeMediaElementStyleSheet);
+ vAPI.userStylesheet.apply();
+}
+
+/******************************************************************************/
+
+const loadMedia = async function(elem) {
+ const src = elem.getAttribute('src') || '';
+ elem.removeAttribute('src');
+
+ await vAPI.messaging.send('scriptlets', {
+ what: 'temporarilyAllowLargeMediaElement',
+ });
+
+ if ( src !== '' ) {
+ elem.setAttribute('src', src);
+ }
+ elem.load();
+};
+
+/******************************************************************************/
+
+const loadImage = async function(elem) {
+ const src = elem.getAttribute('src') || '';
+ elem.removeAttribute('src');
+
+ await vAPI.messaging.send('scriptlets', {
+ what: 'temporarilyAllowLargeMediaElement',
+ });
+
+ if ( src !== '' ) {
+ elem.setAttribute('src', src);
+ }
+};
+
+/******************************************************************************/
+
+const loadMany = function(elems) {
+ for ( const elem of elems ) {
+ switch ( elem.localName ) {
+ case 'audio':
+ case 'video':
+ loadMedia(elem);
+ break;
+ case 'img':
+ loadImage(elem);
+ break;
+ default:
+ break;
+ }
+ }
+};
+
+/******************************************************************************/
+
+const onMouseClick = function(ev) {
+ if ( ev.button !== 0 || ev.isTrusted === false ) { return; }
+
+ const toLoad = [];
+ const elems = document.elementsFromPoint instanceof Function
+ ? document.elementsFromPoint(ev.clientX, ev.clientY)
+ : [ ev.target ];
+ for ( const elem of elems ) {
+ if ( elem.matches(largeMediaElementSelector) === false ) { continue; }
+ elem.removeAttribute(largeMediaElementAttribute);
+ if ( mediaNotLoaded(elem) ) {
+ toLoad.push(elem);
+ }
+ }
+
+ if ( toLoad.length === 0 ) { return; }
+
+ loadMany(toLoad);
+
+ ev.preventDefault();
+ ev.stopPropagation();
+};
+
+document.addEventListener('click', onMouseClick, true);
+
+/******************************************************************************/
+
+const onLoadedData = function(ev) {
+ const media = ev.target;
+ if ( media.localName !== 'audio' && media.localName !== 'video' ) {
+ return;
+ }
+ const src = media.src;
+ if ( typeof src === 'string' && src.startsWith('blob:') === false ) {
+ return;
+ }
+ media.autoplay = false;
+ media.pause();
+};
+
+// https://www.reddit.com/r/uBlockOrigin/comments/mxgpmc/
+// Support cases where the media source is not yet set.
+for ( const media of document.querySelectorAll('audio,video') ) {
+ const src = media.src;
+ if (
+ (typeof src === 'string') &&
+ (src === '' || src.startsWith('blob:'))
+ ) {
+ media.autoplay = false;
+ media.pause();
+ }
+}
+
+document.addEventListener('loadeddata', onLoadedData);
+
+/******************************************************************************/
+
+const onLoad = function(ev) {
+ const elem = ev.target;
+ if ( isMediaElement(elem) === false ) { return; }
+ elem.removeAttribute(largeMediaElementAttribute);
+};
+
+document.addEventListener('load', onLoad, true);
+
+/******************************************************************************/
+
+const onLoadError = function(ev) {
+ const elem = ev.target;
+ if ( isMediaElement(elem) === false ) { return; }
+ if ( mediaNotLoaded(elem) ) {
+ elem.setAttribute(largeMediaElementAttribute, '');
+ }
+};
+
+document.addEventListener('error', onLoadError, true);
+
+/******************************************************************************/
+
+vAPI.loadAllLargeMedia = function() {
+ document.removeEventListener('click', onMouseClick, true);
+ document.removeEventListener('loadeddata', onLoadedData, true);
+ document.removeEventListener('load', onLoad, true);
+ document.removeEventListener('error', onLoadError, true);
+
+ const toLoad = [];
+ for ( const elem of document.querySelectorAll(largeMediaElementSelector) ) {
+ elem.removeAttribute(largeMediaElementAttribute);
+ if ( mediaNotLoaded(elem) ) {
+ toLoad.push(elem);
+ }
+ }
+ loadMany(toLoad);
+};
+
+/******************************************************************************/
+
+})();
+
+
+
+
+
+
+
+
+/*******************************************************************************
+
+ DO NOT:
+ - Remove the following code
+ - Add code beyond the following code
+ Reason:
+ - https://github.com/gorhill/uBlock/pull/3721
+ - uBO never uses the return value from injected content scripts
+
+**/
+
+void 0;
diff --git a/src/js/scriptlets/noscript-spoof.js b/src/js/scriptlets/noscript-spoof.js
new file mode 100644
index 0000000..49e9093
--- /dev/null
+++ b/src/js/scriptlets/noscript-spoof.js
@@ -0,0 +1,89 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2014-present Raymond Hill
+
+ 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 {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+// Code below has been imported from uMatrix and modified to fit uBO:
+// https://github.com/gorhill/uMatrix/blob/3f8794dd899a05e066c24066c6c0a2515d5c60d2/src/js/contentscript.js#L464-L531
+
+'use strict';
+
+/******************************************************************************/
+
+// https://github.com/gorhill/uMatrix/issues/232
+// Force `display` property, Firefox is still affected by the issue.
+
+(( ) => {
+ const noscripts = document.querySelectorAll('noscript');
+ if ( noscripts.length === 0 ) { return; }
+
+ const reMetaContent = /^\s*(\d+)\s*;\s*url=(?:"([^"]+)"|'([^']+)'|(.+))/i;
+ const reSafeURL = /^https?:\/\//;
+ let redirectTimer;
+
+ const autoRefresh = function(root) {
+ const meta = root.querySelector('meta[http-equiv="refresh"][content]');
+ if ( meta === null ) { return; }
+ const match = reMetaContent.exec(meta.getAttribute('content'));
+ if ( match === null ) { return; }
+ const refreshURL = (match[2] || match[3] || match[4] || '').trim();
+ let url;
+ try {
+ url = new URL(refreshURL, document.baseURI);
+ } catch(ex) {
+ return;
+ }
+ if ( reSafeURL.test(url.href) === false ) { return; }
+ redirectTimer = setTimeout(( ) => {
+ location.assign(url.href);
+ },
+ parseInt(match[1], 10) * 1000 + 1
+ );
+ meta.parentNode.removeChild(meta);
+ };
+
+ const morphNoscript = function(from) {
+ if ( /^application\/(?:xhtml\+)?xml/.test(document.contentType) ) {
+ const to = document.createElement('span');
+ while ( from.firstChild !== null ) {
+ to.appendChild(from.firstChild);
+ }
+ return to;
+ }
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(
+ '<span>' + from.textContent + '</span>',
+ 'text/html'
+ );
+ return document.adoptNode(doc.querySelector('span'));
+ };
+
+ for ( const noscript of noscripts ) {
+ const parent = noscript.parentNode;
+ if ( parent === null ) { continue; }
+ const span = morphNoscript(noscript);
+ span.style.setProperty('display', 'inline', 'important');
+ if ( redirectTimer === undefined ) {
+ autoRefresh(span);
+ }
+ parent.replaceChild(span, noscript);
+ }
+})();
+
+/******************************************************************************/
diff --git a/src/js/scriptlets/should-inject-contentscript.js b/src/js/scriptlets/should-inject-contentscript.js
new file mode 100644
index 0000000..b9a2658
--- /dev/null
+++ b/src/js/scriptlets/should-inject-contentscript.js
@@ -0,0 +1,40 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2018-present Raymond Hill
+
+ 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 {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+'use strict';
+
+// If content scripts are already injected, we need to respond with `false`,
+// to "should inject content scripts?"
+//
+// https://github.com/uBlockOrigin/uBlock-issues/issues/403
+// If the content script was not bootstrapped, give it another try.
+
+(( ) => {
+ try {
+ let status = vAPI.uBO !== true;
+ if ( status === false && vAPI.bootstrap ) {
+ self.requestIdleCallback(( ) => vAPI && vAPI.bootstrap());
+ }
+ return status;
+ } catch(ex) {
+ }
+ return true;
+})();
diff --git a/src/js/scriptlets/subscriber.js b/src/js/scriptlets/subscriber.js
new file mode 100644
index 0000000..ea7b209
--- /dev/null
+++ b/src/js/scriptlets/subscriber.js
@@ -0,0 +1,113 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2015-present Raymond Hill
+
+ 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 {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+/* global HTMLDocument */
+
+'use strict';
+
+/******************************************************************************/
+
+// Injected into specific web pages, those which have been pre-selected
+// because they are known to contains `abp:subscribe` links.
+
+/******************************************************************************/
+
+(( ) => {
+// >>>>> start of local scope
+
+/******************************************************************************/
+
+// https://github.com/chrisaljoudi/uBlock/issues/464
+if ( document instanceof HTMLDocument === false ) { return; }
+
+// Maybe uBO has gone away meanwhile.
+if ( typeof vAPI !== 'object' || vAPI === null ) { return; }
+
+const onMaybeSubscriptionLinkClicked = function(target) {
+ if ( vAPI instanceof Object === false ) {
+ document.removeEventListener('click', onMaybeSubscriptionLinkClicked);
+ return;
+ }
+
+ try {
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/763#issuecomment-691696716
+ // Remove replacement patch if/when filterlists.com fixes encoded '&'.
+ const subscribeURL = new URL(
+ target.href.replace('&amp;title=', '&title=')
+ );
+ if (
+ /^(abp|ubo):$/.test(subscribeURL.protocol) === false &&
+ subscribeURL.hostname !== 'subscribe.adblockplus.org'
+ ) {
+ return;
+ }
+ const location = subscribeURL.searchParams.get('location') || '';
+ const title = subscribeURL.searchParams.get('title') || '';
+ if ( location === '' || title === '' ) { return true; }
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/1797
+ if ( /^(file|https?):\/\//.test(location) === false ) { return true; }
+ vAPI.messaging.send('scriptlets', {
+ what: 'subscribeTo',
+ location,
+ title,
+ });
+ return true;
+ } catch (_) {
+ }
+};
+
+// https://github.com/easylist/EasyListHebrew/issues/89
+// Ensure trusted events only.
+
+document.addEventListener('click', ev => {
+ if ( ev.button !== 0 || ev.isTrusted === false ) { return; }
+ const target = ev.target.closest('a');
+ if ( target instanceof HTMLAnchorElement === false ) { return; }
+ if ( onMaybeSubscriptionLinkClicked(target) === true ) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ }
+});
+
+/******************************************************************************/
+
+// <<<<< end of local scope
+})();
+
+
+
+
+
+
+
+
+/*******************************************************************************
+
+ DO NOT:
+ - Remove the following code
+ - Add code beyond the following code
+ Reason:
+ - https://github.com/gorhill/uBlock/pull/3721
+ - uBO never uses the return value from injected content scripts
+
+**/
+
+void 0;
diff --git a/src/js/scriptlets/updater.js b/src/js/scriptlets/updater.js
new file mode 100644
index 0000000..006b663
--- /dev/null
+++ b/src/js/scriptlets/updater.js
@@ -0,0 +1,118 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2014-present Raymond Hill
+
+ 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 {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+/* global HTMLDocument */
+
+'use strict';
+
+/******************************************************************************/
+
+// Injected into specific webpages, those which have been pre-selected
+// because they are known to contain:
+// https://ublockorigin.github.io/uAssets/update-lists?listkeys=[...]
+
+/******************************************************************************/
+
+(( ) => {
+// >>>>> start of local scope
+
+/******************************************************************************/
+
+if ( document instanceof HTMLDocument === false ) { return; }
+
+// Maybe uBO has gone away meanwhile.
+if ( typeof vAPI !== 'object' || vAPI === null ) { return; }
+
+function updateStockLists(target) {
+ if ( vAPI instanceof Object === false ) {
+ document.removeEventListener('click', updateStockLists);
+ return;
+ }
+ try {
+ const updateURL = new URL(target.href);
+ if ( updateURL.hostname !== 'ublockorigin.github.io') { return; }
+ if ( updateURL.pathname !== '/uAssets/update-lists.html') { return; }
+ const listkeys = updateURL.searchParams.get('listkeys') || '';
+ if ( listkeys === '' ) { return; }
+ let auto = true;
+ const manual = updateURL.searchParams.get('manual');
+ if ( manual === '1' ) {
+ auto = false;
+ } else if ( /^\d{6}$/.test(`${manual}`) ) {
+ const year = parseInt(manual.slice(0,2)) || 0;
+ const month = parseInt(manual.slice(2,4)) || 0;
+ const day = parseInt(manual.slice(4,6)) || 0;
+ if ( year !== 0 && month !== 0 && day !== 0 ) {
+ const date = new Date();
+ date.setUTCFullYear(2000 + year, month - 1, day);
+ date.setUTCHours(0);
+ const then = date.getTime() / 1000 / 3600;
+ const now = Date.now() / 1000 / 3600;
+ auto = then < (now - 48) || then > (now + 48);
+ }
+ }
+ vAPI.messaging.send('scriptlets', {
+ what: 'updateLists',
+ listkeys,
+ auto,
+ });
+ return true;
+ } catch (_) {
+ }
+}
+
+// https://github.com/easylist/EasyListHebrew/issues/89
+// Ensure trusted events only.
+
+document.addEventListener('click', ev => {
+ if ( ev.button !== 0 || ev.isTrusted === false ) { return; }
+ const target = ev.target.closest('a');
+ if ( target instanceof HTMLAnchorElement === false ) { return; }
+ if ( updateStockLists(target) === true ) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ }
+});
+
+/******************************************************************************/
+
+// <<<<< end of local scope
+})();
+
+
+
+
+
+
+
+
+/*******************************************************************************
+
+ DO NOT:
+ - Remove the following code
+ - Add code beyond the following code
+ Reason:
+ - https://github.com/gorhill/uBlock/pull/3721
+ - uBO never uses the return value from injected content scripts
+
+**/
+
+void 0;