From 5c68867b92735931a791dfedf4ef9608cc364862 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Wed, 22 Jul 2020 10:21:16 -0400 Subject: [PATCH] Deprecate pseudo user styles code The pseudo user styles code served only browsers based on Chromium 65 and earlier -- Chromium 66 supports native user styles and was first released more than two years ago. In Chromium-based browsers, the pseudo user styles code is being unconditionally injected in every page/frame just in case the browser is version 65 or earlier. Removing pseudo user styles reduce uBO's main content script in Chromium-based browsers by more than 20K. Related thread: - https://github.com/NanoAdblocker/NanoCore/issues/348#issuecomment-653646507 --- platform/chromium/vapi-usercss.js | 54 --- platform/chromium/vapi-usercss.pseudo.js | 562 ----------------------- platform/chromium/vapi-usercss.real.js | 282 ------------ platform/firefox/vapi-usercss.js | 48 -- platform/webext/vapi-usercss.js | 54 --- src/js/contentscript.js | 315 ++++++++++--- tools/make-chromium.sh | 13 - tools/make-firefox.sh | 12 - tools/make-opera.sh | 13 - tools/make-webext.sh | 15 - 10 files changed, 261 insertions(+), 1107 deletions(-) delete mode 100644 platform/chromium/vapi-usercss.js delete mode 100644 platform/chromium/vapi-usercss.pseudo.js delete mode 100644 platform/chromium/vapi-usercss.real.js delete mode 100644 platform/firefox/vapi-usercss.js delete mode 100644 platform/webext/vapi-usercss.js diff --git a/platform/chromium/vapi-usercss.js b/platform/chromium/vapi-usercss.js deleted file mode 100644 index 0cf97b7aa..000000000 --- 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 4d0fda69f..000000000 --- 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 8ec746f6d..000000000 --- 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 de4946c7d..000000000 --- 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 6ec8ebb92..000000000 --- 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/js/contentscript.js b/src/js/contentscript.js index c2b3506b8..7cbc12381 100644 --- a/src/js/contentscript.js +++ b/src/js/contentscript.js @@ -114,6 +114,38 @@ if ( typeof vAPI === 'object' && !vAPI.contentScript ) { vAPI.contentScript = true; +/******************************************************************************/ +/******************************************************************************/ +/******************************************************************************/ + +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 +173,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 +192,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 +202,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 +298,9 @@ vAPI.SafeAnimationFrame.prototype = { /******************************************************************************/ /******************************************************************************/ -vAPI.domWatcher = (( ) => { +// vAPI.domWatcher + +{ vAPI.domMutationTime = Date.now(); const addedNodeLists = []; @@ -406,8 +439,8 @@ vAPI.domWatcher = (( ) => { startMutationObserver(); }; - return { start, addListener, removeListener }; -})(); + vAPI.domWatcher = { start, addListener, removeListener }; +} /******************************************************************************/ /******************************************************************************/ @@ -436,15 +469,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 { @@ -837,9 +866,19 @@ 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.hideNodeAttr = undefined; @@ -847,13 +886,177 @@ vAPI.DOMFilterer = (function() { 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.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(); this.proceduralFilterer.commitNow(); } + commit(commitNow) { + if ( commitNow ) { + this.commitTimer.clear(); + this.commitNow(); + } else { + this.commitTimer.start(); + } + } + addProceduralSelectors(aa) { this.proceduralFilterer.addProceduralSelectors(aa); } @@ -863,8 +1066,10 @@ vAPI.DOMFilterer = (function() { } getAllSelectors() { - const out = super.getAllSelectors(); - out.procedural = Array.from(this.proceduralFilterer.selectors.values()); + const out = this.getAllSelectors_(false); + out.procedural = Array.from( + this.proceduralFilterer.selectors.values() + ); return out; } @@ -872,34 +1077,34 @@ vAPI.DOMFilterer = (function() { return this.exceptions.join(',\n'); } + 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; + } + 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 ); } }; - - return DOMFilterer; -})(); - -vAPI.domFilterer = new vAPI.DOMFilterer(); +} /******************************************************************************/ /******************************************************************************/ /******************************************************************************/ -vAPI.domCollapser = (function() { +// vAPI.domCollapser + +{ const messaging = vAPI.messaging; const toCollapse = new Map(); const src1stProps = { @@ -1136,14 +1341,16 @@ vAPI.domCollapser = (function() { vAPI.domWatcher.addListener(domWatcherInterface); } - return { add, addMany, addIFrame, addIFrames, process }; -})(); + vAPI.domCollapser = { add, addMany, addIFrame, addIFrames, process }; +} /******************************************************************************/ /******************************************************************************/ /******************************************************************************/ -vAPI.domSurveyor = (function() { +// vAPI.domSurveyor + +{ const messaging = vAPI.messaging; const queriedIds = new Set(); const queriedClasses = new Set(); @@ -1402,18 +1609,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 @@ -1488,7 +1695,7 @@ vAPI.bootstrap = (function() { 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,7 +1762,7 @@ vAPI.bootstrap = (function() { } }; - return function() { + vAPI.bootstrap = function() { vAPI.messaging.send('contentscript', { what: 'retrieveContentScriptParameters', url: window.location.href, @@ -1565,7 +1772,7 @@ vAPI.bootstrap = (function() { bootstrapPhase1(response); }); }; -})(); +} // This starts bootstrap process. vAPI.bootstrap(); diff --git a/tools/make-chromium.sh b/tools/make-chromium.sh index 3307fbca5..e1f6e5c37 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 6bca88b7c..d1708cdd4 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 73219157d..8ce392fac 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 601afc4a8..80e66a0f4 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/