From 66de9a3b5c3534cd5add38d49d110b7017c10365 Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 15 Dec 2016 09:53:53 -0500 Subject: [PATCH 01/39] new revision for dev build --- platform/chromium/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index 2d6015ddab486..eafc0b33441f5 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "uBlock Origin", - "version": "1.10.2", + "version": "1.10.3.0", "default_locale": "en", "description": "__MSG_extShortDesc__", From 94637ee4ff410f1f61664c06befc010e8bff1250 Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 15 Dec 2016 10:47:32 -0500 Subject: [PATCH 02/39] fix https://github.com/nikrolls/uBlock-Edge/issues/30 --- platform/firefox/frameModule.js | 39 +++++---- platform/firefox/vapi-client.js | 1 + src/js/contentscript.js | 143 +++++++++++++++++++------------- 3 files changed, 111 insertions(+), 72 deletions(-) diff --git a/platform/firefox/frameModule.js b/platform/firefox/frameModule.js index 77f99a5615738..66c75fa90ad3d 100644 --- a/platform/firefox/frameModule.js +++ b/platform/firefox/frameModule.js @@ -384,23 +384,34 @@ var contentObserver = { svc.scriptloader.loadSubScript(script, sandbox); }; - sandbox.injectCSS = function(sheetURI) { + let canUserStyles = (function() { try { - let wu = win.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils); - wu.loadSheetUsingURIString(sheetURI, wu.USER_SHEET); + return win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .loadSheetUsingURIString instanceof Function; } catch(ex) { } - }; - - sandbox.removeCSS = function(sheetURI) { - try { - let wu = win.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils); - wu.removeSheetUsingURIString(sheetURI, wu.USER_SHEET); - } catch (ex) { - } - }; + return false; + })(); + + if ( canUserStyles ) { + sandbox.injectCSS = function(sheetURI) { + try { + let wu = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + wu.loadSheetUsingURIString(sheetURI, wu.USER_SHEET); + } catch(ex) { + } + }; + sandbox.removeCSS = function(sheetURI) { + try { + let wu = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + wu.removeSheetUsingURIString(sheetURI, wu.USER_SHEET); + } catch (ex) { + } + }; + } sandbox.topContentScript = win === win.top; diff --git a/platform/firefox/vapi-client.js b/platform/firefox/vapi-client.js index c9f8ccf1291ba..b34f2183a718f 100644 --- a/platform/firefox/vapi-client.js +++ b/platform/firefox/vapi-client.js @@ -505,6 +505,7 @@ if ( self.injectCSS ) { return state ? this._load() : this._unload(); } }; + vAPI.hideNode = vAPI.unhideNode = function(){}; } /******************************************************************************/ diff --git a/src/js/contentscript.js b/src/js/contentscript.js index 6106d6a332753..ae0a993038826 100644 --- a/src/js/contentscript.js +++ b/src/js/contentscript.js @@ -126,10 +126,6 @@ vAPI.domFilterer = (function() { /******************************************************************************/ -var shadowId = document.documentElement.shadowRoot !== undefined ? - vAPI.randomToken(): - undefined; - var jobQueue = [ { t: 'css-hide', _0: [] }, // to inject in style tag { t: 'css-style', _0: [] }, // to inject in style tag @@ -165,6 +161,87 @@ var cosmeticFiltersActivated = function() { /******************************************************************************/ +// 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. + +var platformHideNode = vAPI.hideNode, + platformUnhideNode = vAPI.unhideNode; + +(function() { + if ( platformHideNode instanceof Function ) { + return; + } + + var uid, + timerId, + observer, + changedNodes = []; + var observerOptions = { + attributes: true, + attributeFilter: [ 'style' ] + }; + + var overrideInlineStyle = function(node) { + var style = window.getComputedStyle(node), + display = style.getPropertyValue('display'), + attr = node.getAttribute('style') || ''; + if ( node[uid] === undefined ) { + node[uid] = node.hasAttribute('style') && attr; + } + if ( display !== '' && display !== 'none' ) { + if ( attr !== '' ) { attr += '; '; } + node.setAttribute('style', attr + 'display: none !important;'); + } + }; + + var timerHandler = function() { + timerId = undefined; + var nodes = changedNodes, + i = nodes.length, node; + while ( i-- ) { + node = nodes[i]; + if ( node[uid] !== undefined ) { + overrideInlineStyle(node); + } + } + nodes.length = 0; + }; + + var observerHandler = function(mutations) { + var i = mutations.length; + while ( i-- ) { + changedNodes.push(mutations[i].target); + } + if ( timerId === undefined ) { + timerId = vAPI.setTimeout(timerHandler, 1); + } + }; + + platformHideNode = function(node) { + if ( uid === undefined ) { + uid = vAPI.randomToken(); + } + overrideInlineStyle(node); + if ( observer === undefined ) { + observer = new MutationObserver(observerHandler); + } + observer.observe(node, observerOptions); + }; + + platformUnhideNode = function(node) { + if ( uid === undefined ) { return; } + var attr = node[uid]; + if ( attr === false ) { + node.removeAttribute('style'); + } else if ( typeof attr === 'string' ) { + node.setAttribute('style', attr); + } + delete node[uid]; + }; +})(); + +/******************************************************************************/ + var runSimpleSelectorJob = function(job, root, fn) { if ( job._1 === undefined ) { job._1 = job._0.join(cssNotHiddenId + ','); @@ -515,30 +592,7 @@ var domFilterer = { this.hiddenNodeCount += 1; node.hidden = true; node[this.hiddenId] = null; - var style = window.getComputedStyle(node), - display = style.getPropertyValue('display'); - if ( display !== '' && display !== 'none' ) { - var styleAttr = node.getAttribute('style') || ''; - node[this.hiddenId] = node.hasAttribute('style') && styleAttr; - if ( styleAttr !== '' ) { styleAttr += '; '; } - node.setAttribute('style', styleAttr + 'display: none !important;'); - } - if ( shadowId === undefined ) { return; } - var shadow = node.shadowRoot; - if ( shadow ) { - if ( shadow[shadowId] && shadow.firstElementChild !== null ) { - shadow.removeChild(shadow.firstElementChild); - } - return; - } - // https://github.com/gorhill/uBlock/pull/555 - // Not all nodes can be shadowed: - // https://github.com/w3c/webcomponents/issues/102 - try { - shadow = node.createShadowRoot(); - shadow[shadowId] = true; - } catch (ex) { - } + platformHideNode(node); }, init: function() { @@ -561,19 +615,7 @@ var domFilterer = { showNode: function(node) { node.hidden = false; - var styleAttr = node[this.hiddenId]; - if ( styleAttr === false ) { - node.removeAttribute('style'); - } else if ( typeof styleAttr === 'string' ) { - node.setAttribute('style', node[this.hiddenId]); - } - var shadow = node.shadowRoot; - if ( shadow && shadow[shadowId] ) { - if ( shadow.firstElementChild !== null ) { - shadow.removeChild(shadow.firstElementChild); - } - shadow.appendChild(document.createElement('content')); - } + platformUnhideNode(node); }, toggleLogging: function(state) { @@ -601,27 +643,12 @@ var domFilterer = { node.removeAttribute(this.hiddenId); node[this.hiddenId] = undefined; node.hidden = false; - var shadow = node.shadowRoot; - if ( shadow && shadow[shadowId] ) { - if ( shadow.firstElementChild !== null ) { - shadow.removeChild(shadow.firstElementChild); - } - shadow.appendChild(document.createElement('content')); - } + platformUnhideNode(node); }, unshowNode: function(node) { node.hidden = true; - var styleAttr = node[this.hiddenId]; - if ( styleAttr === false ) { - node.setAttribute('style', 'display: none !important;'); - } else if ( typeof styleAttr === 'string' ) { - node.setAttribute('style', node[this.hiddenId] + '; display: none !important;'); - } - var shadow = node.shadowRoot; - if ( shadow && shadow[shadowId] && shadow.firstElementChild !== null ) { - shadow.removeChild(shadow.firstElementChild); - } + platformHideNode(node); }, domChangedHandler: function(addedNodes, removedNodes) { From 786db5a6a41b74851f3b54feea76e6e492574def Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Thu, 15 Dec 2016 13:41:38 -0500 Subject: [PATCH 03/39] Update README.md --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6a11cb3d8c920..e3eb1167eb2bb 100644 --- a/README.md +++ b/README.md @@ -139,11 +139,16 @@ Thanks to Debian contributor [Sean Whitton](https://wiki.debian.org/SeanWhitton) #### Microsoft Edge -Early development version by [@nikrolls](https://github.com/nikrolls): . +Developer: [@nikrolls](https://github.com/nikrolls). + +Stable version available in [Microsoft Store](https://www.microsoft.com/store/p/app/9nblggh444l4). +Development version available at . #### Safari (macOS) -Early development version by [@el1t](https://github.com/el1t): . +Developer: [@el1t](https://github.com/el1t). + +Development version available at . #### Note for all browsers From 34b359aa92821f034237c73ea92490ce11f30061 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Thu, 15 Dec 2016 13:42:11 -0500 Subject: [PATCH 04/39] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e3eb1167eb2bb..72b4f7f996708 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ Thanks to Debian contributor [Sean Whitton](https://wiki.debian.org/SeanWhitton) Developer: [@nikrolls](https://github.com/nikrolls). Stable version available in [Microsoft Store](https://www.microsoft.com/store/p/app/9nblggh444l4). + Development version available at . #### Safari (macOS) From c39adacc5082891f07e6dda2f11a8f8b01bb0bf5 Mon Sep 17 00:00:00 2001 From: gorhill Date: Fri, 16 Dec 2016 16:25:36 -0500 Subject: [PATCH 05/39] 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 ae0a993038826..d297caaa22908 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 8fdac361929e1..cd92eb78042eb 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 1a450e074f5d1..d3a6de8309547 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 50c8391c79708..a31f6988a925b 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 { From cec17097a4aad8387e03b6016228f0b10b36ca0d Mon Sep 17 00:00:00 2001 From: gorhill Date: Mon, 19 Dec 2016 00:30:14 -0500 Subject: [PATCH 06/39] fix #2249 --- src/js/static-net-filtering.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/static-net-filtering.js b/src/js/static-net-filtering.js index 4d6550b4b9fef..85434ce9ddbcf 100644 --- a/src/js/static-net-filtering.js +++ b/src/js/static-net-filtering.js @@ -1735,7 +1735,7 @@ FilterContainer.prototype.getFilterClass = function(details) { if ( details.isRegex ) { return FilterRegexHostname; } - if ( this.reIsGeneric.test(s) ) { + if ( this.reIsGeneric.test(s) || details.token === '*' ) { if ( details.hostnameAnchored ) { return FilterGenericHnAnchoredHostname; } @@ -1766,7 +1766,7 @@ FilterContainer.prototype.getFilterClass = function(details) { if ( details.isRegex ) { return FilterRegex; } - if ( this.reIsGeneric.test(s) ) { + if ( this.reIsGeneric.test(s) || details.token === '*' ) { if ( details.hostnameAnchored ) { return FilterGenericHnAnchored; } From 0b8f27801c5e7229d7a40e72730ca4c60def1d0f Mon Sep 17 00:00:00 2001 From: gorhill Date: Mon, 19 Dec 2016 13:57:17 -0500 Subject: [PATCH 07/39] new revision for dev build --- platform/chromium/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index eafc0b33441f5..5eb101a757544 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "uBlock Origin", - "version": "1.10.3.0", + "version": "1.10.5.0", "default_locale": "en", "description": "__MSG_extShortDesc__", From 2f01fcda54246693f5378fab03a08adeb18a8fa2 Mon Sep 17 00:00:00 2001 From: gorhill Date: Wed, 21 Dec 2016 11:44:03 -0500 Subject: [PATCH 08/39] fix #2256 --- platform/firefox/vapi-client.js | 14 ++++++++++++++ src/js/contentscript.js | 6 +++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/platform/firefox/vapi-client.js b/platform/firefox/vapi-client.js index b34f2183a718f..90c46334d0866 100644 --- a/platform/firefox/vapi-client.js +++ b/platform/firefox/vapi-client.js @@ -510,6 +510,20 @@ if ( self.injectCSS ) { /******************************************************************************/ +// https://bugzilla.mozilla.org/show_bug.cgi?id=444165 +// https://github.com/gorhill/uBlock/issues/2256 +// Not the prettiest solution, but that's the safest/simplest I can think +// of at this point. If/when bugzilla issue above is solved, we will need +// version detection to decide whether the patch needs to be applied. + +vAPI.iframeLoadEventPatch = function(target) { + if ( target.localName === 'iframe' ) { + target.dispatchEvent(new Event('load')); + } +}; + +/******************************************************************************/ + // No need to have vAPI client linger around after shutdown if // we are not a top window (because element picker can still // be injected in top window). diff --git a/src/js/contentscript.js b/src/js/contentscript.js index d297caaa22908..91ef518a98f84 100644 --- a/src/js/contentscript.js +++ b/src/js/contentscript.js @@ -947,6 +947,8 @@ vAPI.domCollapser = (function() { netSelectorCacheCountMax = response.netSelectorCacheCountMax, aa = [ null ], request, key, entry, target, value; + // https://github.com/gorhill/uBlock/issues/2256 + var iframeLoadEventPatch = vAPI.iframeLoadEventPatch; // Important: process in chronological order -- this ensures the // cached selectors are the most useful ones. for ( var i = 0, ni = requests.length; i < ni; i++ ) { @@ -980,6 +982,7 @@ vAPI.domCollapser = (function() { selectors.push(request.tag + '[' + request.attr + '="' + value + '"]'); netSelectorCacheCount += 1; } + if ( iframeLoadEventPatch ) { iframeLoadEventPatch(target); } } } if ( selectors.length !== 0 ) { @@ -1030,9 +1033,6 @@ vAPI.domCollapser = (function() { } }; - // If needed eventually, we could listen to `src` attribute changes - // for iframes. - var add = function(target) { var tag = target.localName; var prop = src1stProps[tag]; From 73a69711f2c415e2f437455aead329c73a6d7bd8 Mon Sep 17 00:00:00 2001 From: gorhill Date: Sun, 25 Dec 2016 16:56:39 -0500 Subject: [PATCH 09/39] add chainable and recursive cosmetic procedural filters --- src/epicker.html | 13 +- src/js/contentscript.js | 414 ++++++++++++++++----------- src/js/cosmetic-filtering.js | 297 +++++++++++++------ src/js/messaging.js | 4 + src/js/reverselookup-worker.js | 17 +- src/js/scriptlets/cosmetic-logger.js | 56 ++-- src/js/scriptlets/dom-inspector.js | 14 +- src/js/scriptlets/element-picker.js | 298 ++++++++----------- 8 files changed, 635 insertions(+), 478 deletions(-) diff --git a/src/epicker.html b/src/epicker.html index f3e038d33f3c9..fc1f34e520a3a 100644 --- a/src/epicker.html +++ b/src/epicker.html @@ -63,7 +63,10 @@ margin: 0; position: relative; } -section > div > textarea { +section.invalidFilter > div:first-child { + border-color: red; +} +section > div:first-child > textarea { background-color: #fff; border: none; box-sizing: border-box; @@ -75,10 +78,7 @@ resize: none; width: 100%; } -section > div > textarea.invalidFilter { - background-color: #fee; -} -section > div > textarea + div { +section > div:first-child > textarea + div { background-color: #aaa; bottom: 0; color: white; @@ -86,6 +86,9 @@ position: absolute; right: 0; } +section.invalidFilter > div:first-child > textarea + div { + background-color: red; +} section > div:first-child + div { direction: ltr; margin: 2px 0; diff --git a/src/js/contentscript.js b/src/js/contentscript.js index 91ef518a98f84..33f241acad538 100644 --- a/src/js/contentscript.js +++ b/src/js/contentscript.js @@ -137,19 +137,9 @@ vAPI.domFilterer = (function() { /******************************************************************************/ -var jobQueue = [ - { t: 'css-hide', _0: [] }, // to inject in style tag - { t: 'css-style', _0: [] }, // to inject in style tag - { t: 'css-ssel', _0: [] }, // to manually hide (incremental) - { t: 'css-csel', _0: [] } // to manually hide (not incremental) -]; - -var reParserEx = /:(?:has|matches-css|matches-css-before|matches-css-after|style|xpath)\(.+?\)$/; - var allExceptions = createSet(), allSelectors = createSet(), - stagedNodes = [], - matchesProp = vAPI.matchesProp; + stagedNodes = []; // Complex selectors, due to their nature may need to be "de-committed". A // Set() is used to implement this functionality. @@ -308,100 +298,179 @@ var platformHideNode = vAPI.hideNode, /******************************************************************************/ -var runSimpleSelectorJob = function(job, root, fn) { - if ( job._1 === undefined ) { - job._1 = job._0.join(cssNotHiddenId + ','); - } - if ( root[matchesProp](job._1) ) { - fn(root); - } - var nodes = root.querySelectorAll(job._1), - i = nodes.length; - while ( i-- ) { - fn(nodes[i], job); +// 'P' stands for 'Procedural' + +var PSelectorHasTask = function(task) { + this.selector = task[1]; +}; +PSelectorHasTask.prototype.exec = function(input) { + var output = []; + for ( var i = 0, n = input.length; i < n; i++ ) { + if ( input[i].querySelector(this.selector) !== null ) { + output.push(input[i]); + } } + return output; }; -var runComplexSelectorJob = function(job, fn) { - if ( job._1 === undefined ) { - job._1 = job._0.join(','); - } - var nodes = document.querySelectorAll(job._1), - i = nodes.length; - while ( i-- ) { - fn(nodes[i], job); +var PSelectorHasTextTask = function(task) { + this.needle = new RegExp(task[1]); +}; +PSelectorHasTextTask.prototype.exec = function(input) { + var output = []; + for ( var i = 0, n = input.length; i < n; i++ ) { + if ( this.needle.test(input[i].textContent) ) { + output.push(input[i]); + } } + return output; }; -var runHasJob = function(job, fn) { - var nodes = document.querySelectorAll(job._0), - i = nodes.length, node; - while ( i-- ) { - node = nodes[i]; - if ( node.querySelector(job._1) !== null ) { - fn(node, job); +var PSelectorIfTask = function(task) { + this.pselector = new PSelector(task[1]); +}; +PSelectorIfTask.prototype.target = true; +PSelectorIfTask.prototype.exec = function(input) { + var output = []; + for ( var i = 0, n = input.length; i < n; i++ ) { + if ( this.pselector.test(input[i]) === this.target ) { + output.push(input[i]); } } + return output; }; -// '/' = ascii 0x2F */ - -var parseMatchesCSSJob = function(raw) { - var prop = raw.trim(); - if ( prop === '' ) { return null; } - var pos = prop.indexOf(':'), - v = pos !== -1 ? prop.slice(pos + 1).trim() : '', - vlen = v.length; - if ( - vlen > 1 && - v.charCodeAt(0) === 0x2F && - v.charCodeAt(vlen-1) === 0x2F - ) { - try { v = new RegExp(v.slice(1, -1)); } catch(ex) { return null; } - } - return { k: prop.slice(0, pos).trim(), v: v }; +var PSelectorIfNotTask = function(task) { + PSelectorIfTask.call(this, task); + this.target = false; }; +PSelectorIfNotTask.prototype = Object.create(PSelectorIfTask.prototype); +PSelectorIfNotTask.prototype.constructor = PSelectorIfNotTask; -var runMatchesCSSJob = function(job, fn) { - var nodes = document.querySelectorAll(job._0), - i = nodes.length; - if ( i === 0 ) { return; } - if ( typeof job._1 === 'string' ) { - job._1 = parseMatchesCSSJob(job._1); - } - if ( job._1 === null ) { return; } - var k = job._1.k, - v = job._1.v, - node, style, match; - while ( i-- ) { - node = nodes[i]; - style = window.getComputedStyle(node, job._2); - if ( style === null ) { continue; } /* FF */ - if ( v instanceof RegExp ) { - match = v.test(style[k]); - } else { - match = style[k] === v; +var PSelectorMatchesCSSTask = function(task) { + this.name = task[1].name; + this.value = new RegExp(task[1].value); +}; +PSelectorMatchesCSSTask.prototype.pseudo = null; +PSelectorMatchesCSSTask.prototype.exec = function(input) { + var output = [], style; + for ( var i = 0, n = input.length; i < n; i++ ) { + style = window.getComputedStyle(input[i], this.pseudo); + if ( style === null ) { return null; } /* FF */ + if ( this.value.test(style[this.name]) ) { + output.push(input[i]); } - if ( match ) { - fn(node, job); + } + return output; +}; + +var PSelectorMatchesCSSAfterTask = function(task) { + PSelectorMatchesCSSTask.call(this, task); + this.pseudo = ':after'; +}; +PSelectorMatchesCSSAfterTask.prototype = Object.create(PSelectorMatchesCSSTask.prototype); +PSelectorMatchesCSSAfterTask.prototype.constructor = PSelectorMatchesCSSAfterTask; + +var PSelectorMatchesCSSBeforeTask = function(task) { + PSelectorMatchesCSSTask.call(this, task); + this.pseudo = ':before'; +}; +PSelectorMatchesCSSBeforeTask.prototype = Object.create(PSelectorMatchesCSSTask.prototype); +PSelectorMatchesCSSBeforeTask.prototype.constructor = PSelectorMatchesCSSBeforeTask; + +var PSelectorXpathTask = function(task) { + this.xpe = document.createExpression(task[1], null); + this.xpr = null; +}; +PSelectorXpathTask.prototype.exec = function(input) { + var output = [], j, node; + for ( var i = 0, n = input.length; i < n; i++ ) { + this.xpr = this.xpe.evaluate( + input[i], + XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, + this.xpr + ); + j = this.xpr.snapshotLength; + while ( j-- ) { + node = this.xpr.snapshotItem(j); + if ( node.nodeType === 1 ) { + output.push(node); + } } } + return output; }; -var runXpathJob = function(job, fn) { - if ( job._1 === undefined ) { - job._1 = document.createExpression(job._0, null); +var PSelector = function(o) { + if ( PSelector.prototype.operatorToTaskMap === undefined ) { + PSelector.prototype.operatorToTaskMap = new Map([ + [ ':has', PSelectorHasTask ], + [ ':has-text', PSelectorHasTextTask ], + [ ':if', PSelectorIfTask ], + [ ':if-not', PSelectorIfNotTask ], + [ ':matches-css', PSelectorMatchesCSSTask ], + [ ':matches-css-after', PSelectorMatchesCSSAfterTask ], + [ ':matches-css-before', PSelectorMatchesCSSBeforeTask ], + [ ':xpath', PSelectorXpathTask ] + ]); } - var xpr = job._2 = job._1.evaluate( - document, - XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, - job._2 || null - ); - var i = xpr.snapshotLength, node; + this.raw = o.raw; + this.selector = o.selector; + this.tasks = []; + var tasks = o.tasks, task, ctor; + for ( var i = 0; i < tasks.length; i++ ) { + task = tasks[i]; + ctor = this.operatorToTaskMap.get(task[0]); + this.tasks.push(new ctor(task)); + } +}; +PSelector.prototype.operatorToTaskMap = undefined; +PSelector.prototype.prime = function(input) { + var root = input || document; + if ( this.selector !== '' ) { + return root.querySelectorAll(this.selector); + } + return [ root ]; +}; +PSelector.prototype.exec = function(input) { + //var t0 = window.performance.now(); + var tasks = this.tasks, nodes = this.prime(input); + for ( var i = 0, n = tasks.length; i < n && nodes.length !== 0; i++ ) { + nodes = tasks[i].exec(nodes); + } + //console.log('%s: %s ms', this.raw, (window.performance.now() - t0).toFixed(2)); + return nodes; +}; +PSelector.prototype.test = function(input) { + //var t0 = window.performance.now(); + var tasks = this.tasks, nodes = this.prime(input), aa0 = [ null ], aa; + for ( var i = 0, ni = nodes.length; i < ni; i++ ) { + aa0[0] = nodes[i]; aa = aa0; + for ( var j = 0, nj = tasks.length; j < nj && aa.length !== 0; j++ ) { + aa = tasks[i].exec(aa); + } + if ( aa.length !== 0 ) { return true; } + } + //console.log('%s: %s ms', this.raw, (window.performance.now() - t0).toFixed(2)); + return false; +}; + +var PSelectors = function() { + this.entries = []; +}; +PSelectors.prototype.add = function(o) { + this.entries.push(new PSelector(o)); +}; +PSelectors.prototype.forEachNode = function(callback) { + var pfilters = this.entries, + i = pfilters.length, + pfilter, nodes, j; while ( i-- ) { - node = xpr.snapshotItem(i); - if ( node.nodeType === 1 ) { - fn(node, job); + pfilter = pfilters[i]; + nodes = pfilter.exec(); + j = nodes.length; + while ( j-- ) { + callback(nodes[j], pfilter); } } }; @@ -418,14 +487,52 @@ var domFilterer = { hiddenNodeCount: 0, hiddenNodeEnforcer: false, loggerEnabled: undefined, - styleTags: [], - jobQueue: jobQueue, - // Stock jobs. - job0: jobQueue[0], - job1: jobQueue[1], - job2: jobQueue[2], - job3: jobQueue[3], + newHideSelectorBuffer: [], // Hide style filter buffer + newStyleRuleBuffer: [], // Non-hide style filter buffer + simpleHideSelectors: { // Hiding filters: simple selectors + entries: [], + matchesProp: vAPI.matchesProp, + selector: undefined, + add: function(selector) { + this.entries.push(selector); + this.selector = undefined; + }, + forEachNodeOfSelector: function(/*callback, root, extra*/) { + }, + forEachNode: function(callback, root, extra) { + if ( this.selector === undefined ) { + this.selector = this.entries.join(extra + ',') + extra; + } + if ( root[this.matchesProp](this.selector) ) { + callback(root); + } + var nodes = root.querySelectorAll(this.selector), + i = nodes.length; + while ( i-- ) { + callback(nodes[i]); + } + } + }, + complexHideSelectors: { // Hiding filters: complex selectors + entries: [], + selector: undefined, + add: function(selector) { + this.entries.push(selector); + this.selector = undefined; + }, + forEachNode: function(callback) { + if ( this.selector === undefined ) { + this.selector = this.entries.join(','); + } + var nodes = document.querySelectorAll(this.selector), + i = nodes.length; + while ( i-- ) { + callback(nodes[i]); + } + } + }, + proceduralSelectors: new PSelectors(), // Hiding filters: procedural addExceptions: function(aa) { for ( var i = 0, n = aa.length; i < n; i++ ) { @@ -433,58 +540,28 @@ var domFilterer = { } }, - // Job: - // Stock jobs in job queue: - // 0 = css rules/css declaration to remove visibility - // 1 = css rules/any css declaration - // 2 = simple css selectors/hide - // 3 = complex css selectors/hide - // Custom jobs: - // matches-css/hide - // has/hide - // xpath/hide - - addSelector: function(s) { - if ( allSelectors.has(s) || allExceptions.has(s) ) { + addSelector: function(selector) { + if ( allSelectors.has(selector) || allExceptions.has(selector) ) { return; } - allSelectors.add(s); - var sel0 = s, sel1 = ''; - if ( s.charCodeAt(s.length - 1) === 0x29 ) { - var parts = reParserEx.exec(s); - if ( parts !== null ) { - sel1 = parts[0]; - } - } - if ( sel1 === '' ) { - this.job0._0.push(sel0); - if ( sel0.indexOf(' ') === -1 ) { - this.job2._0.push(sel0); - this.job2._1 = undefined; + allSelectors.add(selector); + if ( selector.charCodeAt(0) !== 0x7B /* '{' */ ) { + this.newHideSelectorBuffer.push(selector); + if ( selector.indexOf(' ') === -1 ) { + this.simpleHideSelectors.add(selector); } else { - this.job3._0.push(sel0); - this.job3._1 = undefined; + this.complexHideSelectors.add(selector); } return; } - sel0 = sel0.slice(0, sel0.length - sel1.length); - if ( sel1.lastIndexOf(':has', 0) === 0 ) { - this.jobQueue.push({ t: 'has-hide', raw: s, _0: sel0, _1: sel1.slice(5, -1) }); - } else if ( sel1.lastIndexOf(':matches-css', 0) === 0 ) { - if ( sel1.lastIndexOf(':matches-css-before', 0) === 0 ) { - this.jobQueue.push({ t: 'matches-css-hide', raw: s, _0: sel0, _1: sel1.slice(20, -1), _2: ':before' }); - } else if ( sel1.lastIndexOf(':matches-css-after', 0) === 0 ) { - this.jobQueue.push({ t: 'matches-css-hide', raw: s, _0: sel0, _1: sel1.slice(19, -1), _2: ':after' }); - } else { - this.jobQueue.push({ t: 'matches-css-hide', raw: s, _0: sel0, _1: sel1.slice(13, -1), _2: null }); - } - } else if ( sel1.lastIndexOf(':style', 0) === 0 ) { - this.job1._0.push(sel0 + ' { ' + sel1.slice(7, -1) + ' }'); - this.job1._1 = undefined; - } else if ( sel1.lastIndexOf(':xpath', 0) === 0 ) { - this.jobQueue.push({ t: 'xpath-hide', raw: s, _0: sel1.slice(7, -1) }); + var o = JSON.parse(selector); + if ( o.style ) { + this.newStyleRuleBuffer.push(o.parts.join(' ')); + return; + } + if ( o.procedural ) { + this.proceduralSelectors.add(o); } - return; }, addSelectors: function(aa) { @@ -497,27 +574,27 @@ var domFilterer = { this.commitTimer.clear(); var beforeHiddenNodeCount = this.hiddenNodeCount, - styleText = '', i, n; + styleText = '', i; - // Stock job 0 = css rules/hide - if ( this.job0._0.length ) { - styleText = '\n:root ' + this.job0._0.join(',\n:root ') + '\n{ display: none !important; }'; - this.job0._0.length = 0; + // CSS rules/hide + if ( this.newHideSelectorBuffer.length ) { + styleText = '\n:root ' + this.newHideSelectorBuffer.join(',\n:root ') + '\n{ display: none !important; }'; + this.newHideSelectorBuffer.length = 0; } - // Stock job 1 = css rules/any css declaration - if ( this.job1._0.length ) { - styleText += '\n' + this.job1._0.join('\n'); - this.job1._0.length = 0; + // CSS rules/any css declaration + if ( this.newStyleRuleBuffer.length ) { + styleText += '\n' + this.newStyleRuleBuffer.join('\n'); + this.newStyleRuleBuffer.length = 0; } // Simple selectors: incremental. - // Stock job 2 = simple css selectors/hide - if ( this.job2._0.length ) { + // Simple css selectors/hide + if ( this.simpleHideSelectors.entries.length ) { i = stagedNodes.length; while ( i-- ) { - runSimpleSelectorJob(this.job2, stagedNodes[i], hideNode); + this.simpleHideSelectors.forEachNode(hideNode, stagedNodes[i], cssNotHiddenId); } } stagedNodes = []; @@ -526,17 +603,16 @@ var domFilterer = { complexSelectorsOldResultSet = complexSelectorsCurrentResultSet; complexSelectorsCurrentResultSet = createSet('object'); - // Stock job 3 = complex css selectors/hide + // Complex css selectors/hide // The handling of these can be considered optional, since they are // also applied declaratively using a style tag. - if ( this.job3._0.length ) { - runComplexSelectorJob(this.job3, complexHideNode); + if ( this.complexHideSelectors.entries.length ) { + this.complexHideSelectors.forEachNode(complexHideNode); } - // Custom jobs. No optional since they can't be applied in a - // declarative way. - for ( i = 4, n = this.jobQueue.length; i < n; i++ ) { - this.runJob(this.jobQueue[i], complexHideNode); + // Procedural cosmetic filters + if ( this.proceduralSelectors.entries.length ) { + this.proceduralSelectors.forEachNode(complexHideNode); } // https://github.com/gorhill/uBlock/issues/1912 @@ -595,6 +671,10 @@ var domFilterer = { this.commitTimer.start(); }, + createProceduralFilter: function(o) { + return new PSelector(o); + }, + getExcludeId: function() { if ( this.excludeId === undefined ) { this.excludeId = vAPI.randomToken(); @@ -616,20 +696,6 @@ var domFilterer = { this.commitTimer = new vAPI.SafeAnimationFrame(this.commit_.bind(this)); }, - runJob: function(job, fn) { - switch ( job.t ) { - case 'has-hide': - runHasJob(job, fn); - break; - case 'matches-css-hide': - runMatchesCSSJob(job, fn); - break; - case 'xpath-hide': - runXpathJob(job, fn); - break; - } - }, - showNode: function(node) { node.hidden = false; platformUnhideNode(node); @@ -1248,14 +1314,14 @@ vAPI.domSurveyor = (function() { // Need to do this before committing DOM filterer, as needed info // will no longer be there after commit. - if ( firstSurvey || domFilterer.job0._0.length ) { + if ( firstSurvey || domFilterer.newHideSelectorBuffer.length ) { messaging.send( 'contentscript', { what: 'cosmeticFiltersInjected', type: 'cosmetic', hostname: window.location.hostname, - selectors: domFilterer.job0._0, + selectors: domFilterer.newHideSelectorBuffer, first: firstSurvey, cost: surveyCost } @@ -1263,7 +1329,7 @@ vAPI.domSurveyor = (function() { } // Shutdown surveyor if too many consecutive empty resultsets. - if ( domFilterer.job0._0.length === 0 ) { + if ( domFilterer.newHideSelectorBuffer.length === 0 ) { cosmeticSurveyingMissCount += 1; } else { cosmeticSurveyingMissCount = 0; diff --git a/src/js/cosmetic-filtering.js b/src/js/cosmetic-filtering.js index ebfdc7392bd5b..6d693adb5fb4a 100644 --- a/src/js/cosmetic-filtering.js +++ b/src/js/cosmetic-filtering.js @@ -39,6 +39,35 @@ var µb = µBlock; var encode = JSON.stringify; var decode = JSON.parse; +var isValidCSSSelector = (function() { + var div = document.createElement('div'), + matchesFn; + // Keep in mind: + // https://github.com/gorhill/uBlock/issues/693 + // https://github.com/gorhill/uBlock/issues/1955 + if ( div.matches instanceof Function ) { + matchesFn = div.matches.bind(div); + } else if ( div.mozMatchesSelector instanceof Function ) { + matchesFn = div.mozMatchesSelector.bind(div); + } else if ( div.webkitMatchesSelector instanceof Function ) { + matchesFn = div.webkitMatchesSelector.bind(div); + } else if ( div.msMatchesSelector instanceof Function ) { + matchesFn = div.msMatchesSelector.bind(div); + } else { + matchesFn = div.querySelector.bind(div); + } + return function(s) { + try { + matchesFn(s + ', ' + s + ':not(#foo)'); + } catch (ex) { + return false; + } + return true; + }; +})(); + +var reIsRegexLiteral = /^\/.+\/$/; + var isBadRegex = function(s) { try { void new RegExp(s); @@ -218,7 +247,7 @@ var FilterParser = function() { this.hostnames = []; this.invalid = false; this.cosmetic = true; - this.reNeedHostname = /^(?:script:contains|script:inject|.+?:has|.+?:matches-css(?:-before|-after)?|:xpath)\(.+?\)$/; + this.reNeedHostname = /^(?:script:contains|script:inject|.+?:has|.+?:has-text|.+?:if|.+?:if-not|.+?:matches-css(?:-before|-after)?|.*?:xpath)\(.+\)$/; }; /******************************************************************************/ @@ -331,7 +360,12 @@ FilterParser.prototype.parse = function(raw) { // ##script:contains(...) // ##script:inject(...) // ##.foo:has(...) + // ##.foo:has-text(...) + // ##.foo:if(...) + // ##.foo:if-not(...) // ##.foo:matches-css(...) + // ##.foo:matches-css-after(...) + // ##.foo:matches-css-before(...) // ##:xpath(...) if ( this.hostnames.length === 0 && @@ -698,91 +732,178 @@ FilterContainer.prototype.freeze = function() { // implemented (if ever). Unlikely, see: // https://github.com/gorhill/uBlock/issues/1752 -FilterContainer.prototype.isValidSelector = (function() { - var div = document.createElement('div'); - var matchesProp = (function() { - if ( typeof div.matches === 'function' ) { - return 'matches'; +FilterContainer.prototype.compileSelector = (function() { + var reStyleSelector = /^(.+?):style\((.+?)\)$/, + reStyleBad = /url\([^)]+\)/, + reScriptSelector = /^script:(contains|inject)\((.+)\)$/; + + return function(raw) { + if ( isValidCSSSelector(raw) && raw.indexOf('[-abp-properties=') === -1 ) { + return raw; } - if ( typeof div.mozMatchesSelector === 'function' ) { - return 'mozMatchesSelector'; + + // We rarely reach this point. + var matches; + + // `:style` selector? + if ( (matches = reStyleSelector.exec(raw)) !== null ) { + if ( isValidCSSSelector(matches[1]) && reStyleBad.test(matches[2]) === false ) { + return JSON.stringify({ + style: true, + raw: raw, + parts: [ matches[1], '{' + matches[2] + '}' ] + }); + } + return; } - if ( typeof div.webkitMatchesSelector === 'function' ) { - return 'webkitMatchesSelector'; + + // `script:` filter? + if ( (matches = reScriptSelector.exec(raw)) !== null ) { + // :inject + if ( matches[1] === 'inject' ) { + return raw; + } + // :contains + if ( reIsRegexLiteral.test(matches[2]) === false || isBadRegex(matches[2].slice(1, -1)) === false ) { + return raw; + } + return; } - return ''; - })(); - // Not all browsers support `Element.matches`: - // http://caniuse.com/#feat=matchesselector - if ( matchesProp === '' ) { - return function() { - return true; - }; - } - var reHasSelector = /^(.+?):has\((.+?)\)$/, - reMatchesCSSSelector = /^(.+?):matches-css(?:-before|-after)?\((.+?)\)$/, - reXpathSelector = /^:xpath\((.+?)\)$/, - reStyleSelector = /^(.+?):style\((.+?)\)$/, - reStyleBad = /url\([^)]+\)/, - reScriptSelector = /^script:(contains|inject)\((.+)\)$/; + // Procedural selector? + var compiled; + if ( (compiled = this.compileProceduralSelector(raw)) ) { + return compiled; + } - // Keep in mind: - // https://github.com/gorhill/uBlock/issues/693 - // https://github.com/gorhill/uBlock/issues/1955 - var isValidCSSSelector = function(s) { - try { - div[matchesProp](s + ', ' + s + ':not(#foo)'); - } catch (ex) { - return false; + µb.logger.writeOne('', 'error', 'Cosmetic filtering – invalid filter: ' + raw); + return; + }; +})(); + +/******************************************************************************/ + +FilterContainer.prototype.compileProceduralSelector = (function() { + var reParserEx = /(:(?:has|has-text|if|if-not|matches-css|matches-css-after|matches-css-before|xpath))\(.+\)$/, + reFirstParentheses = /^\(*/, + reLastParentheses = /\)*$/, + reEscapeRegex = /[.*+?^${}()|[\]\\]/g; + + var lastProceduralSelector = '', + lastProceduralSelectorCompiled; + + var compileCSSSelector = function(s) { + if ( isValidCSSSelector(s) ) { + return s; } - return true; }; - return function(s) { - if ( isValidCSSSelector(s) && s.indexOf('[-abp-properties=') === -1 ) { - return true; + var compileText = function(s) { + if ( reIsRegexLiteral.test(s) ) { + s = s.slice(1, -1); + if ( isBadRegex(s) ) { return; } + } else { + s = s.replace(reEscapeRegex, '\\$&'); } - // We reach this point very rarely. - var matches; + return s; + }; - // Future `:has`-based filter? If so, validate both parts of the whole - // selector. - matches = reHasSelector.exec(s); - if ( matches !== null ) { - return isValidCSSSelector(matches[1]) && isValidCSSSelector(matches[2]); - } - // Custom `:matches-css`-based filter? - matches = reMatchesCSSSelector.exec(s); - if ( matches !== null ) { - return isValidCSSSelector(matches[1]); - } - // Custom `:xpath`-based filter? - matches = reXpathSelector.exec(s); - if ( matches !== null ) { - try { - return document.createExpression(matches[1], null) instanceof XPathExpression; - } catch (e) { - } - return false; + var compileCSSDeclaration = function(s) { + var name, value, + pos = s.indexOf(':'); + if ( pos === -1 ) { return; } + name = s.slice(0, pos).trim(); + value = s.slice(pos + 1).trim(); + if ( reIsRegexLiteral.test(value) ) { + value = value.slice(1, -1); + if ( isBadRegex(value) ) { return; } + } else { + value = value.replace(reEscapeRegex, '\\$&'); } - // `:style` selector? - matches = reStyleSelector.exec(s); - if ( matches !== null ) { - return isValidCSSSelector(matches[1]) && reStyleBad.test(matches[2]) === false; + return { name: name, value: value }; + }; + + var compileConditionalSelector = function(s) { + return compile(s); + }; + + var compileXpathExpression = function(s) { + var dummy; + try { + dummy = document.createExpression(s, null) instanceof XPathExpression; + } catch (e) { + return; } - // Special `script:` filter? - matches = reScriptSelector.exec(s); - if ( matches !== null ) { - if ( matches[1] === 'inject' ) { - return true; + return s; + }; + + var compileArgument = new Map([ + [ ':has', compileCSSSelector ], + [ ':has-text', compileText ], + [ ':if', compileConditionalSelector ], + [ ':if-not', compileConditionalSelector ], + [ ':matches-css', compileCSSDeclaration ], + [ ':matches-css-after', compileCSSDeclaration ], + [ ':matches-css-before', compileCSSDeclaration ], + [ ':xpath', compileXpathExpression ] + ]); + + var compile = function(raw) { + var matches = reParserEx.exec(raw); + if ( matches === null ) { return; } + var tasks = [], + firstOperand = raw.slice(0, matches.index), + currentOperator = matches[1], + selector = raw.slice(matches.index + currentOperator.length), + currentArgument = '', nextOperand, nextOperator, + depth = 0, opening, closing; + if ( firstOperand !== '' && isValidCSSSelector(firstOperand) === false ) { return; } + for (;;) { + matches = reParserEx.exec(selector); + if ( matches !== null ) { + nextOperand = selector.slice(0, matches.index); + nextOperator = matches[1]; + } else { + nextOperand = selector; + nextOperator = ''; + } + opening = reFirstParentheses.exec(nextOperand)[0].length; + closing = reLastParentheses.exec(nextOperand)[0].length; + if ( opening > closing ) { + if ( depth === 0 ) { currentArgument = ''; } + depth += 1; + } else if ( closing > opening && depth > 0 ) { + depth -= 1; + if ( depth === 0 ) { nextOperand = currentArgument + nextOperand; } } - return matches[2].startsWith('/') === false || - matches[2].endsWith('/') === false || - isBadRegex(matches[2].slice(1, -1)) === false; + if ( depth !== 0 ) { + currentArgument += nextOperand + nextOperator; + } else { + currentArgument = compileArgument.get(currentOperator)(nextOperand.slice(1, -1)); + if ( currentArgument === undefined ) { return; } + tasks.push([ currentOperator, currentArgument ]); + currentOperator = nextOperator; + } + if ( nextOperator === '' ) { break; } + selector = selector.slice(matches.index + nextOperator.length); } - µb.logger.writeOne('', 'error', 'Cosmetic filtering – invalid filter: ' + s); - return false; + if ( tasks.length === 0 || depth !== 0 ) { return; } + return { selector: firstOperand, tasks: tasks }; + }; + + return function(raw) { + if ( raw === lastProceduralSelector ) { + return lastProceduralSelectorCompiled; + } + lastProceduralSelector = raw; + var compiled = compile(raw); + if ( compiled !== undefined ) { + compiled.procedural = true; + compiled.raw = raw; + compiled = JSON.stringify(compiled); + } + lastProceduralSelectorCompiled = compiled; + return compiled; }; })(); @@ -843,7 +964,7 @@ FilterContainer.prototype.compile = function(s, out) { // still the most common, and can easily be tested using a plain regex. if ( this.reClassOrIdSelector.test(parsed.suffix) === false && - this.isValidSelector(parsed.suffix) === false + this.compileSelector(parsed.suffix) === undefined ) { return true; } @@ -895,15 +1016,15 @@ FilterContainer.prototype.compileGenericHideSelector = function(parsed, out) { return; } // Composite CSS rule. - if ( this.isValidSelector(selector) ) { + if ( this.compileSelector(selector) ) { out.push('c\vlg+\v' + key + '\v' + selector); } return; } - if ( this.isValidSelector(selector) !== true ) { - return; - } + var compiled = this.compileSelector(selector); + if ( compiled === undefined ) { return; } + // TODO: Detect and error on procedural cosmetic filters. // ["title"] and ["alt"] will go in high-low generic bin. if ( this.reHighLow.test(selector) ) { @@ -948,10 +1069,6 @@ FilterContainer.prototype.compileGenericHideSelector = function(parsed, out) { FilterContainer.prototype.compileGenericUnhideSelector = function(parsed, out) { var selector = parsed.suffix; - if ( this.isValidSelector(selector) !== true ) { - return; - } - // script:contains(...) // script:inject(...) if ( this.reScriptSelector.test(selector) ) { @@ -959,10 +1076,14 @@ FilterContainer.prototype.compileGenericUnhideSelector = function(parsed, out) { return; } + // Procedural cosmetic filters are acceptable as generic exception filters. + var compiled = this.compileSelector(selector); + if ( compiled === undefined ) { return; } + // https://github.com/chrisaljoudi/uBlock/issues/497 // All generic exception filters are put in the same bucket: they are // expected to be very rare. - out.push('c\vg1\v' + selector); + out.push('c\vg1\v' + compiled); }; /******************************************************************************/ @@ -980,20 +1101,24 @@ FilterContainer.prototype.compileHostnameSelector = function(hostname, parsed, o hostname = this.punycode.toASCII(hostname); } - var domain = this.µburi.domainFromHostname(hostname), + var selector = parsed.suffix, + domain = this.µburi.domainFromHostname(hostname), hash; // script:contains(...) // script:inject(...) - if ( this.reScriptSelector.test(parsed.suffix) ) { + if ( this.reScriptSelector.test(selector) ) { hash = domain !== '' ? domain : this.noDomainHash; if ( unhide ) { hash = '!' + hash; } - out.push('c\vjs\v' + hash + '\v' + hostname + '\v' + parsed.suffix); + out.push('c\vjs\v' + hash + '\v' + hostname + '\v' + selector); return; } + var compiled = this.compileSelector(selector); + if ( compiled === undefined ) { return; } + // https://github.com/chrisaljoudi/uBlock/issues/188 // If not a real domain as per PSL, assign a synthetic one if ( hostname.endsWith('.*') === false ) { @@ -1005,7 +1130,7 @@ FilterContainer.prototype.compileHostnameSelector = function(hostname, parsed, o hash = '!' + hash; } - out.push('c\vh\v' + hash + '\v' + hostname + '\v' + parsed.suffix); + out.push('c\vh\v' + hash + '\v' + hostname + '\v' + compiled); }; /******************************************************************************/ diff --git a/src/js/messaging.js b/src/js/messaging.js index b422d1abc8159..c6ade1f4f4acd 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -100,6 +100,10 @@ var onMessage = function(request, sender, callback) { µb.mouseURL = request.url; break; + case 'compileCosmeticFilterSelector': + response = µb.cosmeticFilteringEngine.compileSelector(request.selector); + break; + case 'cosmeticFiltersInjected': µb.cosmeticFilteringEngine.addToSelectorCache(request); /* falls through */ diff --git a/src/js/reverselookup-worker.js b/src/js/reverselookup-worker.js index 176a2896c1dc4..ff827fdcd7f0c 100644 --- a/src/js/reverselookup-worker.js +++ b/src/js/reverselookup-worker.js @@ -134,14 +134,23 @@ var fromCosmeticFilter = function(details) { } candidates[details.rawFilter] = new RegExp(reStr.join('\\v') + '(?:\\n|$)'); + // Procedural filters, which are pre-compiled, make thing sort of + // complicated. We are going to also search for one portion of the + // compiled form of a filter. + var filterEx = '(' + + reEscape(filter) + + '|[^\\v]+' + + reEscape(JSON.stringify({ raw: filter }).slice(1,-1)) + + '[^\\v]+)'; + // Second step: find hostname-based versions. // Reference: FilterContainer.compileHostnameSelector(). - var pos; - var hostname = details.hostname; + var pos, + hostname = details.hostname; if ( hostname !== '' ) { for ( ;; ) { candidates[hostname + '##' + filter] = new RegExp( - ['c', 'h', '[^\\v]+', reEscape(hostname), reEscape(filter)].join('\\v') + + ['c', 'h', '[^\\v]+', reEscape(hostname), filterEx].join('\\v') + '(?:\\n|$)' ); pos = hostname.indexOf('.'); @@ -159,7 +168,7 @@ var fromCosmeticFilter = function(details) { if ( pos !== -1 ) { var entity = domain.slice(0, pos) + '.*'; candidates[entity + '##' + filter] = new RegExp( - ['c', 'h', '[^\\v]+', reEscape(entity), reEscape(filter)].join('\\v') + + ['c', 'h', '[^\\v]+', reEscape(entity), filterEx].join('\\v') + '(?:\\n|$)' ); } diff --git a/src/js/scriptlets/cosmetic-logger.js b/src/js/scriptlets/cosmetic-logger.js index 29ac23477b1c1..51c67d6fe5dea 100644 --- a/src/js/scriptlets/cosmetic-logger.js +++ b/src/js/scriptlets/cosmetic-logger.js @@ -31,38 +31,36 @@ if ( typeof vAPI !== 'object' || !vAPI.domFilterer ) { return; } -var df = vAPI.domFilterer, - loggedSelectors = vAPI.loggedSelectors || {}, - matchedSelectors = [], - selectors, i, selector; - - -// CSS selectors. -selectors = df.jobQueue[2]._0.concat(df.jobQueue[3]._0); -i = selectors.length; -while ( i-- ) { - selector = selectors[i]; - if ( loggedSelectors.hasOwnProperty(selector) ) { - continue; - } - if ( document.querySelector(selector) === null ) { - continue; - } - loggedSelectors[selector] = true; - matchedSelectors.push(selector); -} +var loggedSelectors = vAPI.loggedSelectors || {}, + matchedSelectors = []; -// Non-CSS selectors. -var logHit = function(node, job) { - if ( !job.raw || loggedSelectors.hasOwnProperty(job.raw) ) { - return; + +var evaluateSelector = function(selector) { + if ( + loggedSelectors.hasOwnProperty(selector) === false && + document.querySelector(selector) !== null + ) { + loggedSelectors[selector] = true; + matchedSelectors.push(selector); } - loggedSelectors[job.raw] = true; - matchedSelectors.push(job.raw); }; -for ( i = 4; i < df.jobQueue.length; i++ ) { - df.runJob(df.jobQueue[i], logHit); -} + +// Simple CSS selector-based cosmetic filters. +vAPI.domFilterer.simpleHideSelectors.entries.forEach(evaluateSelector); + +// Complex CSS selector-based cosmetic filters. +vAPI.domFilterer.complexHideSelectors.entries.forEach(evaluateSelector); + +// Procedural cosmetic filters. +vAPI.domFilterer.proceduralSelectors.entries.forEach(function(pfilter) { + if ( + loggedSelectors.hasOwnProperty(pfilter.raw) === false && + pfilter.exec().length !== 0 + ) { + loggedSelectors[pfilter.raw] = true; + matchedSelectors.push(pfilter.raw); + } +}); vAPI.loggedSelectors = loggedSelectors; diff --git a/src/js/scriptlets/dom-inspector.js b/src/js/scriptlets/dom-inspector.js index a31f6988a925b..5ef7658b6b7a6 100644 --- a/src/js/scriptlets/dom-inspector.js +++ b/src/js/scriptlets/dom-inspector.js @@ -693,7 +693,7 @@ var cosmeticFilterMapper = (function() { i, j; // CSS-based selectors: simple one. - selectors = vAPI.domFilterer.job2._0; + selectors = vAPI.domFilterer.simpleHideSelectors.entries; i = selectors.length; while ( i-- ) { selector = selectors[i]; @@ -709,9 +709,9 @@ var cosmeticFilterMapper = (function() { } } } - + // CSS-based selectors: complex one (must query from doc root). - selectors = vAPI.domFilterer.job3._0; + selectors = vAPI.domFilterer.complexHideSelectors.entries; i = selectors.length; while ( i-- ) { selector = selectors[i]; @@ -726,14 +726,12 @@ var cosmeticFilterMapper = (function() { } // Non-CSS selectors. - var runJobCallback = function(node, job) { + var runJobCallback = function(node, pfilter) { if ( filterMap.has(node) === false ) { - filterMap.set(node, job.raw); + filterMap.set(node, pfilter.raw); } }; - for ( i = 4; i < vAPI.domFilterer.jobQueue.length; i++ ) { - vAPI.domFilterer.runJob(vAPI.domFilterer.jobQueue[i], runJobCallback); - } + vAPI.domFilterer.proceduralSelectors.forEachNode(runJobCallback); }; var incremental = function(rootNode) { diff --git a/src/js/scriptlets/element-picker.js b/src/js/scriptlets/element-picker.js index fc77cbdd0dcd1..1181f77f54bc5 100644 --- a/src/js/scriptlets/element-picker.js +++ b/src/js/scriptlets/element-picker.js @@ -177,6 +177,14 @@ var safeQuerySelectorAll = function(node, selector) { /******************************************************************************/ +var rawFilterFromTextarea = function() { + var s = taCandidate.value, + pos = s.indexOf('\n'); + return pos === -1 ? s.trim() : s.slice(0, pos).trim(); +}; + +/******************************************************************************/ + var getElementBoundingClientRect = function(elem) { var rect = typeof elem.getBoundingClientRect === 'function' ? elem.getBoundingClientRect() : @@ -635,7 +643,9 @@ var filtersFrom = function(x, y) { filterToDOMInterface.set @desc Look-up all the HTML elements matching the filter passed in argument. - @param string, a cosmetic of network filter. + @param string, a cosmetic or network filter. + @param function, called once all items matching the filter have been + collected. @return array, or undefined if the filter is invalid. filterToDOMInterface.preview @@ -733,16 +743,15 @@ var filterToDOMInterface = (function() { // ways to compose a valid href to the same effective URL. One idea is to // normalize all a[href] on the page, but for now I will wait and see, as I // prefer to refrain from tampering with the page content if I can avoid it. - var fromCosmeticFilter = function(filter) { + var fromPlainCosmeticFilter = function(filter) { var elems; try { elems = document.querySelectorAll(filter); } catch (e) { - return fromProceduralCosmeticFilter(filter); + return; } - var out = [], - iElem = elems.length; + var out = [], iElem = elems.length; while ( iElem-- ) { out.push({ type: 'cosmetic', elem: elems[iElem]}); } @@ -751,108 +760,27 @@ var filterToDOMInterface = (function() { // https://github.com/gorhill/uBlock/issues/1772 // Handle procedural cosmetic filters. - var fromProceduralCosmeticFilter = function(filter) { - if ( filter.charCodeAt(filter.length - 1) === 0x29 /* ')' */ ) { - var parts = reProceduralCosmeticFilter.exec(filter); - if ( - parts !== null && - proceduralCosmeticFilterFunctions.hasOwnProperty(parts[2]) - ) { - return proceduralCosmeticFilterFunctions[parts[2]]( - parts[1].trim(), - parts[3].trim() - ); - } + var fromCompiledCosmeticFilter = function(raw) { + if ( typeof raw !== 'string' ) { return; } + var o; + try { + o = JSON.parse(raw); + } catch(ex) { + return; } - }; - - var reProceduralCosmeticFilter = /^(.*?):(matches-css|has|style|xpath)\((.+?)\)$/; - - // Collection of handlers for procedural cosmetic filters. - var proceduralCosmeticFilterFunctions = { - 'has': function(selector, arg) { - if ( selector === '' ) { return; } - var elems; - try { - elems = document.querySelectorAll(selector); - document.querySelector(arg); - } catch(ex) { - return; - } - var out = [], elem; - for ( var i = 0, n = elems.length; i < n; i++ ) { - elem = elems[i]; - if ( elem.querySelector(arg) ) { - out.push({ type: 'cosmetic', elem: elem }); - } - } - return out; - }, - 'matches-css': function(selector, arg) { - if ( selector === '' ) { return; } - var elems; - try { - elems = document.querySelectorAll(selector); - } catch(ex) { - return; - } - var out = [], elem, style, - pos = arg.indexOf(':'); - if ( pos === -1 ) { return; } - var prop = arg.slice(0, pos).trim(), - reText = arg.slice(pos + 1).trim(); - if ( reText === '' ) { return; } - var re = reText !== '*' ? - new RegExp('^' + reText.replace(/[.+?${}()|[\]\\^]/g, '\\$&').replace(/\*+/g, '.*?') + '$') : - /./; - for ( var i = 0, n = elems.length; i < n; i++ ) { - elem = elems[i]; - style = window.getComputedStyle(elem, null); - if ( re.test(style[prop]) ) { - out.push({ type: 'cosmetic', elem: elem }); - } - } - return out; - }, - 'style': function(selector, arg) { - if ( selector === '' || arg === '' ) { return; } - var elems; - try { - elems = document.querySelectorAll(selector); - } catch(ex) { - return; - } - var out = []; - for ( var i = 0, n = elems.length; i < n; i++ ) { - out.push({ type: 'cosmetic', elem: elems[i] }); - } - lastAction = selector + ' { ' + arg + ' }'; - return out; - }, - 'xpath': function(selector, arg) { - if ( selector !== '' ) { return []; } - var result; - try { - result = document.evaluate( - arg, - document, - null, - XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, - null - ); - } catch(ex) { - return; - } - if ( result === undefined ) { return []; } - var out = [], elem, i = result.snapshotLength; - while ( i-- ) { - elem = result.snapshotItem(i); - if ( elem.nodeType === 1 ) { - out.push({ type: 'cosmetic', elem: elem }); - } - } - return out; + var elems; + if ( o.style ) { + elems = document.querySelectorAll(o.parts[0]); + lastAction = o.parts.join(' '); + } else if ( o.procedural ) { + elems = vAPI.domFilterer.createProceduralFilter(o).exec(); + } + if ( !elems ) { return; } + var out = []; + for ( var i = 0, n = elems.length; i < n; i++ ) { + out.push({ type: 'cosmetic', elem: elems[i] }); } + return out; }; var lastFilter, @@ -862,26 +790,44 @@ var filterToDOMInterface = (function() { applied = false, previewing = false; - var queryAll = function(filter) { + var queryAll = function(filter, callback) { filter = filter.trim(); if ( filter === lastFilter ) { - return lastResultset; + callback(lastResultset); + return; } unapply(); if ( filter === '' ) { lastFilter = ''; lastResultset = []; - } else { - lastFilter = filter; - lastAction = undefined; - lastResultset = filter.lastIndexOf('##', 0) === 0 ? - fromCosmeticFilter(filter.slice(2)) : - fromNetworkFilter(filter); - if ( previewing ) { - apply(filter); - } + callback(lastResultset); + return; + } + lastFilter = filter; + lastAction = undefined; + if ( filter.lastIndexOf('##', 0) === -1 ) { + lastResultset = fromNetworkFilter(filter); + if ( previewing ) { apply(); } + callback(lastResultset); + return; + } + var selector = filter.slice(2); + lastResultset = fromPlainCosmeticFilter(selector); + if ( lastResultset ) { + if ( previewing ) { apply(); } + callback(lastResultset); + return; } - return lastResultset; + // Procedural cosmetic filter + vAPI.messaging.send( + 'elementPicker', + { what: 'compileCosmeticFilterSelector', selector: selector }, + function(response) { + lastResultset = fromCompiledCosmeticFilter(response); + if ( previewing ) { apply(); } + callback(lastResultset); + } + ); }; var applyHide = function() { @@ -983,9 +929,11 @@ var filterToDOMInterface = (function() { var preview = function(filter) { previewing = filter !== false; if ( previewing ) { - if ( queryAll(filter) !== undefined ) { - apply(); - } + queryAll(filter, function(items) { + if ( items !== undefined ) { + apply(); + } + }); } else { unapply(); } @@ -999,67 +947,72 @@ var filterToDOMInterface = (function() { }; })(); -// https://www.youtube.com/watch?v=nuUXJ6RfIik - /******************************************************************************/ -var userFilterFromCandidate = function() { - var v = taCandidate.value; - var items = filterToDOMInterface.set(v); - if ( !items || items.length === 0 ) { - return false; - } +var userFilterFromCandidate = function(callback) { + var v = rawFilterFromTextarea(); + filterToDOMInterface.set(v, function(items) { + if ( !items || items.length === 0 ) { + callback(); + return; + } - // https://github.com/gorhill/uBlock/issues/738 - // Trim dots. - var hostname = window.location.hostname; - if ( hostname.slice(-1) === '.' ) { - hostname = hostname.slice(0, -1); - } + // https://github.com/gorhill/uBlock/issues/738 + // Trim dots. + var hostname = window.location.hostname; + if ( hostname.slice(-1) === '.' ) { + hostname = hostname.slice(0, -1); + } - // Cosmetic filter? - if ( v.lastIndexOf('##', 0) === 0 ) { - return hostname + v; - } + // Cosmetic filter? + if ( v.lastIndexOf('##', 0) === 0 ) { + callback(hostname + v); + return; + } - // Assume net filter - var opts = []; + // Assume net filter + var opts = []; - // If no domain included in filter, we need domain option - if ( v.lastIndexOf('||', 0) === -1 ) { - opts.push('domain=' + hostname); - } + // If no domain included in filter, we need domain option + if ( v.lastIndexOf('||', 0) === -1 ) { + opts.push('domain=' + hostname); + } - var item = items[0]; - if ( item.opts ) { - opts.push(item.opts); - } + var item = items[0]; + if ( item.opts ) { + opts.push(item.opts); + } - if ( opts.length ) { - v += '$' + opts.join(','); - } + if ( opts.length ) { + v += '$' + opts.join(','); + } - return v; + callback(v); + }); }; /******************************************************************************/ -var onCandidateChanged = function() { - var elems = [], - items = filterToDOMInterface.set(taCandidate.value), - valid = items !== undefined; - if ( valid ) { - for ( var i = 0; i < items.length; i++ ) { - elems.push(items[i].elem); +var onCandidateChanged = (function() { + var process = function(items) { + var elems = [], valid = items !== undefined; + if ( valid ) { + for ( var i = 0; i < items.length; i++ ) { + elems.push(items[i].elem); + } } - } - pickerBody.querySelector('body section textarea + div').textContent = valid ? - items.length.toLocaleString() : - '0'; - taCandidate.classList.toggle('invalidFilter', !valid); - dialog.querySelector('#create').disabled = elems.length === 0; - highlightElements(elems, true); -}; + pickerBody.querySelector('body section textarea + div').textContent = valid ? + items.length.toLocaleString() : + 'ε'; + dialog.querySelector('section').classList.toggle('invalidFilter', !valid); + dialog.querySelector('#create').disabled = elems.length === 0; + highlightElements(elems, true); + }; + + return function() { + filterToDOMInterface.set(rawFilterFromTextarea(), process); + }; +})(); /******************************************************************************/ @@ -1132,8 +1085,8 @@ var onDialogClicked = function(ev) { // We have to exit from preview mode: this guarantees matching elements // will be found for the candidate filter. filterToDOMInterface.preview(false); - var filter = userFilterFromCandidate(); - if ( filter ) { + userFilterFromCandidate(function(filter) { + if ( !filter ) { return; } var d = new Date(); vAPI.messaging.send( 'elementPicker', @@ -1143,9 +1096,9 @@ var onDialogClicked = function(ev) { pageDomain: window.location.hostname } ); - filterToDOMInterface.preview(taCandidate.value); + filterToDOMInterface.preview(rawFilterFromTextarea()); stopPicker(); - } + }); } else if ( ev.target.id === 'pick' ) { @@ -1161,7 +1114,7 @@ var onDialogClicked = function(ev) { if ( filterToDOMInterface.previewing() ) { filterToDOMInterface.preview(false); } else { - filterToDOMInterface.preview(taCandidate.value); + filterToDOMInterface.preview(rawFilterFromTextarea()); } highlightElements(targetElements, true); } @@ -1300,6 +1253,7 @@ var onKeyPressed = function(ev) { if ( ev.which === 27 ) { ev.stopPropagation(); ev.preventDefault(); + filterToDOMInterface.preview(false); stopPicker(); } }; From d4155bf9e0cb1504c5b8be8ee48b6465b8c5bb64 Mon Sep 17 00:00:00 2001 From: gorhill Date: Sun, 25 Dec 2016 17:00:24 -0500 Subject: [PATCH 10/39] new revision for dev build + force lists to be recompiled --- platform/chromium/manifest.json | 2 +- src/js/background.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index 5eb101a757544..ae2931dd2fba1 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "uBlock Origin", - "version": "1.10.5.0", + "version": "1.10.5.1", "default_locale": "en", "description": "__MSG_extShortDesc__", diff --git a/src/js/background.js b/src/js/background.js index 496a1b5dbafc1..a728f43070875 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -108,8 +108,8 @@ return { // read-only systemSettings: { - compiledMagic: 'lbmqiweqbvha', - selfieMagic: 'mhirtyetynnf' + compiledMagic: 'xhjvmgkamffc', + selfieMagic: 'xhjvmgkamffc' }, restoreBackupSettings: { From 251bbe0f439e4fe65ee3970f0b7c68132a5e1101 Mon Sep 17 00:00:00 2001 From: gorhill Date: Sun, 25 Dec 2016 17:05:30 -0500 Subject: [PATCH 11/39] use plain E instead of Greek epsilon (for some reason does not render fine in FFox) --- src/js/scriptlets/element-picker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/scriptlets/element-picker.js b/src/js/scriptlets/element-picker.js index 1181f77f54bc5..0eb699d6c2c05 100644 --- a/src/js/scriptlets/element-picker.js +++ b/src/js/scriptlets/element-picker.js @@ -1003,7 +1003,7 @@ var onCandidateChanged = (function() { } pickerBody.querySelector('body section textarea + div').textContent = valid ? items.length.toLocaleString() : - 'ε'; + 'E'; dialog.querySelector('section').classList.toggle('invalidFilter', !valid); dialog.querySelector('#create').disabled = elems.length === 0; highlightElements(elems, true); From 6e458dca5cb7f6a3c5f7aee2619058608d90af22 Mon Sep 17 00:00:00 2001 From: gorhill Date: Mon, 26 Dec 2016 11:35:37 -0500 Subject: [PATCH 12/39] fix #2264 --- src/css/whitelist.css | 34 ++++++++++++++--- src/js/messaging.js | 4 ++ src/js/ublock.js | 33 ++++++++++++++--- src/js/whitelist.js | 86 ++++++++++++++++++++++++++++++------------- src/whitelist.html | 5 ++- 5 files changed, 125 insertions(+), 37 deletions(-) diff --git a/src/css/whitelist.css b/src/css/whitelist.css index 5a0e834e8d7ae..b017cf0cd7bad 100644 --- a/src/css/whitelist.css +++ b/src/css/whitelist.css @@ -5,12 +5,36 @@ div > p:last-child { margin-bottom: 0; } #whitelist { - box-sizing: border-box; + border: 1px solid gray; height: 60vh; - text-align: left; + margin: 0; + padding: 1px; + position: relative; + resize: vertical; + } +#whitelist.invalid { + border-color: red; + } +#whitelist textarea { + border: none; + box-sizing: border-box; + height: 100%; + padding: 0.4em; + resize: none; + text-align: left; white-space: pre; width: 100%; } -#whitelist.bad { - background-color: #fee; - } +#whitelist textarea + div { + background-color: red; + bottom: 0; + color: white; + display: none; + padding: 2px 4px; + pointer-events: none; + position: absolute; + right: 0; +} +#whitelist.invalid textarea + div { + display: block; +} diff --git a/src/js/messaging.js b/src/js/messaging.js index c6ade1f4f4acd..360fc7e0f39fd 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -1011,6 +1011,10 @@ var onMessage = function(request, sender, callback) { response = getRules(); break; + case 'validateWhitelistString': + response = µb.validateWhitelistString(request.raw); + break; + case 'writeHiddenSettings': µb.hiddenSettingsFromString(request.content); break; diff --git a/src/js/ublock.js b/src/js/ublock.js index d68a12929df8f..528edb534897f 100644 --- a/src/js/ublock.js +++ b/src/js/ublock.js @@ -194,9 +194,7 @@ var matchBucket = function(url, hostname, bucket, start) { µBlock.whitelistFromString = function(s) { var whitelist = Object.create(null), - reInvalidHostname = /[^a-z0-9.\-\[\]:]/, - reHostnameExtractor = /([a-z0-9\[][a-z0-9.\-]*[a-z0-9\]])(?::[\d*]+)?\/(?:[^\x00-\x20\/]|$)[^\x00-\x20]*$/, - lines = s.split(/[\n\r]+/), + lineIter = new this.LineIterator(s), line, matches, key, directive, re; // Comment bucket must always be ready to be used. @@ -205,8 +203,9 @@ var matchBucket = function(url, hostname, bucket, start) { // New set of directives, scrap cached data. directiveToRegexpMap.clear(); - for ( var i = 0; i < lines.length; i++ ) { - line = lines[i].trim(); + while ( !lineIter.eot() ) { + line = lineIter.next().trim(); + // https://github.com/gorhill/uBlock/issues/171 // Skip empty lines if ( line === '' ) { @@ -228,7 +227,7 @@ var matchBucket = function(url, hostname, bucket, start) { } } // Regex-based (ensure it is valid) - else if ( line.startsWith('/') && line.endsWith('/') ) { + else if ( line.length > 2 && line.startsWith('/') && line.endsWith('/') ) { key = '//'; directive = line; try { @@ -267,6 +266,28 @@ var matchBucket = function(url, hostname, bucket, start) { return whitelist; }; +µBlock.validateWhitelistString = function(s) { + var lineIter = new this.LineIterator(s), line; + while ( !lineIter.eot() ) { + line = lineIter.next().trim(); + if ( line === '' ) { continue; } + if ( line.startsWith('#') ) { continue; } // Comment + if ( line.indexOf('/') === -1 ) { // Plain hostname + if ( reInvalidHostname.test(line) ) { return false; } + continue; + } + if ( line.length > 2 && line.startsWith('/') && line.endsWith('/') ) { // Regex-based + try { new RegExp(line.slice(1, -1)); } catch(ex) { return false; } + continue; + } + if ( reHostnameExtractor.test(line) === false ) { return false; } // URL + } + return true; +}; + +var reInvalidHostname = /[^a-z0-9.\-\[\]:]/, + reHostnameExtractor = /([a-z0-9\[][a-z0-9.\-]*[a-z0-9\]])(?::[\d*]+)?\/(?:[^\x00-\x20\/]|$)[^\x00-\x20]*$/; + /******************************************************************************/ })(); diff --git a/src/js/whitelist.js b/src/js/whitelist.js index 866ffca0f27e6..52a3a4d7c5021 100644 --- a/src/js/whitelist.js +++ b/src/js/whitelist.js @@ -29,30 +29,68 @@ /******************************************************************************/ -var messaging = vAPI.messaging; -var cachedWhitelist = ''; - -// Could make it more fancy if needed. But speed... It's a compromise. -var reUnwantedChars = /[\x00-\x09\x0b\x0c\x0e-\x1f!"'()<>{}|`~]/; +var messaging = vAPI.messaging, + cachedWhitelist = ''; /******************************************************************************/ -var whitelistChanged = function() { - var textarea = uDom.nodeFromId('whitelist'); - var s = textarea.value.trim(); - var changed = s === cachedWhitelist; - var bad = reUnwantedChars.test(s); - uDom.nodeFromId('whitelistApply').disabled = changed || bad; - uDom.nodeFromId('whitelistRevert').disabled = changed; - textarea.classList.toggle('bad', bad); +var getTextareaNode = function() { + var me = getTextareaNode, + node = me.theNode; + if ( node === undefined ) { + node = me.theNode = uDom.nodeFromSelector('#whitelist textarea'); + } + return node; +}; + +var setErrorNodeHorizontalOffset = function(px) { + var me = setErrorNodeHorizontalOffset, + offset = me.theOffset || 0; + if ( px === offset ) { return; } + var node = me.theNode; + if ( node === undefined ) { + node = me.theNode = uDom.nodeFromSelector('#whitelist textarea + div'); + } + node.style.right = px + 'px'; + me.theOffset = px; }; /******************************************************************************/ +var whitelistChanged = (function() { + var changedWhitelist, changed, timer; + + var updateUI = function(good) { + uDom.nodeFromId('whitelistApply').disabled = changed || !good; + uDom.nodeFromId('whitelistRevert').disabled = changed; + uDom.nodeFromId('whitelist').classList.toggle('invalid', !good); + }; + + var validate = function() { + timer = undefined; + messaging.send( + 'dashboard', + { what: 'validateWhitelistString', raw: changedWhitelist }, + updateUI + ); + }; + + return function() { + changedWhitelist = getTextareaNode().value.trim(); + changed = changedWhitelist === cachedWhitelist; + if ( timer !== undefined ) { clearTimeout(timer); } + timer = vAPI.setTimeout(validate, 251); + var textarea = getTextareaNode(); + setErrorNodeHorizontalOffset(textarea.offsetWidth - textarea.clientWidth); + }; +})(); + +/******************************************************************************/ + var renderWhitelist = function() { var onRead = function(whitelist) { cachedWhitelist = whitelist.trim(); - uDom.nodeFromId('whitelist').value = cachedWhitelist + '\n'; + getTextareaNode().value = cachedWhitelist + '\n'; whitelistChanged(); }; messaging.send('dashboard', { what: 'getWhitelist' }, onRead); @@ -62,8 +100,8 @@ var renderWhitelist = function() { var handleImportFilePicker = function() { var fileReaderOnLoadHandler = function() { - var textarea = uDom('#whitelist'); - textarea.val([textarea.val(), this.result].join('\n').trim()); + var textarea = getTextareaNode(); + textarea.value = [textarea.value.trim(), this.result.trim()].join('\n').trim(); whitelistChanged(); }; var file = this.files[0]; @@ -92,10 +130,8 @@ var startImportFilePicker = function() { /******************************************************************************/ var exportWhitelistToFile = function() { - var val = uDom('#whitelist').val().trim(); - if ( val === '' ) { - return; - } + var val = getTextareaNode().value.trim(); + if ( val === '' ) { return; } var filename = vAPI.i18n('whitelistExportFilename') .replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString()) .replace(/ +/g, '_'); @@ -108,7 +144,7 @@ var exportWhitelistToFile = function() { /******************************************************************************/ var applyChanges = function() { - cachedWhitelist = uDom.nodeFromId('whitelist').value.trim(); + cachedWhitelist = getTextareaNode().value.trim(); var request = { what: 'setWhitelist', whitelist: cachedWhitelist @@ -117,21 +153,21 @@ var applyChanges = function() { }; var revertChanges = function() { - uDom.nodeFromId('whitelist').value = cachedWhitelist + '\n'; + getTextareaNode().value = cachedWhitelist + '\n'; whitelistChanged(); }; /******************************************************************************/ var getCloudData = function() { - return uDom.nodeFromId('whitelist').value; + return getTextareaNode().value; }; var setCloudData = function(data, append) { if ( typeof data !== 'string' ) { return; } - var textarea = uDom.nodeFromId('whitelist'); + var textarea = getTextareaNode(); if ( append ) { data = uBlockDashboard.mergeNewLines(textarea.value.trim(), data); } @@ -147,7 +183,7 @@ self.cloud.onPull = setCloudData; uDom('#importWhitelistFromFile').on('click', startImportFilePicker); uDom('#importFilePicker').on('change', handleImportFilePicker); uDom('#exportWhitelistToFile').on('click', exportWhitelistToFile); -uDom('#whitelist').on('input', whitelistChanged); +uDom('#whitelist textarea').on('input', whitelistChanged); uDom('#whitelistApply').on('click', applyChanges); uDom('#whitelistRevert').on('click', revertChanges); diff --git a/src/whitelist.html b/src/whitelist.html index 0059a39641a6c..67a70ab92dabd 100644 --- a/src/whitelist.html +++ b/src/whitelist.html @@ -17,7 +17,10 @@

