From c39adacc5082891f07e6dda2f11a8f8b01bb0bf5 Mon Sep 17 00:00:00 2001 From: gorhill Date: Fri, 16 Dec 2016 16:25:36 -0500 Subject: [PATCH] better abstraction of user styles --- src/js/contentscript.js | 154 +++++++++++++++-------------- src/js/scriptlets/cosmetic-off.js | 14 +-- src/js/scriptlets/cosmetic-on.js | 14 +-- src/js/scriptlets/dom-inspector.js | 26 +---- 4 files changed, 82 insertions(+), 126 deletions(-) diff --git a/src/js/contentscript.js b/src/js/contentscript.js index ae0a99303..d297caaa2 100644 --- a/src/js/contentscript.js +++ b/src/js/contentscript.js @@ -66,6 +66,17 @@ Additionally, the domSurveyor can turn itself off once it decides that it has become pointless (repeatedly not finding new cosmetic filters). + The domFilterer makes use of platform-dependent user styles[1] code, or + provide a default generic implementation if none is present. + At time of writing, only modern Firefox provides a custom implementation, + which makes for solid, reliable and low overhead cosmetic filtering on + Firefox. + The generic implementation[2] performs as best as can be, but won't ever be + as reliable as real user styles. + [1] "user styles" refer to local CSS rules which have priority over, and + can't be overriden by a web page's own CSS rules. + [2] below, see platformUserCSS / platformHideNode / platformUnhideNode + */ /******************************************************************************/ @@ -138,8 +149,7 @@ var reParserEx = /:(?:has|matches-css|matches-css-before|matches-css-after|style var allExceptions = createSet(), allSelectors = createSet(), stagedNodes = [], - matchesProp = vAPI.matchesProp, - userCSS = vAPI.userCSS; + matchesProp = vAPI.matchesProp; // Complex selectors, due to their nature may need to be "de-committed". A // Set() is used to implement this functionality. @@ -161,6 +171,62 @@ var cosmeticFiltersActivated = function() { /******************************************************************************/ +// If a platform does not support its own vAPI.userCSS (user styles), we +// provide a default (imperfect) implementation. + +// Probably no longer need to watch for style tags removal/tampering with fix +// to https://github.com/gorhill/uBlock/issues/963 + +var platformUserCSS = (function() { + if ( vAPI.userCSS instanceof Object ) { + return vAPI.userCSS; + } + + return { + enabled: true, + styles: [], + add: function(css) { + var style = document.createElement('style'); + style.setAttribute('type', 'text/css'); + style.textContent = css; + if ( document.head ) { + document.head.appendChild(style); + } + this.styles.push(style); + if ( style.sheet ) { + style.sheet.disabled = !this.enabled; + } + }, + remove: function(css) { + var i = this.styles.length, + style, parent; + while ( i-- ) { + style = this.styles[i]; + if ( style.textContent !== css ) { continue; } + parent = style.parentNode; + if ( parent !== null ) { + parent.removeChild(style); + } + this.styles.splice(i, 1); + } + }, + toggle: function(state) { + if ( this.styles.length === '' ) { return; } + if ( state === undefined ) { + state = !this.enabled; + } + var i = this.styles.length, style; + while ( i-- ) { + style = this.styles[i]; + if ( style.sheet !== null ) { + style.sheet.disabled = !state; + } + } + this.enabled = state; + } + }; +})(); + // If a platform does not provide its own (improved) vAPI.hideNode, we assign // a default one to try to override author styles as best as can be. @@ -344,7 +410,6 @@ var runXpathJob = function(job, fn) { var domFilterer = { addedNodesHandlerMissCount: 0, - removedNodesHandlerMissCount: 0, commitTimer: null, disabledId: vAPI.randomToken(), enabled: true, @@ -428,54 +493,6 @@ var domFilterer = { } }, - addStyleTag: function(text) { - var styleTag = document.createElement('style'); - styleTag.setAttribute('type', 'text/css'); - styleTag.textContent = text; - if ( document.head ) { - document.head.appendChild(styleTag); - } - this.styleTags.push(styleTag); - if ( userCSS ) { - userCSS.add(text); - } - }, - - checkStyleTags_: function() { - var doc = document, - html = doc.documentElement, - head = doc.head, - newParent = head || html; - if ( newParent === null ) { return; } - this.removedNodesHandlerMissCount += 1; - var styles = this.styleTags, - style, oldParent; - for ( var i = 0; i < styles.length; i++ ) { - style = styles[i]; - oldParent = style.parentNode; - // https://github.com/gorhill/uBlock/issues/1031 - // If our style tag was disabled, re-insert into the page. - if ( - style.disabled && - oldParent !== null && - style.hasAttribute(this.disabledId) === false - ) { - oldParent.removeChild(style); - oldParent = null; - } - if ( oldParent === head || oldParent === html ) { continue; } - style.disabled = false; - newParent.appendChild(style); - this.removedNodesHandlerMissCount = 0; - } - }, - - checkStyleTags: function() { - if ( this.removedNodesHandlerMissCount < 16 ) { - this.checkStyleTags_(); - } - }, - commit_: function() { this.commitTimer.clear(); @@ -538,7 +555,7 @@ var domFilterer = { } if ( styleText !== '' ) { - this.addStyleTag(styleText); + platformUserCSS.add(styleText); } // Un-hide nodes previously hidden. @@ -623,19 +640,17 @@ var domFilterer = { }, toggleOff: function() { - if ( userCSS ) { - userCSS.toggle(false); - } + platformUserCSS.toggle(false); this.enabled = false; }, toggleOn: function() { - if ( userCSS ) { - userCSS.toggle(true); - } + platformUserCSS.toggle(true); this.enabled = true; }, + userCSS: platformUserCSS, + unhideNode: function(node) { if ( node[this.hiddenId] !== undefined ) { this.hiddenNodeCount--; @@ -651,13 +666,8 @@ var domFilterer = { platformHideNode(node); }, - domChangedHandler: function(addedNodes, removedNodes) { + domChangedHandler: function(addedNodes) { this.commit(addedNodes); - // https://github.com/gorhill/uBlock/issues/873 - // This will ensure our style elements will stay in the DOM. - if ( removedNodes ) { - domFilterer.checkStyleTags(); - } }, start: function() { @@ -818,9 +828,9 @@ vAPI.domWatcher = (function() { } addedNodeLists.length = 0; if ( addedNodes.length !== 0 || removedNodes ) { - listeners[0](addedNodes, removedNodes); + listeners[0](addedNodes); if ( listeners[1] ) { - listeners[1](addedNodes, removedNodes); + listeners[1](addedNodes); } addedNodes.length = 0; removedNodes = false; @@ -1485,19 +1495,16 @@ vAPI.domSurveyor = (function() { surveyPhase2(addedNodes); }; - var domChangedHandler = function(addedNodes, removedNodes) { + var domChangedHandler = function(addedNodes) { if ( cosmeticSurveyingMissCount > 255 ) { vAPI.domWatcher.removeListener(domChangedHandler); vAPI.domSurveyor = null; - domFilterer.domChangedHandler(addedNodes, removedNodes); + domFilterer.domChangedHandler(addedNodes); domFilterer.start(); return; } surveyPhase1(addedNodes); - if ( removedNodes ) { - domFilterer.checkStyleTags(); - } }; var start = function() { @@ -1535,11 +1542,6 @@ vAPI.domIsLoaded = function(ev) { vAPI.domCollapser.start(); if ( vAPI.domFilterer ) { - // https://github.com/chrisaljoudi/uBlock/issues/789 - // https://github.com/gorhill/uBlock/issues/873 - // Be sure our style tags used for cosmetic filtering are still - // applied. - vAPI.domFilterer.checkStyleTags(); // To avoid neddless CPU overhead, we commit existing cosmetic filters // only if the page loaded "slowly", i.e. if the code here had to wait // for a DOMContentLoaded event -- in which case the DOM may have diff --git a/src/js/scriptlets/cosmetic-off.js b/src/js/scriptlets/cosmetic-off.js index 8fdac3619..cd92eb780 100644 --- a/src/js/scriptlets/cosmetic-off.js +++ b/src/js/scriptlets/cosmetic-off.js @@ -28,24 +28,12 @@ return; } - var styles = vAPI.domFilterer.styleTags; - - // Disable all cosmetic filtering-related styles from the DOM - var i = styles.length, style; - while ( i-- ) { - style = styles[i]; - if ( style.sheet !== null ) { - style.sheet.disabled = true; - style[vAPI.sessionId] = true; - } - } - var elems = []; try { elems = document.querySelectorAll('[' + vAPI.domFilterer.hiddenId + ']'); } catch (e) { } - i = elems.length; + var i = elems.length; while ( i-- ) { vAPI.domFilterer.showNode(elems[i]); } diff --git a/src/js/scriptlets/cosmetic-on.js b/src/js/scriptlets/cosmetic-on.js index 1a450e074..d3a6de830 100644 --- a/src/js/scriptlets/cosmetic-on.js +++ b/src/js/scriptlets/cosmetic-on.js @@ -28,24 +28,12 @@ return; } - // Insert all cosmetic filtering-related style tags in the DOM - - var styles = vAPI.domFilterer.styleTags; - var i = styles.length, style; - while ( i-- ) { - style = styles[i]; - if ( style.sheet !== null ) { - style.sheet.disabled = false; - style[vAPI.sessionId] = undefined; - } - } - var elems = []; try { elems = document.querySelectorAll('[' + vAPI.domFilterer.hiddenId + ']'); } catch (e) { } - i = elems.length; + var i = elems.length; while ( i-- ) { vAPI.domFilterer.unshowNode(elems[i]); } diff --git a/src/js/scriptlets/dom-inspector.js b/src/js/scriptlets/dom-inspector.js index 50c8391c7..a31f6988a 100644 --- a/src/js/scriptlets/dom-inspector.js +++ b/src/js/scriptlets/dom-inspector.js @@ -686,10 +686,6 @@ var cosmeticFilterMapper = (function() { matchesFnName = 'webkitMatchesSelector'; } - // Why the call to hideNode()? - // Not all target nodes have necessarily been force-hidden, - // do it now so that the inspector does not unhide these - // nodes when disabling style tags. var nodesFromStyleTag = function(rootNode) { var filterMap = nodeToCosmeticFilterMap, selectors, selector, @@ -741,16 +737,7 @@ var cosmeticFilterMapper = (function() { }; var incremental = function(rootNode) { - var styleTags = vAPI.domFilterer.styleTags || []; - var styleTag; - var i = styleTags.length; - while ( i-- ) { - styleTag = styleTags[i]; - if ( styleTag.sheet !== null ) { - styleTag.sheet.disabled = true; - styleTag[vAPI.sessionId] = true; - } - } + vAPI.domFilterer.userCSS.toggle(false); nodesFromStyleTag(rootNode); }; @@ -760,16 +747,7 @@ var cosmeticFilterMapper = (function() { }; var shutdown = function() { - var styleTags = vAPI.domFilterer.styleTags || []; - var styleTag; - var i = styleTags.length; - while ( i-- ) { - styleTag = styleTags[i]; - if ( styleTag.sheet !== null ) { - styleTag.sheet.disabled = false; - styleTag[vAPI.sessionId] = undefined; - } - } + vAPI.domFilterer.userCSS.toggle(true); }; return {