diff --git a/README.md b/README.md index 84855a6808d97..5d59b93494ee8 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ There is also a development version if you want to test uBlock Origin with the l uBlock Origin is compatible with [SeaMonkey](http://www.seamonkey-project.org/), [Pale Moon](https://www.palemoon.org/), and possibly other browsers based on Firefox: for installation, see [Install / Firefox legacy](https://github.com/gorhill/uBlock/blob/master/dist/README.md#firefox-legacy). -uBO mat also be installable as a [Debian package](https://packages.debian.org/stable/source/ublock-origin): +uBO may also be installed as a [Debian package](https://packages.debian.org/stable/source/ublock-origin): - Firefox 56-: `apt-get install xul-ext-ublock-origin` - Firefox 55+: `apt-get install webext-ublock-origin` diff --git a/dist/firefox/updates.json b/dist/firefox/updates.json index c9c95dbda8f7c..058958ed642d7 100644 --- a/dist/firefox/updates.json +++ b/dist/firefox/updates.json @@ -3,10 +3,10 @@ "uBlock0@raymondhill.net": { "updates": [ { - "version": "1.28.5.4", + "version": "1.28.5.13", "browser_specific_settings": { "gecko": { "strict_min_version": "55" } }, - "update_info_url": "https://github.com/gorhill/uBlock/releases/tag/1.28.5b4", - "update_link": "https://github.com/gorhill/uBlock/releases/download/1.28.5b4/uBlock0_1.28.5b4.firefox.signed.xpi" + "update_info_url": "https://github.com/gorhill/uBlock/releases/tag/1.28.5b13", + "update_link": "https://github.com/gorhill/uBlock/releases/download/1.28.5b13/uBlock0_1.28.5b13.firefox.signed.xpi" } ] } diff --git a/dist/version b/dist/version index 7747a5cbb3697..ff5df5723800f 100644 --- a/dist/version +++ b/dist/version @@ -1 +1 @@ -1.28.5.4 +1.28.5.13 diff --git a/docs/tests/static-filtering-parser-checklist.txt b/docs/tests/static-filtering-parser-checklist.txt index dd3bf5ad0b925..85daa48dd7220 100644 --- a/docs/tests/static-filtering-parser-checklist.txt +++ b/docs/tests/static-filtering-parser-checklist.txt @@ -20,8 +20,17 @@ ! valid patterns a* -|* -||* +*$xhr +|*$xhr +|$xhr +||*$xhr +||$xhr +||*|$xhr + +! valid hosts file entries +:: ab +:: AB +:: ab # comment ! valid options $script,redirect=noop.js @@ -47,6 +56,17 @@ a | || $ +* +|* +||* +||*| + +! bad hosts file entries +:: a +:: ab/ +:: ab/ # comment +::/ ab +:: ab$ ! bad regex /(abc|def/$xhr diff --git a/platform/chromium/vapi-common.js b/platform/chromium/vapi-common.js index 30e09c577d4af..e8de131d2d47d 100644 --- a/platform/chromium/vapi-common.js +++ b/platform/chromium/vapi-common.js @@ -75,8 +75,7 @@ vAPI.webextFlavor = { dispatch(); }); if ( browser.runtime.getURL('').startsWith('moz-extension://') ) { - soup.add('mozilla') - .add('firefox') + soup.add('firefox') .add('user_stylesheet') .add('html_filtering'); flavor.major = 60; @@ -85,29 +84,14 @@ vAPI.webextFlavor = { } // Synchronous -- order of tests is important - let match; - if ( (match = /\bEdge\/(\d+)/.exec(ua)) !== null ) { - flavor.major = parseInt(match[1], 10) || 0; - soup.add('microsoft').add('edge'); - } else if ( (match = /\bOPR\/(\d+)/.exec(ua)) !== null ) { - const reEx = /\bChrom(?:e|ium)\/([\d.]+)/; - if ( reEx.test(ua) ) { match = reEx.exec(ua); } - flavor.major = parseInt(match[1], 10) || 0; - soup.add('opera').add('chromium'); - } else if ( (match = /\bChromium\/(\d+)/.exec(ua)) !== null ) { - flavor.major = parseInt(match[1], 10) || 0; + const match = /\bChrom(?:e|ium)\/([\d.]+)/.exec(ua); + if ( match !== null ) { soup.add('chromium'); - } else if ( (match = /\bChrome\/(\d+)/.exec(ua)) !== null ) { - flavor.major = parseInt(match[1], 10) || 0; - soup.add('google').add('chromium'); - } else if ( (match = /\bSafari\/(\d+)/.exec(ua)) !== null ) { flavor.major = parseInt(match[1], 10) || 0; - soup.add('apple').add('safari'); - } - - // https://github.com/gorhill/uBlock/issues/3588 - if ( soup.has('chromium') && flavor.major >= 66 ) { - soup.add('user_stylesheet'); + // https://github.com/gorhill/uBlock/issues/3588 + if ( flavor.major >= 66 ) { + soup.add('user_stylesheet'); + } } // Don't starve potential listeners diff --git a/platform/chromium/vapi-usercss.js b/platform/chromium/vapi-usercss.js deleted file mode 100644 index 0cf97b7aa4162..0000000000000 --- a/platform/chromium/vapi-usercss.js +++ /dev/null @@ -1,54 +0,0 @@ -/******************************************************************************* - - uBlock Origin - a browser extension to block requests. - Copyright (C) 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'; - -// This file can be replaced by platform-specific code. If a platform is -// known to NOT support user stylsheets, vAPI.supportsUserStylesheets can be -// set to `false`. - -// Chromium 66 and above supports user stylesheets: -// https://github.com/gorhill/uBlock/issues/3588 - -if ( typeof vAPI === 'object' ) { - vAPI.supportsUserStylesheets = - /\bChrom(?:e|ium)\/(?:5\d|6[012345])\b/.test(navigator.userAgent) === 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/platform/chromium/vapi-usercss.pseudo.js b/platform/chromium/vapi-usercss.pseudo.js deleted file mode 100644 index 4d0fda69f35cd..0000000000000 --- a/platform/chromium/vapi-usercss.pseudo.js +++ /dev/null @@ -1,562 +0,0 @@ -/******************************************************************************* - - uBlock Origin - a browser extension to block requests. - Copyright (C) 2017-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'; - -// Packaging this file is optional: it is not necessary to package it if the -// platform is known to support user stylesheets. - -// >>>>>>>> start of HUGE-IF-BLOCK -if ( typeof vAPI === 'object' && vAPI.userStylesheet === undefined ) { - -/******************************************************************************/ -/******************************************************************************/ - -vAPI.userStylesheet = { - style: null, - styleFixCount: 0, - css: new Map(), - disabled: false, - apply: function() { - }, - inject: function() { - this.style = document.createElement('style'); - this.style.disabled = this.disabled; - const parent = document.head || document.documentElement; - if ( parent === null ) { return; } - parent.appendChild(this.style); - const observer = new MutationObserver(function() { - if ( this.style === null ) { return; } - if ( this.style.sheet !== null ) { return; } - this.styleFixCount += 1; - if ( this.styleFixCount < 32 ) { - parent.appendChild(this.style); - } else { - observer.disconnect(); - } - }.bind(this)); - observer.observe(parent, { childList: true }); - }, - add: function(cssText) { - if ( cssText === '' || this.css.has(cssText) ) { return; } - if ( this.style === null ) { this.inject(); } - const sheet = this.style.sheet; - if ( !sheet ) { return; } - const i = sheet.cssRules.length; - sheet.insertRule(cssText, i); - this.css.set(cssText, sheet.cssRules[i]); - }, - remove: function(cssText) { - if ( cssText === '' ) { return; } - const cssRule = this.css.get(cssText); - if ( cssRule === undefined ) { return; } - this.css.delete(cssText); - if ( this.style === null ) { return; } - const sheet = this.style.sheet; - if ( !sheet ) { return; } - const rules = sheet.cssRules; - let i = rules.length; - while ( i-- ) { - if ( rules[i] !== cssRule ) { continue; } - sheet.deleteRule(i); - break; - } - if ( rules.length !== 0 ) { return; } - const style = this.style; - this.style = null; - const parent = style.parentNode; - if ( parent !== null ) { - parent.removeChild(style); - } - }, - toggle: function(state) { - if ( state === undefined ) { state = this.disabled; } - if ( state !== this.disabled ) { return; } - this.disabled = !state; - if ( this.style !== null ) { - this.style.disabled = this.disabled; - } - } -}; - -/******************************************************************************/ - -vAPI.DOMFilterer = class { - constructor() { - this.commitTimer = new vAPI.SafeAnimationFrame(this.commitNow.bind(this)); - this.domIsReady = document.readyState !== 'loading'; - this.listeners = []; - this.excludedNodeSet = new WeakSet(); - this.addedNodes = new Set(); - this.removedNodes = false; - - this.specificSimpleHide = new Set(); - this.specificSimpleHideAggregated = undefined; - this.addedSpecificSimpleHide = []; - this.specificComplexHide = new Set(); - this.specificComplexHideAggregated = undefined; - this.addedSpecificComplexHide = []; - this.specificOthers = []; - this.genericSimpleHide = new Set(); - this.genericComplexHide = new Set(); - this.exceptedCSSRules = []; - - this.hideNodeExpando = undefined; - this.hideNodeBatchProcessTimer = undefined; - this.hiddenNodeObserver = undefined; - this.hiddenNodesetToProcess = new Set(); - this.hiddenNodeset = new WeakSet(); - - if ( vAPI.domWatcher instanceof Object ) { - vAPI.domWatcher.addListener(this); - } - - // https://www.w3.org/community/webed/wiki/CSS/Selectors#Combinators - this.reCSSCombinators = /[ >+~]/; - } - - commitNow() { - this.commitTimer.clear(); - - if ( this.domIsReady !== true || vAPI.userStylesheet.disabled ) { - return; - } - - // Filterset changed. - - if ( this.addedSpecificSimpleHide.length !== 0 ) { - //console.time('specific simple filterset changed'); - //console.log('added %d specific simple selectors', this.addedSpecificSimpleHide.length); - const nodes = document.querySelectorAll(this.addedSpecificSimpleHide.join(',')); - for ( const node of nodes ) { - this.hideNode(node); - } - this.addedSpecificSimpleHide = []; - this.specificSimpleHideAggregated = undefined; - //console.timeEnd('specific simple filterset changed'); - } - - if ( this.addedSpecificComplexHide.length !== 0 ) { - //console.time('specific complex filterset changed'); - //console.log('added %d specific complex selectors', this.addedSpecificComplexHide.length); - const nodes = document.querySelectorAll(this.addedSpecificComplexHide.join(',')); - for ( const node of nodes ) { - this.hideNode(node); - } - this.addedSpecificComplexHide = []; - this.specificComplexHideAggregated = undefined; - //console.timeEnd('specific complex filterset changed'); - } - - // DOM layout changed. - - const domNodesAdded = this.addedNodes.size !== 0; - const domLayoutChanged = domNodesAdded || this.removedNodes; - - if ( domNodesAdded === false || domLayoutChanged === false ) { - return; - } - - //console.log('%d nodes added', this.addedNodes.size); - - if ( this.specificSimpleHide.size !== 0 && domNodesAdded ) { - //console.time('dom layout changed/specific simple selectors'); - if ( this.specificSimpleHideAggregated === undefined ) { - this.specificSimpleHideAggregated = - Array.from(this.specificSimpleHide).join(',\n'); - } - for ( const node of this.addedNodes ) { - if ( node.matches(this.specificSimpleHideAggregated) ) { - this.hideNode(node); - } - const nodes = node.querySelectorAll(this.specificSimpleHideAggregated); - for ( const node of nodes ) { - this.hideNode(node); - } - } - //console.timeEnd('dom layout changed/specific simple selectors'); - } - - if ( this.specificComplexHide.size !== 0 && domLayoutChanged ) { - //console.time('dom layout changed/specific complex selectors'); - if ( this.specificComplexHideAggregated === undefined ) { - this.specificComplexHideAggregated = - Array.from(this.specificComplexHide).join(',\n'); - } - const nodes = document.querySelectorAll(this.specificComplexHideAggregated); - for ( const node of nodes ) { - this.hideNode(node); - } - //console.timeEnd('dom layout changed/specific complex selectors'); - } - - this.addedNodes.clear(); - this.removedNodes = false; - } - - commit(now) { - if ( now ) { - this.commitTimer.clear(); - this.commitNow(); - } else { - this.commitTimer.start(); - } - } - - addCSSRule(selectors, declarations, details = {}) { - if ( selectors === undefined ) { return; } - - const selectorsStr = Array.isArray(selectors) ? - selectors.join(',\n') : - selectors; - if ( selectorsStr.length === 0 ) { return; } - - vAPI.userStylesheet.add(selectorsStr + '\n{' + declarations + '}'); - this.commit(); - if ( details.silent !== true && this.hasListeners() ) { - this.triggerListeners({ - declarative: [ [ selectorsStr, declarations ] ] - }); - } - - if ( declarations !== 'display:none!important;' ) { - this.specificOthers.push({ - selectors: selectorsStr, - declarations: declarations - }); - return; - } - - const isGeneric= details.lazy === true; - const isSimple = details.type === 'simple'; - const isComplex = details.type === 'complex'; - - if ( isGeneric ) { - if ( isSimple ) { - this.genericSimpleHide.add(selectorsStr); - return; - } - if ( isComplex ) { - this.genericComplexHide.add(selectorsStr); - return; - } - } - - const selectorsArr = Array.isArray(selectors) ? - selectors : - selectors.split(',\n'); - - if ( isGeneric ) { - for ( const selector of selectorsArr ) { - if ( this.reCSSCombinators.test(selector) ) { - this.genericComplexHide.add(selector); - } else { - this.genericSimpleHide.add(selector); - } - } - return; - } - - // Specific cosmetic filters. - for ( const selector of selectorsArr ) { - if ( - isComplex || - isSimple === false && this.reCSSCombinators.test(selector) - ) { - if ( this.specificComplexHide.has(selector) === false ) { - this.specificComplexHide.add(selector); - this.addedSpecificComplexHide.push(selector); - } - } else if ( this.specificSimpleHide.has(selector) === false ) { - this.specificSimpleHide.add(selector); - this.addedSpecificSimpleHide.push(selector); - } - } - } - - exceptCSSRules(exceptions) { - if ( exceptions.length === 0 ) { return; } - this.exceptedCSSRules.push(...exceptions); - if ( this.hasListeners() ) { - this.triggerListeners({ exceptions }); - } - } - - onDOMCreated() { - this.domIsReady = true; - this.addedNodes.clear(); - this.removedNodes = false; - this.commit(); - } - - onDOMChanged(addedNodes, removedNodes) { - for ( const node of addedNodes ) { - this.addedNodes.add(node); - } - this.removedNodes = this.removedNodes || removedNodes; - this.commit(); - } - - addListener(listener) { - if ( this.listeners.indexOf(listener) !== -1 ) { return; } - this.listeners.push(listener); - } - - removeListener(listener) { - const pos = this.listeners.indexOf(listener); - if ( pos === -1 ) { return; } - this.listeners.splice(pos, 1); - } - - hasListeners() { - return this.listeners.length !== 0; - } - - triggerListeners(changes) { - for ( const listener of this.listeners ) { - listener.onFiltersetChanged(changes); - } - } - - // https://jsperf.com/clientheight-and-clientwidth-vs-getcomputedstyle - // Avoid getComputedStyle(), detecting whether a node is visible can be - // achieved with clientWidth/clientHeight. - // https://gist.github.com/paulirish/5d52fb081b3570c81e3a - // Do not interleave read-from/write-to the DOM. Write-to DOM - // operations would cause the first read-from to be expensive, and - // interleaving means that potentially all single read-from operation - // would be expensive rather than just the 1st one. - // Benchmarking toggling off/on cosmetic filtering confirms quite an - // improvement when: - // - batching as much as possible handling of all nodes; - // - avoiding to interleave read-from/write-to operations. - // However, toggling off/on cosmetic filtering repeatedly is not - // a real use case, but this shows this will help performance - // on sites which try to use inline styles to bypass blockers. - hideNodeBatchProcess() { - this.hideNodeBatchProcessTimer.clear(); - const expando = this.hideNodeExpando; - for ( const node of this.hiddenNodesetToProcess ) { - if ( - this.hiddenNodeset.has(node) === false || - node[expando] === undefined || - node.clientHeight === 0 || node.clientWidth === 0 - ) { - continue; - } - let attr = node.getAttribute('style'); - if ( attr === null ) { - attr = ''; - } else if ( attr.length !== 0 ) { - if ( attr.endsWith('display:none!important;') ) { continue; } - if ( attr.charCodeAt(attr.length - 1) !== 0x3B /* ';' */ ) { - attr += ';'; - } - } - node.setAttribute('style', attr + 'display:none!important;'); - } - this.hiddenNodesetToProcess.clear(); - } - - hideNodeObserverHandler(mutations) { - if ( vAPI.userStylesheet.disabled ) { return; } - const stagedNodes = this.hiddenNodesetToProcess; - for ( const mutation of mutations ) { - stagedNodes.add(mutation.target); - } - this.hideNodeBatchProcessTimer.start(); - } - - hideNodeInit() { - this.hideNodeExpando = vAPI.randomToken(); - this.hideNodeBatchProcessTimer = - new vAPI.SafeAnimationFrame(this.hideNodeBatchProcess.bind(this)); - this.hiddenNodeObserver = - new MutationObserver(this.hideNodeObserverHandler.bind(this)); - if ( this.hideNodeStyleSheetInjected === false ) { - this.hideNodeStyleSheetInjected = true; - vAPI.userStylesheet.add( - `[${this.hideNodeAttr}]\n{display:none!important;}` - ); - } - } - - excludeNode(node) { - this.excludedNodeSet.add(node); - this.unhideNode(node); - } - - unexcludeNode(node) { - this.excludedNodeSet.delete(node); - } - - hideNode(node) { - if ( this.excludedNodeSet.has(node) ) { return; } - if ( this.hideNodeAttr === undefined ) { return; } - if ( this.hiddenNodeset.has(node) ) { return; } - node.hidden = true; - this.hiddenNodeset.add(node); - if ( this.hideNodeExpando === undefined ) { this.hideNodeInit(); } - node.setAttribute(this.hideNodeAttr, ''); - if ( node[this.hideNodeExpando] === undefined ) { - node[this.hideNodeExpando] = - node.hasAttribute('style') && - (node.getAttribute('style') || ''); - } - this.hiddenNodesetToProcess.add(node); - this.hideNodeBatchProcessTimer.start(); - this.hiddenNodeObserver.observe(node, this.hiddenNodeObserverOptions); - } - - unhideNode(node) { - if ( this.hiddenNodeset.has(node) === false ) { return; } - node.hidden = false; - node.removeAttribute(this.hideNodeAttr); - this.hiddenNodesetToProcess.delete(node); - if ( this.hideNodeExpando === undefined ) { return; } - const attr = node[this.hideNodeExpando]; - if ( attr === false ) { - node.removeAttribute('style'); - } else if ( typeof attr === 'string' ) { - node.setAttribute('style', attr); - } - node[this.hideNodeExpando] = undefined; - this.hiddenNodeset.delete(node); - } - - showNode(node) { - node.hidden = false; - const attr = node[this.hideNodeExpando]; - if ( attr === false ) { - node.removeAttribute('style'); - } else if ( typeof attr === 'string' ) { - node.setAttribute('style', attr); - } - } - - unshowNode(node) { - node.hidden = true; - this.hiddenNodesetToProcess.add(node); - } - - toggle(state, callback) { - vAPI.userStylesheet.toggle(state); - const disabled = vAPI.userStylesheet.disabled; - const nodes = document.querySelectorAll(`[${this.hideNodeAttr}]`); - for ( const node of nodes ) { - if ( disabled ) { - this.showNode(node); - } else { - this.unshowNode(node); - } - } - if ( disabled === false && this.hideNodeExpando !== undefined ) { - this.hideNodeBatchProcessTimer.start(); - } - if ( typeof callback === 'function' ) { - callback(); - } - } - - getAllSelectors_(all) { - const out = { - declarative: [], - exceptions: this.exceptedCSSRules, - }; - if ( this.specificSimpleHide.size !== 0 ) { - out.declarative.push([ - Array.from(this.specificSimpleHide).join(',\n'), - 'display:none!important;' - ]); - } - if ( this.specificComplexHide.size !== 0 ) { - out.declarative.push([ - Array.from(this.specificComplexHide).join(',\n'), - 'display:none!important;' - ]); - } - if ( this.genericSimpleHide.size !== 0 ) { - out.declarative.push([ - Array.from(this.genericSimpleHide).join(',\n'), - 'display:none!important;' - ]); - } - if ( this.genericComplexHide.size !== 0 ) { - out.declarative.push([ - Array.from(this.genericComplexHide).join(',\n'), - 'display:none!important;' - ]); - } - if ( all ) { - out.declarative.push([ - '[' + this.hideNodeAttr + ']', - 'display:none!important;' - ]); - } - for ( const entry of this.specificOthers ) { - out.declarative.push([ entry.selectors, entry.declarations ]); - } - return out; - } - - getFilteredElementCount() { - const details = this.getAllSelectors_(true); - if ( Array.isArray(details.declarative) === false ) { return 0; } - const selectors = details.declarative.map(entry => entry[0]); - if ( selectors.length === 0 ) { return 0; } - return document.querySelectorAll(selectors.join(',\n')).length; - } - - getAllSelectors() { - return this.getAllSelectors_(false); - } -}; - -vAPI.DOMFilterer.prototype.hiddenNodeObserverOptions = { - attributes: true, - attributeFilter: [ 'style' ] -}; - -/******************************************************************************/ -/******************************************************************************/ - -} -// <<<<<<<< end of HUGE-IF-BLOCK - - - - - - - - -/******************************************************************************* - - 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/platform/chromium/vapi-usercss.real.js b/platform/chromium/vapi-usercss.real.js deleted file mode 100644 index 8ec746f6de57a..0000000000000 --- a/platform/chromium/vapi-usercss.real.js +++ /dev/null @@ -1,282 +0,0 @@ -/******************************************************************************* - - uBlock Origin - a browser extension to block requests. - Copyright (C) 2017-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'; - -// Packaging this file is optional: it is not necessary to package it if the -// platform is known to not support user stylesheets. - -// >>>>>>>> start of HUGE-IF-BLOCK -if ( typeof vAPI === 'object' && vAPI.supportsUserStylesheets ) { - -/******************************************************************************/ -/******************************************************************************/ - -vAPI.userStylesheet = { - added: new Set(), - removed: new Set(), - apply: function(callback) { - if ( this.added.size === 0 && this.removed.size === 0 ) { return; } - vAPI.messaging.send('vapi', { - what: 'userCSS', - add: Array.from(this.added), - remove: Array.from(this.removed), - }).then(( ) => { - if ( callback instanceof Function === false ) { return; } - callback(); - }); - this.added.clear(); - this.removed.clear(); - }, - add: function(cssText, now) { - if ( cssText === '' ) { return; } - this.added.add(cssText); - if ( now ) { this.apply(); } - }, - remove: function(cssText, now) { - if ( cssText === '' ) { return; } - this.removed.add(cssText); - if ( now ) { this.apply(); } - } -}; - -/******************************************************************************/ - -vAPI.DOMFilterer = class { - constructor() { - this.commitTimer = new vAPI.SafeAnimationFrame(this.commitNow.bind(this)); - this.domIsReady = document.readyState !== 'loading'; - this.disabled = false; - this.listeners = []; - this.filterset = new Set(); - this.excludedNodeSet = new WeakSet(); - this.addedCSSRules = new Set(); - this.exceptedCSSRules = []; - this.reOnlySelectors = /\n\{[^\n]+/g; - - // https://github.com/uBlockOrigin/uBlock-issues/issues/167 - // By the time the DOMContentLoaded is fired, the content script might - // have been disconnected from the background page. Unclear why this - // would happen, so far seems to be a Chromium-specific behavior at - // launch time. - if ( this.domIsReady !== true ) { - document.addEventListener('DOMContentLoaded', ( ) => { - if ( vAPI instanceof Object === false ) { return; } - this.domIsReady = true; - this.commit(); - }); - } - } - - // Here we will deal with: - // - Injecting low priority user styles; - // - Notifying listeners about changed filterset. - // https://www.reddit.com/r/uBlockOrigin/comments/9jj0y1/no_longer_blocking_ads/ - // Ensure vAPI is still valid -- it can go away by the time we are - // called, since the port could be force-disconnected from the main - // process. Another approach would be to have vAPI.SafeAnimationFrame - // register a shutdown job: to evaluate. For now I will keep the fix - // trivial. - commitNow() { - this.commitTimer.clear(); - if ( vAPI instanceof Object === false ) { return; } - const userStylesheet = vAPI.userStylesheet; - for ( const entry of this.addedCSSRules ) { - if ( - this.disabled === false && - entry.lazy && - entry.injected === false - ) { - userStylesheet.add( - entry.selectors + '\n{' + entry.declarations + '}' - ); - } - } - this.addedCSSRules.clear(); - userStylesheet.apply(); - } - - commit(commitNow) { - if ( commitNow ) { - this.commitTimer.clear(); - this.commitNow(); - } else { - this.commitTimer.start(); - } - } - - addCSSRule(selectors, declarations, details = {}) { - if ( selectors === undefined ) { return; } - const selectorsStr = Array.isArray(selectors) - ? selectors.join(',\n') - : selectors; - if ( selectorsStr.length === 0 ) { return; } - const entry = { - selectors: selectorsStr, - declarations, - lazy: details.lazy === true, - injected: details.injected === true - }; - this.addedCSSRules.add(entry); - this.filterset.add(entry); - if ( - this.disabled === false && - entry.lazy !== true && - entry.injected !== true - ) { - vAPI.userStylesheet.add(selectorsStr + '\n{' + declarations + '}'); - } - this.commit(); - if ( details.silent !== true && this.hasListeners() ) { - this.triggerListeners({ - declarative: [ [ selectorsStr, declarations ] ] - }); - } - } - - exceptCSSRules(exceptions) { - if ( exceptions.length === 0 ) { return; } - this.exceptedCSSRules.push(...exceptions); - if ( this.hasListeners() ) { - this.triggerListeners({ exceptions }); - } - } - - addListener(listener) { - if ( this.listeners.indexOf(listener) !== -1 ) { return; } - this.listeners.push(listener); - } - - removeListener(listener) { - const pos = this.listeners.indexOf(listener); - if ( pos === -1 ) { return; } - this.listeners.splice(pos, 1); - } - - hasListeners() { - return this.listeners.length !== 0; - } - - triggerListeners(changes) { - for ( const listener of this.listeners ) { - listener.onFiltersetChanged(changes); - } - } - - excludeNode(node) { - this.excludedNodeSet.add(node); - this.unhideNode(node); - } - - unexcludeNode(node) { - this.excludedNodeSet.delete(node); - } - - hideNode(node) { - if ( this.excludedNodeSet.has(node) ) { return; } - if ( this.hideNodeAttr === undefined ) { return; } - node.setAttribute(this.hideNodeAttr, ''); - if ( this.hideNodeStyleSheetInjected ) { return; } - this.hideNodeStyleSheetInjected = true; - this.addCSSRule( - `[${this.hideNodeAttr}]`, - 'display:none!important;', - { silent: true } - ); - } - - unhideNode(node) { - if ( this.hideNodeAttr === undefined ) { return; } - node.removeAttribute(this.hideNodeAttr); - } - - toggle(state, callback) { - if ( state === undefined ) { state = this.disabled; } - if ( state !== this.disabled ) { return; } - this.disabled = !state; - const userStylesheet = vAPI.userStylesheet; - for ( const entry of this.filterset ) { - const rule = `${entry.selectors}\n{${entry.declarations}}`; - if ( this.disabled ) { - userStylesheet.remove(rule); - } else { - userStylesheet.add(rule); - } - } - userStylesheet.apply(callback); - } - - getAllSelectors_(all) { - const out = { - declarative: [], - exceptions: this.exceptedCSSRules, - }; - for ( const entry of this.filterset ) { - let selectors = entry.selectors; - if ( all !== true && this.hideNodeAttr !== undefined ) { - selectors = selectors - .replace(`[${this.hideNodeAttr}]`, '') - .replace(/^,\n|,\n$/gm, ''); - if ( selectors === '' ) { continue; } - } - out.declarative.push([ selectors, entry.declarations ]); - } - return out; - } - - getFilteredElementCount() { - const details = this.getAllSelectors_(true); - if ( Array.isArray(details.declarative) === false ) { return 0; } - const selectors = details.declarative.map(entry => entry[0]); - if ( selectors.length === 0 ) { return 0; } - return document.querySelectorAll(selectors.join(',\n')).length; - } - - getAllSelectors() { - return this.getAllSelectors_(false); - } -}; - -/******************************************************************************/ -/******************************************************************************/ - -} -// <<<<<<<< end of HUGE-IF-BLOCK - - - - - - - - -/******************************************************************************* - - 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/platform/firefox/vapi-usercss.js b/platform/firefox/vapi-usercss.js deleted file mode 100644 index de4946c7da281..0000000000000 --- a/platform/firefox/vapi-usercss.js +++ /dev/null @@ -1,48 +0,0 @@ -/******************************************************************************* - - uBlock Origin - a browser extension to block requests. - Copyright (C) 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'; - -// User stylesheets are always supported with Firefox/webext . - -if ( typeof vAPI === 'object' ) { - vAPI.supportsUserStylesheets = 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/platform/webext/vapi-usercss.js b/platform/webext/vapi-usercss.js deleted file mode 100644 index 6ec8ebb9224a7..0000000000000 --- a/platform/webext/vapi-usercss.js +++ /dev/null @@ -1,54 +0,0 @@ -/******************************************************************************* - - uBlock Origin - a browser extension to block requests. - Copyright (C) 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'; - -// This file can be replaced by platform-specific code. If a platform is -// known to NOT support user stylsheets, vAPI.supportsUserStylesheets can be -// set to `false`. - -// Chromium 66 and above supports user stylesheets: -// https://github.com/gorhill/uBlock/issues/3588 - -if ( typeof vAPI === 'object' ) { - vAPI.supportsUserStylesheets = - /\bChrom(?:e|ium)\/(?:6[6789]|[789]|1\d\d)|\bFirefox\/\d/.test(navigator.userAgent); -} - - - - - - - - -/******************************************************************************* - - 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/1p-filters.html b/src/1p-filters.html index b5b0d25d922bf..e99dd8a05bacd 100644 --- a/src/1p-filters.html +++ b/src/1p-filters.html @@ -47,11 +47,11 @@ - + diff --git a/src/_locales/nl/messages.json b/src/_locales/nl/messages.json index e248909384774..404a4fe90497e 100644 --- a/src/_locales/nl/messages.json +++ b/src/_locales/nl/messages.json @@ -112,11 +112,11 @@ "description": "English: Click to open the dashboard" }, "popupTipZapper": { - "message": "Elementwissermodus openen", + "message": "Elementwissermodus openen", "description": "Tooltip for the element-zapper icon in the popup panel" }, "popupTipPicker": { - "message": "Elementkiezermodus openen", + "message": "Elementkiezermodus openen", "description": "English: Enter element picker mode" }, "popupTipLog": { diff --git a/src/_locales/pl/messages.json b/src/_locales/pl/messages.json index 2cab196c3159e..dc63a633f8cef 100644 --- a/src/_locales/pl/messages.json +++ b/src/_locales/pl/messages.json @@ -552,7 +552,7 @@ "description": "English: dynamic rule syntax and full documentation." }, "whitelistPrompt": { - "message": "Wytyczne wyjątków nakazują, na których stronach uBlock Origin powinien zostać wyłączony. Jeden wpis na linię. Nieprawidłowe wytyczne zostaną bez powiadomienia zignorowane i wykomentowane.", + "message": "Dyrektywy białej listy wskazują, na których stronach uBlock Origin powinien zostać wyłączony. Jeden wpis na linię. Nieprawidłowe wpisy zostaną bez powiadomienia zignorowane i wykomentowane.", "description": "The name of the trusted sites pane." }, "whitelistImport": { @@ -564,7 +564,7 @@ "description": "English: Export" }, "whitelistExportFilename": { - "message": "my-ublock-whitelist_{{datetime}}.txt", + "message": "ublock-biała-lista_{{datetime}}.txt", "description": "The default filename to use for import/export purpose" }, "whitelistApply": { diff --git a/src/asset-viewer.html b/src/asset-viewer.html index 0aca82e511b2c..c3a2badde5bce 100644 --- a/src/asset-viewer.html +++ b/src/asset-viewer.html @@ -35,11 +35,11 @@ - + diff --git a/src/css/codemirror.css b/src/css/codemirror.css index 411804f7fb057..7f1668c57f203 100644 --- a/src/css/codemirror.css +++ b/src/css/codemirror.css @@ -64,14 +64,17 @@ div.CodeMirror span.CodeMirror-matchingbracket { direction: ltr; display: flex; flex-shrink: 0; - font-size: 110%; - justify-content: center; + justify-content: space-between; padding: 0.5em; user-select: none; -moz-user-select: none; -webkit-user-select: none; z-index: 1000; } +.cm-search-widget-input { + display: inline-flex; + flex-grow: 1; + } .cm-search-widget .fa-icon { fill: #888; font-size: 140%; @@ -79,39 +82,26 @@ div.CodeMirror span.CodeMirror-matchingbracket { .cm-search-widget .fa-icon:not(.fa-icon-ro):hover { fill: #000; } -.cm-search-widget-input { +.cm-search-widget-input input { border: 1px solid gray; - border-radius: 3px; display: inline-flex; - max-width: 50vw; - width: 16em; - } -.cm-search-widget-input > input { - border: 0; flex-grow: 1; - width: 100%; + max-width: 16em; } -.cm-search-widget-input > .cm-search-widget-count { +.cm-search-widget-count { align-items: center; - color: #888; - display: none; + display: inline-flex; flex-grow: 0; - font-size: 80%; - padding: 0 0.4em; - pointer-events: none; + font-size: 95%; + min-width: 6em; + visibility: hidden; } -.cm-search-widget[data-query] .cm-search-widget-count { - display: inline-flex; +.cm-search-widget[data-query] .cm-search-widget-count:not(:empty) { + visibility: visible; } .cm-search-widget .cm-search-widget-button:hover { color: #000; } -.cm-search-widget .sourceURL { - padding-left: 0.5em; - padding-right: 0.5em; - position: absolute; - left: 0; - } .cm-search-widget .sourceURL[href=""] { display: none; } diff --git a/src/js/assets.js b/src/js/assets.js index aac3126e15f33..a6be36169e8cc 100644 --- a/src/js/assets.js +++ b/src/js/assets.js @@ -884,7 +884,7 @@ const updateNext = async function() { ]); const now = Date.now(); - let assetKeyToUpdate; + const toUpdate = []; for ( const assetKey in assetDict ) { const assetEntry = assetDict[assetKey]; if ( assetEntry.hasRemoteURL !== true ) { continue; } @@ -902,23 +902,30 @@ const updateNext = async function() { type: assetEntry.content }) === true ) { - assetKeyToUpdate = assetKey; - break; + toUpdate.push(assetKey); + continue; } // This will remove a cached asset when it's no longer in use. if ( cacheEntry && cacheEntry.readTime < assetCacheRegistryStartTime ) { assetCacheRemove(assetKey); } } - if ( assetKeyToUpdate === undefined ) { + if ( toUpdate.length === 0 ) { return updateDone(); } - updaterFetched.add(assetKeyToUpdate); + // https://github.com/uBlockOrigin/uBlock-issues/issues/1165 + // Update most obsolete asset first. + toUpdate.sort((a, b) => { + const ta = cacheDict[a] !== undefined ? cacheDict[a].writeTime : 0; + const tb = cacheDict[b] !== undefined ? cacheDict[b].writeTime : 0; + return ta - tb; + }); + updaterFetched.add(toUpdate[0]); // In auto-update context, be gentle on remote servers. remoteServerFriendly = updaterAuto; - const result = await getRemote(assetKeyToUpdate); + const result = await getRemote(toUpdate[0]); remoteServerFriendly = false; diff --git a/src/js/codemirror/search-thread.js b/src/js/codemirror/search-thread.js new file mode 100644 index 0000000000000..7b33fb126589f --- /dev/null +++ b/src/js/codemirror/search-thread.js @@ -0,0 +1,203 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + 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'; + +/******************************************************************************/ + +(( ) => { +// >>>>> start of local scope + +/******************************************************************************/ + +// Worker context + +if ( + self.WorkerGlobalScope instanceof Object && + self instanceof self.WorkerGlobalScope +) { + let content = ''; + + const doSearch = function(details) { + const reEOLs = /\n\r|\r\n|\n|\r/g; + const t1 = Date.now() + 750; + + let reSearch; + try { + reSearch = new RegExp(details.pattern, details.flags); + } catch(ex) { + return; + } + + const response = []; + const maxOffset = content.length; + let iLine = 0; + let iOffset = 0; + let size = 0; + while ( iOffset < maxOffset ) { + // Find next match + const match = reSearch.exec(content); + if ( match === null ) { break; } + // Find number of line breaks between last and current match. + reEOLs.lastIndex = 0; + const eols = content.slice(iOffset, match.index).match(reEOLs); + if ( Array.isArray(eols) ) { + iLine += eols.length; + } + // Store line + response.push(iLine); + size += 1; + // Find next line break. + reEOLs.lastIndex = reSearch.lastIndex; + const eol = reEOLs.exec(content); + iOffset = eol !== null + ? reEOLs.lastIndex + : content.length; + reSearch.lastIndex = iOffset; + iLine += 1; + // Quit if this takes too long + if ( (size & 0x3FF) === 0 && Date.now() >= t1 ) { break; } + } + + return response; + }; + + self.onmessage = function(e) { + const msg = e.data; + + switch ( msg.what ) { + case 'setHaystack': + content = msg.content; + break; + + case 'doSearch': + const response = doSearch(msg); + self.postMessage({ id: msg.id, response }); + break; + } + }; + + return; +} + +/******************************************************************************/ + +// Main context + +{ + const workerTTL = 5 * 60 * 1000; + const pendingResponses = new Map(); + + let worker; + let workerTTLTimer; + let messageId = 1; + + const onWorkerMessage = function(e) { + const msg = e.data; + const resolver = pendingResponses.get(msg.id); + if ( resolver === undefined ) { return; } + pendingResponses.delete(msg.id); + resolver(msg.response); + }; + + const cancelPendingTasks = function() { + for ( const resolver of pendingResponses.values() ) { + resolver(); + } + pendingResponses.clear(); + }; + + const destroy = function() { + shutdown(); + self.searchThread = undefined; + }; + + const shutdown = function() { + if ( workerTTLTimer !== undefined ) { + clearTimeout(workerTTLTimer); + workerTTLTimer = undefined; + } + if ( worker === undefined ) { return; } + worker.terminate(); + worker.onmessage = undefined; + worker = undefined; + cancelPendingTasks(); + }; + + const init = function() { + if ( self.searchThread instanceof Object === false ) { return; } + if ( worker === undefined ) { + worker = new Worker('js/codemirror/search-thread.js'); + worker.onmessage = onWorkerMessage; + } + if ( workerTTLTimer !== undefined ) { + clearTimeout(workerTTLTimer); + } + workerTTLTimer = vAPI.setTimeout(shutdown, workerTTL); + }; + + const needHaystack = function() { + return worker instanceof Object === false; + }; + + const setHaystack = function(content) { + init(); + worker.postMessage({ what: 'setHaystack', content }); + }; + + const search = function(query, overwrite = true) { + init(); + if ( worker instanceof Object === false ) { + return Promise.resolve(); + } + if ( overwrite ) { + cancelPendingTasks(); + } + const id = messageId++; + worker.postMessage({ + what: 'doSearch', + id, + pattern: query.source, + flags: query.flags, + isRE: query instanceof RegExp + }); + return new Promise(resolve => { + pendingResponses.set(id, resolve); + }); + }; + + self.addEventListener( + 'beforeunload', + ( ) => { destroy(); }, + { once: true } + ); + + self.searchThread = { needHaystack, setHaystack, search, shutdown }; +} + +/******************************************************************************/ + +// <<<<< end of local scope +})(); + +/******************************************************************************/ + +void 0; diff --git a/src/js/codemirror/search.js b/src/js/codemirror/search.js index d3420f7bb8861..9da1b401ceac6 100644 --- a/src/js/codemirror/search.js +++ b/src/js/codemirror/search.js @@ -3,8 +3,16 @@ // I added/removed and modified code in order to get a closer match to a // browser's built-in find-in-page feature which are just enough for // uBlock Origin. - - +// +// This file was originally wholly imported from: +// https://github.com/codemirror/CodeMirror/blob/3e1bb5fff682f8f6cbfaef0e56c61d62403d4798/addon/search/search.js +// +// And has been modified over time to better suit uBO's usage and coding style: +// https://github.com/gorhill/uBlock/commits/master/src/js/codemirror/search.js +// +// The original copyright notice is reproduced below: + +// ===== // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE @@ -15,44 +23,39 @@ // Ctrl-G (or whatever is bound to findNext) press. You prevent a // replace by making sure the match is no longer selected when hitting // Ctrl-G. - -/* globals define, require, CodeMirror */ +// ===== 'use strict'; -(function(mod) { - if (typeof exports === "object" && typeof module === "object") // CommonJS - mod(require("../../lib/codemirror"), require("./searchcursor"), require("../dialog/dialog")); - else if (typeof define === "function" && define.amd) // AMD - define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - - function searchOverlay(query, caseInsensitive) { - if (typeof query === "string") - query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseInsensitive ? "gi" : "g"); - else if (!query.global) - query = new RegExp(query.source, query.ignoreCase ? "gi" : "g"); +(function(CodeMirror) { + + const searchOverlay = function(query, caseInsensitive) { + if ( typeof query === 'string' ) + query = new RegExp( + query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), + caseInsensitive ? 'gi' : 'g' + ); + else if ( !query.global ) + query = new RegExp(query.source, query.ignoreCase ? 'gi' : 'g'); return { token: function(stream) { query.lastIndex = stream.pos; - var match = query.exec(stream.string); - if (match && match.index === stream.pos) { + const match = query.exec(stream.string); + if ( match && match.index === stream.pos ) { stream.pos += match[0].length || 1; - return "searching"; - } else if (match) { + return 'searching'; + } else if ( match ) { stream.pos = match.index; } else { stream.skipToEnd(); } } }; - } + }; - function searchWidgetKeydownHandler(cm, ev) { - var keyName = CodeMirror.keyName(ev); + const searchWidgetKeydownHandler = function(cm, ev) { + const keyName = CodeMirror.keyName(ev); if ( !keyName ) { return; } CodeMirror.lookupKey( keyName, @@ -64,9 +67,9 @@ } } ); - } + }; - function searchWidgetInputHandler(cm) { + const searchWidgetInputHandler = function(cm) { let state = getSearchState(cm); if ( queryTextFromSearchWidget(cm) === state.queryText ) { return; } if ( state.queryTimer !== null ) { @@ -79,10 +82,10 @@ }, 350 ); - } + }; - function searchWidgetClickHandler(cm, ev) { - var tcl = ev.target.classList; + const searchWidgetClickHandler = function(cm, ev) { + const tcl = ev.target.classList; if ( tcl.contains('cm-search-widget-up') ) { findNext(cm, -1); } else if ( tcl.contains('cm-search-widget-down') ) { @@ -93,27 +96,25 @@ } else { ev.stopImmediatePropagation(); } - } + }; - function queryTextFromSearchWidget(cm) { + const queryTextFromSearchWidget = function(cm) { return getSearchState(cm).widget.querySelector('input[type="search"]').value; - } + }; - function queryTextToSearchWidget(cm, q) { - var input = getSearchState(cm).widget.querySelector('input[type="search"]'); + const queryTextToSearchWidget = function(cm, q) { + const input = getSearchState(cm).widget.querySelector('input[type="search"]'); if ( typeof q === 'string' && q !== input.value ) { input.value = q; } input.setSelectionRange(0, input.value.length); input.focus(); - } + }; - function SearchState(cm) { + const SearchState = function(cm) { this.query = null; - this.overlay = null; this.panel = null; - const widgetParent = - document.querySelector('.cm-search-widget-template').cloneNode(true); + const widgetParent = document.querySelector('.cm-search-widget-template').cloneNode(true); this.widget = widgetParent.children[0]; this.widget.addEventListener('keydown', searchWidgetKeydownHandler.bind(null, cm)); this.widget.addEventListener('input', searchWidgetInputHandler.bind(null, cm)); @@ -123,16 +124,29 @@ } this.queryText = ''; this.queryTimer = null; - } + this.dirty = true; + this.lines = []; + cm.on('changes', (cm, changes) => { + for ( const change of changes ) { + if ( change.text.length !== 0 || change.removed !== 0 ) { + this.dirty = true; + break; + } + } + }); + cm.on('cursorActivity', cm => { + updateCount(cm); + }); + }; // We want the search widget to behave as if the focus was on the // CodeMirror editor. const reSearchCommands = /^(?:find|findNext|findPrev|newlineAndIndent)$/; - function widgetCommandHandler(cm, command) { + const widgetCommandHandler = function(cm, command) { if ( reSearchCommands.test(command) === false ) { return false; } - var queryText = queryTextFromSearchWidget(cm); + const queryText = queryTextFromSearchWidget(cm); if ( command === 'find' ) { queryTextToSearchWidget(cm); return true; @@ -141,101 +155,192 @@ findNext(cm, command === 'findPrev' ? -1 : 1); } return true; - } + }; - function getSearchState(cm) { + const getSearchState = function(cm) { return cm.state.search || (cm.state.search = new SearchState(cm)); - } + }; - function queryCaseInsensitive(query) { - return typeof query === "string" && query === query.toLowerCase(); - } + const queryCaseInsensitive = function(query) { + return typeof query === 'string' && query === query.toLowerCase(); + }; - function getSearchCursor(cm, query, pos) { - // Heuristic: if the query string is all lowercase, do a case insensitive search. + // Heuristic: if the query string is all lowercase, do a case insensitive search. + const getSearchCursor = function(cm, query, pos) { return cm.getSearchCursor( query, pos, { caseFold: queryCaseInsensitive(query), multiline: false } ); - } + }; // https://github.com/uBlockOrigin/uBlock-issues/issues/658 // Modified to backslash-escape ONLY widely-used control characters. - function parseString(string) { - return string.replace(/\\[nrt\\]/g, function(match) { - if (match === "\\n") return "\n"; - if (match === "\\r") return "\r"; - if (match === '\\t') return '\t'; - if (match === '\\\\') return '\\'; + const parseString = function(string) { + return string.replace(/\\[nrt\\]/g, match => { + if ( match === '\\n' ) { return '\n'; } + if ( match === '\\r' ) { return '\r'; } + if ( match === '\\t' ) { return '\t'; } + if ( match === '\\\\' ) { return '\\'; } return match; }); - } - - // FIX: use all potential regex flags as is, and if this throws, treat - // the query string as plain text. - function parseQuery(query) { - let isRE = query.match(/^\/(.*)\/([a-z]*)$/); - if ( isRE ) { + }; + + const reEscape = /[.*+\-?^${}()|[\]\\]/g; + + // Must always return a RegExp object. + // + // Assume case-sensitivity if there is at least one uppercase in plain + // query text. + const parseQuery = function(query) { + let flags = 'i'; + let reParsed = query.match(/^\/(.+)\/([iu]*)$/); + if ( reParsed !== null ) { try { - query = new RegExp(isRE[1], isRE[2]); + const re = new RegExp(reParsed[1], reParsed[2]); + query = re.source; + flags = re.flags; } catch (e) { - isRE = false; + reParsed = null; } } - if ( isRE === false ) { - query = parseString(query); + if ( reParsed === null ) { + if ( /[A-Z]/.test(query) ) { flags = ''; } + query = parseString(query).replace(reEscape, '\\$&'); } if ( typeof query === 'string' ? query === '' : query.test('') ) { - query = /x^/; + query = 'x^'; } - return query; - } - - function startSearch(cm, state) { + return new RegExp(query, 'gm' + flags); + }; + + let intlNumberFormat; + + const formatNumber = function(n) { + if ( intlNumberFormat === undefined ) { + intlNumberFormat = null; + if ( Intl.NumberFormat instanceof Function ) { + const intl = new Intl.NumberFormat(undefined, { + notation: 'compact', + maximumSignificantDigits: 3 + }); + if ( + intl.resolvedOptions instanceof Function && + intl.resolvedOptions().hasOwnProperty('notation') + ) { + intlNumberFormat = intl; + } + } + } + return n > 10000 && intlNumberFormat instanceof Object + ? intlNumberFormat.format(n) + : n.toLocaleString(); + }; + + const updateCount = function(cm) { + const state = getSearchState(cm); + const lines = state.lines; + const current = cm.getCursor().line; + let l = 0; + let r = lines.length; + let i = -1; + while ( l < r ) { + i = l + r >>> 1; + const candidate = lines[i]; + if ( current === candidate ) { break; } + if ( current < candidate ) { + r = i; + } else /* if ( current > candidate ) */ { + l = i + 1; + } + } + let text = ''; + if ( i !== -1 ) { + text = formatNumber(i + 1); + if ( lines[i] !== current ) { + text = '~' + text; + } + text = text + '\xA0/\xA0'; + } + const count = lines.length; + text += formatNumber(count); + const span = state.widget.querySelector('.cm-search-widget-count'); + span.textContent = text; + span.title = count.toLocaleString(); + }; + + const startSearch = function(cm, state) { state.query = parseQuery(state.queryText); - if ( state.overlay ) { + if ( state.overlay !== undefined ) { cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query)); } state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query)); cm.addOverlay(state.overlay); - if ( cm.showMatchesOnScrollbar ) { - if ( state.annotate ) { + if ( state.dirty || self.searchThread.needHaystack() ) { + self.searchThread.setHaystack(cm.getValue()); + state.dirty = false; + } + self.searchThread.search(state.query).then(lines => { + if ( Array.isArray(lines) === false ) { return; } + state.lines = lines; + const count = lines.length; + updateCount(cm); + if ( state.annotate !== undefined ) { state.annotate.clear(); - state.annotate = null; + state.annotate = undefined; } - state.annotate = cm.showMatchesOnScrollbar( - state.query, - queryCaseInsensitive(state.query), - { multiline: false } - ); - let count = state.annotate.matches.length; - state.widget - .querySelector('.cm-search-widget-count > span:nth-of-type(2)') - .textContent = count > 1000 ? '1000+' : count; - state.widget.setAttribute('data-query', state.queryText); - // Ensure the caret is visible - let input = state.widget.querySelector('.cm-search-widget-input > input'); - input.selectionStart = input.selectionStart; - } - } + if ( count === 0 ) { return; } + state.annotate = cm.annotateScrollbar('CodeMirror-search-match'); + const annotations = []; + let lineBeg = -1; + let lineEnd = -1; + for ( const line of lines ) { + if ( lineBeg === -1 ) { + lineBeg = line; + lineEnd = line + 1; + continue; + } else if ( line === lineEnd ) { + lineEnd = line + 1; + continue; + } + annotations.push({ + from: { line: lineBeg, ch: 0 }, + to: { line: lineEnd, ch: 0 } + }); + lineBeg = -1; + } + if ( lineBeg !== -1 ) { + annotations.push({ + from: { line: lineBeg, ch: 0 }, + to: { line: lineEnd, ch: 0 } + }); + } + state.annotate.update(annotations); + }); + state.widget.setAttribute('data-query', state.queryText); + // Ensure the caret is visible + const input = state.widget.querySelector('.cm-search-widget-input input'); + input.selectionStart = input.selectionStart; + }; - function findNext(cm, dir, callback) { + const findNext = function(cm, dir, callback) { cm.operation(function() { - var state = getSearchState(cm); + const state = getSearchState(cm); if ( !state.query ) { return; } - var cursor = getSearchCursor( + let cursor = getSearchCursor( cm, state.query, dir <= 0 ? cm.getCursor('from') : cm.getCursor('to') ); - let previous = dir < 0; + const previous = dir < 0; if (!cursor.find(previous)) { cursor = getSearchCursor( cm, state.query, - previous ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0) + previous + ? CodeMirror.Pos(cm.lastLine()) + : CodeMirror.Pos(cm.firstLine(), 0) ); if (!cursor.find(previous)) return; } @@ -243,21 +348,22 @@ cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 20); if (callback) callback(cursor.from(), cursor.to()); }); - } + }; - function clearSearch(cm, hard) { + const clearSearch = function(cm, hard) { cm.operation(function() { - var state = getSearchState(cm); + const state = getSearchState(cm); if ( state.query ) { state.query = state.queryText = null; } - if ( state.overlay ) { + state.lines = []; + if ( state.overlay !== undefined ) { cm.removeOverlay(state.overlay); - state.overlay = null; + state.overlay = undefined; } if ( state.annotate ) { state.annotate.clear(); - state.annotate = null; + state.annotate = undefined; } state.widget.removeAttribute('data-query'); if ( hard ) { @@ -267,15 +373,15 @@ cm.state.search = null; } }); - } + }; - function findCommit(cm, dir) { - var state = getSearchState(cm); + const findCommit = function(cm, dir) { + const state = getSearchState(cm); if ( state.queryTimer !== null ) { clearTimeout(state.queryTimer); state.queryTimer = null; } - var queryText = queryTextFromSearchWidget(cm); + const queryText = queryTextFromSearchWidget(cm); if ( queryText === state.queryText ) { return; } state.queryText = queryText; if ( state.queryText === '' ) { @@ -286,12 +392,12 @@ findNext(cm, dir); }); } - } + }; - function findCommand(cm) { - var queryText = cm.getSelection() || undefined; + const findCommand = function(cm) { + let queryText = cm.getSelection() || undefined; if ( !queryText ) { - var word = cm.findWordAt(cm.getCursor()); + const word = cm.findWordAt(cm.getCursor()); queryText = cm.getRange(word.anchor, word.head); if ( /^\W|\W$/.test(queryText) ) { queryText = undefined; @@ -300,31 +406,29 @@ } queryTextToSearchWidget(cm, queryText); findCommit(cm, 1); - } + }; - function findNextCommand(cm) { - var state = getSearchState(cm); + const findNextCommand = function(cm) { + const state = getSearchState(cm); if ( state.query ) { return findNext(cm, 1); } - } + }; - function findPrevCommand(cm) { - var state = getSearchState(cm); + const findPrevCommand = function(cm) { + const state = getSearchState(cm); if ( state.query ) { return findNext(cm, -1); } - } + }; { const searchWidgetTemplate = '
'; @@ -341,4 +445,4 @@ CodeMirror.defineInitHook(function(cm) { getSearchState(cm); }); -}); +})(self.CodeMirror); diff --git a/src/js/contentscript.js b/src/js/contentscript.js index c2b3506b86f9b..da8e4c7aac1c6 100644 --- a/src/js/contentscript.js +++ b/src/js/contentscript.js @@ -114,6 +114,57 @@ if ( typeof vAPI === 'object' && !vAPI.contentScript ) { vAPI.contentScript = true; +/******************************************************************************/ +/******************************************************************************/ +/******************************************************************************/ + +// https://github.com/uBlockOrigin/uBlock-issues/issues/688#issuecomment-663657508 +{ + let context = self; + try { + while ( + context !== self.top && + context.location.protocol === 'about:' + ) { + context = context.parent; + } + } catch(ex) { + } + vAPI.effectiveSelf = context; +} + +/******************************************************************************/ +/******************************************************************************/ +/******************************************************************************/ + +vAPI.userStylesheet = { + added: new Set(), + removed: new Set(), + apply: function(callback) { + if ( this.added.size === 0 && this.removed.size === 0 ) { return; } + vAPI.messaging.send('vapi', { + what: 'userCSS', + add: Array.from(this.added), + remove: Array.from(this.removed), + }).then(( ) => { + if ( callback instanceof Function === false ) { return; } + callback(); + }); + this.added.clear(); + this.removed.clear(); + }, + add: function(cssText, now) { + if ( cssText === '' ) { return; } + this.added.add(cssText); + if ( now ) { this.apply(); } + }, + remove: function(cssText, now) { + if ( cssText === '' ) { return; } + this.removed.add(cssText); + if ( now ) { this.apply(); } + } +}; + /******************************************************************************/ /******************************************************************************/ /******************************************************************************* @@ -141,13 +192,12 @@ vAPI.contentScript = true; // https://github.com/gorhill/uBlock/issues/2147 -vAPI.SafeAnimationFrame = function(callback) { - this.fid = this.tid = undefined; - this.callback = callback; -}; - -vAPI.SafeAnimationFrame.prototype = { - start: function(delay) { +vAPI.SafeAnimationFrame = class { + constructor(callback) { + this.fid = this.tid = undefined; + this.callback = callback; + } + start(delay) { if ( self.vAPI instanceof Object === false ) { return; } if ( delay === undefined ) { if ( this.fid === undefined ) { @@ -161,8 +211,8 @@ vAPI.SafeAnimationFrame.prototype = { if ( this.fid === undefined && this.tid === undefined ) { this.tid = vAPI.setTimeout(( ) => { this.macroToMicro(); }, delay); } - }, - clear: function() { + } + clear() { if ( this.fid !== undefined ) { cancelAnimationFrame(this.fid); this.fid = undefined; @@ -171,27 +221,27 @@ vAPI.SafeAnimationFrame.prototype = { clearTimeout(this.tid); this.tid = undefined; } - }, - macroToMicro: function() { + } + macroToMicro() { this.tid = undefined; this.start(); - }, - onRAF: function() { + } + onRAF() { if ( this.tid !== undefined ) { clearTimeout(this.tid); this.tid = undefined; } this.fid = undefined; this.callback(); - }, - onSTO: function() { + } + onSTO() { if ( this.fid !== undefined ) { cancelAnimationFrame(this.fid); this.fid = undefined; } this.tid = undefined; this.callback(); - }, + } }; /******************************************************************************/ @@ -267,7 +317,9 @@ vAPI.SafeAnimationFrame.prototype = { /******************************************************************************/ /******************************************************************************/ -vAPI.domWatcher = (( ) => { +// vAPI.domWatcher + +{ vAPI.domMutationTime = Date.now(); const addedNodeLists = []; @@ -276,11 +328,11 @@ vAPI.domWatcher = (( ) => { const ignoreTags = new Set([ 'br', 'head', 'link', 'meta', 'script', 'style' ]); const listeners = []; - let domIsReady = false, - domLayoutObserver, - listenerIterator = [], listenerIteratorDirty = false, - removedNodes = false, - safeObserverHandlerTimer; + let domLayoutObserver; + let listenerIterator = []; + let listenerIteratorDirty = false; + let removedNodes = false; + let safeObserverHandlerTimer; const safeObserverHandler = function() { let i = addedNodeLists.length; @@ -340,7 +392,7 @@ vAPI.domWatcher = (( ) => { }; const startMutationObserver = function() { - if ( domLayoutObserver !== undefined || !domIsReady ) { return; } + if ( domLayoutObserver !== undefined ) { return; } domLayoutObserver = new MutationObserver(observerHandler); domLayoutObserver.observe(document.documentElement, { //attributeFilter: [ 'class', 'id' ], @@ -370,7 +422,7 @@ vAPI.domWatcher = (( ) => { if ( listeners.indexOf(listener) !== -1 ) { return; } listeners.push(listener); listenerIteratorDirty = true; - if ( domIsReady !== true ) { return; } + if ( domLayoutObserver === undefined ) { return; } try { listener.onDOMCreated(); } catch (ex) { } startMutationObserver(); @@ -398,7 +450,6 @@ vAPI.domWatcher = (( ) => { }; const start = function() { - domIsReady = true; for ( const listener of getListenerIterator() ) { try { listener.onDOMCreated(); } catch (ex) { } @@ -406,8 +457,8 @@ vAPI.domWatcher = (( ) => { startMutationObserver(); }; - return { start, addListener, removeListener }; -})(); + vAPI.domWatcher = { start, addListener, removeListener }; +} /******************************************************************************/ /******************************************************************************/ @@ -436,15 +487,11 @@ vAPI.injectScriptlet = function(doc, text) { The DOM filterer is the heart of uBO's cosmetic filtering. - DOMBaseFilterer: platform-specific - | - | - +---- DOMFilterer: adds procedural cosmetic filtering + DOMFilterer: adds procedural cosmetic filtering */ -vAPI.DOMFilterer = (function() { - +{ // 'P' stands for 'Procedural' const PSelectorHasTextTask = class { @@ -526,8 +573,12 @@ vAPI.DOMFilterer = (function() { const PSelectorSpathTask = class { constructor(task) { this.spath = task[1]; + this.nth = /^(?:\s*[+~]|:)/.test(this.spath); } - transpose(node, output) { + qsa(node) { + if ( this.nth === false ) { + return node.querySelectorAll(this.spath); + } const parent = node.parentElement; if ( parent === null ) { return; } let pos = 1; @@ -536,9 +587,13 @@ vAPI.DOMFilterer = (function() { if ( node === null ) { break; } pos += 1; } - const nodes = parent.querySelectorAll( + return parent.querySelectorAll( `:scope > :nth-child(${pos})${this.spath}` ); + } + transpose(node, output) { + const nodes = this.qsa(node); + if ( nodes === undefined ) { return; } for ( const node of nodes ) { output.push(node); } @@ -713,6 +768,9 @@ vAPI.DOMFilterer = (function() { this.mustApplySelectors = false; this.selectors = new Map(); this.hiddenNodes = new Set(); + if ( vAPI.domWatcher instanceof Object ) { + vAPI.domWatcher.addListener(this); + } } addProceduralSelectors(aa) { @@ -824,7 +882,7 @@ vAPI.DOMFilterer = (function() { onDOMCreated() { this.domIsReady = true; - this.domFilterer.commitNow(); + this.domFilterer.commit(); } onDOMChanged(addedNodes, removedNodes) { @@ -837,34 +895,217 @@ vAPI.DOMFilterer = (function() { } }; - const DOMFilterer = class extends vAPI.DOMFilterer { + vAPI.DOMFilterer = class { constructor() { - super(); + this.commitTimer = new vAPI.SafeAnimationFrame( + ( ) => { this.commitNow(); } + ); + this.domIsReady = document.readyState !== 'loading'; + this.disabled = false; + this.listeners = []; + this.filterset = new Set(); + this.excludedNodeSet = new WeakSet(); + this.addedCSSRules = new Set(); + this.exceptedCSSRules = []; + this.reOnlySelectors = /\n\{[^\n]+/g; this.exceptions = []; - this.proceduralFilterer = new DOMProceduralFilterer(this); + this.proceduralFilterer = null; this.hideNodeAttr = undefined; this.hideNodeStyleSheetInjected = false; - if ( vAPI.domWatcher instanceof Object ) { - vAPI.domWatcher.addListener(this); + // https://github.com/uBlockOrigin/uBlock-issues/issues/167 + // By the time the DOMContentLoaded is fired, the content script might + // have been disconnected from the background page. Unclear why this + // would happen, so far seems to be a Chromium-specific behavior at + // launch time. + if ( this.domIsReady !== true ) { + document.addEventListener('DOMContentLoaded', ( ) => { + if ( vAPI instanceof Object === false ) { return; } + this.domIsReady = true; + this.commit(); + }); + } + } + + addCSSRule(selectors, declarations, details = {}) { + if ( selectors === undefined ) { return; } + const selectorsStr = Array.isArray(selectors) + ? selectors.join(',\n') + : selectors; + if ( selectorsStr.length === 0 ) { return; } + const entry = { + selectors: selectorsStr, + declarations, + lazy: details.lazy === true, + injected: details.injected === true + }; + this.addedCSSRules.add(entry); + this.filterset.add(entry); + if ( + this.disabled === false && + entry.lazy !== true && + entry.injected !== true + ) { + vAPI.userStylesheet.add(`${selectorsStr}\n{${declarations}}`); + } + this.commit(); + if ( details.silent !== true && this.hasListeners() ) { + this.triggerListeners({ + declarative: [ [ selectorsStr, declarations ] ] + }); + } + } + + exceptCSSRules(exceptions) { + if ( exceptions.length === 0 ) { return; } + this.exceptedCSSRules.push(...exceptions); + if ( this.hasListeners() ) { + this.triggerListeners({ exceptions }); + } + } + + addListener(listener) { + if ( this.listeners.indexOf(listener) !== -1 ) { return; } + this.listeners.push(listener); + } + + removeListener(listener) { + const pos = this.listeners.indexOf(listener); + if ( pos === -1 ) { return; } + this.listeners.splice(pos, 1); + } + + hasListeners() { + return this.listeners.length !== 0; + } + + triggerListeners(changes) { + for ( const listener of this.listeners ) { + listener.onFiltersetChanged(changes); + } + } + + excludeNode(node) { + this.excludedNodeSet.add(node); + this.unhideNode(node); + } + + unexcludeNode(node) { + this.excludedNodeSet.delete(node); + } + + hideNode(node) { + if ( this.excludedNodeSet.has(node) ) { return; } + if ( this.hideNodeAttr === undefined ) { return; } + node.setAttribute(this.hideNodeAttr, ''); + if ( this.hideNodeStyleSheetInjected ) { return; } + this.hideNodeStyleSheetInjected = true; + this.addCSSRule( + `[${this.hideNodeAttr}]`, + 'display:none!important;', + { silent: true } + ); + } + + unhideNode(node) { + if ( this.hideNodeAttr === undefined ) { return; } + node.removeAttribute(this.hideNodeAttr); + } + + toggle(state, callback) { + if ( state === undefined ) { state = this.disabled; } + if ( state !== this.disabled ) { return; } + this.disabled = !state; + const userStylesheet = vAPI.userStylesheet; + for ( const entry of this.filterset ) { + const rule = `${entry.selectors}\n{${entry.declarations}}`; + if ( this.disabled ) { + userStylesheet.remove(rule); + } else { + userStylesheet.add(rule); + } } + userStylesheet.apply(callback); } + getAllSelectors_(all) { + const out = { + declarative: [], + exceptions: this.exceptedCSSRules, + }; + for ( const entry of this.filterset ) { + let selectors = entry.selectors; + if ( all !== true && this.hideNodeAttr !== undefined ) { + selectors = selectors + .replace(`[${this.hideNodeAttr}]`, '') + .replace(/^,\n|,\n$/gm, ''); + if ( selectors === '' ) { continue; } + } + out.declarative.push([ selectors, entry.declarations ]); + } + return out; + } + + // Here we will deal with: + // - Injecting low priority user styles; + // - Notifying listeners about changed filterset. + // https://www.reddit.com/r/uBlockOrigin/comments/9jj0y1/no_longer_blocking_ads/ + // Ensure vAPI is still valid -- it can go away by the time we are + // called, since the port could be force-disconnected from the main + // process. Another approach would be to have vAPI.SafeAnimationFrame + // register a shutdown job: to evaluate. For now I will keep the fix + // trivial. commitNow() { - super.commitNow(); - this.proceduralFilterer.commitNow(); + this.commitTimer.clear(); + if ( vAPI instanceof Object === false ) { return; } + const userStylesheet = vAPI.userStylesheet; + for ( const entry of this.addedCSSRules ) { + if ( + this.disabled === false && + entry.lazy && + entry.injected === false + ) { + userStylesheet.add( + `${entry.selectors}\n{${entry.declarations}}` + ); + } + } + this.addedCSSRules.clear(); + userStylesheet.apply(); + if ( this.proceduralFilterer instanceof Object ) { + this.proceduralFilterer.commitNow(); + } + } + + commit(commitNow) { + if ( commitNow ) { + this.commitTimer.clear(); + this.commitNow(); + } else { + this.commitTimer.start(); + } + } + + proceduralFiltererInstance() { + if ( this.proceduralFilterer instanceof Object === false ) { + this.proceduralFilterer = new DOMProceduralFilterer(this); + } + return this.proceduralFilterer; } addProceduralSelectors(aa) { - this.proceduralFilterer.addProceduralSelectors(aa); + if ( aa.length === 0 ) { return; } + this.proceduralFiltererInstance().addProceduralSelectors(aa); } createProceduralFilter(o) { - return this.proceduralFilterer.createProceduralFilter(o); + return this.proceduralFiltererInstance().createProceduralFilter(o); } getAllSelectors() { - const out = super.getAllSelectors(); - out.procedural = Array.from(this.proceduralFilterer.selectors.values()); + const out = this.getAllSelectors_(false); + out.procedural = this.proceduralFilterer instanceof Object + ? Array.from(this.proceduralFilterer.selectors.values()) + : []; return out; } @@ -872,34 +1113,23 @@ vAPI.DOMFilterer = (function() { return this.exceptions.join(',\n'); } - onDOMCreated() { - if ( super.onDOMCreated instanceof Function ) { - super.onDOMCreated(); - } - this.proceduralFilterer.onDOMCreated(); - } - - onDOMChanged() { - if ( super.onDOMChanged instanceof Function ) { - super.onDOMChanged.apply(this, arguments); - } - this.proceduralFilterer.onDOMChanged.apply( - this.proceduralFilterer, - arguments - ); + getFilteredElementCount() { + const details = this.getAllSelectors_(true); + if ( Array.isArray(details.declarative) === false ) { return 0; } + const selectors = details.declarative.map(entry => entry[0]); + if ( selectors.length === 0 ) { return 0; } + return document.querySelectorAll(selectors.join(',\n')).length; } }; - - return DOMFilterer; -})(); - -vAPI.domFilterer = new vAPI.DOMFilterer(); +} /******************************************************************************/ /******************************************************************************/ /******************************************************************************/ -vAPI.domCollapser = (function() { +// vAPI.domCollapser + +{ const messaging = vAPI.messaging; const toCollapse = new Map(); const src1stProps = { @@ -1079,6 +1309,24 @@ vAPI.domCollapser = (function() { } }; + const stop = function() { + document.removeEventListener('error', onResourceFailed, true); + if ( processTimer !== undefined ) { + clearTimeout(processTimer); + } + if ( vAPI.domWatcher instanceof Object ) { + vAPI.domWatcher.removeListener(domWatcherInterface); + } + vAPI.shutdown.remove(stop); + vAPI.domCollapser = null; + }; + + const start = function() { + if ( vAPI.domWatcher instanceof Object ) { + vAPI.domWatcher.addListener(domWatcherInterface); + } + }; + const domWatcherInterface = { onDOMCreated: function() { if ( self.vAPI instanceof Object === false ) { return; } @@ -1109,12 +1357,7 @@ vAPI.domCollapser = (function() { document.addEventListener('error', onResourceFailed, true); - vAPI.shutdown.add(function() { - document.removeEventListener('error', onResourceFailed, true); - if ( processTimer !== undefined ) { - clearTimeout(processTimer); - } - }); + vAPI.shutdown.add(stop); }, onDOMChanged: function(addedNodes) { if ( addedNodes.length === 0 ) { return; } @@ -1132,18 +1375,16 @@ vAPI.domCollapser = (function() { } }; - if ( vAPI.domWatcher instanceof Object ) { - vAPI.domWatcher.addListener(domWatcherInterface); - } - - return { add, addMany, addIFrame, addIFrames, process }; -})(); + vAPI.domCollapser = { start }; +} /******************************************************************************/ /******************************************************************************/ /******************************************************************************/ -vAPI.domSurveyor = (function() { +// vAPI.domSurveyor + +{ const messaging = vAPI.messaging; const queriedIds = new Set(); const queriedClasses = new Set(); @@ -1402,18 +1643,18 @@ vAPI.domSurveyor = (function() { vAPI.domWatcher.addListener(domWatcherInterface); }; - return { start }; -})(); + vAPI.domSurveyor = { start }; +} /******************************************************************************/ /******************************************************************************/ /******************************************************************************/ -// Bootstrapping allows all components of the content script to be launched -// if/when needed. - -vAPI.bootstrap = (function() { +// vAPI.bootstrap: +// Bootstrapping allows all components of the content script +// to be launched if/when needed. +{ const bootstrapPhase2 = function() { // This can happen on Firefox. For instance: // https://github.com/gorhill/uBlock/issues/1893 @@ -1484,11 +1725,13 @@ vAPI.bootstrap = (function() { return; } + vAPI.domCollapser.start(); + if ( response.noCosmeticFiltering ) { vAPI.domFilterer = null; vAPI.domSurveyor = null; } else { - const domFilterer = vAPI.domFilterer; + const domFilterer = vAPI.domFilterer = new vAPI.DOMFilterer(); if ( response.noGenericCosmeticFiltering || cfeDetails.noDOMSurveying ) { vAPI.domSurveyor = null; } @@ -1555,17 +1798,17 @@ vAPI.bootstrap = (function() { } }; - return function() { + vAPI.bootstrap = function() { vAPI.messaging.send('contentscript', { what: 'retrieveContentScriptParameters', - url: window.location.href, - isRootFrame: window === window.top, + url: vAPI.effectiveSelf.location.href, + isRootFrame: self === self.top, charset: document.characterSet, }).then(response => { bootstrapPhase1(response); }); }; -})(); +} // This starts bootstrap process. vAPI.bootstrap(); diff --git a/src/js/messaging.js b/src/js/messaging.js index d0e18a1f2a2bb..a8d6128f08c73 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -524,11 +524,15 @@ const µb = µBlock; const retrieveContentScriptParameters = function(senderDetails, request) { if ( µb.readyToFilter !== true ) { return; } - const { url, tabId, frameId } = senderDetails; - if ( url === undefined || tabId === undefined || frameId === undefined ) { + const { url: senderURL, tabId, frameId } = senderDetails; + if ( + tabId === undefined || + frameId === undefined || + senderURL === undefined || + senderURL !== request.url && senderURL.startsWith('about:') === false + ) { return; } - if ( request.url !== url ) { return; } const pageStore = µb.pageStoreFromTabId(tabId); if ( pageStore === null || pageStore.getNetFilteringSwitch() === false ) { return; @@ -714,7 +718,7 @@ const onMessage = function(request, sender, callback) { xhr.responseType = 'text'; xhr.onload = function() { this.onload = null; - var i18n = { + const i18n = { bidi_dir: document.body.getAttribute('dir'), create: vAPI.i18n('pickerCreate'), pick: vAPI.i18n('pickerPick'), diff --git a/src/js/reverselookup-worker.js b/src/js/reverselookup-worker.js deleted file mode 100644 index 9aee1f9a4726c..0000000000000 --- a/src/js/reverselookup-worker.js +++ /dev/null @@ -1,293 +0,0 @@ -/******************************************************************************* - - uBlock Origin - a browser extension to block requests. - 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 onmessage, postMessage */ - -'use strict'; - -/******************************************************************************/ - -const reBlockStart = /^#block-start-(\d+)\n/gm; -let listEntries = Object.create(null); - -/******************************************************************************/ - -const extractBlocks = function(content, begId, endId) { - reBlockStart.lastIndex = 0; - const out = []; - let match = reBlockStart.exec(content); - while ( match !== null ) { - const beg = match.index + match[0].length; - const blockId = parseInt(match[1], 10); - if ( blockId >= begId && blockId < endId ) { - const end = content.indexOf('#block-end-' + match[1], beg); - out.push(content.slice(beg, end)); - reBlockStart.lastIndex = end; - } - match = reBlockStart.exec(content); - } - return out.join('\n'); -}; - -/******************************************************************************/ - -// https://github.com/MajkiIT/polish-ads-filter/issues/14768#issuecomment-536006312 -// Avoid reporting badfilter-ed filters. - -const fromNetFilter = function(details) { - const lists = []; - const compiledFilter = details.compiledFilter; - - for ( const assetKey in listEntries ) { - const entry = listEntries[assetKey]; - if ( entry === undefined ) { continue; } - const content = extractBlocks(entry.content, 0, 1); - let pos = 0; - for (;;) { - pos = content.indexOf(compiledFilter, pos); - if ( pos === -1 ) { break; } - // We need an exact match. - // https://github.com/gorhill/uBlock/issues/1392 - // https://github.com/gorhill/uBlock/issues/835 - const notFound = pos !== 0 && content.charCodeAt(pos - 1) !== 0x0A; - pos += compiledFilter.length; - if ( - notFound || - pos !== content.length && content.charCodeAt(pos) !== 0x0A - ) { - continue; - } - lists.push({ - assetKey: assetKey, - title: entry.title, - supportURL: entry.supportURL - }); - break; - } - } - - const response = {}; - response[details.rawFilter] = lists; - - postMessage({ - id: details.id, - response: response - }); -}; - -/******************************************************************************/ - -// Looking up filter lists from a cosmetic filter is a bit more complicated -// than with network filters: -// -// The filter is its raw representation, not its compiled version. This is -// because the cosmetic filtering engine can't translate a live cosmetic -// filter into its compiled version. Reason is I do not want to burden -// cosmetic filtering with the resource overhead of being able to re-compile -// live cosmetic filters. I want the cosmetic filtering code to be left -// completely unaffected by reverse lookup requirements. -// -// Mainly, given a CSS selector and a hostname as context, we will derive -// various versions of compiled filters and see if there are matches. This way -// the whole CPU cost is incurred by the reverse lookup code -- in a worker -// thread, and the cosmetic filtering engine incurs no cost at all. -// -// For this though, the reverse lookup code here needs some knowledge of -// the inners of the cosmetic filtering engine. -// FilterContainer.fromCompiledContent() is our reference code to create -// the various compiled versions. - -const fromCosmeticFilter = function(details) { - const match = /^#@?#\^?/.exec(details.rawFilter); - const prefix = match[0]; - const exception = prefix.charAt(1) === '@'; - const selector = details.rawFilter.slice(prefix.length); - const isHtmlFilter = prefix.endsWith('^'); - const hostname = details.hostname; - - // The longer the needle, the lower the number of false positives. - // https://github.com/uBlockOrigin/uBlock-issues/issues/1139 - // Mind that there is no guarantee a selector has `\w` characters. - const needle = selector.match(/\w+|\*/g).reduce(function(a, b) { - return a.length > b.length ? a : b; - }); - - const regexFromLabels = (prefix, hn, suffix) => - new RegExp( - prefix + - hn.split('.').reduce((acc, item) => `(${acc}\\.)?${item}`) + - suffix - ); - - // https://github.com/uBlockOrigin/uBlock-issues/issues/803 - // Support looking up selectors of the form `*##...` - const reHostname = regexFromLabels('^', hostname, '$'); - let reEntity; - { - const domain = details.domain; - const pos = domain.indexOf('.'); - if ( pos !== -1 ) { - reEntity = regexFromLabels( - '^(', - hostname.slice(0, pos + hostname.length - domain.length), - '\\.)?\\*$' - ); - } - } - - const hostnameMatches = hn => { - return hn === '' || - reHostname.test(hn) || - reEntity !== undefined && reEntity.test(hn); - }; - - const response = Object.create(null); - - for ( const assetKey in listEntries ) { - const entry = listEntries[assetKey]; - if ( entry === undefined ) { continue; } - let content = extractBlocks(entry.content, 1000, 2000), - isProcedural, - found; - let pos = 0; - while ( (pos = content.indexOf(needle, pos)) !== -1 ) { - let beg = content.lastIndexOf('\n', pos); - if ( beg === -1 ) { beg = 0; } - let end = content.indexOf('\n', pos); - if ( end === -1 ) { end = content.length; } - pos = end; - const fargs = JSON.parse(content.slice(beg, end)); - const filterType = fargs[0]; - - // https://github.com/gorhill/uBlock/issues/2763 - if ( filterType >= 0 && filterType <= 5 && details.ignoreGeneric ) { - continue; - } - - // Do not confuse cosmetic filters with HTML ones. - if ( (filterType === 64) !== isHtmlFilter ) { continue; } - - switch ( filterType ) { - // Lowly generic cosmetic filters - case 0: // simple id-based - if ( exception ) { break; } - if ( fargs[1] !== selector.slice(1) ) { break; } - if ( selector.charAt(0) !== '#' ) { break; } - found = prefix + selector; - break; - case 2: // simple class-based - if ( exception ) { break; } - if ( fargs[1] !== selector.slice(1) ) { break; } - if ( selector.charAt(0) !== '.' ) { break; } - found = prefix + selector; - break; - case 1: // complex id-based - case 3: // complex class-based - if ( exception ) { break; } - if ( fargs[2] !== selector ) { break; } - found = prefix + selector; - break; - // Highly generic cosmetic filters - case 4: // simple highly generic - case 5: // complex highly generic - if ( exception ) { break; } - if ( fargs[1] !== selector ) { break; } - found = prefix + selector; - break; - // Specific cosmetic filtering - // Generic exception - case 8: - // HTML filtering - case 64: - if ( exception !== ((fargs[2] & 0b001) !== 0) ) { break; } - isProcedural = (fargs[2] & 0b010) !== 0; - if ( - isProcedural === false && fargs[3] !== selector || - isProcedural && JSON.parse(fargs[3]).raw !== selector - ) { - break; - } - if ( hostnameMatches(fargs[1]) === false ) { break; } - // https://www.reddit.com/r/uBlockOrigin/comments/d6vxzj/ - // Ignore match if specific cosmetic filters are disabled - if ( - filterType === 8 && - exception === false && - details.ignoreSpecific - ) { - break; - } - found = fargs[1] + prefix + selector; - break; - // Scriptlet injection - case 32: - if ( exception !== ((fargs[2] & 0b001) !== 0) ) { break; } - if ( fargs[3] !== selector ) { break; } - if ( hostnameMatches(fargs[1]) ) { - found = fargs[1] + prefix + selector; - } - break; - } - if ( found !== undefined ) { - if ( response[found] === undefined ) { - response[found] = []; - } - response[found].push({ - assetKey: assetKey, - title: entry.title, - supportURL: entry.supportURL - }); - break; - } - } - } - - postMessage({ - id: details.id, - response: response - }); -}; - -/******************************************************************************/ - -onmessage = function(e) { // jshint ignore:line - const msg = e.data; - - switch ( msg.what ) { - case 'resetLists': - listEntries = Object.create(null); - break; - - case 'setList': - listEntries[msg.details.assetKey] = msg.details; - break; - - case 'fromNetFilter': - fromNetFilter(msg); - break; - - case 'fromCosmeticFilter': - fromCosmeticFilter(msg); - break; - } -}; - -/******************************************************************************/ diff --git a/src/js/reverselookup.js b/src/js/reverselookup.js index 8be3519677601..7f39244feb871 100644 --- a/src/js/reverselookup.js +++ b/src/js/reverselookup.js @@ -23,198 +23,461 @@ /******************************************************************************/ -µBlock.staticFilteringReverseLookup = (( ) => { +(( ) => { +// >>>>> start of local scope /******************************************************************************/ -const workerTTL = 5 * 60 * 1000; -const pendingResponses = new Map(); +// Worker context + +if ( + self.WorkerGlobalScope instanceof Object && + self instanceof self.WorkerGlobalScope +) { + const reBlockStart = /^#block-start-(\d+)\n/gm; + let listEntries = Object.create(null); + + const extractBlocks = function(content, begId, endId) { + reBlockStart.lastIndex = 0; + const out = []; + let match = reBlockStart.exec(content); + while ( match !== null ) { + const beg = match.index + match[0].length; + const blockId = parseInt(match[1], 10); + if ( blockId >= begId && blockId < endId ) { + const end = content.indexOf('#block-end-' + match[1], beg); + out.push(content.slice(beg, end)); + reBlockStart.lastIndex = end; + } + match = reBlockStart.exec(content); + } + return out.join('\n'); + }; -let worker = null; -let workerTTLTimer; -let needLists = true; -let messageId = 1; + // https://github.com/MajkiIT/polish-ads-filter/issues/14768#issuecomment-536006312 + // Avoid reporting badfilter-ed filters. + + const fromNetFilter = function(details) { + const lists = []; + const compiledFilter = details.compiledFilter; + + for ( const assetKey in listEntries ) { + const entry = listEntries[assetKey]; + if ( entry === undefined ) { continue; } + const content = extractBlocks(entry.content, 0, 1); + let pos = 0; + for (;;) { + pos = content.indexOf(compiledFilter, pos); + if ( pos === -1 ) { break; } + // We need an exact match. + // https://github.com/gorhill/uBlock/issues/1392 + // https://github.com/gorhill/uBlock/issues/835 + const notFound = pos !== 0 && + content.charCodeAt(pos - 1) !== 0x0A; + pos += compiledFilter.length; + if ( + notFound || + pos !== content.length && content.charCodeAt(pos) !== 0x0A + ) { + continue; + } + lists.push({ + assetKey: assetKey, + title: entry.title, + supportURL: entry.supportURL + }); + break; + } + } -/******************************************************************************/ + const response = {}; + response[details.rawFilter] = lists; + + self.postMessage({ id: details.id, response }); + }; -const onWorkerMessage = function(e) { - const msg = e.data; - const resolver = pendingResponses.get(msg.id); - pendingResponses.delete(msg.id); - resolver(msg.response); -}; + // Looking up filter lists from a cosmetic filter is a bit more complicated + // than with network filters: + // + // The filter is its raw representation, not its compiled version. This is + // because the cosmetic filtering engine can't translate a live cosmetic + // filter into its compiled version. Reason is I do not want to burden + // cosmetic filtering with the resource overhead of being able to recompile + // live cosmetic filters. I want the cosmetic filtering code to be left + // completely unaffected by reverse lookup requirements. + // + // Mainly, given a CSS selector and a hostname as context, we will derive + // various versions of compiled filters and see if there are matches. This + // way the whole CPU cost is incurred by the reverse lookup code -- in a + // worker thread, and the cosmetic filtering engine incurs no cost at all. + // + // For this though, the reverse lookup code here needs some knowledge of + // the inners of the cosmetic filtering engine. + // FilterContainer.fromCompiledContent() is our reference code to create + // the various compiled versions. + + const fromCosmeticFilter = function(details) { + const match = /^#@?#\^?/.exec(details.rawFilter); + const prefix = match[0]; + const exception = prefix.charAt(1) === '@'; + const selector = details.rawFilter.slice(prefix.length); + const isHtmlFilter = prefix.endsWith('^'); + const hostname = details.hostname; + + // The longer the needle, the lower the number of false positives. + // https://github.com/uBlockOrigin/uBlock-issues/issues/1139 + // Mind that there is no guarantee a selector has `\w` characters. + const needle = selector.match(/\w+|\*/g).reduce(function(a, b) { + return a.length > b.length ? a : b; + }); -/******************************************************************************/ + const regexFromLabels = (prefix, hn, suffix) => + new RegExp( + prefix + + hn.split('.').reduce((acc, item) => `(${acc}\\.)?${item}`) + + suffix + ); + + // https://github.com/uBlockOrigin/uBlock-issues/issues/803 + // Support looking up selectors of the form `*##...` + const reHostname = regexFromLabels('^', hostname, '$'); + let reEntity; + { + const domain = details.domain; + const pos = domain.indexOf('.'); + if ( pos !== -1 ) { + reEntity = regexFromLabels( + '^(', + hostname.slice(0, pos + hostname.length - domain.length), + '\\.)?\\*$' + ); + } + } + + const hostnameMatches = hn => { + return hn === '' || + reHostname.test(hn) || + reEntity !== undefined && reEntity.test(hn); + }; + + const response = Object.create(null); + + for ( const assetKey in listEntries ) { + const entry = listEntries[assetKey]; + if ( entry === undefined ) { continue; } + let content = extractBlocks(entry.content, 1000, 2000), + isProcedural, + found; + let pos = 0; + while ( (pos = content.indexOf(needle, pos)) !== -1 ) { + let beg = content.lastIndexOf('\n', pos); + if ( beg === -1 ) { beg = 0; } + let end = content.indexOf('\n', pos); + if ( end === -1 ) { end = content.length; } + pos = end; + const fargs = JSON.parse(content.slice(beg, end)); + const filterType = fargs[0]; + + // https://github.com/gorhill/uBlock/issues/2763 + if ( + filterType >= 0 && + filterType <= 5 && + details.ignoreGeneric + ) { + continue; + } + + // Do not confuse cosmetic filters with HTML ones. + if ( (filterType === 64) !== isHtmlFilter ) { continue; } + + switch ( filterType ) { + // Lowly generic cosmetic filters + case 0: // simple id-based + if ( exception ) { break; } + if ( fargs[1] !== selector.slice(1) ) { break; } + if ( selector.charAt(0) !== '#' ) { break; } + found = prefix + selector; + break; + case 2: // simple class-based + if ( exception ) { break; } + if ( fargs[1] !== selector.slice(1) ) { break; } + if ( selector.charAt(0) !== '.' ) { break; } + found = prefix + selector; + break; + case 1: // complex id-based + case 3: // complex class-based + if ( exception ) { break; } + if ( fargs[2] !== selector ) { break; } + found = prefix + selector; + break; + // Highly generic cosmetic filters + case 4: // simple highly generic + case 5: // complex highly generic + if ( exception ) { break; } + if ( fargs[1] !== selector ) { break; } + found = prefix + selector; + break; + // Specific cosmetic filtering + // Generic exception + case 8: + // HTML filtering + case 64: + if ( exception !== ((fargs[2] & 0b001) !== 0) ) { break; } + isProcedural = (fargs[2] & 0b010) !== 0; + if ( + isProcedural === false && fargs[3] !== selector || + isProcedural && JSON.parse(fargs[3]).raw !== selector + ) { + break; + } + if ( hostnameMatches(fargs[1]) === false ) { break; } + // https://www.reddit.com/r/uBlockOrigin/comments/d6vxzj/ + // Ignore match if specific cosmetic filters are disabled + if ( + filterType === 8 && + exception === false && + details.ignoreSpecific + ) { + break; + } + found = fargs[1] + prefix + selector; + break; + // Scriptlet injection + case 32: + if ( exception !== ((fargs[2] & 0b001) !== 0) ) { break; } + if ( fargs[3] !== selector ) { break; } + if ( hostnameMatches(fargs[1]) ) { + found = fargs[1] + prefix + selector; + } + break; + } + if ( found !== undefined ) { + if ( response[found] === undefined ) { + response[found] = []; + } + response[found].push({ + assetKey: assetKey, + title: entry.title, + supportURL: entry.supportURL + }); + break; + } + } + } + + self.postMessage({ id: details.id, response }); + }; + + self.onmessage = function(e) { // jshint ignore:line + const msg = e.data; + + switch ( msg.what ) { + case 'resetLists': + listEntries = Object.create(null); + break; + + case 'setList': + listEntries[msg.details.assetKey] = msg.details; + break; + + case 'fromNetFilter': + fromNetFilter(msg); + break; -const stopWorker = function() { - if ( workerTTLTimer !== undefined ) { - clearTimeout(workerTTLTimer); - workerTTLTimer = undefined; - } - if ( worker === null ) { return; } - worker.terminate(); - worker = null; - needLists = true; - for ( const resolver of pendingResponses.values() ) { - resolver(); - } - pendingResponses.clear(); -}; + case 'fromCosmeticFilter': + fromCosmeticFilter(msg); + break; + } + }; + + return; +} /******************************************************************************/ -const initWorker = function() { - if ( worker === null ) { - worker = new Worker('js/reverselookup-worker.js'); - worker.onmessage = onWorkerMessage; - } +// Main context + +{ + if ( typeof µBlock !== 'object' ) { return; } - // The worker will be shutdown after n minutes without being used. - if ( workerTTLTimer !== undefined ) { - clearTimeout(workerTTLTimer); - } - workerTTLTimer = vAPI.setTimeout(stopWorker, workerTTL); + const workerTTL = 5 * 60 * 1000; + const pendingResponses = new Map(); - if ( needLists === false ) { - return Promise.resolve(); - } - needLists = false; + let worker = null; + let workerTTLTimer; + let needLists = true; + let messageId = 1; - const entries = new Map(); + const onWorkerMessage = function(e) { + const msg = e.data; + const resolver = pendingResponses.get(msg.id); + pendingResponses.delete(msg.id); + resolver(msg.response); + }; + + const stopWorker = function() { + if ( workerTTLTimer !== undefined ) { + clearTimeout(workerTTLTimer); + workerTTLTimer = undefined; + } + if ( worker === null ) { return; } + worker.terminate(); + worker = null; + needLists = true; + for ( const resolver of pendingResponses.values() ) { + resolver(); + } + pendingResponses.clear(); + }; - const onListLoaded = function(details) { - const entry = entries.get(details.assetKey); + const initWorker = function() { + if ( worker === null ) { + worker = new Worker('js/reverselookup.js'); + worker.onmessage = onWorkerMessage; + } - // https://github.com/gorhill/uBlock/issues/536 - // Use assetKey when there is no filter list title. + // The worker will be shutdown after n minutes without being used. + if ( workerTTLTimer !== undefined ) { + clearTimeout(workerTTLTimer); + } + workerTTLTimer = vAPI.setTimeout(stopWorker, workerTTL); - worker.postMessage({ - what: 'setList', - details: { - assetKey: details.assetKey, - title: entry.title || details.assetKey, - supportURL: entry.supportURL, - content: details.content + if ( needLists === false ) { + return Promise.resolve(); + } + needLists = false; + + const entries = new Map(); + + const onListLoaded = function(details) { + const entry = entries.get(details.assetKey); + + // https://github.com/gorhill/uBlock/issues/536 + // Use assetKey when there is no filter list title. + + worker.postMessage({ + what: 'setList', + details: { + assetKey: details.assetKey, + title: entry.title || details.assetKey, + supportURL: entry.supportURL, + content: details.content + } + }); + }; + + const µb = µBlock; + for ( const listKey in µb.availableFilterLists ) { + if ( µb.availableFilterLists.hasOwnProperty(listKey) === false ) { + continue; } - }); - }; + const entry = µb.availableFilterLists[listKey]; + if ( entry.off === true ) { continue; } + entries.set(listKey, { + title: listKey !== µb.userFiltersPath ? + entry.title : + vAPI.i18n('1pPageName'), + supportURL: entry.supportURL || '' + }); + } + if ( entries.size === 0 ) { + return Promise.resolve(); + } - const µb = µBlock; - for ( const listKey in µb.availableFilterLists ) { - if ( µb.availableFilterLists.hasOwnProperty(listKey) === false ) { - continue; + const promises = []; + for ( const listKey of entries.keys() ) { + promises.push( + µb.getCompiledFilterList(listKey).then(details => { + onListLoaded(details); + }) + ); } - const entry = µb.availableFilterLists[listKey]; - if ( entry.off === true ) { continue; } - entries.set(listKey, { - title: listKey !== µb.userFiltersPath ? - entry.title : - vAPI.i18n('1pPageName'), - supportURL: entry.supportURL || '' - }); - } - if ( entries.size === 0 ) { - return Promise.resolve(); - } - - const promises = []; - for ( const listKey of entries.keys() ) { - promises.push( - µb.getCompiledFilterList(listKey).then(details => { - onListLoaded(details); - }) - ); - } - return Promise.all(promises); -}; + return Promise.all(promises); + }; -/******************************************************************************/ + const fromNetFilter = async function(rawFilter) { + if ( typeof rawFilter !== 'string' || rawFilter === '' ) { return; } -const fromNetFilter = async function(rawFilter) { - if ( typeof rawFilter !== 'string' || rawFilter === '' ) { return; } + const µb = µBlock; + const writer = new µb.CompiledLineIO.Writer(); + const parser = new vAPI.StaticFilteringParser(); + parser.setMaxTokenLength(µb.urlTokenizer.MAX_TOKEN_LENGTH); + parser.analyze(rawFilter); - const µb = µBlock; - const writer = new µb.CompiledLineIO.Writer(); - const parser = new vAPI.StaticFilteringParser(); - parser.setMaxTokenLength(µb.urlTokenizer.MAX_TOKEN_LENGTH); - parser.analyze(rawFilter); + if ( µb.staticNetFilteringEngine.compile(parser, writer) === false ) { + return; + } - if ( µb.staticNetFilteringEngine.compile(parser, writer) === false ) { - return; - } + await initWorker(); - await initWorker(); + const id = messageId++; + worker.postMessage({ + what: 'fromNetFilter', + id: id, + compiledFilter: writer.last(), + rawFilter: rawFilter + }); - const id = messageId++; - worker.postMessage({ - what: 'fromNetFilter', - id: id, - compiledFilter: writer.last(), - rawFilter: rawFilter - }); + return new Promise(resolve => { + pendingResponses.set(id, resolve); + }); + }; - return new Promise(resolve => { - pendingResponses.set(id, resolve); - }); -}; + const fromCosmeticFilter = async function(details) { + if ( + typeof details.rawFilter !== 'string' || + details.rawFilter === '' + ) { + return; + } -/******************************************************************************/ + await initWorker(); -const fromCosmeticFilter = async function(details) { - if ( typeof details.rawFilter !== 'string' || details.rawFilter === '' ) { - return; - } - - await initWorker(); - - const id = messageId++; - const hostname = µBlock.URI.hostnameFromURI(details.url); - - worker.postMessage({ - what: 'fromCosmeticFilter', - id: id, - domain: µBlock.URI.domainFromHostname(hostname), - hostname: hostname, - ignoreGeneric: - µBlock.staticNetFilteringEngine.matchStringReverse( - 'generichide', - details.url - ) === 2, - ignoreSpecific: - µBlock.staticNetFilteringEngine.matchStringReverse( - 'specifichide', - details.url - ) === 2, - rawFilter: details.rawFilter - }); - - return new Promise(resolve => { - pendingResponses.set(id, resolve); - }); - -}; + const id = messageId++; + const hostname = µBlock.URI.hostnameFromURI(details.url); -/******************************************************************************/ + worker.postMessage({ + what: 'fromCosmeticFilter', + id: id, + domain: µBlock.URI.domainFromHostname(hostname), + hostname: hostname, + ignoreGeneric: + µBlock.staticNetFilteringEngine.matchStringReverse( + 'generichide', + details.url + ) === 2, + ignoreSpecific: + µBlock.staticNetFilteringEngine.matchStringReverse( + 'specifichide', + details.url + ) === 2, + rawFilter: details.rawFilter + }); -// This tells the worker that filter lists may have changed. + return new Promise(resolve => { + pendingResponses.set(id, resolve); + }); + }; -const resetLists = function() { - needLists = true; - if ( worker === null ) { return; } - worker.postMessage({ what: 'resetLists' }); -}; + // This tells the worker that filter lists may have changed. -/******************************************************************************/ + const resetLists = function() { + needLists = true; + if ( worker === null ) { return; } + worker.postMessage({ what: 'resetLists' }); + }; -return { - fromNetFilter, - fromCosmeticFilter, - resetLists, - shutdown: stopWorker -}; + µBlock.staticFilteringReverseLookup = { + fromNetFilter, + fromCosmeticFilter, + resetLists, + shutdown: stopWorker + }; +} /******************************************************************************/ +// <<<<< end of local scope })(); /******************************************************************************/ diff --git a/src/js/scriptlets/cosmetic-logger.js b/src/js/scriptlets/cosmetic-logger.js index 57ee4723a17c0..ca17d53216724 100644 --- a/src/js/scriptlets/cosmetic-logger.js +++ b/src/js/scriptlets/cosmetic-logger.js @@ -30,7 +30,6 @@ if ( typeof vAPI !== 'object' || - vAPI.domFilterer instanceof Object === false || vAPI.domWatcher instanceof Object === false ) { return; @@ -211,10 +210,12 @@ const processTimer = new vAPI.SafeAnimationFrame(( ) => { if ( toLog.length === 0 ) { return; } + const location = vAPI.effectiveSelf.location; + vAPI.messaging.send('scriptlets', { what: 'logCosmeticFilteringData', - frameURL: window.location.href, - frameHostname: window.location.hostname, + frameURL: location.href, + frameHostname: location.hostname, matchedSelectors: toLog, }); //console.timeEnd('dom logger/scanning for matches'); @@ -295,6 +296,9 @@ const handlers = { }, onDOMCreated: function() { + if ( vAPI.domFilterer instanceof Object === false ) { + return shutdown(); + } handlers.onFiltersetChanged(vAPI.domFilterer.getAllSelectors()); vAPI.domFilterer.addListener(handlers); attributeObserver.observe(document.body, { @@ -317,17 +321,35 @@ const handlers = { /******************************************************************************/ +const shutdown = function() { + 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); + } + if ( vAPI.broadcastListener instanceof Object ) { + vAPI.broadcastListener.remove(broadcastListener); + } +}; + +/******************************************************************************/ + +const broadcastListener = msg => { + if ( msg.what === 'loggerDisabled' ) { + shutdown(); + } +}; + +/******************************************************************************/ + vAPI.messaging.extend().then(extended => { - if ( extended !== true ) { return; } - const broadcastListener = msg => { - if ( msg.what === 'loggerDisabled' ) { - processTimer.clear(); - attributeObserver.disconnect(); - vAPI.domFilterer.removeListener(handlers); - vAPI.domWatcher.removeListener(handlers); - vAPI.broadcastListener.remove(broadcastListener); - } - }; + if ( extended !== true ) { + return shutdown(); + } vAPI.broadcastListener.add(broadcastListener); }); diff --git a/src/js/static-filtering-parser.js b/src/js/static-filtering-parser.js index 7610ea580d474..e1186ff59eff1 100644 --- a/src/js/static-filtering-parser.js +++ b/src/js/static-filtering-parser.js @@ -103,7 +103,9 @@ const Parser = class { this.extOptionsIterator = new ExtOptionsIterator(this); this.maxTokenLength = Number.MAX_SAFE_INTEGER; this.reIsLocalhostRedirect = /(?:0\.0\.0\.0|(?:broadcast|local)host|local|ip6-\w+)\b/; - this.reHostname = /^[^\x00-\x24\x26-\x29\x2B\x2C\x2F\x3A-\x5E\x60\x7B-\x7F]+/; + this.reHostname = /^[^\x00-\x24\x26-\x29\x2B\x2C\x2F\x3A-\x40\x5B-\x5E\x60\x7B-\x7F]+/; + this.reHostsSink = /^[\w-.:\[\]]+$/; + this.reHostsSource = /^[^\x00-\x24\x26-\x29\x2B\x2C\x2F\x3A-\x40\x5B-\x5E\x60\x7B-\x7F]+$/; this.reUnicodeChar = /[^\x00-\x7F]/; this.reUnicodeChars = /[^\x00-\x7F]/g; this.punycoder = new URL(self.location); @@ -507,36 +509,35 @@ const Parser = class { // Patterns with more than one space are dubious. { const { i, len } = this.patternSpan; + const noOptionsAnchor = this.optionsAnchorSpan.len === 0; let j = len; for (;;) { if ( j === 0 ) { break; } j -= 3; const bits = this.slices[i+j]; - if ( hasBits(bits, BITSpace) ) { break; } + if ( noOptionsAnchor && hasBits(bits, BITSpace) ) { break; } this.patternBits |= bits; } if ( j !== 0 ) { - let dubious = false; - for ( let k = this.patternSpan.i; k < j; k += 3 ) { - if ( hasNoBits(this.slices[k], BITSpace) ) { continue; } - this.patternBits |= BITSpace; - if ( this.interactive ) { - this.markSlices(this.patternSpan.i, j, BITError); - } - dubious = true; - break; - } - if ( dubious === false ) { + const sink = this.strFromSlices(this.patternSpan.i, j - 3); + if ( this.reHostsSink.test(sink) ) { this.patternSpan.i += j + 3; this.patternSpan.len -= j + 3; - if ( this.reIsLocalhostRedirect.test(this.getNetPattern()) ) { - this.flavorBits |= BITFlavorIgnore; - } if ( this.interactive ) { this.markSlices(0, this.patternSpan.i, BITIgnore); } + const source = this.getNetPattern(); + if ( this.reIsLocalhostRedirect.test(source) ) { + this.flavorBits |= BITFlavorIgnore; + } else if ( this.reHostsSource.test(source) === false ) { + this.patternBits |= BITError; + } + } else { + this.patternBits |= BITError; + } + if ( hasBits(this.patternBits, BITError) ) { + this.markSpan(this.patternSpan, BITError); } - // TODO: test again for regex? } } @@ -631,10 +632,8 @@ const Parser = class { this.markSpan(this.patternSpan, BITError); } } else if ( - this.patternIsDubious() || ( - this.patternHasUnicode() && - this.toASCII(true) === false - ) + this.patternIsDubious() === false && + this.toASCII(true) === false ) { this.markSlices( this.patternLeftAnchorSpan.i, @@ -909,21 +908,43 @@ const Parser = class { } // https://github.com/chrisaljoudi/uBlock/issues/1096 + // https://github.com/ryanbr/fanboy-adblock/issues/1384 // Examples of dubious filter content: // - Spaces characters - // - Single character other than `*` wildcard - // - Zero-length pattern with anchors - // https://github.com/ryanbr/fanboy-adblock/issues/1384 + // - Single character with no options + // - Wildcard(s) with no options + // - Zero-length pattern with no options patternIsDubious() { - return hasBits(this.patternBits, BITSpace) || ( - this.patternBits !== BITAsterisk && - this.optionsSpan.len === 0 && ( - this.patternSpan.len === 0 && - this.patternLeftAnchorSpan.len !== 0 || - this.patternSpan.len === 3 && - this.slices[this.patternSpan.i+2] === 1 - ) - ); + if ( hasBits(this.patternBits, BITError) ) { return true; } + if ( hasBits(this.patternBits, BITSpace) ) { + if ( this.interactive ) { + this.markSpan(this.patternSpan, BITError); + } + return true; + } + if ( this.patternSpan.len > 3 || this.optionsSpan.len !== 0 ) { + return false; + } + if ( + this.patternSpan.len === 3 && + this.slices[this.patternSpan.i+2] !== 1 && + hasNoBits(this.patternBits, BITAsterisk) + ) { + return false; + } + if ( this.interactive === false ) { return true; } + let l, r; + if ( this.patternSpan.len !== 0 ) { + l = this.patternSpan.i; + r = this.optionsAnchorSpan.i; + } else { + l = this.patternLeftAnchorSpan.i; + r = this.patternLeftAnchorSpan.len !== 0 + ? this.optionsAnchorSpan.i + : this.optionsSpan.i; + } + this.markSlices(l, r, BITError); + return true; } patternIsMatchAll() { diff --git a/src/js/storage.js b/src/js/storage.js index a6a546a4e0526..957294755fa4a 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -387,10 +387,13 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { this.hiddenSettings.autoCommentFilterTemplate.indexOf('{{') !== -1 ) { const d = new Date(); + // Date in YYYY-MM-DD format - https://stackoverflow.com/a/50130338 + const ISO8061Date = new Date(d.getTime() + + (d.getTimezoneOffset()*60000)).toISOString().split('T')[0]; comment = '! ' + this.hiddenSettings.autoCommentFilterTemplate - .replace('{{date}}', d.toLocaleDateString()) + .replace('{{date}}', ISO8061Date) .replace('{{time}}', d.toLocaleTimeString()) .replace('{{origin}}', options.origin); } @@ -532,7 +535,7 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { vAPI.storage.get('availableFilterLists'), this.assets.metadata(), ]); - + oldAvailableLists = bin && bin.availableFilterLists || {}; for ( const assetKey in entries ) { diff --git a/src/js/tab.js b/src/js/tab.js index 2e978163d128b..55b400dd5e214 100644 --- a/src/js/tab.js +++ b/src/js/tab.js @@ -513,6 +513,10 @@ housekeep itself. } }; + // https://github.com/uBlockOrigin/uBlock-issues/issues/1184 + // Do not consider a tab opened from `about:newtab` to be a popup + // candidate. + const onTabCreated = async function(createDetails) { const { sourceTabId, sourceFrameId, tabId } = createDetails; const popup = popupCandidates.get(tabId); @@ -533,6 +537,13 @@ housekeep itself. catch (reason) { return; } + if ( + Array.isArray(openerDetails) === false || + openerDetails.length !== 2 || + openerDetails[1].url === 'about:newtab' + ) { + return; + } popupCandidates.set( tabId, new PopupCandidate(createDetails, openerDetails) diff --git a/src/lib/codemirror/addon/search/matchesonscrollbar.js b/src/lib/codemirror/addon/search/matchesonscrollbar.js deleted file mode 100644 index 8a4a82758495b..0000000000000 --- a/src/lib/codemirror/addon/search/matchesonscrollbar.js +++ /dev/null @@ -1,97 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("./searchcursor"), require("../scroll/annotatescrollbar")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "./searchcursor", "../scroll/annotatescrollbar"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineExtension("showMatchesOnScrollbar", function(query, caseFold, options) { - if (typeof options == "string") options = {className: options}; - if (!options) options = {}; - return new SearchAnnotation(this, query, caseFold, options); - }); - - function SearchAnnotation(cm, query, caseFold, options) { - this.cm = cm; - this.options = options; - var annotateOptions = {listenForChanges: false}; - for (var prop in options) annotateOptions[prop] = options[prop]; - if (!annotateOptions.className) annotateOptions.className = "CodeMirror-search-match"; - this.annotation = cm.annotateScrollbar(annotateOptions); - this.query = query; - this.caseFold = caseFold; - this.gap = {from: cm.firstLine(), to: cm.lastLine() + 1}; - this.matches = []; - this.update = null; - - this.findMatches(); - this.annotation.update(this.matches); - - var self = this; - cm.on("change", this.changeHandler = function(_cm, change) { self.onChange(change); }); - } - - var MAX_MATCHES = 1000; - - SearchAnnotation.prototype.findMatches = function() { - if (!this.gap) return; - for (var i = 0; i < this.matches.length; i++) { - var match = this.matches[i]; - if (match.from.line >= this.gap.to) break; - if (match.to.line >= this.gap.from) this.matches.splice(i--, 1); - } - var cursor = this.cm.getSearchCursor(this.query, CodeMirror.Pos(this.gap.from, 0), {caseFold: this.caseFold, multiline: this.options.multiline}); - var maxMatches = this.options && this.options.maxMatches || MAX_MATCHES; - while (cursor.findNext()) { - var match = {from: cursor.from(), to: cursor.to()}; - if (match.from.line >= this.gap.to) break; - this.matches.splice(i++, 0, match); - if (this.matches.length > maxMatches) break; - } - this.gap = null; - }; - - function offsetLine(line, changeStart, sizeChange) { - if (line <= changeStart) return line; - return Math.max(changeStart, line + sizeChange); - } - - SearchAnnotation.prototype.onChange = function(change) { - var startLine = change.from.line; - var endLine = CodeMirror.changeEnd(change).line; - var sizeChange = endLine - change.to.line; - if (this.gap) { - this.gap.from = Math.min(offsetLine(this.gap.from, startLine, sizeChange), change.from.line); - this.gap.to = Math.max(offsetLine(this.gap.to, startLine, sizeChange), change.from.line); - } else { - this.gap = {from: change.from.line, to: endLine + 1}; - } - - if (sizeChange) for (var i = 0; i < this.matches.length; i++) { - var match = this.matches[i]; - var newFrom = offsetLine(match.from.line, startLine, sizeChange); - if (newFrom != match.from.line) match.from = CodeMirror.Pos(newFrom, match.from.ch); - var newTo = offsetLine(match.to.line, startLine, sizeChange); - if (newTo != match.to.line) match.to = CodeMirror.Pos(newTo, match.to.ch); - } - clearTimeout(this.update); - var self = this; - this.update = setTimeout(function() { self.updateAfterChange(); }, 250); - }; - - SearchAnnotation.prototype.updateAfterChange = function() { - this.findMatches(); - this.annotation.update(this.matches); - }; - - SearchAnnotation.prototype.clear = function() { - this.cm.off("change", this.changeHandler); - this.annotation.clear(); - }; -}); diff --git a/src/lib/diff/swatinem_diff.js b/src/lib/diff/swatinem_diff.js index 0601df0eeaf2f..481f2cfbc45e9 100644 --- a/src/lib/diff/swatinem_diff.js +++ b/src/lib/diff/swatinem_diff.js @@ -241,3 +241,23 @@ return Diff; })(self); + + + + + + + + +/******************************************************************************* + + 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/whitelist.html b/src/whitelist.html index 2a1dbe022fd53..b06744c742a11 100644 --- a/src/whitelist.html +++ b/src/whitelist.html @@ -41,12 +41,12 @@ - + diff --git a/tools/make-chromium.sh b/tools/make-chromium.sh index 3307fbca55578..e1f6e5c373518 100755 --- a/tools/make-chromium.sh +++ b/tools/make-chromium.sh @@ -11,19 +11,6 @@ mkdir -p $DES echo "*** uBlock0.chromium: copying common files" bash ./tools/copy-common-files.sh $DES -echo "*** uBlock0.chromium: concatenating content scripts" -cat $DES/js/vapi-usercss.js > /tmp/contentscript.js -echo >> /tmp/contentscript.js -grep -v "^'use strict';$" $DES/js/vapi-usercss.real.js >> /tmp/contentscript.js -echo >> /tmp/contentscript.js -grep -v "^'use strict';$" $DES/js/vapi-usercss.pseudo.js >> /tmp/contentscript.js -echo >> /tmp/contentscript.js -grep -v "^'use strict';$" $DES/js/contentscript.js >> /tmp/contentscript.js -mv /tmp/contentscript.js $DES/js/contentscript.js -rm $DES/js/vapi-usercss.js -rm $DES/js/vapi-usercss.real.js -rm $DES/js/vapi-usercss.pseudo.js - # Chrome store-specific cp -R $DES/_locales/nb $DES/_locales/no diff --git a/tools/make-firefox.sh b/tools/make-firefox.sh index 6bca88b7c9d44..d1708cdd45973 100755 --- a/tools/make-firefox.sh +++ b/tools/make-firefox.sh @@ -16,20 +16,8 @@ cp -R $DES/_locales/nb $DES/_locales/no cp platform/firefox/manifest.json $DES/ cp platform/firefox/webext.js $DES/js/ -cp platform/firefox/vapi-usercss.js $DES/js/ cp platform/firefox/vapi-webrequest.js $DES/js/ -echo "*** uBlock0.firefox: concatenating content scripts" -cat $DES/js/vapi-usercss.js > /tmp/contentscript.js -echo >> /tmp/contentscript.js -grep -v "^'use strict';$" $DES/js/vapi-usercss.real.js >> /tmp/contentscript.js -echo >> /tmp/contentscript.js -grep -v "^'use strict';$" $DES/js/contentscript.js >> /tmp/contentscript.js -mv /tmp/contentscript.js $DES/js/contentscript.js -rm $DES/js/vapi-usercss.js -rm $DES/js/vapi-usercss.real.js -rm $DES/js/vapi-usercss.pseudo.js - # Firefox/webext-specific rm $DES/img/icon_128.png diff --git a/tools/make-opera.sh b/tools/make-opera.sh index 73219157dbd9a..8ce392fac0cfe 100755 --- a/tools/make-opera.sh +++ b/tools/make-opera.sh @@ -11,19 +11,6 @@ mkdir -p $DES echo "*** uBlock0.opera: copying common files" bash ./tools/copy-common-files.sh $DES -echo "*** uBlock0.opera: concatenating content scripts" -cat $DES/js/vapi-usercss.js > /tmp/contentscript.js -echo >> /tmp/contentscript.js -grep -v "^'use strict';$" $DES/js/vapi-usercss.real.js >> /tmp/contentscript.js -echo >> /tmp/contentscript.js -grep -v "^'use strict';$" $DES/js/vapi-usercss.pseudo.js >> /tmp/contentscript.js -echo >> /tmp/contentscript.js -grep -v "^'use strict';$" $DES/js/contentscript.js >> /tmp/contentscript.js -mv /tmp/contentscript.js $DES/js/contentscript.js -rm $DES/js/vapi-usercss.js -rm $DES/js/vapi-usercss.real.js -rm $DES/js/vapi-usercss.pseudo.js - # Opera-specific cp platform/opera/manifest.json $DES/ rm -r $DES/_locales/az diff --git a/tools/make-webext.sh b/tools/make-webext.sh index 601afc4a8ac7a..80e66a0f47c37 100755 --- a/tools/make-webext.sh +++ b/tools/make-webext.sh @@ -17,28 +17,13 @@ bash ./tools/copy-common-files.sh $DES cp -R $DES/_locales/nb $DES/_locales/no cp platform/webext/manifest.json $DES/ -cp platform/webext/vapi-usercss.js $DES/js/ # https://github.com/uBlockOrigin/uBlock-issues/issues/407 echo "*** uBlock0.webext: concatenating vapi-webrequest.js" cat platform/chromium/vapi-webrequest.js > /tmp/vapi-webrequest.js -echo >> /tmp/contentscript.js grep -v "^'use strict';$" platform/firefox/vapi-webrequest.js >> /tmp/vapi-webrequest.js mv /tmp/vapi-webrequest.js $DES/js/vapi-webrequest.js -echo "*** uBlock0.webext: concatenating content scripts" -cat $DES/js/vapi-usercss.js > /tmp/contentscript.js -echo >> /tmp/contentscript.js -grep -v "^'use strict';$" $DES/js/vapi-usercss.real.js >> /tmp/contentscript.js -echo >> /tmp/contentscript.js -grep -v "^'use strict';$" $DES/js/vapi-usercss.pseudo.js >> /tmp/contentscript.js -echo >> /tmp/contentscript.js -grep -v "^'use strict';$" $DES/js/contentscript.js >> /tmp/contentscript.js -mv /tmp/contentscript.js $DES/js/contentscript.js -rm $DES/js/vapi-usercss.js -rm $DES/js/vapi-usercss.real.js -rm $DES/js/vapi-usercss.pseudo.js - echo "*** uBlock0.webext: Generating meta..." python3 tools/make-webext-meta.py $DES/