-

+

+ +
E
+

From 914599431b810a367065ee4a810f0a064190c2af Mon Sep 17 00:00:00 2001 From: gorhill Date: Mon, 26 Dec 2016 11:56:51 -0500 Subject: [PATCH 13/39] new revision for dev build --- platform/chromium/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index ae2931dd2fba1..83ae9bf1719cd 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "uBlock Origin", - "version": "1.10.5.1", + "version": "1.10.5.2", "default_locale": "en", "description": "__MSG_extShortDesc__", From 7558fedc38b6e93521854a90665728a8b9df9871 Mon Sep 17 00:00:00 2001 From: gorhill Date: Mon, 26 Dec 2016 12:37:43 -0500 Subject: [PATCH 14/39] fix typo in new :if/:if-not procedural operators --- src/js/contentscript.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/js/contentscript.js b/src/js/contentscript.js index 33f241acad538..12d249e54fd45 100644 --- a/src/js/contentscript.js +++ b/src/js/contentscript.js @@ -443,11 +443,11 @@ PSelector.prototype.exec = function(input) { }; PSelector.prototype.test = function(input) { //var t0 = window.performance.now(); - var tasks = this.tasks, nodes = this.prime(input), aa0 = [ null ], aa; + var tasks = this.tasks, nodes = this.prime(input), AA = [ null ], aa; for ( var i = 0, ni = nodes.length; i < ni; i++ ) { - aa0[0] = nodes[i]; aa = aa0; + AA[0] = nodes[i]; aa = AA; for ( var j = 0, nj = tasks.length; j < nj && aa.length !== 0; j++ ) { - aa = tasks[i].exec(aa); + aa = tasks[j].exec(aa); } if ( aa.length !== 0 ) { return true; } } From 5aa122e856b80b28dd1d2354e825f9759a173058 Mon Sep 17 00:00:00 2001 From: gorhill Date: Tue, 27 Dec 2016 12:32:52 -0500 Subject: [PATCH 15/39] allow lone css selector in :if/:if-not operators --- src/js/cosmetic-filtering.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/js/cosmetic-filtering.js b/src/js/cosmetic-filtering.js index 6d693adb5fb4a..666f9dccf0eb2 100644 --- a/src/js/cosmetic-filtering.js +++ b/src/js/cosmetic-filtering.js @@ -850,7 +850,12 @@ FilterContainer.prototype.compileProceduralSelector = (function() { var compile = function(raw) { var matches = reParserEx.exec(raw); - if ( matches === null ) { return; } + if ( matches === null ) { + if ( isValidCSSSelector(raw) ) { + return { selector: raw, tasks: [] }; + } + return; + } var tasks = [], firstOperand = raw.slice(0, matches.index), currentOperator = matches[1], From e09b70247088e0583ec7c1cf64456ba9f0b54330 Mon Sep 17 00:00:00 2001 From: gorhill Date: Wed, 28 Dec 2016 23:39:15 -0500 Subject: [PATCH 16/39] fix #2274 (hopefully) --- platform/firefox/vapi-background.js | 2 +- src/css/popup.css | 3 + src/js/popup.js | 89 +++++++++++++++++------------ 3 files changed, 58 insertions(+), 36 deletions(-) diff --git a/platform/firefox/vapi-background.js b/platform/firefox/vapi-background.js index 36964825e0652..d960c8f0db503 100644 --- a/platform/firefox/vapi-background.js +++ b/platform/firefox/vapi-background.js @@ -2563,7 +2563,7 @@ vAPI.toolbarButton = { var win = winWatcher.getCurrentWindow(); var curTabId = tabWatcher.tabIdFromTarget(getTabBrowser(win).selectedTab); vAPI.tabs.open({ - url: 'popup.html?tabId=' + curTabId, + url: 'popup.html?tabId=' + curTabId + '&mobile=1', index: -1, select: true }); diff --git a/src/css/popup.css b/src/css/popup.css index 2e8dfefc96749..2d47a0a95d6d3 100644 --- a/src/css/popup.css +++ b/src/css/popup.css @@ -11,6 +11,9 @@ body { body.fullsize { overflow: auto; } +body.mobile { + overflow-y: auto; + } /** https://github.com/gorhill/uBlock/issues/83 .portrait = portrait mode = width is constrained = optimize layout accordingly. diff --git a/src/js/popup.js b/src/js/popup.js index 50eddd7816c05..e3453ed503398 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -40,26 +40,15 @@ if ( typeof popupFontSize === 'string' && popupFontSize !== 'unset' ) { var dfPaneVisibleStored = vAPI.localStorage.getItem('popupFirewallPane') === 'true'; -// Hacky? I couldn't figure a CSS recipe for this problem. -// I do not want the left pane -- optional and hidden by defaut -- to -// dictate the height of the popup. The right pane dictates the height -// of the popup, and the left pane will have a scrollbar if ever its -// height is more than what is available. -(function() { - // No restriction on vertical size? - if ( /[\?&]fullsize=1/.test(window.location.search) ) { - document.body.classList.add('fullsize'); - return; - } +// No restriction on vertical size? +if ( /[\?&]fullsize=1/.test(window.location.search) ) { + document.body.classList.add('fullsize'); +} - var rpane = document.querySelector('#panes > div:nth-of-type(1)'); - if ( typeof rpane.offsetHeight === 'number' ) { - document.querySelector('#panes > div:nth-of-type(2)').style.setProperty( - 'height', - rpane.offsetHeight + 'px' - ); - } -})(); +// Mobile device? +if ( /[\?&]mobile=1/.test(window.location.search) ) { + document.body.classList.add('mobile'); +} // The padlock/eraser must be manually positioned: // - Its vertical position depends on the height of the popup title bar @@ -390,23 +379,10 @@ var renderPrivacyExposure = function() { // Assume everything has to be done incrementally. var renderPopup = function() { - if ( popupData.fontSize !== popupFontSize ) { - popupFontSize = popupData.fontSize; - if ( popupFontSize !== 'unset' ) { - document.body.style.setProperty('font-size', popupFontSize); - vAPI.localStorage.setItem('popupFontSize', popupFontSize); - } else { - document.body.style.removeProperty('font-size'); - vAPI.localStorage.removeItem('popupFontSize'); - } - } - if ( popupData.tabTitle ) { document.title = popupData.appName + ' - ' + popupData.tabTitle; } - uDom.nodeFromId('appname').textContent = popupData.appName; - uDom.nodeFromId('version').textContent = popupData.appVersion; uDom('body') .toggleClass('advancedUser', popupData.advancedUserEnabled) .toggleClass( @@ -419,9 +395,9 @@ var renderPopup = function() { // If you think the `=== true` is pointless, you are mistaken uDom.nodeFromId('gotoPick').classList.toggle('enabled', popupData.canElementPicker === true); - var text; - var blocked = popupData.pageBlockedRequestCount; - var total = popupData.pageAllowedRequestCount + blocked; + var text, + blocked = popupData.pageBlockedRequestCount, + total = popupData.pageAllowedRequestCount + blocked; if ( total === 0 ) { text = formatNumber(0); } else { @@ -499,6 +475,48 @@ var renderPopup = function() { /******************************************************************************/ +// All rendering code which need to be executed only once. + +var renderOnce = function() { + if ( popupData.fontSize !== popupFontSize ) { + popupFontSize = popupData.fontSize; + if ( popupFontSize !== 'unset' ) { + document.body.style.setProperty('font-size', popupFontSize); + vAPI.localStorage.setItem('popupFontSize', popupFontSize); + } else { + document.body.style.removeProperty('font-size'); + vAPI.localStorage.removeItem('popupFontSize'); + } + } + + uDom.nodeFromId('appname').textContent = popupData.appName; + uDom.nodeFromId('version').textContent = popupData.appVersion; + + var rpane = uDom.nodeFromSelector('#panes > div:first-of-type'), + lpane = uDom.nodeFromSelector('#panes > div:last-of-type'); + + // I do not want the left pane -- optional and hidden by defaut -- to + // dictate the height of the popup. The right pane dictates the height + // of the popup, and the left pane will have a scrollbar if ever its + // height is more than what is available. + var lpaneHeight = rpane.offsetHeight; + + // https://github.com/gorhill/uBlock/issues/2274 + // Make use of the whole viewport on mobile devices. + if ( document.body.classList.contains('mobile') ) { + lpaneHeight = Math.max( + window.innerHeight - uDom.nodeFromSelector('#gotoPrefs').offsetHeight, + lpaneHeight + ); + lpane.style.setProperty('width', (window.innerWidth - rpane.offsetWidth) + 'px'); + } + lpane.style.setProperty('height', lpaneHeight + 'px'); + + renderOnce = function(){}; +}; + +/******************************************************************************/ + var renderPopupLazy = function() { messaging.send('popupPanel', { what: 'getPopupLazyData', tabId: popupData.tabId }); }; @@ -864,6 +882,7 @@ var pollForContentChange = (function() { var getPopupData = function(tabId) { var onDataReceived = function(response) { cachePopupData(response); + renderOnce(); renderPopup(); renderPopupLazy(); // low priority rendering hashFromPopupData(true); From 35a63c784f20fb93ff9ce6b09f59484d84c5760a Mon Sep 17 00:00:00 2001 From: gorhill Date: Wed, 28 Dec 2016 23:45:22 -0500 Subject: [PATCH 17/39] new revision for dev build --- platform/chromium/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index 83ae9bf1719cd..fbf40a8585dbe 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "uBlock Origin", - "version": "1.10.5.2", + "version": "1.10.5.3", "default_locale": "en", "description": "__MSG_extShortDesc__", From e5f435c3b1a60e15a6d84fbd8622fed910c76389 Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 29 Dec 2016 14:43:20 -0500 Subject: [PATCH 18/39] complete fix to #2274: detect device rotation --- src/js/popup.js | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/js/popup.js b/src/js/popup.js index e3453ed503398..7d5acfd7d4b40 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -478,6 +478,8 @@ var renderPopup = function() { // All rendering code which need to be executed only once. var renderOnce = function() { + renderOnce = function(){}; + if ( popupData.fontSize !== popupFontSize ) { popupFontSize = popupData.fontSize; if ( popupFontSize !== 'unset' ) { @@ -492,27 +494,35 @@ var renderOnce = function() { uDom.nodeFromId('appname').textContent = popupData.appName; uDom.nodeFromId('version').textContent = popupData.appVersion; + // For large displays: we do not want the left pane -- optional and + // hidden by defaut -- to dictate the height of the popup. The right pane + // dictates the height of the popup, and the left pane will have a + // scrollbar if ever its height is more than what is available. + // For small displays: we use the whole viewport. + var rpane = uDom.nodeFromSelector('#panes > div:first-of-type'), lpane = uDom.nodeFromSelector('#panes > div:last-of-type'); - // I do not want the left pane -- optional and hidden by defaut -- to - // dictate the height of the popup. The right pane dictates the height - // of the popup, and the left pane will have a scrollbar if ever its - // height is more than what is available. - var lpaneHeight = rpane.offsetHeight; + var fillViewport = function() { + lpane.style.setProperty( + 'height', + Math.max( + window.innerHeight - uDom.nodeFromSelector('#gotoPrefs').offsetHeight, + rpane.offsetHeight + ) + 'px' + ); + lpane.style.setProperty('width', (window.innerWidth - rpane.offsetWidth) + 'px'); + }; // https://github.com/gorhill/uBlock/issues/2274 // Make use of the whole viewport on mobile devices. if ( document.body.classList.contains('mobile') ) { - lpaneHeight = Math.max( - window.innerHeight - uDom.nodeFromSelector('#gotoPrefs').offsetHeight, - lpaneHeight - ); - lpane.style.setProperty('width', (window.innerWidth - rpane.offsetWidth) + 'px'); + fillViewport(); + window.addEventListener('resize', fillViewport); + return; } - lpane.style.setProperty('height', lpaneHeight + 'px'); - renderOnce = function(){}; + lpane.style.setProperty('height', rpane.offsetHeight + 'px'); }; /******************************************************************************/ From 7d08b9da390449c1fe08ce07bd03d030b5994891 Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 29 Dec 2016 14:44:06 -0500 Subject: [PATCH 19/39] new revision for dev build --- platform/chromium/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index fbf40a8585dbe..5ef677b40a181 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "uBlock Origin", - "version": "1.10.5.3", + "version": "1.10.5.4", "default_locale": "en", "description": "__MSG_extShortDesc__", From c6dbdbd23b436dafba94281c15e3b8fbe79d341b Mon Sep 17 00:00:00 2001 From: gorhill Date: Fri, 30 Dec 2016 10:32:17 -0500 Subject: [PATCH 20/39] code review of procedural cosmetic filters + better validate :style option (#2278) --- src/js/background.js | 4 +- src/js/contentscript.js | 57 +++++++++++++++------------- src/js/cosmetic-filtering.js | 41 +++++++++----------- src/js/reverselookup-worker.js | 4 +- src/js/scriptlets/cosmetic-logger.js | 11 ++++++ src/js/scriptlets/element-picker.js | 6 +-- 6 files changed, 66 insertions(+), 57 deletions(-) diff --git a/src/js/background.js b/src/js/background.js index a728f43070875..00b82a1c64997 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -108,8 +108,8 @@ return { // read-only systemSettings: { - compiledMagic: 'xhjvmgkamffc', - selfieMagic: 'xhjvmgkamffc' + compiledMagic: 'zelhzxrhkfjr', + selfieMagic: 'zelhzxrhkfjr' }, restoreBackupSettings: { diff --git a/src/js/contentscript.js b/src/js/contentscript.js index 12d249e54fd45..bf3729b708ce5 100644 --- a/src/js/contentscript.js +++ b/src/js/contentscript.js @@ -417,8 +417,9 @@ var PSelector = function(o) { this.raw = o.raw; this.selector = o.selector; this.tasks = []; - var tasks = o.tasks, task, ctor; - for ( var i = 0; i < tasks.length; i++ ) { + var tasks = o.tasks; + if ( !tasks ) { return; } + for ( var i = 0, task, ctor; i < tasks.length; i++ ) { task = tasks[i]; ctor = this.operatorToTaskMap.get(task[0]); this.tasks.push(new ctor(task)); @@ -455,26 +456,6 @@ PSelector.prototype.test = function(input) { return false; }; -var PSelectors = function() { - this.entries = []; -}; -PSelectors.prototype.add = function(o) { - this.entries.push(new PSelector(o)); -}; -PSelectors.prototype.forEachNode = function(callback) { - var pfilters = this.entries, - i = pfilters.length, - pfilter, nodes, j; - while ( i-- ) { - pfilter = pfilters[i]; - nodes = pfilter.exec(); - j = nodes.length; - while ( j-- ) { - callback(nodes[j], pfilter); - } - } -}; - /******************************************************************************/ var domFilterer = { @@ -498,8 +479,6 @@ var domFilterer = { this.entries.push(selector); this.selector = undefined; }, - forEachNodeOfSelector: function(/*callback, root, extra*/) { - }, forEachNode: function(callback, root, extra) { if ( this.selector === undefined ) { this.selector = this.entries.join(extra + ',') + extra; @@ -532,7 +511,29 @@ var domFilterer = { } } }, - proceduralSelectors: new PSelectors(), // Hiding filters: procedural + styleSelectors: { // Style filters + entries: [], + add: function(o) { + this.entries.push(o); + } + }, + proceduralSelectors: { // Hiding filters: procedural + entries: [], + add: function(o) { + this.entries.push(new PSelector(o)); + }, + forEachNode: function(callback) { + var pfilters = this.entries, i = pfilters.length, pfilter, nodes, j; + while ( i-- ) { + pfilter = pfilters[i]; + nodes = pfilter.exec(); + j = nodes.length; + while ( j-- ) { + callback(nodes[j], pfilter); + } + } + } + }, addExceptions: function(aa) { for ( var i = 0, n = aa.length; i < n; i++ ) { @@ -556,11 +557,13 @@ var domFilterer = { } var o = JSON.parse(selector); if ( o.style ) { - this.newStyleRuleBuffer.push(o.parts.join(' ')); + this.newStyleRuleBuffer.push(o.style.join(' ')); + this.styleSelectors.add(o); return; } - if ( o.procedural ) { + if ( o.tasks ) { this.proceduralSelectors.add(o); + return; } }, diff --git a/src/js/cosmetic-filtering.js b/src/js/cosmetic-filtering.js index 666f9dccf0eb2..4d35539f07b2e 100644 --- a/src/js/cosmetic-filtering.js +++ b/src/js/cosmetic-filtering.js @@ -242,7 +242,7 @@ FilterBucket.fromSelfie = function() { /******************************************************************************/ var FilterParser = function() { - this.prefix = this.suffix = this.style = ''; + this.prefix = this.suffix = ''; this.unhide = 0; this.hostnames = []; this.invalid = false; @@ -254,7 +254,7 @@ var FilterParser = function() { FilterParser.prototype.reset = function() { this.raw = ''; - this.prefix = this.suffix = this.style = ''; + this.prefix = this.suffix = ''; this.unhide = 0; this.hostnames.length = 0; this.invalid = false; @@ -628,7 +628,6 @@ var FilterContainer = function() { this.netSelectorCacheCountMax = netSelectorCacheHighWaterMark; this.selectorCacheTimer = null; this.reHasUnicode = /[^\x00-\x7F]/; - this.reClassOrIdSelector = /^[#.][\w-]+$/; this.rePlainSelector = /^[#.][\w\\-]+/; this.rePlainSelectorEscaped = /^[#.](?:\\[0-9A-Fa-f]+ |\\.|\w|-)+/; this.rePlainSelectorEx = /^[^#.\[(]+([#.][\w-]+)/; @@ -735,7 +734,16 @@ FilterContainer.prototype.freeze = function() { FilterContainer.prototype.compileSelector = (function() { var reStyleSelector = /^(.+?):style\((.+?)\)$/, reStyleBad = /url\([^)]+\)/, - reScriptSelector = /^script:(contains|inject)\((.+)\)$/; + reScriptSelector = /^script:(contains|inject)\((.+)\)$/, + div = document.createElement('div'); + + var isValidStyleProperty = function(cssText) { + if ( reStyleBad.test(cssText) ) { return false; } + div.style.cssText = cssText; + if ( div.style.cssText === '' ) { return false; } + div.style.cssText = ''; + return true; + }; return function(raw) { if ( isValidCSSSelector(raw) && raw.indexOf('[-abp-properties=') === -1 ) { @@ -747,11 +755,10 @@ FilterContainer.prototype.compileSelector = (function() { // `:style` selector? if ( (matches = reStyleSelector.exec(raw)) !== null ) { - if ( isValidCSSSelector(matches[1]) && reStyleBad.test(matches[2]) === false ) { + if ( isValidCSSSelector(matches[1]) && isValidStyleProperty(matches[2]) ) { return JSON.stringify({ - style: true, raw: raw, - parts: [ matches[1], '{' + matches[2] + '}' ] + style: [ matches[1], '{' + matches[2] + '}' ] }); } return; @@ -784,7 +791,7 @@ FilterContainer.prototype.compileSelector = (function() { /******************************************************************************/ FilterContainer.prototype.compileProceduralSelector = (function() { - var reParserEx = /(:(?:has|has-text|if|if-not|matches-css|matches-css-after|matches-css-before|xpath))\(.+\)$/, + var reOperatorParser = /(:(?:has|has-text|if|if-not|matches-css|matches-css-after|matches-css-before|xpath))\(.+\)$/, reFirstParentheses = /^\(*/, reLastParentheses = /\)*$/, reEscapeRegex = /[.*+?^${}()|[\]\\]/g; @@ -849,11 +856,9 @@ FilterContainer.prototype.compileProceduralSelector = (function() { ]); var compile = function(raw) { - var matches = reParserEx.exec(raw); + var matches = reOperatorParser.exec(raw); if ( matches === null ) { - if ( isValidCSSSelector(raw) ) { - return { selector: raw, tasks: [] }; - } + if ( isValidCSSSelector(raw) ) { return { selector: raw }; } return; } var tasks = [], @@ -864,7 +869,7 @@ FilterContainer.prototype.compileProceduralSelector = (function() { depth = 0, opening, closing; if ( firstOperand !== '' && isValidCSSSelector(firstOperand) === false ) { return; } for (;;) { - matches = reParserEx.exec(selector); + matches = reOperatorParser.exec(selector); if ( matches !== null ) { nextOperand = selector.slice(0, matches.index); nextOperator = matches[1]; @@ -903,7 +908,6 @@ FilterContainer.prototype.compileProceduralSelector = (function() { lastProceduralSelector = raw; var compiled = compile(raw); if ( compiled !== undefined ) { - compiled.procedural = true; compiled.raw = raw; compiled = JSON.stringify(compiled); } @@ -965,15 +969,6 @@ FilterContainer.prototype.compile = function(s, out) { return true; } - // For hostname- or entity-based filters, class- or id-based selectors are - // still the most common, and can easily be tested using a plain regex. - if ( - this.reClassOrIdSelector.test(parsed.suffix) === false && - this.compileSelector(parsed.suffix) === undefined - ) { - return true; - } - // https://github.com/chrisaljoudi/uBlock/issues/151 // Negated hostname means the filter applies to all non-negated hostnames // of same filter OR globally if there is no non-negated hostnames. diff --git a/src/js/reverselookup-worker.js b/src/js/reverselookup-worker.js index ff827fdcd7f0c..f52002e676750 100644 --- a/src/js/reverselookup-worker.js +++ b/src/js/reverselookup-worker.js @@ -139,9 +139,9 @@ var fromCosmeticFilter = function(details) { // compiled form of a filter. var filterEx = '(' + reEscape(filter) + - '|[^\\v]+' + + '|\{[^\\v]*' + reEscape(JSON.stringify({ raw: filter }).slice(1,-1)) + - '[^\\v]+)'; + '[^\\v]*\})'; // Second step: find hostname-based versions. // Reference: FilterContainer.compileHostnameSelector(). diff --git a/src/js/scriptlets/cosmetic-logger.js b/src/js/scriptlets/cosmetic-logger.js index 51c67d6fe5dea..efcf6110512c7 100644 --- a/src/js/scriptlets/cosmetic-logger.js +++ b/src/js/scriptlets/cosmetic-logger.js @@ -51,6 +51,17 @@ vAPI.domFilterer.simpleHideSelectors.entries.forEach(evaluateSelector); // Complex CSS selector-based cosmetic filters. vAPI.domFilterer.complexHideSelectors.entries.forEach(evaluateSelector); +// Style cosmetic filters. +vAPI.domFilterer.styleSelectors.entries.forEach(function(filter) { + if ( + loggedSelectors.hasOwnProperty(filter.raw) === false && + document.querySelector(filter.style[0]) !== null + ) { + loggedSelectors[filter.raw] = true; + matchedSelectors.push(filter.raw); + } +}); + // Procedural cosmetic filters. vAPI.domFilterer.proceduralSelectors.entries.forEach(function(pfilter) { if ( diff --git a/src/js/scriptlets/element-picker.js b/src/js/scriptlets/element-picker.js index 0eb699d6c2c05..80d0b4aa49fda 100644 --- a/src/js/scriptlets/element-picker.js +++ b/src/js/scriptlets/element-picker.js @@ -770,9 +770,9 @@ var filterToDOMInterface = (function() { } var elems; if ( o.style ) { - elems = document.querySelectorAll(o.parts[0]); - lastAction = o.parts.join(' '); - } else if ( o.procedural ) { + elems = document.querySelectorAll(o.style[0]); + lastAction = o.style.join(' '); + } else if ( o.tasks ) { elems = vAPI.domFilterer.createProceduralFilter(o).exec(); } if ( !elems ) { return; } From 38a5f5751b575a9e6ba1bb24616788354653b462 Mon Sep 17 00:00:00 2001 From: gorhill Date: Fri, 30 Dec 2016 10:41:16 -0500 Subject: [PATCH 21/39] code review: be sure all invalid cosmetic filters are reported in logger --- src/js/cosmetic-filtering.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/js/cosmetic-filtering.js b/src/js/cosmetic-filtering.js index 4d35539f07b2e..8782fb9ff8377 100644 --- a/src/js/cosmetic-filtering.js +++ b/src/js/cosmetic-filtering.js @@ -754,14 +754,15 @@ FilterContainer.prototype.compileSelector = (function() { var matches; // `:style` selector? - if ( (matches = reStyleSelector.exec(raw)) !== null ) { - if ( isValidCSSSelector(matches[1]) && isValidStyleProperty(matches[2]) ) { - return JSON.stringify({ - raw: raw, - style: [ matches[1], '{' + matches[2] + '}' ] - }); - } - return; + if ( + (matches = reStyleSelector.exec(raw)) !== null && + isValidCSSSelector(matches[1]) && + isValidStyleProperty(matches[2]) + ) { + return JSON.stringify({ + raw: raw, + style: [ matches[1], '{' + matches[2] + '}' ] + }); } // `script:` filter? @@ -774,7 +775,6 @@ FilterContainer.prototype.compileSelector = (function() { if ( reIsRegexLiteral.test(matches[2]) === false || isBadRegex(matches[2].slice(1, -1)) === false ) { return raw; } - return; } // Procedural selector? @@ -784,7 +784,6 @@ FilterContainer.prototype.compileSelector = (function() { } µb.logger.writeOne('', 'error', 'Cosmetic filtering – invalid filter: ' + raw); - return; }; })(); From c196bf5f535c9f6a81bca4c14c61b0a88612c842 Mon Sep 17 00:00:00 2001 From: gorhill Date: Fri, 30 Dec 2016 10:43:07 -0500 Subject: [PATCH 22/39] new revision for dev build --- platform/chromium/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index 5ef677b40a181..580efaa1c00fd 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "uBlock Origin", - "version": "1.10.5.4", + "version": "1.10.5.5", "default_locale": "en", "description": "__MSG_extShortDesc__", From 1a92fff6418884edad2991ecd49a305a18b8612f Mon Sep 17 00:00:00 2001 From: gorhill Date: Mon, 2 Jan 2017 10:50:03 -0500 Subject: [PATCH 23/39] fix https://github.com/uBlockOrigin/uAssets/issues/255 --- platform/firefox/vapi-background.js | 44 ++++++++++++++++++++--------- src/js/tab.js | 19 +++++++------ 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/platform/firefox/vapi-background.js b/platform/firefox/vapi-background.js index d960c8f0db503..a421749245340 100644 --- a/platform/firefox/vapi-background.js +++ b/platform/firefox/vapi-background.js @@ -2361,9 +2361,27 @@ vAPI.net.registerListeners = function() { null; } - var shouldLoadPopupListenerMessageName = location.host + ':shouldLoadPopup'; - var shouldLoadPopupListener = function(openerURL, popupTabId) { - var uri, openerTabId; + var shouldLoadPopupListenerMessageName = location.host + ':shouldLoadPopup', + shouldLoadPopupListenerMap = new Map(), + shouldLoadPopupListenerMapToD = 0; + var shouldLoadPopupListener = function(openerURL, target) { + var popupTabId = tabWatcher.tabIdFromTarget(target), + popupURL = target.currentURI && target.currentURI.asciiSpec || '', + openerTabId, + uri; + if ( shouldLoadPopupListenerMapToD > Date.now() ) { + openerTabId = shouldLoadPopupListenerMap.get(popupURL); + } + + // https://github.com/uBlockOrigin/uAssets/issues/255 + // Handle chained popups. + if ( openerTabId !== undefined ) { + shouldLoadPopupListenerMap.set(target.currentURI.asciiSpec, openerTabId); + shouldLoadPopupListenerMapToD = Date.now() + 10000; + vAPI.tabs.onPopupCreated(popupTabId, openerTabId); + return; + } + for ( var browser of tabWatcher.browsers() ) { uri = browser.currentURI; @@ -2375,15 +2393,16 @@ vAPI.net.registerListeners = function() { // believe this may have to do with those very temporary // browser objects created when opening a new tab, i.e. related // to https://github.com/gorhill/uBlock/issues/212 - if ( !uri || uri.spec !== openerURL ) { - continue; - } + if ( !uri || uri.spec !== openerURL ) { continue; } openerTabId = tabWatcher.tabIdFromTarget(browser); - if ( openerTabId !== popupTabId ) { - vAPI.tabs.onPopupCreated(popupTabId, openerTabId); - break; - } + if ( openerTabId === popupTabId ) { continue; } + + shouldLoadPopupListenerMap = new Map(); + shouldLoadPopupListenerMapToD = Date.now() + 10000; + shouldLoadPopupListenerMap.set(popupURL, openerTabId); + vAPI.tabs.onPopupCreated(popupTabId, openerTabId); + break; } }; var shouldLoadPopupListenerAsync = function(e) { @@ -2391,10 +2410,7 @@ vAPI.net.registerListeners = function() { return; } // We are handling a synchronous message: do not block. - vAPI.setTimeout( - shouldLoadPopupListener.bind(null, e.data, tabWatcher.tabIdFromTarget(e.target)), - 1 - ); + vAPI.setTimeout(shouldLoadPopupListener.bind(null, e.data, e.target), 1); }; vAPI.messaging.globalMessageManager.addMessageListener( diff --git a/src/js/tab.js b/src/js/tab.js index ffad7a4dff3bc..af8c68a6371c7 100644 --- a/src/js/tab.js +++ b/src/js/tab.js @@ -568,22 +568,25 @@ vAPI.tabs.onPopupUpdated = (function() { // URL. // https://github.com/gorhill/uBlock/issues/1735 // Do not bail out on `data:` URI, they are commonly used for popups. + // https://github.com/uBlockOrigin/uAssets/issues/255 + // Do not bail out on `about:blank`: an `about:blank` popup can be + // opened, with the sole purpose to serve as an intermediary in + // a sequence of chained popups. if ( context.requestHostname === '' && - targetURL.startsWith('data:') === false + targetURL.startsWith('data:') === false && + targetURL !== 'about:blank' ) { return ''; } // Dynamic filtering makes sense only when we have a valid hostname. if ( openerHostname !== '' ) { - // Check user switch first - if ( - typeof clickedURL === 'string' && - areDifferentURLs(targetURL, clickedURL) && - µb.hnSwitches.evaluateZ('no-popups', openerHostname) - ) { - return 'ub:no-popups: ' + µb.hnSwitches.z + ' true'; + // Check per-site switch first + if ( µb.hnSwitches.evaluateZ('no-popups', openerHostname) ) { + if ( typeof clickedURL !== 'string' || areDifferentURLs(targetURL, clickedURL) ) { + return 'ub:no-popups: ' + µb.hnSwitches.z + ' true'; + } } // https://github.com/gorhill/uBlock/issues/581 From bacf5021e0ee943421964d0438eeed21df6c583b Mon Sep 17 00:00:00 2001 From: gorhill Date: Fri, 6 Jan 2017 12:39:37 -0500 Subject: [PATCH 24/39] performance work: - refactor "domain=" option matcher in light of https://gorhill.github.io/obj-vs-set-vs-map/set-vs-regexp.html - reuse existing instance of "domain=" matchers and filters wherever possible --- src/js/static-net-filtering.js | 328 +++++++++++++++++---------------- 1 file changed, 168 insertions(+), 160 deletions(-) diff --git a/src/js/static-net-filtering.js b/src/js/static-net-filtering.js index 85434ce9ddbcf..f389281bf0117 100644 --- a/src/js/static-net-filtering.js +++ b/src/js/static-net-filtering.js @@ -259,126 +259,139 @@ var reURLPostHostnameAnchors = /[\/?#]/; /******************************************************************************/ -// Hostname test helpers: the optimal test function is picked -// according to the content of the `domain` filter option, +// Hostname test helpers: the optimal test function is picked according to the +// content of the `domain=` filter option. -var hostnameTestPicker = function(owner) { - var domainOpt = owner.domainOpt; +// Re-factored in light of: +// - https://gorhill.github.io/obj-vs-set-vs-map/set-vs-regexp.html +// The re-factoring made possible to reuse instances of a matcher. As of +// writing, I observed that just with EasyList, there were ~1,200 reused +// instances out of ~2,800. - // Only one hostname - if ( domainOpt.indexOf('|') === -1 ) { - if ( domainOpt.startsWith('~') ) { - owner._notHostname = domainOpt.slice(1); - return hostnameMissTest; - } - return hostnameHitTest; +var hnMatcherFactory = function(domainOpt) { + var me = hnMatcherFactory; + + // Reuse last instance if possible. + if ( domainOpt === me.domainOpt ) { + return me.hnMatcher; } - // Multiple hostnames: use a dictionary. - var hostnames = domainOpt.split('|'); - var i, hostname, dict; + me.domainOpt = domainOpt; - // First find out whether we have a homogeneous dictionary - var hit = false, miss = false; - i = hostnames.length; - while ( i-- ) { - if ( hostnames[i].startsWith('~') ) { - miss = true; - if ( hit ) { - break; - } - } else { - hit = true; - if ( miss ) { - break; - } + // Only one hostname + if ( domainOpt.indexOf('|') === -1 ) { + if ( domainOpt.charCodeAt(0) === 0x7E /* '~' */ ) { + return (me.hnMatcher = new me.Miss(domainOpt)); } + return (me.hnMatcher = new me.Hit(domainOpt)); } - // Heterogenous dictionary: this can happen, though VERY rarely. - // Spotted one occurrence in EasyList Lite (cjxlist.txt): - // domain=photobucket.com|~secure.photobucket.com - if ( hit && miss ) { - dict = owner._hostnameDict = new Map(); - i = hostnames.length; - while ( i-- ) { - hostname = hostnames[i]; - if ( hostname.startsWith('~') ) { - dict.set(hostname.slice(1), false); - } else { - dict.set(hostname, true); - } - } - return hostnameMixedSetTest; + // Many hostnames. + + // Must be in set (none negated). + if ( domainOpt.indexOf('~') === -1 ) { + return (me.hnMatcher = new me.HitSet(domainOpt)); } - // Homogeneous dictionary. - dict = owner._hostnameDict = new Set(); - i = hostnames.length; - while ( i-- ) { - hostname = hostnames[i]; - dict.add(hostname.startsWith('~') ? hostname.slice(1) : hostname); + // Must not be in set (all negated). + if ( me.reAllNegated.test(domainOpt) ) { + return (me.hnMatcher = new me.MissSet(domainOpt)); } - return hit ? hostnameHitSetTest : hostnameMissSetTest; + // Must be in one set, but not in the other. + return (me.hnMatcher = new me.MixedSet(domainOpt)); }; -var hostnameHitTest = function(owner) { - var current = pageHostnameRegister; - var target = owner.domainOpt; - return current.endsWith(target) && - (current.length === target.length || - current.charAt(current.length - target.length - 1) === '.'); +hnMatcherFactory.reAllNegated = /^~(?:[^|~]+\|~)+[^|~]+$/; +hnMatcherFactory.domainOpt = undefined; +hnMatcherFactory.hnMatcher = undefined; + +hnMatcherFactory.Hit = function(domainOpt) { + this.hostname = domainOpt; +}; +hnMatcherFactory.Hit.prototype.toDomainOpt = function() { + return this.hostname; +}; +hnMatcherFactory.Hit.prototype.test = function() { + var needle = this.hostname, + haystack = pageHostnameRegister; + return haystack.endsWith(needle) && + (haystack.length === needle.length || + haystack.charCodeAt(haystack.length - needle.length - 1) === 0x2E /* '.' */); }; -var hostnameMissTest = function(owner) { - var current = pageHostnameRegister; - var target = owner._notHostname; - return current.endsWith(target) === false || - (current.length !== target.length && - current.charAt(current.length - target.length - 1) !== '.'); +hnMatcherFactory.Miss = function(domainOpt) { + this.hostname = domainOpt.slice(1); +}; +hnMatcherFactory.Miss.prototype.toDomainOpt = function() { + return '~' + this.hostname; +}; +hnMatcherFactory.Miss.prototype.test = function() { + var needle = this.hostname, + haystack = pageHostnameRegister; + return haystack.endsWith(needle) === false || + (haystack.length !== needle.length && + haystack.charCodeAt(haystack.length - needle.length - 1) !== 0x2E /* '.' */); }; -var hostnameHitSetTest = function(owner) { - var dict = owner._hostnameDict, - needle = pageHostnameRegister, - pos; - for (;;) { - if ( dict.has(needle) ) { return true; } - pos = needle.indexOf('.'); - if ( pos === -1 ) { break; } - needle = needle.slice(pos + 1); - } - return false; +hnMatcherFactory.HitSet = function(domainOpt) { + this.domainOpt = domainOpt; +}; +hnMatcherFactory.HitSet.prototype.oneOf = null; +hnMatcherFactory.HitSet.prototype.toDomainOpt = function() { + return this.domainOpt; +}; +hnMatcherFactory.HitSet.prototype.init = function() { + this.oneOf = new RegExp('(?:^|\\.)(?:' + this.domainOpt.replace(/\./g, '\\.') + ')$'); +}; +hnMatcherFactory.HitSet.prototype.test = function() { + if ( this.oneOf === null ) { this.init(); } + return this.oneOf.test(pageHostnameRegister); }; -var hostnameMissSetTest = function(owner) { - var dict = owner._hostnameDict, - needle = pageHostnameRegister, - pos; - for (;;) { - if ( dict.has(needle) ) { return false; } - pos = needle.indexOf('.'); - if ( pos === -1 ) { break; } - needle = needle.slice(pos + 1); - } - return true; +hnMatcherFactory.MissSet = function(domainOpt) { + this.domainOpt = domainOpt; +}; +hnMatcherFactory.MissSet.prototype.noneOf = null; +hnMatcherFactory.MissSet.prototype.toDomainOpt = function() { + return this.domainOpt; +}; +hnMatcherFactory.MissSet.prototype.init = function() { + this.noneOf = new RegExp('(?:^|\\.)(?:' + this.domainOpt.replace(/~/g, '').replace(/\./g, '\\.') + ')$'); +}; +hnMatcherFactory.MissSet.prototype.test = function() { + if ( this.noneOf === null ) { this.init(); } + return this.noneOf.test(pageHostnameRegister) === false; }; -var hostnameMixedSetTest = function(owner) { - var dict = owner._hostnameDict, - needle = pageHostnameRegister, - hit = false, - v, pos; - for (;;) { - v = dict.get(needle); - if ( v === false ) { return false; } - if ( v === true ) { hit = true; } - pos = needle.indexOf('.'); - if ( pos === -1 ) { break; } - needle = needle.slice(pos + 1); +hnMatcherFactory.MixedSet = function(domainOpt) { + this.domainOpt = domainOpt; +}; +hnMatcherFactory.MixedSet.prototype.oneOf = null; +hnMatcherFactory.MixedSet.prototype.noneOf = null; +hnMatcherFactory.MixedSet.prototype.toDomainOpt = function() { + return this.domainOpt; +}; +hnMatcherFactory.MixedSet.prototype.init = function() { + var oneOf = [], noneOf = [], + hostnames = this.domainOpt.split('|'), + i = hostnames.length, + hostname; + while ( i-- ) { + hostname = hostnames[i].replace(/\./g, '\\.'); + if ( hostname.charCodeAt(0) === 0x7E /* '~' */ ) { + noneOf.push(hostname.slice(1)); + } else { + oneOf.push(hostname); + } } - return hit; + this.oneOf = new RegExp('(?:^|\\.)(?:' + oneOf.join('|') + ')$'); + this.noneOf = new RegExp('(?:^|\\.)(?:' + noneOf.join('|') + ')$'); +}; +hnMatcherFactory.MixedSet.prototype.test = function() { + if ( this.oneOf === null ) { this.init(); } + var needle = pageHostnameRegister; + return this.oneOf.test(needle) && this.noneOf.test(needle) === false; }; /******************************************************************************* @@ -443,13 +456,11 @@ FilterPlain.fromSelfie = function(s) { var FilterPlainHostname = function(s, tokenBeg, domainOpt) { this.s = s; this.tokenBeg = tokenBeg; - this.domainOpt = domainOpt; - this.hostnameTest = hostnameTestPicker(this); + this.hnMatcher = hnMatcherFactory(domainOpt); }; FilterPlainHostname.prototype.match = function(url, tokenBeg) { - return url.startsWith(this.s, tokenBeg - this.tokenBeg) && - this.hostnameTest(this); + return url.startsWith(this.s, tokenBeg - this.tokenBeg) && this.hnMatcher.test(); }; FilterPlainHostname.fid = @@ -458,7 +469,7 @@ FilterPlainHostname.prototype.rtfid = 'ah'; FilterPlainHostname.prototype.toSelfie = FilterPlainHostname.prototype.rtCompile = function() { - return this.s + '\t' + this.tokenBeg + '\t' + this.domainOpt; + return this.s + '\t' + this.tokenBeg + '\t' + this.hnMatcher.toDomainOpt(); }; FilterPlainHostname.compile = function(details) { @@ -501,13 +512,11 @@ FilterPlainPrefix0.fromSelfie = function(s) { var FilterPlainPrefix0Hostname = function(s, domainOpt) { this.s = s; - this.domainOpt = domainOpt; - this.hostnameTest = hostnameTestPicker(this); + this.hnMatcher = hnMatcherFactory(domainOpt); }; FilterPlainPrefix0Hostname.prototype.match = function(url, tokenBeg) { - return url.startsWith(this.s, tokenBeg) && - this.hostnameTest(this); + return url.startsWith(this.s, tokenBeg) && this.hnMatcher.test(); }; FilterPlainPrefix0Hostname.fid = @@ -516,7 +525,7 @@ FilterPlainPrefix0Hostname.prototype.rtfid = '0ah'; FilterPlainPrefix0Hostname.prototype.toSelfie = FilterPlainPrefix0Hostname.prototype.rtCompile = function() { - return this.s + '\t' + this.domainOpt; + return this.s + '\t' + this.hnMatcher.toDomainOpt(); }; FilterPlainPrefix0Hostname.compile = function(details) { @@ -559,13 +568,11 @@ FilterPlainPrefix1.fromSelfie = function(s) { var FilterPlainPrefix1Hostname = function(s, domainOpt) { this.s = s; - this.domainOpt = domainOpt; - this.hostnameTest = hostnameTestPicker(this); + this.hnMatcher = hnMatcherFactory(domainOpt); }; FilterPlainPrefix1Hostname.prototype.match = function(url, tokenBeg) { - return url.startsWith(this.s, tokenBeg - 1) && - this.hostnameTest(this); + return url.startsWith(this.s, tokenBeg - 1) && this.hnMatcher.test(); }; FilterPlainPrefix1Hostname.fid = @@ -574,7 +581,7 @@ FilterPlainPrefix1Hostname.prototype.rtfid = '1ah'; FilterPlainPrefix1Hostname.prototype.toSelfie = FilterPlainPrefix1Hostname.prototype.rtCompile = function() { - return this.s + '\t' + this.domainOpt; + return this.s + '\t' + this.hnMatcher.toDomainOpt(); }; FilterPlainPrefix1Hostname.compile = function(details) { @@ -617,13 +624,11 @@ FilterPlainLeftAnchored.fromSelfie = function(s) { var FilterPlainLeftAnchoredHostname = function(s, domainOpt) { this.s = s; - this.domainOpt = domainOpt; - this.hostnameTest = hostnameTestPicker(this); + this.hnMatcher = hnMatcherFactory(domainOpt); }; FilterPlainLeftAnchoredHostname.prototype.match = function(url) { - return url.startsWith(this.s) && - this.hostnameTest(this); + return url.startsWith(this.s) && this.hnMatcher.test(); }; FilterPlainLeftAnchoredHostname.fid = @@ -632,7 +637,7 @@ FilterPlainLeftAnchoredHostname.prototype.rtfid = '|ah'; FilterPlainLeftAnchoredHostname.prototype.toSelfie = FilterPlainLeftAnchoredHostname.prototype.rtCompile = function() { - return this.s + '\t' + this.domainOpt; + return this.s + '\t' + this.hnMatcher.toDomainOpt(); }; FilterPlainLeftAnchoredHostname.compile = function(details) { @@ -675,13 +680,11 @@ FilterPlainRightAnchored.fromSelfie = function(s) { var FilterPlainRightAnchoredHostname = function(s, domainOpt) { this.s = s; - this.domainOpt = domainOpt; - this.hostnameTest = hostnameTestPicker(this); + this.hnMatcher = hnMatcherFactory(domainOpt); }; FilterPlainRightAnchoredHostname.prototype.match = function(url) { - return url.endsWith(this.s) && - this.hostnameTest(this); + return url.endsWith(this.s) && this.hnMatcher.test(); }; FilterPlainRightAnchoredHostname.fid = @@ -690,7 +693,7 @@ FilterPlainRightAnchoredHostname.prototype.rtfid = 'a|h'; FilterPlainRightAnchoredHostname.prototype.toSelfie = FilterPlainRightAnchoredHostname.prototype.rtCompile = function() { - return this.s + '\t' + this.domainOpt; + return this.s + '\t' + this.hnMatcher.toDomainOpt(); }; FilterPlainRightAnchoredHostname.compile = function(details) { @@ -742,13 +745,12 @@ FilterPlainHnAnchored.fromSelfie = function(s) { var FilterPlainHnAnchoredHostname = function(s, domainOpt) { this.s = s; - this.domainOpt = domainOpt; - this.hostnameTest = hostnameTestPicker(this); + this.hnMatcher = hnMatcherFactory(domainOpt); }; FilterPlainHnAnchoredHostname.prototype.match = function(url, tokenBeg) { return url.startsWith(this.s, tokenBeg) && - this.hostnameTest(this) && + this.hnMatcher.test() && isHnAnchored(url, tokenBeg); }; @@ -758,7 +760,7 @@ FilterPlainHnAnchoredHostname.prototype.rtfid = '||ah'; FilterPlainHnAnchoredHostname.prototype.toSelfie = FilterPlainHnAnchoredHostname.prototype.rtCompile = function() { - return this.s + '\t' + this.domainOpt; + return this.s + '\t' + this.hnMatcher.toDomainOpt(); }; FilterPlainHnAnchoredHostname.compile = function(details) { @@ -811,15 +813,13 @@ FilterGeneric.fromSelfie = function(s) { var FilterGenericHostname = function(s, anchor, domainOpt) { FilterGeneric.call(this, s, anchor); - this.domainOpt = domainOpt; - this.hostnameTest = hostnameTestPicker(this); + this.hnMatcher = hnMatcherFactory(domainOpt); }; FilterGenericHostname.prototype = Object.create(FilterGeneric.prototype); FilterGenericHostname.prototype.constructor = FilterGenericHostname; FilterGenericHostname.prototype.match = function(url) { - return this.hostnameTest(this) && - FilterGeneric.prototype.match.call(this, url); + return this.hnMatcher.test() && FilterGeneric.prototype.match.call(this, url); }; FilterGenericHostname.fid = @@ -828,7 +828,7 @@ FilterGenericHostname.prototype.rtfid = '_h'; FilterGenericHostname.prototype.toSelfie = FilterGenericHostname.prototype.rtCompile = function() { - return FilterGeneric.prototype.toSelfie.call(this) + '\t' + this.domainOpt; + return FilterGeneric.prototype.toSelfie.call(this) + '\t' + this.hnMatcher.toDomainOpt(); }; FilterGenericHostname.compile = function(details) { @@ -882,15 +882,13 @@ FilterGenericHnAnchored.fromSelfie = function(s) { var FilterGenericHnAnchoredHostname = function(s, anchor, domainOpt) { FilterGenericHnAnchored.call(this, s, anchor); - this.domainOpt = domainOpt; - this.hostnameTest = hostnameTestPicker(this); + this.hnMatcher = hnMatcherFactory(domainOpt); }; FilterGenericHnAnchoredHostname.prototype = Object.create(FilterGenericHnAnchored.prototype); FilterGenericHnAnchoredHostname.prototype.constructor = FilterGenericHnAnchoredHostname; FilterGenericHnAnchoredHostname.prototype.match = function(url) { - return this.hostnameTest(this) && - FilterGenericHnAnchored.prototype.match.call(this, url); + return this.hnMatcher.test() && FilterGenericHnAnchored.prototype.match.call(this, url); }; FilterGenericHnAnchoredHostname.fid = @@ -899,7 +897,7 @@ FilterGenericHnAnchoredHostname.prototype.rtfid = '||_h'; FilterGenericHnAnchoredHostname.prototype.toSelfie = FilterGenericHnAnchoredHostname.prototype.rtCompile = function() { - return this.s + '\t' + this.anchor + '\t' + this.domainOpt; + return this.s + '\t' + this.anchor + '\t' + this.hnMatcher.toDomainOpt(); }; FilterGenericHnAnchoredHostname.compile = function(details) { @@ -944,14 +942,12 @@ FilterRegex.fromSelfie = function(s) { var FilterRegexHostname = function(s, domainOpt) { this.re = new RegExp(s, 'i'); - this.domainOpt = domainOpt; - this.hostnameTest = hostnameTestPicker(this); + this.hnMatcher = hnMatcherFactory(domainOpt); }; FilterRegexHostname.prototype.match = function(url) { // test hostname first, it's cheaper than evaluating a regex - return this.hostnameTest(this) && - this.re.test(url); + return this.hnMatcher.test() && this.re.test(url); }; FilterRegexHostname.fid = @@ -960,7 +956,7 @@ FilterRegexHostname.prototype.rtfid = '//h'; FilterRegexHostname.prototype.toSelfie = FilterRegexHostname.prototype.rtCompile = function() { - return this.re.source + '\t' + this.domainOpt; + return this.re.source + '\t' + this.hnMatcher.toDomainOpt(); }; FilterRegexHostname.compile = function(details) { @@ -1582,6 +1578,11 @@ FilterContainer.prototype.reset = function() { this.filterParser.reset(); this.filterCounts = {}; + // Reuse filter instances whenever possible at load time. + this.fclassLast = null; + this.fdataLast = null; + this.filterLast = null; + // Runtime registers this.keyRegister = undefined; this.tokenRegister = undefined; @@ -1594,6 +1595,9 @@ FilterContainer.prototype.freeze = function() { histogram('allFilters', this.categories); this.duplicateBuster = new Set(); this.filterParser.reset(); + this.fclassLast = null; + this.fdataLast = null; + this.filterLast = null; this.frozen = true; }; @@ -1624,6 +1628,17 @@ FilterContainer.prototype.factories = { /******************************************************************************/ +FilterContainer.prototype.filterFromSelfie = function(fclass, fdata) { + if ( fdata !== this.fdataLast || fclass !== this.fclassLast ) { + this.fclassLast = fclass; + this.fdataLast = fdata; + this.filterLast = this.factories[fclass].fromSelfie(fdata); + } + return this.filterLast; +}; + +/******************************************************************************/ + FilterContainer.prototype.toSelfie = function() { var categoryToSelfie = function(map) { var selfie = [], @@ -1631,15 +1646,11 @@ FilterContainer.prototype.toSelfie = function() { entry, bucket, ff, f; for (;;) { entry = iterator.next(); - if ( entry.done ) { - break; - } + if ( entry.done ) { break; } selfie.push('k2\t' + entry.value[0]); bucket = entry.value[1]; selfie.push(bucket.fid + '\t' + bucket.toSelfie()); - if ( bucket.fid !== '[]' ) { - continue; - } + if ( bucket.fid !== '[]' ) { continue; } ff = bucket.filters; for ( var i = 0, ni = ff.length; i < ni; i++ ) { f = ff[i]; @@ -1655,9 +1666,7 @@ FilterContainer.prototype.toSelfie = function() { entry; for (;;) { entry = iterator.next(); - if ( entry.done ) { - break; - } + if ( entry.done ) { break; } selfie.push('k1\t' + entry.value[0]); selfie.push(categoryToSelfie(entry.value[1])); } @@ -1692,7 +1701,7 @@ FilterContainer.prototype.fromSelfie = function(selfie) { var rawText = selfie.categories; var rawEnd = rawText.length; var lineBeg = 0, lineEnd; - var line, pos, what, factory; + var line, pos, what, data, filter; while ( lineBeg < rawEnd ) { lineEnd = rawText.indexOf('\n', lineBeg); if ( lineEnd < 0 ) { @@ -1702,27 +1711,28 @@ FilterContainer.prototype.fromSelfie = function(selfie) { lineBeg = lineEnd + 1; pos = line.indexOf('\t'); what = line.slice(0, pos); + data = line.slice(pos + 1); if ( what === 'k1' ) { - catKey = line.slice(pos + 1); + catKey = data; submap = new Map(); map.set(catKey, submap); bucket = null; continue; } if ( what === 'k2' ) { - tokenKey = line.slice(pos + 1); + tokenKey = data; bucket = null; continue; } - factory = this.factories[what]; + filter = this.filterFromSelfie(what, data); if ( bucket === null ) { - bucket = factory.fromSelfie(line.slice(pos + 1)); + bucket = filter; submap.set(tokenKey, bucket); continue; } // When token key is reused, it can't be anything // else than FilterBucket - bucket.add(factory.fromSelfie(line.slice(pos + 1))); + bucket.add(filter); } }; @@ -1946,7 +1956,7 @@ FilterContainer.prototype.compileToAtomicFilter = function(filterClass, parsed, FilterContainer.prototype.fromCompiledContent = function(lineIter) { var line, hash, token, fclass, fdata, - bucket, entry, factory, filter, + bucket, entry, filter, fieldIter = new µb.FieldIterator('\v'); while ( lineIter.eot() === false ) { @@ -1996,9 +2006,7 @@ FilterContainer.prototype.fromCompiledContent = function(lineIter) { } this.duplicateBuster.add(line); - factory = this.factories[fclass]; - - filter = factory.fromSelfie(fdata); + filter = this.filterFromSelfie(fclass, fdata); if ( entry === undefined ) { bucket.set(token, filter); continue; From 30e02a72508d752d16a0398c99ff5c06aa6ff0e4 Mon Sep 17 00:00:00 2001 From: gorhill Date: Fri, 6 Jan 2017 13:26:01 -0500 Subject: [PATCH 25/39] new revision for dev build --- platform/chromium/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index 580efaa1c00fd..0dbf79df188e1 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "uBlock Origin", - "version": "1.10.5.5", + "version": "1.10.5.6", "default_locale": "en", "description": "__MSG_extShortDesc__", From 6175a216b7974699244a5a7b1942ff3bb22dea6d Mon Sep 17 00:00:00 2001 From: gorhill Date: Sat, 7 Jan 2017 10:50:53 -0500 Subject: [PATCH 26/39] fix #2291 --- src/js/tab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/tab.js b/src/js/tab.js index af8c68a6371c7..de97ff97b815a 100644 --- a/src/js/tab.js +++ b/src/js/tab.js @@ -584,7 +584,7 @@ vAPI.tabs.onPopupUpdated = (function() { if ( openerHostname !== '' ) { // Check per-site switch first if ( µb.hnSwitches.evaluateZ('no-popups', openerHostname) ) { - if ( typeof clickedURL !== 'string' || areDifferentURLs(targetURL, clickedURL) ) { + if ( typeof clickedURL === 'string' && areDifferentURLs(targetURL, clickedURL) ) { return 'ub:no-popups: ' + µb.hnSwitches.z + ' true'; } } From 8d8905aab304cbfad257b2d8535345dcbea0db43 Mon Sep 17 00:00:00 2001 From: gorhill Date: Sat, 7 Jan 2017 10:57:16 -0500 Subject: [PATCH 27/39] dont focus newly opened logger window: FF webext complains about it and not needed anyway --- platform/chromium/vapi-background.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index 3aac3c4a1fcab..db6b0305ec29c 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -443,11 +443,7 @@ vAPI.tabs.open = function(details) { // Open in a standalone window if ( details.popup === true ) { - chrome.windows.create({ - url: details.url, - focused: details.active, - type: 'popup' - }); + chrome.windows.create({ url: details.url, type: 'popup' }); return; } From 4e747fd39e3f10bd753172fcfe6b8fc79981c877 Mon Sep 17 00:00:00 2001 From: gorhill Date: Sat, 7 Jan 2017 10:58:25 -0500 Subject: [PATCH 28/39] minor code review --- platform/firefox/vapi-background.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/firefox/vapi-background.js b/platform/firefox/vapi-background.js index a421749245340..594c83c9d6e37 100644 --- a/platform/firefox/vapi-background.js +++ b/platform/firefox/vapi-background.js @@ -2376,7 +2376,7 @@ vAPI.net.registerListeners = function() { // https://github.com/uBlockOrigin/uAssets/issues/255 // Handle chained popups. if ( openerTabId !== undefined ) { - shouldLoadPopupListenerMap.set(target.currentURI.asciiSpec, openerTabId); + shouldLoadPopupListenerMap.set(popupURL, openerTabId); shouldLoadPopupListenerMapToD = Date.now() + 10000; vAPI.tabs.onPopupCreated(popupTabId, openerTabId); return; From 2691ac95b446198b8ab5acf0e2fcf82b4f45da69 Mon Sep 17 00:00:00 2001 From: gorhill Date: Sat, 7 Jan 2017 13:02:33 -0500 Subject: [PATCH 29/39] fix fullsize popup regression (https://github.com/gorhill/uBlock/issues/2153#issuecomment-271095067) --- src/js/popup.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/js/popup.js b/src/js/popup.js index 7d5acfd7d4b40..f99747c7b4a64 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -522,7 +522,9 @@ var renderOnce = function() { return; } - lpane.style.setProperty('height', rpane.offsetHeight + 'px'); + if ( document.body.classList.contains('fullsize') === false ) { + lpane.style.setProperty('height', rpane.offsetHeight + 'px'); + } }; /******************************************************************************/ From a927725bd9389389f1a4f0f71b6c47941878d48e Mon Sep 17 00:00:00 2001 From: gorhill Date: Sat, 7 Jan 2017 17:18:22 -0500 Subject: [PATCH 30/39] code review: one getter per instance is wasteful --- src/js/static-net-filtering.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/js/static-net-filtering.js b/src/js/static-net-filtering.js index f389281bf0117..9f092072b4bac 100644 --- a/src/js/static-net-filtering.js +++ b/src/js/static-net-filtering.js @@ -1066,14 +1066,14 @@ var FilterBucket = function(a, b) { this.filters[1] = b; } } - - Object.defineProperty(this, 'rtfid', { - get: function() { - return this.f.rtfid; - } - }); }; +Object.defineProperty(FilterBucket.prototype, 'rtfid', { + get: function() { + return this.f.rtfid; + } +}); + FilterBucket.prototype.add = function(a) { this.filters.push(a); }; From a303c7800ed441c1741e203e04bdee2a945f02c0 Mon Sep 17 00:00:00 2001 From: gorhill Date: Sun, 8 Jan 2017 14:36:08 -0500 Subject: [PATCH 31/39] fix #2290 --- platform/firefox/frameModule.js | 158 ++++++++++++---------------- platform/firefox/vapi-background.js | 40 ++----- src/js/tab.js | 5 +- 3 files changed, 78 insertions(+), 125 deletions(-) diff --git a/platform/firefox/frameModule.js b/platform/firefox/frameModule.js index 66c75fa90ad3d..19997ffa9e335 100644 --- a/platform/firefox/frameModule.js +++ b/platform/firefox/frameModule.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2014-2016 The uBlock Origin authors + Copyright (C) 2014-2017 The uBlock Origin authors 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 @@ -161,7 +161,7 @@ var contentObserver = { popupMessageName: hostName + ':shouldLoadPopup', ignoredPopups: new WeakMap(), uniqueSandboxId: 1, - canE10S: Services.vc.compare(Services.appinfo.platformVersion, '44') > 0, + modernFirefox: Services.vc.compare(Services.appinfo.platformVersion, '44') > 0, get componentRegistrar() { return Components.manager.QueryInterface(Ci.nsIComponentRegistrar); @@ -189,31 +189,40 @@ var contentObserver = { register: function() { Services.obs.addObserver(this, 'document-element-inserted', true); + Services.obs.addObserver(this, 'content-document-global-created', true); - this.componentRegistrar.registerFactory( - this.classID, - this.classDescription, - this.contractID, - this - ); - this.categoryManager.addCategoryEntry( - 'content-policy', - this.contractID, - this.contractID, - false, - true - ); + // https://bugzilla.mozilla.org/show_bug.cgi?id=1232354 + // For modern versions of Firefox, the frameId/parentFrameId + // information can be found in channel.loadInfo of the HTTP observer. + if ( this.modernFirefox !== true ) { + this.componentRegistrar.registerFactory( + this.classID, + this.classDescription, + this.contractID, + this + ); + this.categoryManager.addCategoryEntry( + 'content-policy', + this.contractID, + this.contractID, + false, + true + ); + } }, unregister: function() { Services.obs.removeObserver(this, 'document-element-inserted'); - - this.componentRegistrar.unregisterFactory(this.classID, this); - this.categoryManager.deleteCategoryEntry( - 'content-policy', - this.contractID, - false - ); + Services.obs.removeObserver(this, 'content-document-global-created'); + + if ( this.modernFirefox !== true ) { + this.componentRegistrar.unregisterFactory(this.classID, this); + this.categoryManager.deleteCategoryEntry( + 'content-policy', + this.contractID, + false + ); + } }, getFrameId: function(win) { @@ -223,48 +232,6 @@ var contentObserver = { .outerWindowID; }, - handlePopup: function(location, origin, context) { - let openeeContext = context.contentWindow || context; - if ( - typeof openeeContext.opener !== 'object' || - openeeContext.opener === null || - openeeContext.opener === context || - this.ignoredPopups.has(openeeContext) - ) { - return; - } - // https://github.com/gorhill/uBlock/issues/452 - // Use location of top window, not that of a frame, as this - // would cause tab id lookup (necessary for popup blocking) to - // always fail. - // https://github.com/gorhill/uBlock/issues/1305 - // Opener could be a dead object, using it would cause a throw. - // Repro case: - // - Open http://delishows.to/show/chicago-med/season/1/episode/6 - // - Click anywhere in the background - let openerURL = null; - try { - let opener = openeeContext.opener.top || openeeContext.opener; - openerURL = opener.location && opener.location.href; - } catch(ex) { - } - // If no valid opener URL found, use the origin URL. - if ( openerURL === null ) { - openerURL = origin.asciiSpec; - } - let messageManager = getMessageManager(openeeContext); - if ( messageManager === null ) { - return; - } - if ( typeof messageManager.sendRpcMessage === 'function' ) { - // https://bugzil.la/1092216 - messageManager.sendRpcMessage(this.popupMessageName, openerURL); - } else { - // Compatibility for older versions - messageManager.sendSyncMessage(this.popupMessageName, openerURL); - } - }, - // https://bugzil.la/612921 shouldLoad: function(type, location, origin, context) { // For whatever reason, sometimes the global scope is completely @@ -278,17 +245,6 @@ var contentObserver = { return this.ACCEPT; } - if ( type === this.MAIN_FRAME ) { - this.handlePopup(location, origin, context); - } - - // https://bugzilla.mozilla.org/show_bug.cgi?id=1232354 - // For modern versions of Firefox, the frameId/parentFrameId - // information can be found in channel.loadInfo of the HTTP observer. - if ( this.canE10S ) { - return this.ACCEPT; - } - if ( !location.schemeIs('http') && !location.schemeIs('https') ) { return this.ACCEPT; } @@ -504,17 +460,44 @@ var contentObserver = { }, ignorePopup: function(e) { - if ( e.isTrusted === false ) { - return; - } - + if ( e.isTrusted === false ) { return; } let contObs = contentObserver; contObs.ignoredPopups.set(this, true); this.removeEventListener('keydown', contObs.ignorePopup, true); this.removeEventListener('mousedown', contObs.ignorePopup, true); }, + lookupPopupOpenerURL: function(popup) { + var opener, openerURL; + for (;;) { + opener = popup.opener; + if ( !opener ) { break; } + if ( opener.top ) { opener = opener.top; } + if ( opener === popup ) { break; } + if ( !opener.location ) { break; } + if ( !this.reGoodPopupURLs.test(opener.location.href) ) { break; } + openerURL = opener.location.href; + // https://github.com/uBlockOrigin/uAssets/issues/255 + // - Mind chained about:blank popups. + if ( openerURL !== 'about:blank' ) { break; } + popup = opener; + } + return openerURL; + }, + reGoodPopupURLs: /^(?:about:blank|blob:|data:|https?:|javascript:)/, + + observe: function(subject, topic) { + // https://github.com/gorhill/uBlock/issues/2290 + if ( topic === 'content-document-global-created' ) { + if ( subject !== subject.top ) { return; } + if ( this.ignoredPopups.has(subject) ) { return; } + let openerURL = this.lookupPopupOpenerURL(subject); + if ( !openerURL ) { return; } + let messager = getMessageManager(subject); + if ( !messager ) { return; } + messager.sendAsyncMessage(this.popupMessageName, openerURL); + return; + } - observe: function(doc) { // For whatever reason, sometimes the global scope is completely // uninitialized at this point. Repro steps: // - Launch FF with uBlock enabled @@ -522,14 +505,11 @@ var contentObserver = { // - Enable uBlock // - Services and all other global variables are undefined // Hopefully will eventually understand why this happens. - if ( Services === undefined ) { - return; - } + if ( Services === undefined ) { return; } + let doc = subject; let win = doc.defaultView || null; - if ( win === null ) { - return; - } + if ( win === null ) { return; } if ( win.opener && this.ignoredPopups.has(win) === false ) { win.addEventListener('keydown', this.ignorePopup, true); @@ -543,17 +523,13 @@ var contentObserver = { // TODO: We may have to exclude more types, for now let's be // conservative and focus only on the one issue reported, i.e. let's // not test against 'text/html'. - if ( doc.contentType.startsWith('image/') ) { - return; - } + if ( doc.contentType.startsWith('image/') ) { return; } let loc = win.location; - if ( loc.protocol !== 'http:' && loc.protocol !== 'https:' && loc.protocol !== 'file:' ) { if ( loc.protocol === 'chrome:' && loc.host === hostName ) { this.initContentScripts(win); } - // What about data: and about:blank? return; } diff --git a/platform/firefox/vapi-background.js b/platform/firefox/vapi-background.js index 594c83c9d6e37..9024583dec869 100644 --- a/platform/firefox/vapi-background.js +++ b/platform/firefox/vapi-background.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2014-2106 The uBlock Origin authors + Copyright (C) 2014-2107 The uBlock Origin authors 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 @@ -2361,26 +2361,14 @@ vAPI.net.registerListeners = function() { null; } - var shouldLoadPopupListenerMessageName = location.host + ':shouldLoadPopup', - shouldLoadPopupListenerMap = new Map(), - shouldLoadPopupListenerMapToD = 0; - var shouldLoadPopupListener = function(openerURL, target) { - var popupTabId = tabWatcher.tabIdFromTarget(target), - popupURL = target.currentURI && target.currentURI.asciiSpec || '', + var shouldLoadPopupListenerMessageName = location.host + ':shouldLoadPopup'; + var shouldLoadPopupListener = function(e) { + if ( typeof vAPI.tabs.onPopupCreated !== 'function' ) { return; } + var target = e.target, + openerURL = e.data, + popupTabId = tabWatcher.tabIdFromTarget(target), openerTabId, uri; - if ( shouldLoadPopupListenerMapToD > Date.now() ) { - openerTabId = shouldLoadPopupListenerMap.get(popupURL); - } - - // https://github.com/uBlockOrigin/uAssets/issues/255 - // Handle chained popups. - if ( openerTabId !== undefined ) { - shouldLoadPopupListenerMap.set(popupURL, openerTabId); - shouldLoadPopupListenerMapToD = Date.now() + 10000; - vAPI.tabs.onPopupCreated(popupTabId, openerTabId); - return; - } for ( var browser of tabWatcher.browsers() ) { uri = browser.currentURI; @@ -2398,24 +2386,14 @@ vAPI.net.registerListeners = function() { openerTabId = tabWatcher.tabIdFromTarget(browser); if ( openerTabId === popupTabId ) { continue; } - shouldLoadPopupListenerMap = new Map(); - shouldLoadPopupListenerMapToD = Date.now() + 10000; - shouldLoadPopupListenerMap.set(popupURL, openerTabId); vAPI.tabs.onPopupCreated(popupTabId, openerTabId); break; } }; - var shouldLoadPopupListenerAsync = function(e) { - if ( typeof vAPI.tabs.onPopupCreated !== 'function' ) { - return; - } - // We are handling a synchronous message: do not block. - vAPI.setTimeout(shouldLoadPopupListener.bind(null, e.data, e.target), 1); - }; vAPI.messaging.globalMessageManager.addMessageListener( shouldLoadPopupListenerMessageName, - shouldLoadPopupListenerAsync + shouldLoadPopupListener ); var shouldLoadListenerMessageName = location.host + ':shouldLoad'; @@ -2501,7 +2479,7 @@ vAPI.net.registerListeners = function() { cleanupTasks.push(function() { vAPI.messaging.globalMessageManager.removeMessageListener( shouldLoadPopupListenerMessageName, - shouldLoadPopupListenerAsync + shouldLoadPopupListener ); vAPI.messaging.globalMessageManager.removeMessageListener( diff --git a/src/js/tab.js b/src/js/tab.js index de97ff97b815a..4ddbc6dda25e4 100644 --- a/src/js/tab.js +++ b/src/js/tab.js @@ -174,10 +174,9 @@ housekeep itself. vAPI.tabs.onPopupCreated = function(targetTabId, openerTabId) { var popup = popupCandidates[targetTabId]; - if ( popup !== undefined ) { - return; + if ( popup === undefined ) { + popupCandidates[targetTabId] = new PopupCandidate(targetTabId, openerTabId); } - popupCandidates[targetTabId] = new PopupCandidate(targetTabId, openerTabId); popupCandidateTest(targetTabId); }; From eca33ea65968f7f69a8c234b4708c8387908a149 Mon Sep 17 00:00:00 2001 From: gorhill Date: Sun, 8 Jan 2017 14:37:44 -0500 Subject: [PATCH 32/39] new revision for dev build --- platform/chromium/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index 0dbf79df188e1..5622b90a45a87 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "uBlock Origin", - "version": "1.10.5.6", + "version": "1.10.5.7", "default_locale": "en", "description": "__MSG_extShortDesc__", From 3b0d3e3330b9455c11b89e3f499bab8bf25d6510 Mon Sep 17 00:00:00 2001 From: gorhill Date: Sun, 8 Jan 2017 17:52:38 -0500 Subject: [PATCH 33/39] code review: saner way to find a popup's opener tab id --- platform/firefox/frameModule.js | 42 +++++++++++++-------- platform/firefox/vapi-background.js | 57 ++++++++++++++++++----------- 2 files changed, 61 insertions(+), 38 deletions(-) diff --git a/platform/firefox/frameModule.js b/platform/firefox/frameModule.js index 19997ffa9e335..5777c4703df68 100644 --- a/platform/firefox/frameModule.js +++ b/platform/firefox/frameModule.js @@ -160,6 +160,7 @@ var contentObserver = { cpMessageName: hostName + ':shouldLoad', popupMessageName: hostName + ':shouldLoadPopup', ignoredPopups: new WeakMap(), + uniquePopupEventId: 1, uniqueSandboxId: 1, modernFirefox: Services.vc.compare(Services.appinfo.platformVersion, '44') > 0, @@ -466,35 +467,44 @@ var contentObserver = { this.removeEventListener('keydown', contObs.ignorePopup, true); this.removeEventListener('mousedown', contObs.ignorePopup, true); }, - lookupPopupOpenerURL: function(popup) { - var opener, openerURL; + lookupPopupOpener: function(popup) { for (;;) { - opener = popup.opener; - if ( !opener ) { break; } + let opener = popup.opener; + if ( !opener ) { return; } if ( opener.top ) { opener = opener.top; } - if ( opener === popup ) { break; } - if ( !opener.location ) { break; } - if ( !this.reGoodPopupURLs.test(opener.location.href) ) { break; } - openerURL = opener.location.href; + if ( opener === popup ) { return; } + if ( !opener.location ) { return; } + if ( this.reValidPopups.test(opener.location.protocol) ) { + return opener; + } // https://github.com/uBlockOrigin/uAssets/issues/255 // - Mind chained about:blank popups. - if ( openerURL !== 'about:blank' ) { break; } + if ( opener.location.href !== 'about:blank' ) { return; } popup = opener; } - return openerURL; }, - reGoodPopupURLs: /^(?:about:blank|blob:|data:|https?:|javascript:)/, + reValidPopups: /^(?:blob|data|https?|javascript):/, observe: function(subject, topic) { // https://github.com/gorhill/uBlock/issues/2290 if ( topic === 'content-document-global-created' ) { if ( subject !== subject.top ) { return; } if ( this.ignoredPopups.has(subject) ) { return; } - let openerURL = this.lookupPopupOpenerURL(subject); - if ( !openerURL ) { return; } - let messager = getMessageManager(subject); - if ( !messager ) { return; } - messager.sendAsyncMessage(this.popupMessageName, openerURL); + let opener = this.lookupPopupOpener(subject); + if ( !opener ) { return; } + let popupMessager = getMessageManager(subject); + if ( !popupMessager ) { return; } + let openerMessager = getMessageManager(opener); + if ( !openerMessager ) { return; } + popupMessager.sendAsyncMessage(this.popupMessageName, { + id: this.uniquePopupEventId, + popup: true + }); + openerMessager.sendAsyncMessage(this.popupMessageName, { + id: this.uniquePopupEventId, + opener: true + }); + this.uniquePopupEventId += 1; return; } diff --git a/platform/firefox/vapi-background.js b/platform/firefox/vapi-background.js index 9024583dec869..742a99258f2ff 100644 --- a/platform/firefox/vapi-background.js +++ b/platform/firefox/vapi-background.js @@ -2362,32 +2362,45 @@ vAPI.net.registerListeners = function() { } var shouldLoadPopupListenerMessageName = location.host + ':shouldLoadPopup'; + var shouldLoadPopupListenerEntries = []; var shouldLoadPopupListener = function(e) { if ( typeof vAPI.tabs.onPopupCreated !== 'function' ) { return; } - var target = e.target, - openerURL = e.data, - popupTabId = tabWatcher.tabIdFromTarget(target), - openerTabId, - uri; - - for ( var browser of tabWatcher.browsers() ) { - uri = browser.currentURI; - - // Probably isn't the best method to identify the source tab. - - // https://github.com/gorhill/uBlock/issues/450 - // Skip entry if no valid URI available. - // Apparently URI can be undefined under some circumstances: I - // believe this may have to do with those very temporary - // browser objects created when opening a new tab, i.e. related - // to https://github.com/gorhill/uBlock/issues/212 - if ( !uri || uri.spec !== openerURL ) { continue; } - openerTabId = tabWatcher.tabIdFromTarget(browser); - if ( openerTabId === popupTabId ) { continue; } + var target = e.target, + data = e.data, + now = Date.now(), + entries = shouldLoadPopupListenerEntries, + entry; - vAPI.tabs.onPopupCreated(popupTabId, openerTabId); - break; + var i = entries.length; + while ( i-- ) { + entry = entries[i]; + if ( entry.id === data.id ) { + entries.splice(i, 1); + break; + } + if ( entry.expire <= now ) { + entries.splice(i, 1); + } + entry = undefined; + } + if ( !entry ) { + entry = { + id: data.id, + popupTabId: undefined, + openerTabId: undefined, + expire: now + 10000 + }; + entries.push(entry); + } + var tabId = tabWatcher.tabIdFromTarget(target); + if ( data.popup ) { + entry.popupTabId = tabId; + } else /* if ( data.opener ) */ { + entry.openerTabId = tabId; + } + if ( entry.popupTabId && entry.openerTabId ) { + vAPI.tabs.onPopupCreated(entry.popupTabId, entry.openerTabId); } }; From 693758aacbb4904507d6d3d569b47839682b9a62 Mon Sep 17 00:00:00 2001 From: gorhill Date: Sun, 8 Jan 2017 18:03:20 -0500 Subject: [PATCH 34/39] new revision for dev build --- platform/chromium/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index 5622b90a45a87..1ddf9032f4a96 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "uBlock Origin", - "version": "1.10.5.7", + "version": "1.10.5.8", "default_locale": "en", "description": "__MSG_extShortDesc__", From 9c4fbeb1fc1ee0258807c0f1e584461326c8045e Mon Sep 17 00:00:00 2001 From: gorhill Date: Mon, 9 Jan 2017 08:56:42 -0500 Subject: [PATCH 35/39] fix #2294 --- src/js/static-net-filtering.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/js/static-net-filtering.js b/src/js/static-net-filtering.js index 9f092072b4bac..404e2b4bd252c 100644 --- a/src/js/static-net-filtering.js +++ b/src/js/static-net-filtering.js @@ -1134,6 +1134,7 @@ FilterBucket.fromSelfie = function() { var FilterParser = function() { this.cantWebsocket = vAPI.cantWebsocket; + this.reBadDomainOptChars = /[*+?^${}()[\]\\]/; this.reHostnameRule1 = /^[0-9a-z][0-9a-z.-]*[0-9a-z]$/i; this.reHostnameRule2 = /^\**[0-9a-z][0-9a-z.-]*[0-9a-z]\^?$/i; this.reCleanupHostnameRule2 = /^\**|\^$/g; @@ -1285,8 +1286,15 @@ FilterParser.prototype.parseOptions = function(s) { } continue; } + // https://github.com/gorhill/uBlock/issues/2294 + // Detect and discard filter if domain option contains nonsensical + // characters. if ( opt.startsWith('domain=') ) { this.domainOpt = opt.slice(7); + if ( this.reBadDomainOptChars.test(this.domainOpt) ) { + this.unsupported = true; + break; + } continue; } if ( opt === 'important' ) { @@ -1824,7 +1832,7 @@ FilterContainer.prototype.compile = function(raw, out) { // Ignore filters with unsupported options if ( parsed.unsupported ) { - //console.log('static-net-filtering.js > FilterContainer.add(): unsupported filter "%s"', raw); + µb.logger.writeOne('', 'error', 'Network filtering – invalid filter: ' + raw); return false; } From b21e765f5c2a5188324e340d6df64cfda07f14ef Mon Sep 17 00:00:00 2001 From: gorhill Date: Mon, 9 Jan 2017 09:16:37 -0500 Subject: [PATCH 36/39] minor code review --- platform/firefox/frameModule.js | 67 ++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/platform/firefox/frameModule.js b/platform/firefox/frameModule.js index 5777c4703df68..03ef0bebdf6fe 100644 --- a/platform/firefox/frameModule.js +++ b/platform/firefox/frameModule.js @@ -460,6 +460,21 @@ var contentObserver = { return sandbox; }, + injectOtherContentScripts: function(doc, sandbox) { + let docReady = (e) => { + let doc = e.target; + doc.removeEventListener(e.type, docReady, true); + if ( doc.querySelector('a[href^="abp:"],a[href^="https://subscribe.adblockplus.org/?"]') ) { + Services.scriptloader.loadSubScript(this.contentBaseURI + 'scriptlets/subscriber.js', sandbox); + } + }; + if ( doc.readyState === 'loading') { + doc.addEventListener('DOMContentLoaded', docReady, true); + } else { + docReady({ target: doc, type: 'DOMContentLoaded' }); + } + }, + ignorePopup: function(e) { if ( e.isTrusted === false ) { return; } let contObs = contentObserver; @@ -467,6 +482,7 @@ var contentObserver = { this.removeEventListener('keydown', contObs.ignorePopup, true); this.removeEventListener('mousedown', contObs.ignorePopup, true); }, + lookupPopupOpener: function(popup) { for (;;) { let opener = popup.opener; @@ -483,15 +499,28 @@ var contentObserver = { popup = opener; } }, + reValidPopups: /^(?:blob|data|https?|javascript):/, + reMustInjectScript: /^(?:file|https?):/, observe: function(subject, topic) { + // For whatever reason, sometimes the global scope is completely + // uninitialized at this point. Repro steps: + // - Launch FF with uBlock enabled + // - Disable uBlock + // - Enable uBlock + // - Services and all other global variables are undefined + // Hopefully will eventually understand why this happens. + if ( Services === undefined ) { return; } + // https://github.com/gorhill/uBlock/issues/2290 if ( topic === 'content-document-global-created' ) { - if ( subject !== subject.top ) { return; } + if ( subject !== subject.top || !subject.opener ) { return; } if ( this.ignoredPopups.has(subject) ) { return; } let opener = this.lookupPopupOpener(subject); if ( !opener ) { return; } + subject.addEventListener('keydown', this.ignorePopup, true); + subject.addEventListener('mousedown', this.ignorePopup, true); let popupMessager = getMessageManager(subject); if ( !popupMessager ) { return; } let openerMessager = getMessageManager(opener); @@ -508,24 +537,12 @@ var contentObserver = { return; } - // For whatever reason, sometimes the global scope is completely - // uninitialized at this point. Repro steps: - // - Launch FF with uBlock enabled - // - Disable uBlock - // - Enable uBlock - // - Services and all other global variables are undefined - // Hopefully will eventually understand why this happens. - if ( Services === undefined ) { return; } + // topic === 'document-element-inserted' let doc = subject; let win = doc.defaultView || null; if ( win === null ) { return; } - if ( win.opener && this.ignoredPopups.has(win) === false ) { - win.addEventListener('keydown', this.ignorePopup, true); - win.addEventListener('mousedown', this.ignorePopup, true); - } - // https://github.com/gorhill/uBlock/issues/260 // https://developer.mozilla.org/en-US/docs/Web/API/Document/contentType // "Non-standard, only supported by Gecko. To be used in @@ -536,7 +553,7 @@ var contentObserver = { if ( doc.contentType.startsWith('image/') ) { return; } let loc = win.location; - if ( loc.protocol !== 'http:' && loc.protocol !== 'https:' && loc.protocol !== 'file:' ) { + if ( this.reMustInjectScript.test(loc.protocol) === false ) { if ( loc.protocol === 'chrome:' && loc.host === hostName ) { this.initContentScripts(win); } @@ -544,9 +561,9 @@ var contentObserver = { return; } + // Content scripts injection. let lss = Services.scriptloader.loadSubScript; let sandbox = this.initContentScripts(win, true); - try { lss(this.contentBaseURI + 'vapi-client.js', sandbox); lss(this.contentBaseURI + 'contentscript.js', sandbox); @@ -554,20 +571,10 @@ var contentObserver = { //console.exception(ex.msg, ex.stack); return; } - - let docReady = (e) => { - let doc = e.target; - doc.removeEventListener(e.type, docReady, true); - - if ( doc.querySelector('a[href^="abp:"],a[href^="https://subscribe.adblockplus.org/?"]') ) { - lss(this.contentBaseURI + 'scriptlets/subscriber.js', sandbox); - } - }; - - if ( doc.readyState === 'loading') { - doc.addEventListener('DOMContentLoaded', docReady, true); - } else { - docReady({ target: doc, type: 'DOMContentLoaded' }); + // The remaining scripts are worth injecting only on a top-level window + // and at document_idle time. + if ( win === win.top ) { + this.injectOtherContentScripts(doc, sandbox); } } }; From 7f4863fbcc35705107ac8e2144fa5b5636f1cc04 Mon Sep 17 00:00:00 2001 From: gorhill Date: Mon, 9 Jan 2017 09:16:48 -0500 Subject: [PATCH 37/39] new revision for dev build --- platform/chromium/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index 1ddf9032f4a96..e63d49aad9fb8 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "uBlock Origin", - "version": "1.10.5.8", + "version": "1.10.5.9", "default_locale": "en", "description": "__MSG_extShortDesc__", From 2b1ab2234ff8c85803f55ec84941e6cf4c293db1 Mon Sep 17 00:00:00 2001 From: gorhill Date: Mon, 9 Jan 2017 09:53:57 -0500 Subject: [PATCH 38/39] re #2294: mind unicode in "domain=" option + update URL of Adguard lists --- assets/ublock/filter-lists.json | 12 +++++++++++ src/js/static-net-filtering.js | 37 ++++++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/assets/ublock/filter-lists.json b/assets/ublock/filter-lists.json index be69f11dfd704..27cd938eb045e 100644 --- a/assets/ublock/filter-lists.json +++ b/assets/ublock/filter-lists.json @@ -20,6 +20,12 @@ "supportURL": "http://noads.it/" }, "https://adguard.com/en/filter-rules.html?id=1": { + "off": true, + "title": "RUS: Adguard Russian Filter (obsolete, will be removed)", + "group": "regions", + "supportURL": "http://forum.adguard.com/forumdisplay.php?69-%D0%A4%D0%B8%D0%BB%D1%8C%D1%82%D1%80%D1%8B-Adguard" + }, + "https://filters.adtidy.org/extension/chromium/filters/1.txt": { "off": true, "title": "RUS: Adguard Russian Filter", "group": "regions", @@ -328,6 +334,12 @@ "supportURL": "https://forums.lanik.us/" }, "https://adguard.com/filter-rules.html?id=13": { + "off": true, + "title": "TUR: Adguard Turkish Filter (obsolete, will be removed)", + "group": "regions", + "supportURL": "http://forum.adguard.com/forumdisplay.php?51-Filter-Rules" + }, + "http://filters.adtidy.org/extension/chromium/filters/13.txt": { "off": true, "title": "TUR: Adguard Turkish Filter", "group": "regions", diff --git a/src/js/static-net-filtering.js b/src/js/static-net-filtering.js index 404e2b4bd252c..dfa6f3a7a62ad 100644 --- a/src/js/static-net-filtering.js +++ b/src/js/static-net-filtering.js @@ -1211,7 +1211,7 @@ FilterParser.prototype.bitFromType = function(type) { // https://github.com/chrisaljoudi/uBlock/issues/589 // Be ready to handle multiple negated types -FilterParser.prototype.parseOptType = function(raw, not) { +FilterParser.prototype.parseTypeOption = function(raw, not) { var typeBit = this.bitFromType(this.toNormalizedType[raw]); if ( !not ) { @@ -1231,7 +1231,7 @@ FilterParser.prototype.parseOptType = function(raw, not) { /******************************************************************************/ -FilterParser.prototype.parseOptParty = function(firstParty, not) { +FilterParser.prototype.parsePartyOption = function(firstParty, not) { if ( firstParty ) { not = !not; } @@ -1244,6 +1244,23 @@ FilterParser.prototype.parseOptParty = function(firstParty, not) { /******************************************************************************/ +FilterParser.prototype.parseDomainOption = function(s) { + if ( this.reHasUnicode.test(s) ) { + var hostnames = s.split('|'), + i = hostnames.length; + while ( i-- ) { + hostnames[i] = punycode.toASCII(hostnames[i]); + } + s = hostnames.join('|'); + } + if ( this.reBadDomainOptChars.test(s) ) { + return ''; + } + return s; +}; + +/******************************************************************************/ + FilterParser.prototype.parseOptions = function(s) { this.fopts = s; var opts = s.split(','); @@ -1255,7 +1272,7 @@ FilterParser.prototype.parseOptions = function(s) { opt = opt.slice(1); } if ( opt === 'third-party' ) { - this.parseOptParty(false, not); + this.parsePartyOption(false, not); continue; } // https://issues.adblockplus.org/ticket/616 @@ -1263,7 +1280,7 @@ FilterParser.prototype.parseOptions = function(s) { // adding support for the new keyword. if ( opt === 'elemhide' || opt === 'generichide' ) { if ( not === false ) { - this.parseOptType('generichide', false); + this.parseTypeOption('generichide', false); continue; } this.unsupported = true; @@ -1271,18 +1288,18 @@ FilterParser.prototype.parseOptions = function(s) { } if ( opt === 'document' ) { if ( this.action === BlockAction ) { - this.parseOptType('document', not); + this.parseTypeOption('document', not); continue; } this.unsupported = true; break; } if ( this.toNormalizedType.hasOwnProperty(opt) ) { - this.parseOptType(opt, not); + this.parseTypeOption(opt, not); // Due to ABP categorizing `websocket` requests as `other`, we need // to add `websocket` for when `other` is used. if ( opt === 'other' ) { - this.parseOptType('websocket', not); + this.parseTypeOption('websocket', not); } continue; } @@ -1290,8 +1307,8 @@ FilterParser.prototype.parseOptions = function(s) { // Detect and discard filter if domain option contains nonsensical // characters. if ( opt.startsWith('domain=') ) { - this.domainOpt = opt.slice(7); - if ( this.reBadDomainOptChars.test(this.domainOpt) ) { + this.domainOpt = this.parseDomainOption(opt.slice(7)); + if ( this.domainOpt === '' ) { this.unsupported = true; break; } @@ -1302,7 +1319,7 @@ FilterParser.prototype.parseOptions = function(s) { continue; } if ( opt === 'first-party' ) { - this.parseOptParty(true, not); + this.parsePartyOption(true, not); continue; } if ( opt.startsWith('redirect=') ) { From 257dd27e5dfe79b57a532f448f4668055dc339dd Mon Sep 17 00:00:00 2001 From: gorhill Date: Mon, 9 Jan 2017 10:00:13 -0500 Subject: [PATCH 39/39] oops, always use https when available --- assets/ublock/filter-lists.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/ublock/filter-lists.json b/assets/ublock/filter-lists.json index 27cd938eb045e..cd3877f8e2abb 100644 --- a/assets/ublock/filter-lists.json +++ b/assets/ublock/filter-lists.json @@ -23,13 +23,13 @@ "off": true, "title": "RUS: Adguard Russian Filter (obsolete, will be removed)", "group": "regions", - "supportURL": "http://forum.adguard.com/forumdisplay.php?69-%D0%A4%D0%B8%D0%BB%D1%8C%D1%82%D1%80%D1%8B-Adguard" + "supportURL": "https://forum.adguard.com/forumdisplay.php?69-%D0%A4%D0%B8%D0%BB%D1%8C%D1%82%D1%80%D1%8B-Adguard" }, "https://filters.adtidy.org/extension/chromium/filters/1.txt": { "off": true, "title": "RUS: Adguard Russian Filter", "group": "regions", - "supportURL": "http://forum.adguard.com/forumdisplay.php?69-%D0%A4%D0%B8%D0%BB%D1%8C%D1%82%D1%80%D1%8B-Adguard" + "supportURL": "https://forum.adguard.com/forumdisplay.php?69-%D0%A4%D0%B8%D0%BB%D1%8C%D1%82%D1%80%D1%8B-Adguard" }, "https://easylist-downloads.adblockplus.org/advblock.txt": { "off": true, @@ -339,12 +339,12 @@ "group": "regions", "supportURL": "http://forum.adguard.com/forumdisplay.php?51-Filter-Rules" }, - "http://filters.adtidy.org/extension/chromium/filters/13.txt": { + "https://filters.adtidy.org/extension/chromium/filters/13.txt": { "off": true, "title": "TUR: Adguard Turkish Filter", "group": "regions", "lang": "tr", - "supportURL": "http://forum.adguard.com/forumdisplay.php?51-Filter-Rules" + "supportURL": "https://forum.adguard.com/forumdisplay.php?51-Filter-Rules" }, "https://www.fanboy.co.nz/fanboy-vietnam.txt": { "off": true,