diff --git a/src/js/background.js b/src/js/background.js index c89349dee..baa3a8e1e 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -138,7 +138,7 @@ var µBlock = (function() { // jshint ignore:line // read-only systemSettings: { - compiledMagic: 1, + compiledMagic: 2, selfieMagic: 1 }, diff --git a/src/js/contentscript.js b/src/js/contentscript.js index eb97b75fa..b0528e45f 100644 --- a/src/js/contentscript.js +++ b/src/js/contentscript.js @@ -374,16 +374,16 @@ vAPI.DOMFilterer = (function() { // 'P' stands for 'Procedural' - var PSelectorHasTextTask = function(task) { - var arg0 = task[1], arg1; + const PSelectorHasTextTask = function(task) { + let arg0 = task[1], arg1; if ( Array.isArray(task[1]) ) { arg1 = arg0[1]; arg0 = arg0[0]; } this.needle = new RegExp(arg0, arg1); }; PSelectorHasTextTask.prototype.exec = function(input) { - var output = []; - for ( var node of input ) { + const output = []; + for ( const node of input ) { if ( this.needle.test(node.textContent) ) { output.push(node); } @@ -391,13 +391,13 @@ vAPI.DOMFilterer = (function() { return output; }; - var PSelectorIfTask = function(task) { + const PSelectorIfTask = function(task) { this.pselector = new PSelector(task[1]); }; PSelectorIfTask.prototype.target = true; PSelectorIfTask.prototype.exec = function(input) { - var output = []; - for ( var node of input ) { + const output = []; + for ( const node of input ) { if ( this.pselector.test(node) === this.target ) { output.push(node); } @@ -405,16 +405,16 @@ vAPI.DOMFilterer = (function() { return output; }; - var PSelectorIfNotTask = function(task) { + const PSelectorIfNotTask = function(task) { PSelectorIfTask.call(this, task); this.target = false; }; PSelectorIfNotTask.prototype = Object.create(PSelectorIfTask.prototype); PSelectorIfNotTask.prototype.constructor = PSelectorIfNotTask; - var PSelectorMatchesCSSTask = function(task) { + const PSelectorMatchesCSSTask = function(task) { this.name = task[1].name; - var arg0 = task[1].value, arg1; + let arg0 = task[1].value, arg1; if ( Array.isArray(arg0) ) { arg1 = arg0[1]; arg0 = arg0[0]; } @@ -422,9 +422,9 @@ vAPI.DOMFilterer = (function() { }; PSelectorMatchesCSSTask.prototype.pseudo = null; PSelectorMatchesCSSTask.prototype.exec = function(input) { - var output = [], style; - for ( var node of input ) { - style = window.getComputedStyle(node, this.pseudo); + const output = []; + for ( const node of input ) { + const style = window.getComputedStyle(node, this.pseudo); if ( style === null ) { return null; } /* FF */ if ( this.value.test(style[this.name]) ) { output.push(node); @@ -433,35 +433,35 @@ vAPI.DOMFilterer = (function() { return output; }; - var PSelectorMatchesCSSAfterTask = function(task) { + const PSelectorMatchesCSSAfterTask = function(task) { PSelectorMatchesCSSTask.call(this, task); this.pseudo = ':after'; }; PSelectorMatchesCSSAfterTask.prototype = Object.create(PSelectorMatchesCSSTask.prototype); PSelectorMatchesCSSAfterTask.prototype.constructor = PSelectorMatchesCSSAfterTask; - var PSelectorMatchesCSSBeforeTask = function(task) { + const PSelectorMatchesCSSBeforeTask = function(task) { PSelectorMatchesCSSTask.call(this, task); this.pseudo = ':before'; }; PSelectorMatchesCSSBeforeTask.prototype = Object.create(PSelectorMatchesCSSTask.prototype); PSelectorMatchesCSSBeforeTask.prototype.constructor = PSelectorMatchesCSSBeforeTask; - var PSelectorXpathTask = function(task) { + const PSelectorXpathTask = function(task) { this.xpe = document.createExpression(task[1], null); this.xpr = null; }; PSelectorXpathTask.prototype.exec = function(input) { - var output = [], j; - for ( var node of input ) { + const output = []; + for ( const node of input ) { this.xpr = this.xpe.evaluate( node, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, this.xpr ); - j = this.xpr.snapshotLength; + let j = this.xpr.snapshotLength; while ( j-- ) { - node = this.xpr.snapshotItem(j); + const node = this.xpr.snapshotItem(j); if ( node.nodeType === 1 ) { output.push(node); } @@ -470,7 +470,7 @@ vAPI.DOMFilterer = (function() { return output; }; - var PSelector = function(o) { + const PSelector = function(o) { if ( PSelector.prototype.operatorToTaskMap === undefined ) { PSelector.prototype.operatorToTaskMap = new Map([ [ ':has', PSelectorIfTask ], @@ -480,6 +480,7 @@ vAPI.DOMFilterer = (function() { [ ':matches-css', PSelectorMatchesCSSTask ], [ ':matches-css-after', PSelectorMatchesCSSAfterTask ], [ ':matches-css-before', PSelectorMatchesCSSBeforeTask ], + [ ':not', PSelectorIfNotTask ], [ ':xpath', PSelectorXpathTask ] ]); } @@ -489,33 +490,35 @@ vAPI.DOMFilterer = (function() { this.lastAllowanceTime = 0; this.selector = o.selector; this.tasks = []; - var tasks = o.tasks; + const tasks = o.tasks; if ( !tasks ) { return; } - for ( var task of tasks ) { + for ( const task of tasks ) { this.tasks.push(new (this.operatorToTaskMap.get(task[0]))(task)); } }; PSelector.prototype.operatorToTaskMap = undefined; PSelector.prototype.prime = function(input) { - var root = input || document; + const root = input || document; if ( this.selector !== '' ) { return root.querySelectorAll(this.selector); } return [ root ]; }; PSelector.prototype.exec = function(input) { - var nodes = this.prime(input); - for ( var task of this.tasks ) { + let nodes = this.prime(input); + for ( const task of this.tasks ) { if ( nodes.length === 0 ) { break; } nodes = task.exec(nodes); } return nodes; }; PSelector.prototype.test = function(input) { - var nodes = this.prime(input), AA = [ null ], aa; - for ( var node of nodes ) { - AA[0] = node; aa = AA; - for ( var task of this.tasks ) { + const nodes = this.prime(input); + const AA = [ null ]; + for ( const node of nodes ) { + AA[0] = node; + let aa = AA; + for ( const task of this.tasks ) { aa = task.exec(aa); if ( aa.length === 0 ) { break; } } @@ -524,24 +527,23 @@ vAPI.DOMFilterer = (function() { return false; }; - var DOMProceduralFilterer = function(domFilterer) { + const DOMProceduralFilterer = function(domFilterer) { this.domFilterer = domFilterer; this.domIsReady = false; this.domIsWatched = false; - this.addedSelectors = new Map(); - this.addedNodes = false; - this.removedNodes = false; + this.mustApplySelectors = false; this.selectors = new Map(); + this.hiddenNodes = new Set(); }; DOMProceduralFilterer.prototype = { addProceduralSelectors: function(aa) { - var raw, o, pselector, - mustCommit = this.domIsWatched; - for ( var i = 0, n = aa.length; i < n; i++ ) { - raw = aa[i]; - o = JSON.parse(raw); + const addedSelectors = []; + let mustCommit = this.domIsWatched; + for ( let i = 0, n = aa.length; i < n; i++ ) { + const raw = aa[i]; + const o = JSON.parse(raw); if ( o.style ) { this.domFilterer.addCSSRule(o.style[0], o.style[1]); mustCommit = true; @@ -557,19 +559,20 @@ vAPI.DOMFilterer = (function() { } if ( o.tasks ) { if ( this.selectors.has(raw) === false ) { - pselector = new PSelector(o); + const pselector = new PSelector(o); this.selectors.set(raw, pselector); - this.addedSelectors.set(raw, pselector); + addedSelectors.push(pselector); mustCommit = true; } continue; } } if ( mustCommit === false ) { return; } + this.mustApplySelectors = this.selectors.size !== 0; this.domFilterer.commit(); if ( this.domFilterer.hasListeners() ) { this.domFilterer.triggerListeners({ - procedural: Array.from(this.addedSelectors.values()) + procedural: addedSelectors }); } }, @@ -579,56 +582,46 @@ vAPI.DOMFilterer = (function() { return; } - if ( this.addedNodes || this.removedNodes ) { - this.addedSelectors.clear(); - } - - var entry, nodes, i; - - if ( this.addedSelectors.size !== 0 ) { - //console.time('procedural selectors/filterset changed'); - for ( entry of this.addedSelectors ) { - nodes = entry[1].exec(); - i = nodes.length; - while ( i-- ) { - this.domFilterer.hideNode(nodes[i]); - } - } - this.addedSelectors.clear(); - //console.timeEnd('procedural selectors/filterset changed'); - return; - } + this.mustApplySelectors = false; //console.time('procedural selectors/dom layout changed'); - this.addedNodes = this.removedNodes = false; + // https://github.com/uBlockOrigin/uBlock-issues/issues/341 + // Be ready to unhide nodes which no longer matches any of + // the procedural selectors. + const toRemove = this.hiddenNodes; + this.hiddenNodes = new Set(); - var t0 = Date.now(), - t1, pselector, allowance; + let t0 = Date.now(); - for ( entry of this.selectors ) { - pselector = entry[1]; - allowance = Math.floor((t0 - pselector.lastAllowanceTime) / 2000); + for ( const entry of this.selectors ) { + const pselector = entry[1]; + const allowance = Math.floor((t0 - pselector.lastAllowanceTime) / 2000); if ( allowance >= 1 ) { pselector.budget += allowance * 50; if ( pselector.budget > 200 ) { pselector.budget = 200; } pselector.lastAllowanceTime = t0; } if ( pselector.budget <= 0 ) { continue; } - nodes = pselector.exec(); - t1 = Date.now(); + const nodes = pselector.exec(); + const t1 = Date.now(); pselector.budget += t0 - t1; if ( pselector.budget < -500 ) { console.info('uBO: disabling %s', pselector.raw); pselector.budget = -0x7FFFFFFF; } t0 = t1; - i = nodes.length; + let i = nodes.length; while ( i-- ) { this.domFilterer.hideNode(nodes[i]); + this.hiddenNodes.add(nodes[i]); } } + for ( const node of toRemove ) { + if ( this.hiddenNodes.has(node) ) { continue; } + this.domFilterer.unhideNode(node); + } //console.timeEnd('procedural selectors/dom layout changed'); }, @@ -643,15 +636,17 @@ vAPI.DOMFilterer = (function() { onDOMChanged: function(addedNodes, removedNodes) { if ( this.selectors.size === 0 ) { return; } - this.addedNodes = this.addedNodes || addedNodes.length !== 0; - this.removedNodes = this.removedNodes || removedNodes; + this.mustApplySelectors = + this.mustApplySelectors || + addedNodes.length !== 0 || + removedNodes; this.domFilterer.commit(); } }; - var DOMFiltererBase = vAPI.DOMFilterer; + const DOMFiltererBase = vAPI.DOMFilterer; - var domFilterer = function() { + const domFilterer = function() { DOMFiltererBase.call(this); this.exceptions = []; this.proceduralFilterer = new DOMProceduralFilterer(this); @@ -682,7 +677,7 @@ vAPI.DOMFilterer = (function() { }; domFilterer.prototype.getAllSelectors = function() { - var out = DOMFiltererBase.prototype.getAllSelectors.call(this); + const out = DOMFiltererBase.prototype.getAllSelectors.call(this); out.procedural = Array.from(this.proceduralFilterer.selectors.values()); return out; }; diff --git a/src/js/scriptlet-filtering.js b/src/js/scriptlet-filtering.js index cdaed732d..ade91daa0 100644 --- a/src/js/scriptlet-filtering.js +++ b/src/js/scriptlet-filtering.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2017 Raymond Hill + Copyright (C) 2017-present Raymond Hill This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -97,7 +97,7 @@ 'cosmetic', { source: 'cosmetic', - raw: (isException ? '#@#' : '##') + 'script:inject(' + token + ')' + raw: (isException ? '#@#' : '##') + '+js(' + token + ')' }, 'dom', details.url, @@ -134,14 +134,14 @@ // Ignore instances of exception filter with negated hostnames, // because there is no way to create an exception to an exception. - var µburi = µb.URI; + let µburi = µb.URI; - for ( var hostname of parsed.hostnames ) { - var negated = hostname.charCodeAt(0) === 0x7E /* '~' */; + for ( let hostname of parsed.hostnames ) { + let negated = hostname.charCodeAt(0) === 0x7E /* '~' */; if ( negated ) { hostname = hostname.slice(1); } - var hash = µburi.domainFromHostname(hostname); + let hash = µburi.domainFromHostname(hostname); if ( parsed.exception ) { if ( negated ) { continue; } hash = '!' + hash; @@ -153,9 +153,9 @@ }; // 01234567890123456789 - // script:inject(token[, arg[, ...]]) - // ^ ^ - // 14 -1 + // +js(token[, arg[, ...]]) + // ^ ^ + // 4 -1 api.fromCompiledContent = function(reader) { // 1001 = scriptlet injection @@ -163,17 +163,17 @@ while ( reader.next() ) { acceptedCount += 1; - var fingerprint = reader.fingerprint(); + let fingerprint = reader.fingerprint(); if ( duplicates.has(fingerprint) ) { discardedCount += 1; continue; } duplicates.add(fingerprint); - var args = reader.args(); + let args = reader.args(); if ( args.length < 4 ) { continue; } scriptletDB.add( args[1], - { hostname: args[2], token: args[3].slice(14, -1) } + { hostname: args[2], token: args[3].slice(4, -1) } ); } }; @@ -223,7 +223,7 @@ exceptionsRegister.add(entry.token); } - // Return an array of scriptlets, and log results if needed. + // Return an array of scriptlets, and log results if needed. var out = [], logger = µb.logger.isEnabled() ? µb.logger : null, isException; diff --git a/src/js/static-ext-filtering.js b/src/js/static-ext-filtering.js index 4ad94ea68..07a463089 100644 --- a/src/js/static-ext-filtering.js +++ b/src/js/static-ext-filtering.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2017-2018 Raymond Hill + Copyright (C) 2017-present Raymond Hill This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -51,25 +51,24 @@ **/ µBlock.staticExtFilteringEngine = (function() { - var µb = µBlock, - reHostnameSeparator = /\s*,\s*/, - reHasUnicode = /[^\x00-\x7F]/, - reParseRegexLiteral = /^\/(.+)\/([im]+)?$/, - emptyArray = [], - parsed = { - hostnames: [], - exception: false, - suffix: '' - }; + const µb = µBlock; + const reHasUnicode = /[^\x00-\x7F]/; + const reParseRegexLiteral = /^\/(.+)\/([imu]+)?$/; + const emptyArray = []; + const parsed = { + hostnames: [], + exception: false, + suffix: '' + }; // To be called to ensure no big parent string of a string slice is // left into memory after parsing filter lists is over. - var resetParsed = function() { + const resetParsed = function() { parsed.hostnames = []; parsed.suffix = ''; }; - var isValidCSSSelector = (function() { + const isValidCSSSelector = (function() { var div = document.createElement('div'), matchesFn; // Keep in mind: @@ -109,7 +108,7 @@ })(); - var isBadRegex = function(s) { + const isBadRegex = function(s) { try { void new RegExp(s); } catch (ex) { @@ -119,24 +118,35 @@ return false; }; - var translateAdguardCSSInjectionFilter = function(suffix) { + const translateAdguardCSSInjectionFilter = function(suffix) { var matches = /^([^{]+)\{([^}]+)\}$/.exec(suffix); if ( matches === null ) { return ''; } return matches[1].trim() + ':style(' + matches[2].trim() + ')'; }; - var toASCIIHostnames = function(hostnames) { - var i = hostnames.length; - while ( i-- ) { - var hostname = hostnames[i]; - hostnames[i] = hostname.charCodeAt(0) === 0x7E /* '~' */ ? - '~' + punycode.toASCII(hostname.slice(1)) : - punycode.toASCII(hostname); + const hostnamesFromPrefix = function(s) { + const hostnames = []; + const hasUnicode = reHasUnicode.test(s); + let beg = 0; + while ( beg < s.length ) { + let end = s.indexOf(',', beg); + if ( end === -1 ) { end = s.length; } + let hostname = s.slice(beg, end).trim(); + if ( hostname.length !== 0 ) { + if ( hasUnicode ) { + hostname = hostname.charCodeAt(0) === 0x7E /* '~' */ + ? '~' + punycode.toASCII(hostname.slice(1)) + : punycode.toASCII(hostname); + } + hostnames.push(hostname); + } + beg = end + 1; } + return hostnames; }; - var compileProceduralSelector = (function() { - var reProceduralOperator = new RegExp([ + const compileProceduralSelector = (function() { + const reProceduralOperator = new RegExp([ '^(?:', [ '-abp-contains', @@ -149,22 +159,23 @@ 'matches-css', 'matches-css-after', 'matches-css-before', + 'not', 'xpath' ].join('|'), ')\\(' ].join('')); - var reEscapeRegex = /[.*+?^${}()|[\]\\]/g, + const reEscapeRegex = /[.*+?^${}()|[\]\\]/g, reNeedScope = /^\s*[+>~]/, reIsDanglingSelector = /(?:[+>~]\s*|\s+)$/; - var lastProceduralSelector = '', - lastProceduralSelectorCompiled, - regexToRawValue = new Map(); + const regexToRawValue = new Map(); + let lastProceduralSelector = '', + lastProceduralSelectorCompiled; - var compileText = function(s) { - var regexDetails, - match = reParseRegexLiteral.exec(s); + const compileText = function(s) { + const match = reParseRegexLiteral.exec(s); + let regexDetails; if ( match !== null ) { regexDetails = match[1]; if ( isBadRegex(regexDetails) ) { return; } @@ -178,13 +189,13 @@ return regexDetails; }; - var compileCSSDeclaration = function(s) { - var name, value, regexDetails, - pos = s.indexOf(':'); + const compileCSSDeclaration = function(s) { + const pos = s.indexOf(':'); if ( pos === -1 ) { return; } - name = s.slice(0, pos).trim(); - value = s.slice(pos + 1).trim(); - var match = reParseRegexLiteral.exec(value); + const name = s.slice(0, pos).trim(); + const value = s.slice(pos + 1).trim(); + const match = reParseRegexLiteral.exec(value); + let regexDetails; if ( match !== null ) { regexDetails = match[1]; if ( isBadRegex(regexDetails) ) { return; } @@ -198,7 +209,7 @@ return { name: name, value: regexDetails }; }; - var compileConditionalSelector = function(s) { + const compileConditionalSelector = function(s) { // https://github.com/AdguardTeam/ExtendedCss/issues/31#issuecomment-302391277 // Prepend `:scope ` if needed. if ( reNeedScope.test(s) ) { @@ -207,7 +218,17 @@ return compile(s); }; - var compileXpathExpression = function(s) { + const compileNotSelector = function(s) { + // https://github.com/uBlockOrigin/uBlock-issues/issues/341#issuecomment-447603588 + // Reject instances of :not() filters for which the argument is + // a valid CSS selector, otherwise we would be adversely + // changing the behavior of CSS4's :not(). + if ( isValidCSSSelector(s) === false ) { + return compileConditionalSelector(s); + } + }; + + const compileXpathExpression = function(s) { try { document.createExpression(s, null); } catch (e) { @@ -217,13 +238,13 @@ }; // https://github.com/gorhill/uBlock/issues/2793 - var normalizedOperators = new Map([ + const normalizedOperators = new Map([ [ ':-abp-contains', ':has-text' ], - [ ':-abp-has', ':if' ], - [ ':contains', ':has-text' ] + [ ':-abp-has', ':has' ], + [ ':contains', ':has-text' ], ]); - var compileArgument = new Map([ + const compileArgument = new Map([ [ ':has', compileConditionalSelector ], [ ':has-text', compileText ], [ ':if', compileConditionalSelector ], @@ -231,6 +252,7 @@ [ ':matches-css', compileCSSDeclaration ], [ ':matches-css-after', compileCSSDeclaration ], [ ':matches-css-before', compileCSSDeclaration ], + [ ':not', compileNotSelector ], [ ':xpath', compileXpathExpression ] ]); @@ -241,15 +263,14 @@ // to other blockers. // The normalized string version is what is reported in the logger, // by design. - var decompile = function(compiled) { - var tasks = compiled.tasks; + const decompile = function(compiled) { + const tasks = compiled.tasks; if ( Array.isArray(tasks) === false ) { return compiled.selector; } - var raw = [ compiled.selector ], - value; - for ( var i = 0, n = tasks.length, task; i < n; i++ ) { - task = tasks[i]; + const raw = [ compiled.selector ]; + let value; + for ( let task of tasks ) { switch ( task[0] ) { case ':xpath': raw.push(task[0], '(', task[1], ')'); @@ -280,22 +301,26 @@ break; case ':has': case ':if': + raw.push(':has', '(', decompile(task[1]), ')'); + break; case ':if-not': - raw.push(task[0], '(', decompile(task[1]), ')'); + case ':not': + raw.push(':not', '(', decompile(task[1]), ')'); break; } } return raw.join(''); }; - var compile = function(raw) { + const compile = function(raw) { if ( raw === '' ) { return; } - var prefix = '', + let prefix = '', tasks = []; + let i = 0, + n = raw.length, + opPrefixBeg = 0; for (;;) { - var i = 0, - n = raw.length, - c, match; + let c, match; // Advance to next operator. while ( i < n ) { c = raw.charCodeAt(i++); @@ -305,13 +330,14 @@ } } if ( i === n ) { break; } - var opNameBeg = i - 1; - var opNameEnd = i + match[0].length - 1; + const opNameBeg = i - 1; + const opNameEnd = i + match[0].length - 1; i += match[0].length; // Find end of argument: first balanced closing parenthesis. // Note: unbalanced parenthesis can be used in a regex literal // when they are escaped using `\`. - var pcnt = 1; + // TODO: need to handle quoted parentheses. + let pcnt = 1; while ( i < n ) { c = raw.charCodeAt(i++); if ( c === 0x5C /* '\\' */ ) { @@ -323,21 +349,34 @@ if ( pcnt === 0 ) { break; } } } - // Unbalanced parenthesis? - if ( pcnt !== 0 ) { return; } + // Unbalanced parenthesis? An unbalanced parenthesis is fine + // as long as the last character is a closing parenthesis. + if ( pcnt !== 0 && c !== 0x29 ) { return; } + // https://github.com/uBlockOrigin/uBlock-issues/issues/341#issuecomment-447603588 + // Maybe that one operator is a valid CSS selector and if so, + // then consider it to be part of the prefix. If there is + // at least one task present, then we fail, as we do not + // support suffix CSS selectors. + // TODO: AdGuard does support suffix CSS selectors, so + // supporting this would increase compatibility with + // AdGuard filter lists. + if ( isValidCSSSelector(raw.slice(opNameBeg, i)) ) { + if ( opPrefixBeg !== 0 ) { return; } + continue; + } // Extract and remember operator details. - var operator = raw.slice(opNameBeg, opNameEnd); + let operator = raw.slice(opNameBeg, opNameEnd); operator = normalizedOperators.get(operator) || operator; - var args = raw.slice(opNameEnd + 1, i - 1); + let args = raw.slice(opNameEnd + 1, i - 1); args = compileArgument.get(operator)(args); if ( args === undefined ) { return; } - if ( tasks.length === 0 ) { + if ( opPrefixBeg === 0 ) { prefix = raw.slice(0, opNameBeg); - } else if ( opNameBeg !== 0 ) { + } else if ( opNameBeg !== opPrefixBeg ) { return; } tasks.push([ operator, args ]); - raw = raw.slice(i); + opPrefixBeg = i; if ( i === n ) { break; } } // No task found: then we have a CSS selector. @@ -345,7 +384,7 @@ if ( tasks.length === 0 ) { prefix = raw; tasks = undefined; - } else if ( raw.length !== 0 ) { + } else if ( opPrefixBeg < n ) { return; } // https://github.com/NanoAdblocker/NanoCore/issues/1#issuecomment-354394894 @@ -356,7 +395,7 @@ return { selector: prefix, tasks: tasks }; }; - var entryPoint = function(raw) { + const entryPoint = function(raw) { if ( raw === lastProceduralSelector ) { return lastProceduralSelectorCompiled; } @@ -371,7 +410,7 @@ }; entryPoint.reset = function() { - regexToRawValue = new Map(); + regexToRawValue.clear(); lastProceduralSelector = ''; lastProceduralSelectorCompiled = undefined; }; @@ -383,7 +422,18 @@ // Public API //-------------------------------------------------------------------------- - var api = {}; + const api = { + get acceptedCount() { + return µb.cosmeticFilteringEngine.acceptedCount + + µb.scriptletFilteringEngine.acceptedCount + + µb.htmlFilteringEngine.acceptedCount; + }, + get discardedCount() { + return µb.cosmeticFilteringEngine.discardedCount + + µb.scriptletFilteringEngine.discardedCount + + µb.htmlFilteringEngine.discardedCount; + } + }; //-------------------------------------------------------------------------- // Public classes @@ -437,12 +487,12 @@ }; api.HostnameBasedDB.prototype[Symbol.iterator] = (function() { - var Iter = function(db) { + const Iter = function(db) { this.mapIter = db.values(); this.arrayIter = undefined; }; Iter.prototype.next = function() { - var result; + let result; if ( this.arrayIter !== undefined ) { result = this.arrayIter.next(); if ( result.done === false ) { return result; } @@ -491,22 +541,22 @@ // Convert Adguard's `-ext-has='...'` into uBO's `:has(...)`. api.compileSelector = (function() { - var reAfterBeforeSelector = /^(.+?)(::?after|::?before)$/, - reStyleSelector = /^(.+?):style\((.+?)\)$/, - reStyleBad = /url\(/, - reExtendedSyntax = /\[-(?:abp|ext)-[a-z-]+=(['"])(?:.+?)(?:\1)\]/, - reExtendedSyntaxParser = /\[-(?:abp|ext)-([a-z-]+)=(['"])(.+?)\2\]/, - div = document.createElement('div'); - - var normalizedExtendedSyntaxOperators = new Map([ + const reAfterBeforeSelector = /^(.+?)(::?after|::?before)$/; + const reStyleSelector = /^(.+?):style\((.+?)\)$/; + const reStyleBad = /url\(/; + const reExtendedSyntax = /\[-(?:abp|ext)-[a-z-]+=(['"])(?:.+?)(?:\1)\]/; + const reExtendedSyntaxParser = /\[-(?:abp|ext)-([a-z-]+)=(['"])(.+?)\2\]/; + const div = document.createElement('div'); + + const normalizedExtendedSyntaxOperators = new Map([ [ 'contains', ':has-text' ], - [ 'has', ':if' ], + [ 'has', ':has' ], [ 'matches-css', ':matches-css' ], [ 'matches-css-after', ':matches-css-after' ], [ 'matches-css-before', ':matches-css-before' ], ]); - var isValidStyleProperty = function(cssText) { + const isValidStyleProperty = function(cssText) { if ( reStyleBad.test(cssText) ) { return false; } div.style.cssText = cssText; if ( div.style.cssText === '' ) { return false; } @@ -514,8 +564,8 @@ return true; }; - var entryPoint = function(raw) { - var extendedSyntax = reExtendedSyntax.test(raw); + const entryPoint = function(raw) { + const extendedSyntax = reExtendedSyntax.test(raw); if ( isValidCSSSelector(raw) && extendedSyntax === false ) { return raw; } @@ -523,7 +573,7 @@ // We rarely reach this point -- majority of selectors are plain // CSS selectors. - var matches, operator; + let matches, operator; // Supported Adguard/ABP advanced selector syntax: will translate into // uBO's syntax before further processing. @@ -543,7 +593,7 @@ return entryPoint(raw); } - var selector = raw, + let selector = raw, pseudoclass, style; // `:style` selector? @@ -580,7 +630,7 @@ } // Procedural selector? - var compiled; + let compiled; if ( (compiled = compileProceduralSelector(raw)) ) { return compiled; } @@ -596,9 +646,9 @@ })(); api.compile = function(raw, writer) { - var lpos = raw.indexOf('#'); + let lpos = raw.indexOf('#'); if ( lpos === -1 ) { return false; } - var rpos = lpos + 1; + let rpos = lpos + 1; if ( raw.charCodeAt(rpos) !== 0x23 /* '#' */ ) { rpos = raw.indexOf('#', rpos + 1); if ( rpos === -1 ) { return false; } @@ -611,7 +661,7 @@ if ( (rpos - lpos) > 3 ) { return false; } // Extract the selector. - var suffix = raw.slice(rpos + 1).trim(); + let suffix = raw.slice(rpos + 1).trim(); if ( suffix.length === 0 ) { return false; } parsed.suffix = suffix; @@ -622,7 +672,7 @@ // We have an Adguard/ABP cosmetic filter if and only if the // character is `$`, `%` or `?`, otherwise it's not a cosmetic // filter. - var cCode = raw.charCodeAt(rpos - 1); + let cCode = raw.charCodeAt(rpos - 1); if ( cCode !== 0x23 /* '#' */ && cCode !== 0x40 /* '@' */ ) { // Adguard's scriptlet injection: not supported. if ( cCode === 0x25 /* '%' */ ) { return true; } @@ -645,37 +695,22 @@ if ( lpos === 0 ) { parsed.hostnames = emptyArray; } else { - var prefix = raw.slice(0, lpos); - parsed.hostnames = prefix.split(reHostnameSeparator); - if ( reHasUnicode.test(prefix) ) { - toASCIIHostnames(parsed.hostnames); - } + parsed.hostnames = hostnamesFromPrefix(raw.slice(0, lpos)); } + // Backward compatibility with deprecated syntax. if ( suffix.startsWith('script:') ) { - // Scriptlet injection engine. if ( suffix.startsWith('script:inject') ) { - µb.scriptletFilteringEngine.compile(parsed, writer); - return true; - } - // Script tag filtering: courtesy-conversion to HTML filtering. - if ( suffix.startsWith('script:contains') ) { - console.info( - 'uBO: ##script:contains(...) is deprecated, ' + - 'converting to ##^script:has-text(...)' - ); - suffix = suffix.replace(/^script:contains/, '^script:has-text'); - parsed.suffix = suffix; + suffix = parsed.suffix = '+js' + suffix.slice(13); + } else if ( suffix.startsWith('script:contains') ) { + suffix = parsed.suffix = '^script:has-text' + suffix.slice(15); } } - var c0 = suffix.charCodeAt(0); + let c0 = suffix.charCodeAt(0); // New shorter syntax for scriptlet injection engine. if ( c0 === 0x2B /* '+' */ && suffix.startsWith('+js') ) { - // Convert to deprecated syntax for now. Once 1.15.12 is - // widespread, `+js` form will be the official syntax. - parsed.suffix = 'script:inject' + parsed.suffix.slice(3); µb.scriptletFilteringEngine.compile(parsed, writer); return true; } @@ -707,23 +742,6 @@ }; }; - Object.defineProperties(api, { - acceptedCount: { - get: function() { - return µb.cosmeticFilteringEngine.acceptedCount + - µb.scriptletFilteringEngine.acceptedCount + - µb.htmlFilteringEngine.acceptedCount; - } - }, - discardedCount: { - get: function() { - return µb.cosmeticFilteringEngine.discardedCount + - µb.scriptletFilteringEngine.discardedCount + - µb.htmlFilteringEngine.discardedCount; - } - } - }); - api.fromSelfie = function(selfie) { µb.cosmeticFilteringEngine.fromSelfie(selfie.cosmetic); µb.scriptletFilteringEngine.fromSelfie(selfie.scriptlets);