diff --git a/src/background.html b/src/background.html index 1da6f3387..7edea355e 100644 --- a/src/background.html +++ b/src/background.html @@ -16,7 +16,8 @@ - + + diff --git a/src/css/popup.css b/src/css/popup.css index 3a832fd2e..8a2f2e665 100644 --- a/src/css/popup.css +++ b/src/css/popup.css @@ -1,12 +1,12 @@ body { - margin: 0; - border: 0; - padding: 0; - font: 13px sans-serif; background-color: white; - min-width: 180px; - overflow: hidden; + border: 0; float: left; + font: 13px sans-serif; + margin: 0; + overflow: hidden; + padding: 0; + white-space: nowrap; } h1,h2,h3,h4 { margin: 0; @@ -30,7 +30,13 @@ a { font-size: 11px; } body > div { - padding: 4px 12px 0 12px; + background-color: transparent; + display: inline-block; + vertical-align: top; + } +body > div:nth-of-type(2) { + padding: 4px 12px 0 5px; + position: relative; } p { margin: 16px 0; @@ -53,6 +59,9 @@ p { font-size: 11px; color: #888; } +[data-i18n="popupBlockedRequestPrompt"] { + font-size: 16px; + } #page-blocked { margin-top: 4px; font-size: 20px; @@ -80,6 +89,20 @@ p { color: #444; } +#dynamicFilteringToggler { + pointer-events: none; + } +#dynamicFilteringToggler::before { + color: gray; + content: '+\202F'; + cursor: pointer; + font-size: 13px; + pointer-events: auto; + } +body.dynamicFilteringEnabled #dynamicFilteringToggler::before { + content: '\2212\202F'; + } + .dynamicFiltering div > .tip { background-color: #fffff4; border: 1px solid #888; @@ -105,157 +128,67 @@ p { font: normal monospace; padding: 1px 0; } -#dynamicFilteringToggler { - margin: 0; - border: 0; - padding: 0; - width: 100%; - text-align: center; - cursor: pointer; - position: relative; - } -#dynamicFilteringToggler.hasBlock:not(.on) { - background-color: #fbb; - } -#dynamicFilteringToggler > div { - font-size: 9px; - line-height: 100%; - color: #888; - position: absolute; - bottom: 0; - width: 25%; - display: none; - pointer-events: none; - } -#dynamicFilteringToggler.on:hover > div { - display: initial; - } -#dynamicFilteringToggler > div:nth-of-type(1) { - left: 0; - } -#dynamicFilteringToggler > div:nth-of-type(2) { - left: 25%; - } -#dynamicFilteringToggler > div:nth-of-type(3) { - left: 50%; - } -#dynamicFilteringToggler > div:nth-of-type(4) { - left: 75%; - } -#dynamicFilteringToggler > a { - padding: 0 2px 0 8px; - position: absolute; - right: 0; - color: #666; - visibility: hidden; - } -body[dir=rtl] #dynamicFilteringToggler > a { - right: auto; - left: 0; -} -#dynamicFilteringToggler a:hover { - color: #444; - } -#dynamicFilteringToggler.on:hover a { - visibility: visible; - } -#dynamicFilteringToggler::before { - content: '\f107'; - } -#dynamicFilteringToggler.on::before { - content: '\f106'; - } #dynamicFilteringContainer { - margin: 0; border: 0; - padding: 0; - display: none; - } -#dynamicFilteringToggler.on + #dynamicFilteringContainer { - display: initial; - } -.dynamicFiltering { + direction: rtl; + font-size: 12px; margin: 0; + overflow: hidden; + padding: 0; + text-align: right; + width: 7px; + } +body.dynamicFilteringEnabled #dynamicFilteringContainer { + display: block; + width: 200px; + } +#dynamicFilteringContainer > div { border: 0; - padding: 0; - height: 2.75em; - position: relative; - } -.dynamicFiltering.local { - border-bottom: 1px solid white; - } -.dynamicFiltering.global { - height: 1.25em; - } -.dynamicFiltering > div { + direction: ltr; margin: 0; - border: 1px solid #e6e6e6; padding: 0; - box-sizing: border-box; - background-color: #e6e6e6; - position: absolute; - cursor: pointer; + width: 200px; } -.dynamicFiltering > div:not(:first-child) { - border-left: 1px solid white !important; - } -.dynamicFiltering > div:nth-of-type(1) { - left: 0; - width: 7%; - height: 100%; - } -.dynamicFiltering > div:nth-of-type(2) { - left: 7%; - width: 18%; - height: 100%; - } -.dynamicFiltering > div:nth-of-type(3) { - left: 25%; - width: 25%; - height: 100%; - } -.dynamicFiltering > div:nth-of-type(4) { - left: 50%; - width: 25%; - height: 100%; - } -.dynamicFiltering > div:nth-of-type(5) { - left: 75%; - width: 25%; - height: 100%; - } -.dynamicFiltering > div:nth-of-type(6) { - left: 0; - width: 50%; - } -.dynamicFiltering > div:nth-of-type(7) { - left: 50%; - width: 50%; - } -.dynamicFiltering > div.label { - margin: 0; - border: 0; - padding: 0; - pointer-events: none; - color: black; - opacity: 0.4; - font: 12px monospace; - text-align: center; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); +#dynamicFilteringContainer > div > span { background-color: transparent; + border: none; + border-bottom: 1px solid white; + box-sizing: border-box; + color: gray; + display: inline-block; + height: 2em; + line-height: 2em; + pointer-events: none; + vertical-align: top; } -.dynamicFiltering > div.blocked { - border-color: #fbb; +body.dynamicFilteringEnabled #dynamicFilteringContainer > div > span { + background-color: #e6e6e6; + pointer-events: auto; + } +#dynamicFilteringContainer > div > span:nth-of-type(1) { + border-right: 1px solid white; + padding-right: 4px; + width: 75%; + } +#dynamicFilteringContainer > div > span:nth-of-type(2) { + cursor: pointer; + width: 9%; + } +#dynamicFilteringContainer > div > span:nth-of-type(3) { + border-left: 1px solid white; + cursor: pointer; + width: 16%; + } +body.dynamicFilteringEnabled #dynamicFilteringContainer > div > span:nth-of-type(3) { + pointer-events: auto; + } +#dynamicFilteringContainer span.blocked[data-src] { background-color: #fbb; } -.dynamicFiltering > div.ownFilter { - border-color: #bbb; +#dynamicFilteringContainer span.ownFilter[data-src] { background-color: #bbb; } -.dynamicFiltering > div.blocked.ownFilter { - border-color: #f66; +#dynamicFilteringContainer span.blocked.ownFilter[data-src] { background-color: #f66; - } + } \ No newline at end of file diff --git a/src/js/background.js b/src/js/background.js index 0f3340567..c1816168a 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -55,6 +55,7 @@ return { autoUpdate: true, collapseBlocked: true, contextMenuEnabled: true, + dynamicFilteringString: '', dynamicFilteringSelfie: '', dynamicFilteringEnabled: false, experimentalEnabled: false, @@ -106,7 +107,7 @@ return { firstUpdateAfter: 5 * oneMinute, nextUpdateAfter: 7 * oneHour, - selfieMagic: 'odyxfmbsqllh', + selfieMagic: 'qidcglrwobsm', selfieAfter: 7 * oneMinute, pageStores: {}, diff --git a/src/js/contentscript-end.js b/src/js/contentscript-end.js index 1d8cf7f68..b6a5c7674 100644 --- a/src/js/contentscript-end.js +++ b/src/js/contentscript-end.js @@ -216,7 +216,7 @@ var messager = vAPI.messaging.channel('contentscript-end.js'); var selector; while ( i-- ) { selector = generics[i]; - if ( injectedSelectors[selector] !== undefined ) { + if ( injectedSelectors.hasOwnProperty(selector) ) { continue; } injectedSelectors[selector] = true; @@ -241,14 +241,14 @@ var messager = vAPI.messaging.channel('contentscript-end.js'); if ( !attrValue ) { continue; } selector = '[' + attr + '="' + attrValue + '"]'; if ( generics[selector] ) { - if ( injectedSelectors[selector] === undefined ) { + if ( injectedSelectors.hasOwnProperty(selector) === false ) { injectedSelectors[selector] = true; out.push(selector); } } selector = node.tagName.toLowerCase() + selector; if ( generics[selector] ) { - if ( injectedSelectors[selector] === undefined ) { + if ( injectedSelectors.hasOwnProperty(selector) === false ) { injectedSelectors[selector] = true; out.push(selector); } @@ -277,7 +277,7 @@ var messager = vAPI.messaging.channel('contentscript-end.js'); iSelector = selectors.length; while ( iSelector-- ) { selector = selectors[iSelector]; - if ( injectedSelectors[selector] === undefined ) { + if ( injectedSelectors.hasOwnProperty(selector) === false ) { injectedSelectors[selector] = true; out.push(selector); } @@ -293,7 +293,7 @@ var messager = vAPI.messaging.channel('contentscript-end.js'); var processHighHighGenerics = function() { processHighHighGenericsTimer = null; - if ( injectedSelectors['{{highHighGenerics}}'] !== undefined ) { return; } + if ( injectedSelectors.hasOwnProperty('{{highHighGenerics}}') ) { return; } if ( document.querySelector(highGenerics.hideHigh) === null ) { return; } injectedSelectors['{{highHighGenerics}}'] = true; // We need to filter out possible exception cosmetic filters from @@ -443,10 +443,10 @@ var messager = vAPI.messaging.channel('contentscript-end.js'); } } if ( addedNodeListsTimer === null ) { - // I arbitrarily chose 50 ms for now: + // I arbitrarily chose 100 ms for now: // I have to compromise between the overhead of processing too few // nodes too often and the delay of many nodes less often. - addedNodeListsTimer = setTimeout(mutationObservedHandler, 75); + addedNodeListsTimer = setTimeout(mutationObservedHandler, 100); } }; diff --git a/src/js/dynamic-net-filtering.js b/src/js/dynamic-net-filtering.js new file mode 100644 index 000000000..6e137da55 --- /dev/null +++ b/src/js/dynamic-net-filtering.js @@ -0,0 +1,485 @@ +/******************************************************************************* + + µBlock - a Chromium browser extension to black/white list requests. + Copyright (C) 2014 Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock +*/ + +/* global punycode, µBlock */ +/* jshint bitwise: false */ + +/******************************************************************************/ + +µBlock.dynamicNetFilteringEngine = (function() { + +/******************************************************************************/ + +var magicId = 'chmdgxwtetgu'; + +/******************************************************************************/ + +var Matrix = function() { + this.reset(); +}; + +/******************************************************************************/ + +var typeBitOffsets = { + '*': 0, +'inline-script': 2, + '1p-script': 4, + '3p-script': 6, + '3p-frame': 8, + 'image': 10 +}; + +var stateToNameMap = { + '1': 'block', + '2': 'allow', + '3': 'noop' +}; + +var nameToStateMap = { + 'block': 1, + 'allow': 2, + 'noop': 3 +}; + +/******************************************************************************/ + +// For performance purpose, as simple tests as possible +var reHostnameVeryCoarse = /[g-z_-]/; +var reIPv4VeryCoarse = /\.\d+$/; + +// http://tools.ietf.org/html/rfc5952 +// 4.3: "MUST be represented in lowercase" +// Also: http://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_network_resource_identifiers + +var isIPAddress = function(hostname) { + if ( reHostnameVeryCoarse.test(hostname) ) { + return false; + } + if ( reIPv4VeryCoarse.test(hostname) ) { + return true; + } + return hostname.charAt(0) === '['; +}; + +/******************************************************************************/ + +var toBroaderHostname = function(hostname) { + if ( hostname === '*' ) { + return ''; + } + if ( isIPAddress(hostname) ) { + return '*'; + } + var pos = hostname.indexOf('.'); + if ( pos === -1 ) { + return '*'; + } + return hostname.slice(pos + 1); +}; + +Matrix.toBroaderHostname = toBroaderHostname; + +/******************************************************************************/ + +Matrix.prototype.reset = function() { + this.r = 0; + this.type = ''; + this.y = ''; + this.z = ''; + this.rules = {}; +}; + +/******************************************************************************/ + +Matrix.prototype.setCell = function(srcHostname, desHostname, type, state) { + var bitOffset = typeBitOffsets[type]; + var k = srcHostname + ' ' + desHostname; + var oldBitmap = this.rules[k]; + if ( oldBitmap === undefined ) { + oldBitmap = 0; + } + var newBitmap = oldBitmap & ~(3 << bitOffset) | (state << bitOffset); + if ( newBitmap === oldBitmap ) { + return false; + } + if ( newBitmap === 0 ) { + delete this.rules[k]; + } else { + this.rules[k] = newBitmap; + } + return true; +}; + +/******************************************************************************/ + +Matrix.prototype.blockCell = function(srcHostname, desHostname, type) { + this.evaluateCellZY(srcHostname, desHostname, type); + if ( this.r === 1 ) { + return false; + } + this.setCell(srcHostname, desHostname, type, 0); + this.evaluateCellZY(srcHostname, desHostname, type); + if ( this.r === 1 ) { + return true; + } + this.setCell(srcHostname, desHostname, type, 1); + return true; +}; + +// https://www.youtube.com/watch?v=Csewb_eIStY + +/******************************************************************************/ + +Matrix.prototype.allowCell = function(srcHostname, desHostname, type) { + this.evaluateCellZY(srcHostname, desHostname, type); + if ( this.r === 2 ) { + return false; + } + this.setCell(srcHostname, desHostname, type, 0); + this.evaluateCellZY(srcHostname, desHostname, type); + if ( this.r === 2 ) { + return true; + } + this.setCell(srcHostname, desHostname, type, 2); + return true; +}; + +/******************************************************************************/ + +Matrix.prototype.unsetCell = function(srcHostname, desHostname, type) { + this.evaluateCellZY(srcHostname, desHostname, type); + if ( this.r === 0 ) { + return false; + } + this.setCell(srcHostname, desHostname, type, 0); + this.evaluateCellZY(srcHostname, desHostname, type); + if ( this.r === 0 || this.r === 3 ) { + return true; + } + this.setCell(srcHostname, desHostname, type, 3); + return true; +}; + +/******************************************************************************/ + +Matrix.prototype.evaluateCell = function(srcHostname, desHostname, type) { + var key = srcHostname + ' ' + desHostname; + var bitmap = this.rules[key]; + if ( bitmap === undefined ) { + return 0; + } + return bitmap >> typeBitOffsets[type] & 3; +}; + +/******************************************************************************/ + +Matrix.prototype.clearRegisters = function() { + this.r = 0; + this.type = ''; + this.y = ''; + this.z = ''; +}; + +/******************************************************************************/ + +Matrix.prototype.evaluateCellZ = function(srcHostname, desHostname, type) { + var bitOffset = typeBitOffsets[type]; + var s = srcHostname; + var v; + for (;;) { + this.z = s; + v = this.rules[s + ' ' + desHostname]; + if ( v !== undefined ) { + v = v >> bitOffset & 3; + if ( v !== 0 ) { + return v; + } + } + s = toBroaderHostname(s); + if ( s === '' ) { + break; + } + } + // srcHostname is '*' at this point + return 0; +}; + +/******************************************************************************/ + +Matrix.prototype.evaluateCellZY = function(srcHostname, desHostname, type) { + if ( typeBitOffsets.hasOwnProperty(type) === false ) { + this.type = ''; + this.r = 0; + return this; + } + this.type = type; + // Specific-hostname specific-type cell + this.y = desHostname; + this.r = this.evaluateCellZ(srcHostname, desHostname, type); + if ( this.r !== 0 ) { return this; } + + var d = desHostname; + for (;;) { + d = toBroaderHostname(d); + if ( d === '*' ) { + break; + } + // specific-hostname specific-type cell + this.y = d; + this.r = this.evaluateCellZ(srcHostname, d, type); + if ( this.r !== 0 ) { return this; } + } + + // Any-hostname specific-type cells + this.y = '*'; + this.r = this.evaluateCellZ(srcHostname, '*', type); + return this; +}; + +// http://youtu.be/gSGk1bQ9rcU?t=25m6s + +/******************************************************************************/ + +Matrix.prototype.mustBlockOrAllow = function() { + return this.r === 1 || this.r === 2; +}; + +/******************************************************************************/ + +Matrix.prototype.toFilterString = function() { + if ( this.type === '' ) { + return ''; + } + if ( this.r === 1 ) { + return 'db:' + this.z + ' ' + this.y + ' ' + this.type + ' block'; + } + if ( this.r === 2 ) { + return 'da:' + this.z + ' ' + this.y + ' ' + this.type + ' allow'; + } + if ( this.r === 3 ) { + return 'dn:' + this.z + ' ' + this.y + ' ' + this.type + ' noop'; + } + return ''; +}; + +/******************************************************************************/ + +Matrix.prototype.mustBlock = function(srcHostname, desHostname, type) { + this.evaluateCellZY(srcHostname, desHostname, type); + return this.r === 1; +}; + +/******************************************************************************/ + +Matrix.prototype.srcHostnameFromRule = function(rule) { + return rule.slice(0, rule.indexOf(' ')); +}; + +/******************************************************************************/ + +Matrix.prototype.desHostnameFromRule = function(rule) { + return rule.slice(rule.indexOf(' ') + 1); +}; + +/******************************************************************************/ + +Matrix.prototype.toString = function() { + var out = []; + var rule, type, val; + var srcHostname, desHostname; + for ( rule in this.rules ) { + if ( this.rules.hasOwnProperty(rule) === false ) { + continue; + } + srcHostname = this.srcHostnameFromRule(rule); + desHostname = this.desHostnameFromRule(rule); + for ( type in typeBitOffsets ) { + if ( typeBitOffsets.hasOwnProperty(type) === false ) { + continue; + } + val = this.evaluateCell(srcHostname, desHostname, type); + if ( val === 0 ) { + continue; + } + out.push( + punycode.toUnicode(srcHostname) + ' ' + + punycode.toUnicode(desHostname) + ' ' + + type + ' ' + + stateToNameMap[val] + ); + } + } + return out.join('\n'); +}; + +/******************************************************************************/ + +Matrix.prototype.fromString = function(text, append) { + var textEnd = text.length; + var lineBeg = 0, lineEnd; + var line, pos; + var fields, fieldVal; + var srcHostname = ''; + var desHostname = ''; + var type, state; + + while ( lineBeg < textEnd ) { + lineEnd = text.indexOf('\n', lineBeg); + if ( lineEnd < 0 ) { + lineEnd = text.indexOf('\r', lineBeg); + if ( lineEnd < 0 ) { + lineEnd = textEnd; + } + } + line = text.slice(lineBeg, lineEnd).trim(); + lineBeg = lineEnd + 1; + + pos = line.indexOf('# '); + if ( pos !== -1 ) { + line = line.slice(0, pos).trim(); + } + if ( line === '' ) { + continue; + } + + fields = line.split(/\s+/); + + // Less than 2 fields makes no sense + if ( fields.length < 2 ) { + continue; + } + + fieldVal = fields[0]; + + // Valid rule syntax: + + // srcHostname desHostname [type [state]] + // type = a valid request type + // state = [`block`, `allow`, `inherit`] + + // srcHostname desHostname type + // type = a valid request type + // state = `allow` + + // srcHostname desHostname + // type = `*` + // state = `allow` + + // Lines with invalid syntax silently ignored + + srcHostname = punycode.toASCII(fields[0]); + desHostname = punycode.toASCII(fields[1]); + + fieldVal = fields[2]; + + if ( fieldVal !== undefined ) { + type = fieldVal; + // Unknown type: reject + if ( typeBitOffsets.hasOwnProperty(type) === false ) { + continue; + } + } else { + type = '*'; + } + + fieldVal = fields[3]; + + if ( fieldVal !== undefined ) { + // Unknown state: reject + if ( nameToStateMap.hasOwnProperty(fieldVal) === false ) { + continue; + } + state = nameToStateMap[fieldVal]; + } else { + state = 2; + } + + this.setCell(srcHostname, desHostname, type, state); + } +}; + +/******************************************************************************/ + +Matrix.prototype.fromObsoleteSelfie = function(selfie) { + if ( selfie === '' ) { + return ''; + } + var bin = JSON.parse(selfie); + var filters = bin.filters; + var bits, val; + for ( var hostname in filters ) { + if ( filters.hasOwnProperty(hostname) === false ) { + continue; + } + bits = filters[hostname]; + val = bits & 3; + if ( val === 1 ) { + this.setCell(hostname, '*', 'inline-script', 1); + } else if ( val === 2 ) { + this.setCell(hostname, '*', 'inline-script', 3); + } + val = (bits >> 2) & 3; + if ( val === 1 ) { + this.setCell(hostname, '*', '1p-script', 1); + } else if ( val === 2 ) { + this.setCell(hostname, '*', '1p-script', 3); + } + val = (bits >> 4) & 3; + if ( val === 1 ) { + this.setCell(hostname, '*', '3p-script', 1); + } else if ( val === 2 ) { + this.setCell(hostname, '*', '3p-script', 3); + } + val = (bits >> 8) & 3; + if ( val === 1 ) { + this.setCell(hostname, '*', '3p-frame', 1); + } else if ( val === 2 ) { + this.setCell(hostname, '*', '3p-frame', 3); + } + } +}; + +/******************************************************************************/ + +Matrix.prototype.toSelfie = function() { + return { + magicId: magicId, + rules: this.rules + }; +}; + +/******************************************************************************/ + +Matrix.prototype.fromSelfie = function(selfie) { + this.rules = selfie.rules; +}; + +/******************************************************************************/ + +return new Matrix; + +/******************************************************************************/ + +// http://youtu.be/5-K8R1hDG9E?t=31m1s + +})(); + +/******************************************************************************/ diff --git a/src/js/messaging.js b/src/js/messaging.js index 35ad5be00..db662c506 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -42,10 +42,6 @@ var onMessage = function(request, sender, callback) { µb.assets.get(request.url, callback); return; - case 'loadUbiquitousAllowRules': - µb.loadUbiquitousWhitelists(); - return; - default: break; } @@ -108,13 +104,14 @@ var µb = µBlock; /******************************************************************************/ var getDynamicFilterResults = function(scope) { - return [ - µb.netFilteringEngine.matchDynamicFilters(scope, 'inline-script', true), - µb.netFilteringEngine.matchDynamicFilters(scope, 'script', true), - µb.netFilteringEngine.matchDynamicFilters(scope, 'script', false), - µb.netFilteringEngine.matchDynamicFilters(scope, 'sub_frame', true), - µb.netFilteringEngine.matchDynamicFilters(scope, 'sub_frame', false) - ]; + var r = {}; + var dFiltering = µb.dynamicNetFilteringEngine; + r['image'] = dFiltering.evaluateCellZY(scope, '*', 'image').toFilterString(); + r['inline-script'] = dFiltering.evaluateCellZY(scope, '*', 'inline-script').toFilterString(); + r['1p-script'] = dFiltering.evaluateCellZY(scope, '*', '1p-script').toFilterString(); + r['3p-script'] = dFiltering.evaluateCellZY(scope, '*', '3p-script').toFilterString(); + r['3p-frame'] = dFiltering.evaluateCellZY(scope, '*', '3p-frame').toFilterString(); + return r; }; /******************************************************************************/ @@ -134,7 +131,7 @@ var getStats = function(tab) { logRequests: µb.userSettings.logRequests, dynamicFilteringEnabled: µb.userSettings.dynamicFilteringEnabled, dynamicFilterResults: { - '/': getDynamicFilterResults('*') + '*': getDynamicFilterResults('*') } }; var pageStore = tab && µb.pageStoreFromTabId(tab.id); @@ -145,7 +142,7 @@ var getStats = function(tab) { r.pageBlockedRequestCount = pageStore.perLoadBlockedRequestCount; r.pageAllowedRequestCount = pageStore.perLoadAllowedRequestCount; r.netFilteringSwitch = pageStore.getNetFilteringSwitch(); - r.dynamicFilterResults['.'] = getDynamicFilterResults(r.pageHostname); + r.dynamicFilterResults['local'] = getDynamicFilterResults(r.pageHostname); } return r; }; @@ -185,9 +182,9 @@ var onMessage = function(request, sender, callback) { case 'toggleDynamicFilter': µb.toggleDynamicFilter(request); - response = { '/': getDynamicFilterResults('*') }; + response = { '*': getDynamicFilterResults('*') }; if ( request.pageHostname ) { - response['.'] = getDynamicFilterResults(request.pageHostname); + response['local'] = getDynamicFilterResults(request.pageHostname); } break; @@ -281,9 +278,13 @@ var tagNameToRequestTypeMap = { // Evaluate many requests var filterRequests = function(pageStore, details) { - details.pageDomain = µb.URI.domainFromHostname(details.pageHostname); + var µburi = µb.URI; + + // Create evaluation context + details.pageDomain = µburi.domainFromHostname(details.pageHostname); details.rootHostname = pageStore.rootHostname; details.rootDomain = pageStore.rootDomain; + details.requestHostname = ''; var inRequests = details.requests; var outRequests = []; @@ -294,11 +295,10 @@ var filterRequests = function(pageStore, details) { if ( tagNameToRequestTypeMap.hasOwnProperty(request.tagName) === false ) { continue; } - result = pageStore.filterRequest( - details, - tagNameToRequestTypeMap[request.tagName], - request.url - ); + details.requestURL = request.url; + details.requestHostname = µburi.hostnameFromURI(request.url); + details.requestType = tagNameToRequestTypeMap[request.tagName]; + result = pageStore.filterRequest(details); if ( pageStore.boolFromResult(result) ) { outRequests.push(request); } @@ -317,14 +317,13 @@ var filterRequest = function(pageStore, details) { if ( tagNameToRequestTypeMap.hasOwnProperty(details.tagName) === false ) { return; } - details.pageDomain = µb.URI.domainFromHostname(details.pageHostname); + var µburi = µb.URI; + details.pageDomain = µburi.domainFromHostname(details.pageHostname); details.rootHostname = pageStore.rootHostname; details.rootDomain = pageStore.rootDomain; - var result = pageStore.filterRequest( - details, - tagNameToRequestTypeMap[details.tagName], - details.requestURL - ); + details.requestHostname = µburi.hostnameFromURI(details.requestURL); + details.requestType = tagNameToRequestTypeMap[details.tagName]; + var result = pageStore.filterRequest(details); if ( pageStore.boolFromResult(result) ) { return { collapse: µb.userSettings.collapseBlocked }; } @@ -485,7 +484,7 @@ var getLists = function(callback) { available: null, current: µb.remoteBlacklists, cosmetic: µb.userSettings.parseAllABPHideFilters, - netFilterCount: µb.netFilteringEngine.getFilterCount(), + netFilterCount: µb.staticNetFilteringEngine.getFilterCount(), cosmeticFilterCount: µb.cosmeticFilteringEngine.getFilterCount(), autoUpdate: µb.userSettings.autoUpdate, userFiltersPath: µb.userFiltersPath, diff --git a/src/js/pagestore.js b/src/js/pagestore.js index d042f6479..51d67d0f1 100644 --- a/src/js/pagestore.js +++ b/src/js/pagestore.js @@ -245,6 +245,9 @@ FrameStore.prototype.init = function(rootHostname, frameURL) { this.pageDomain = µburi.domainFromHostname(this.pageHostname) || this.pageHostname; this.rootHostname = rootHostname; this.rootDomain = µburi.domainFromHostname(rootHostname) || rootHostname; + // This is part of the filtering evaluation context + this.requestURL = this.requestHostname = this.requestType = ''; + return this; }; @@ -252,7 +255,8 @@ FrameStore.prototype.init = function(rootHostname, frameURL) { FrameStore.prototype.dispose = function() { this.pageHostname = this.pageDomain = - this.rootHostname = this.rootDomain = ''; + this.rootHostname = this.rootDomain = + this.requestURL = this.requestHostname = this.requestType = ''; if ( frameStoreJunkyard.length < frameStoreJunkyardMax ) { frameStoreJunkyard.push(this); } @@ -292,6 +296,16 @@ PageStore.factory = function(tabId, pageURL) { /******************************************************************************/ +PageStore.prototype.bitFromRequestType = { + '': 1, + 'sb': 2, + 'sa': 4, + 'db': 8, + 'da': 16 +}; + +/******************************************************************************/ + PageStore.prototype.init = function(tabId, pageURL) { this.tabId = tabId; this.previousPageURL = ''; @@ -304,6 +318,10 @@ PageStore.prototype.init = function(tabId, pageURL) { this.rootHostname = this.pageHostname; this.rootDomain = this.pageDomain; + // This is part of the filtering evaluation context + this.requestURL = this.requestHostname = this.requestType = ''; + this.requestHostnames = {}; + this.frames = {}; this.netFiltering = true; this.netFilteringReadTime = 0; @@ -361,7 +379,9 @@ PageStore.prototype.dispose = function() { // used as a key). this.pageURL = this.previousPageURL = this.pageHostname = this.pageDomain = - this.rootHostname = this.rootDomain = ''; + this.rootHostname = this.rootDomain = + this.requestURL = this.requestHostname = this.requestType = ''; + this.requestHostnames = null; this.disposeFrameStores(); this.netFilteringCache = this.netFilteringCache.dispose(); if ( pageStoreJunkyard.length < pageStoreJunkyardMax ) { @@ -374,11 +394,8 @@ PageStore.prototype.dispose = function() { PageStore.prototype.disposeFrameStores = function() { var frames = this.frames; - if ( typeof frames === 'object' ) { - for ( var k in frames ) { - if ( frames.hasOwnProperty(k) === false ) { - continue; - } + for ( var k in frames ) { + if ( frames.hasOwnProperty(k) ) { frames[k].dispose(); } } @@ -414,20 +431,32 @@ PageStore.prototype.getNetFilteringSwitch = function() { /******************************************************************************/ -PageStore.prototype.filterRequest = function(context, requestType, requestURL) { - var result = ''; - if ( this.getNetFilteringSwitch() ) { - var entry = this.netFilteringCache.lookup(requestURL); - if ( entry !== undefined ) { - //console.debug(' cache HIT: PageStore.filterRequest("%s")', requestURL); - return entry.result; - } - //console.debug('cache MISS: PageStore.filterRequest("%s")', requestURL); - result = µb.netFilteringEngine.matchString(context, requestURL, requestType); +PageStore.prototype.filterRequest = function(context) { + var requestURL = context.requestURL; + + if ( this.getNetFilteringSwitch() === false ) { + this.recordResult(context.requestType, requestURL, ''); + return ''; } - if ( collapsibleRequestTypes.indexOf(requestType) !== -1 || µb.userSettings.logRequests ) { - this.netFilteringCache.add(requestURL, result, requestType, 0); + + var entry = this.netFilteringCache.lookup(requestURL); + if ( entry !== undefined ) { + //console.debug('cache HIT: PageStore.filterRequest("%s")', requestURL); + return entry.result; } + + var result = µb.filterRequest(context); + + //console.debug('cache MISS: PageStore.filterRequest("%s")', requestURL); + this.recordResult(context.requestType, requestURL, result); + + var requestHostname = context.requestHostname; + if ( this.requestHostnames.hasOwnProperty(requestHostname) ) { + this.requestHostnames[requestHostname] |= this.bitFromRequestType[result.slice(0, 2)]; + } else { + this.requestHostnames[requestHostname] = this.bitFromRequestType[result.slice(0, 2)]; + } + return result; }; @@ -454,7 +483,7 @@ PageStore.prototype.recordResult = function(requestType, requestURL, result) { // true: blocked PageStore.prototype.boolFromResult = function(result) { - return typeof result === 'string' && result !== '' && result.slice(0, 2) !== '@@'; + return typeof result === 'string' && result.charAt(1) === 'b'; }; /******************************************************************************/ diff --git a/src/js/popup.js b/src/js/popup.js index 554cb55ce..fdb82a669 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -30,7 +30,6 @@ /******************************************************************************/ var stats; -var reResultParser = /^(@@)?(\*|\|\|([^$^]+)\^)\$(.+)$/; /******************************************************************************/ @@ -50,23 +49,23 @@ var formatNumber = function(count) { /******************************************************************************/ -var syncDynamicFilter = function(scope, i, result) { - var el = uDom('[data-scope="' + scope + '"] > div:nth-of-type(' + i + ')'); - var matches = reResultParser.exec(result) || []; - var blocked = matches.length !== 0 && matches[1] !== '@@'; +var syncDynamicFilter = function(scope, des, type, result) { + var el = uDom('span[data-src="' + scope + '"][data-des="' + des + '"][data-type="' + type + '"]'); + var blocked = result.charAt(1) === 'b'; el.toggleClass('blocked', blocked); // https://github.com/gorhill/uBlock/issues/340 // Use dark shade visual cue if the filter is specific to the page hostname // or one of the ancestor hostname. var ownFilter = false; - // There might be no page hostname on pages where uBlock can't be active, - // like on browser's built-in pages, etc. - if ( stats.pageHostname ) { - var filterHostname = matches[3] || '*'; - if ( stats.pageHostname.slice(0 - filterHostname.length) === filterHostname ) { - ownFilter = (stats.pageHostname.length === filterHostname.length) || - (stats.pageHostname.substr(0 - filterHostname.length - 1, 1) === '.'); + var matches = /^d[abn]:([^ ]+)/.exec(result); + if ( matches !== null ) { + var thisSrc = scope === 'local' ? stats.pageHostname : '*'; + var otherSrc = matches[1]; + ownFilter = thisSrc.slice(0 - otherSrc.length) === thisSrc; + if ( ownFilter && thisSrc.length !== otherSrc.length ) { + var c = thisSrc.substr(0 - otherSrc.length - 1, 1); + ownFilter = c === '' || c === '.'; } } el.toggleClass('ownFilter', ownFilter); @@ -76,23 +75,25 @@ var syncDynamicFilter = function(scope, i, result) { var syncAllDynamicFilters = function() { var hasBlock = false; - var scopes = ['.', '/']; + var scopes = ['*', 'local']; var scope, results, i, result; while ( scope = scopes.pop() ) { if ( stats.dynamicFilterResults.hasOwnProperty(scope) === false ) { continue; } results = stats.dynamicFilterResults[scope]; - i = 5; - while ( i-- ) { - result = results[i]; - syncDynamicFilter(scope, i + 1, result); - if ( scope === '.' && result.length !== 0 && result.slice(0, 2) !== '@@' ) { + for ( var type in results ) { + if ( results.hasOwnProperty(type) === false ) { + continue; + } + result = results[type]; + syncDynamicFilter(scope, '*', type, result); + if ( scope === 'local' && result.charAt(1) === 'b' ) { hasBlock = true; } } } - uDom('#dynamicFilteringToggler').toggleClass('hasBlock', hasBlock); + uDom('body').toggleClass('hasDynamicBlock', hasBlock); }; /******************************************************************************/ @@ -165,7 +166,7 @@ var renderPopup = function(details) { uDom('#total-blocked').html(html.join('')); uDom('#switch .fa').toggleClass('off', stats.pageURL === '' || !stats.netFilteringSwitch); - uDom('#dynamicFilteringToggler').toggleClass('on', stats.dynamicFilteringEnabled); + uDom('body').toggleClass('dynamicFilteringEnabled', stats.dynamicFilteringEnabled); }; /******************************************************************************/ @@ -174,12 +175,11 @@ var toggleNetFilteringSwitch = function(ev) { if ( !stats || !stats.pageURL ) { return; } - var off = uDom(this).toggleClass('off').hasClassName('off'); messager.send({ what: 'toggleNetFiltering', url: stats.pageURL, scope: ev.ctrlKey || ev.metaKey ? 'page' : '', - state: !off, + state: !uDom(this).toggleClass('off').hasClass('off'), tabId: stats.tabId }); }; @@ -242,37 +242,35 @@ var gotoLink = function(ev) { /******************************************************************************/ var onDynamicFilterClicked = function(ev) { - var elScope = uDom(ev.currentTarget); - var scope = elScope.attr('data-scope') === '/' ? '*' : stats.pageHostname; + // This can happen on pages where uBlock does not work + if ( typeof stats.pageHostname !== 'string' || stats.pageHostname === '' ) { + return; + } var elFilter = uDom(ev.target); + var scope = elFilter.attr('data-src') === '*' ? '*' : stats.pageHostname; var onDynamicFilterChanged = function(details) { stats.dynamicFilterResults = details; syncAllDynamicFilters(); }; messager.send({ what: 'toggleDynamicFilter', - hostname: scope, + pageHostname: stats.pageHostname, + srcHostname: scope, + desHostname: elFilter.attr('data-des'), requestType: elFilter.attr('data-type'), - firstParty: elFilter.attr('data-first-party') !== null, - block: elFilter.hasClassName('blocked') === false, - pageHostname: stats.pageHostname + block: elFilter.hasClassName('blocked') === false }, onDynamicFilterChanged); - }; /******************************************************************************/ var toggleDynamicFiltering = function(ev) { - // Discard events destined to child elements. - if ( ev !== undefined && ev.target !== this ) { - return; - } - var el = uDom('#dynamicFilteringToggler'); - el.toggleClass('on'); + var el = uDom('body'); + el.toggleClass('dynamicFilteringEnabled'); messager.send({ what: 'userSettings', name: 'dynamicFilteringEnabled', - value: el.hasClassName('on') + value: el.hasClassName('dynamicFilteringEnabled') }); }; @@ -285,7 +283,7 @@ var installEventHandlers = function() { uDom('#gotoPick').on('click', gotoPick); uDom('a[href^=http]').on('click', gotoLink); uDom('#dynamicFilteringToggler').on('click', toggleDynamicFiltering); - uDom('.dynamicFiltering').on('click', 'div', onDynamicFilterClicked); + uDom('#dynamicFilteringContainer').on('click', 'span[data-type]', onDynamicFilterClicked); }; /******************************************************************************/ diff --git a/src/js/net-filtering.js b/src/js/static-net-filtering.js similarity index 83% rename from src/js/net-filtering.js rename to src/js/static-net-filtering.js index 5f9cd674b..3e2647a69 100644 --- a/src/js/net-filtering.js +++ b/src/js/static-net-filtering.js @@ -19,7 +19,7 @@ Home: https://github.com/gorhill/uBlock */ -/* jshint bitwise: false */ +/* jshint bitwise: false, esnext: true */ /* global µBlock */ // Older Safari throws an exception for const when it's used with 'use strict'. @@ -27,7 +27,7 @@ /******************************************************************************/ -µBlock.netFilteringEngine = (function(){ +µBlock.staticNetFilteringEngine = (function(){ /******************************************************************************/ @@ -1284,7 +1284,6 @@ var FilterContainer = function() { this.buckets = new Array(4); this.blockedAnyPartyHostnames = new µb.LiquidDict(); this.blocked3rdPartyHostnames = new µb.LiquidDict(); - this.dynamicFilters = {}; this.filterParser = new FilterParser(); this.reset(); }; @@ -1642,193 +1641,6 @@ FilterContainer.prototype.addToCategory = function(category, tokenKey, filter) { /******************************************************************************/ -// Dynamic filters - -// Bits: -// 0: inline script blocked -// 1: inline script allowed -// 2: first-party script blocked -// 3: first-party script allowed -// 4: third-party script blocked -// 5: third-party script allowed -// 6: first-party frame blocked -// 7: first-party frame allowed -// 8: third-party frame blocked -// 9: third-party frame allowed -// -// I chose separate bits for blocked/unblocked as I want to have an "undefined" -// state, which will be used to inherit from wider-scoped filters. -// -// undefined: 0x00 -// blocked: 0x01 -// allowed: 0x02 -// unused: 0x03 - -FilterContainer.prototype.dynamicFilterBitOffsets = {}; -FilterContainer.prototype.dynamicFilterBitOffsets[FirstParty | typeNameToTypeValue['inline-script']] = 0; -FilterContainer.prototype.dynamicFilterBitOffsets[FirstParty | typeNameToTypeValue.script] = 2; -FilterContainer.prototype.dynamicFilterBitOffsets[ThirdParty | typeNameToTypeValue.script] = 4; -FilterContainer.prototype.dynamicFilterBitOffsets[FirstParty | typeNameToTypeValue.sub_frame] = 6; -FilterContainer.prototype.dynamicFilterBitOffsets[ThirdParty | typeNameToTypeValue.sub_frame] = 8; -FilterContainer.prototype.dynamicFiltersMagicId = 'numrebvoacir'; - -/******************************************************************************/ - -FilterContainer.prototype.dynamicFilterSet = function(hostname, requestType, firstParty, value) { - if ( typeNameToTypeValue.hasOwnProperty(requestType) === false ) { - return false; - } - var party = firstParty ? FirstParty : ThirdParty; - var categoryKey = party | typeNameToTypeValue[requestType]; - if ( this.dynamicFilterBitOffsets.hasOwnProperty(categoryKey) === false ) { - return false; - } - var bitOffset = this.dynamicFilterBitOffsets[categoryKey]; - var oldFilter = this.dynamicFilters[hostname] || 0; - var newFilter = (oldFilter & ~(0x0003 << bitOffset)) | (value << bitOffset); - if ( newFilter === oldFilter ) { - return false; - } - if ( newFilter === 0 ) { - delete this.dynamicFilters[hostname]; - } else { - this.dynamicFilters[hostname] = newFilter; - } - return true; -}; - -/******************************************************************************/ - -FilterContainer.prototype.dynamicFilterBlock = function(hostname, requestType, firstParty) { - if ( typeof hostname !== 'string' || hostname === '' ) { - return false; - } - var result = this.matchDynamicFilters(hostname, requestType, firstParty); - if ( result !== '' && result.slice(0, 2) !== '@@' ) { - return false; - } - this.dynamicFilterSet(hostname, requestType, firstParty, 0x00); - result = this.matchDynamicFilters(hostname, requestType, firstParty); - if ( result !== '' && result.slice(0, 2) !== '@@' ) { - return true; - } - this.dynamicFilterSet(hostname, requestType, firstParty, 0x01); - return true; -}; - -/******************************************************************************/ - -FilterContainer.prototype.dynamicFilterUnblock = function(hostname, requestType, firstParty) { - if ( typeof hostname !== 'string' || hostname === '' ) { - return false; - } - var result = this.matchDynamicFilters(hostname, requestType, firstParty); - if ( result === '' || result.slice(0, 2) === '@@' ) { - return false; - } - this.dynamicFilterSet(hostname, requestType, firstParty, 0x00); - result = this.matchDynamicFilters(hostname, requestType, firstParty); - if ( result === '' || result.slice(0, 2) === '@@' ) { - return true; - } - this.dynamicFilterSet(hostname, requestType, firstParty, 0x02); - return true; -}; - -/******************************************************************************/ - -FilterContainer.prototype.dynamicFilterStateToType = { - 0x0001: '', - 0x0002: '@@', - 0x0004: '', - 0x0008: '@@', - 0x0010: '', - 0x0020: '@@', - 0x0040: '', - 0x0080: '@@', - 0x0100: '', - 0x0200: '@@' -}; - -FilterContainer.prototype.dynamicFilterStateToOption = { - 0x0001: '$inline-script,important', - 0x0002: '$inline-script', - 0x0004: '$~third-party,script,important', - 0x0008: '$~third-party,script', - 0x0010: '$third-party,script,important', - 0x0020: '$third-party,script', - 0x0040: '$~third-party,subdocument,important', - 0x0080: '$~third-party,subdocument', - 0x0100: '$third-party,subdocument,important', - 0x0200: '$third-party,subdocument' -}; - -FilterContainer.prototype.matchDynamicFilters = function(hostname, requestType, firstParty) { - if ( typeof hostname !== 'string' || hostname === '' ) { - return ''; - } - var party = firstParty ? FirstParty : ThirdParty; - if ( typeNameToTypeValue.hasOwnProperty(requestType) === false ) { - return ''; - } - var categoryKey = party | typeNameToTypeValue[requestType]; - if ( this.dynamicFilterBitOffsets.hasOwnProperty(categoryKey) === false ) { - return ''; - } - var bitOffset = this.dynamicFilterBitOffsets[categoryKey]; - var bitMask = 0x0003 << bitOffset; - var bitState, pos; - for ( ;; ) { - if ( this.dynamicFilters.hasOwnProperty(hostname) !== false ) { - bitState = this.dynamicFilters[hostname] & bitMask; - if ( bitState !== 0 ) { - if ( hostname !== '*' ) { - hostname = '||' + hostname + '^'; - } - return this.dynamicFilterStateToType[bitState] + - hostname + - this.dynamicFilterStateToOption[bitState]; - } - } - pos = hostname.indexOf('.'); - if ( pos === -1 ) { - if ( hostname === '*' ) { - return ''; - } - hostname = '*'; - } else { - hostname = hostname.slice(pos + 1); - } - } - // unreachable -}; - -/******************************************************************************/ - - -FilterContainer.prototype.selfieFromDynamicFilters = function() { - var bin = { - magicId: this.dynamicFiltersMagicId, - filters: this.dynamicFilters - }; - return JSON.stringify(bin); -}; - -/******************************************************************************/ - -FilterContainer.prototype.dynamicFiltersFromSelfie = function(selfie) { - if ( selfie === '' ) { - return; - } - var bin = JSON.parse(selfie); - if ( bin.magicId !== this.dynamicFiltersMagicId ) { - return; - } - this.dynamicFilters = bin.filters; -}; - -/******************************************************************************/ - // Since the addition of the `important` evaluation, this means it is now // likely that the url will have to be scanned more than once. So this is // to ensure we do it once only, and reuse results. @@ -1858,15 +1670,9 @@ FilterContainer.prototype.tokenize = function(url) { /******************************************************************************/ -FilterContainer.prototype.matchTokens = function(url) { - var buckets = this.buckets; - var bucket0 = buckets[0]; - var bucket1 = buckets[1]; - var bucket2 = buckets[2]; - var bucket3 = buckets[3]; - +FilterContainer.prototype.matchTokens = function(bucket, url) { var tokens = this.tokens; - var tokenEntry, beg, token, f; + var tokenEntry, token, f; var i = 0; for (;;) { tokenEntry = tokens[i++]; @@ -1874,30 +1680,9 @@ FilterContainer.prototype.matchTokens = function(url) { if ( token === '' ) { break; } - beg = tokenEntry.beg; - if ( bucket0 !== undefined ) { - f = bucket0[token]; - if ( f !== undefined && f.match(url, beg) !== false ) { - return f; - } - } - if ( bucket1 !== undefined ) { - f = bucket1[token]; - if ( f !== undefined && f.match(url, beg) !== false ) { - return f; - } - } - if ( bucket2 !== undefined ) { - f = bucket2[token]; - if ( f !== undefined && f.match(url, beg) !== false ) { - return f; - } - } - if ( bucket3 !== undefined ) { - f = bucket3[token]; - if ( f !== undefined && f.match(url, beg) !== false ) { - return f; - } + f = bucket[token]; + if ( f !== undefined && f.match(url, tokenEntry.beg) !== false ) { + return f; } } return false; @@ -1955,73 +1740,77 @@ FilterContainer.prototype.match3rdPartyHostname = function(requestHostname) { // Some type of requests are exceptional, they need custom handling, // not the generic handling. -FilterContainer.prototype.matchStringExactType = function(pageDetails, requestURL, requestType) { +FilterContainer.prototype.matchStringExactType = function(context, requestURL, requestType) { var url = requestURL.toLowerCase(); var requestHostname = µb.URI.hostnameFromURI(requestURL); - - // Evaluate dynamic filters first. "Block" dynamic filters are always - // "important", they override everything else. - var bf = this.matchDynamicFilters( - pageDetails.rootHostname, - requestType, - isFirstParty(pageDetails.rootDomain, requestHostname) - ); - if ( bf !== '' && bf.slice(0, 2) !== '@@' ) { - return bf; - } - - var party = isFirstParty(pageDetails.pageDomain, requestHostname) ? FirstParty : ThirdParty; + var party = isFirstParty(context.pageDomain, requestHostname) ? FirstParty : ThirdParty; // This will be used by hostname-based filters - pageHostname = pageDetails.pageHostname || ''; + pageHostname = context.pageHostname || ''; var type = typeNameToTypeValue[requestType]; var categories = this.categories; - var buckets = this.buckets; + var bucket; // Tokenize only once this.tokenize(url); - // We are testing for a specific type, skip "any type" buckets - buckets[0] = buckets[1] = undefined; - // https://github.com/gorhill/uBlock/issues/139 // Test against important block filters - buckets[2] = categories[this.makeCategoryKey(BlockAnyParty | Important | type)]; - buckets[3] = categories[this.makeCategoryKey(BlockAction | Important | type | party)]; - bf = this.matchTokens(url); - if ( bf !== false ) { - return bf.toString(); + if ( bucket = categories[this.makeCategoryKey(BlockAnyParty | Important | type)] ) { + bf = this.matchTokens(bucket, url); + if ( bf !== false ) { + return 'sb:' + bf.toString(); + } + } + if ( bucket = categories[this.makeCategoryKey(BlockAction | Important | type | party)] ) { + bf = this.matchTokens(bucket, url); + if ( bf !== false ) { + return 'sb:' + bf.toString(); + } } // Test against block filters + bf = false; + if ( bucket = categories[this.makeCategoryKey(BlockAnyParty | type)] ) { + bf = this.matchTokens(bucket, url); + } + if ( bf === false ) { + if ( bucket = categories[this.makeCategoryKey(BlockAction | type | party)] ) { + bf = this.matchTokens(bucket, url); + } + } // If there is no block filter, no need to test against allow filters - buckets[2] = categories[this.makeCategoryKey(BlockAnyParty | type)]; - buckets[3] = categories[this.makeCategoryKey(BlockAction | type | party)]; - bf = this.matchTokens(url); if ( bf === false ) { return ''; } // Test against allow filters - buckets[2] = categories[this.makeCategoryKey(AllowAnyParty | type)]; - buckets[3] = categories[this.makeCategoryKey(AllowAction | type | party)]; - var af = this.matchTokens(url); - if ( af !== false ) { - return '@@' + af.toString(); + var af; + if ( bucket = categories[this.makeCategoryKey(AllowAnyParty | type)] ) { + af = this.matchTokens(bucket, url); + if ( af !== false ) { + return 'sa:' + af.toString(); + } + } + if ( bucket = categories[this.makeCategoryKey(AllowAction | type | party)] ) { + af = this.matchTokens(bucket, url); + if ( af !== false ) { + return 'sa:' + af.toString(); + } } - return bf.toString(); + return 'sb:' + bf.toString(); }; /******************************************************************************/ -FilterContainer.prototype.matchString = function(pageDetails, requestURL, requestType) { +FilterContainer.prototype.matchString = function(context) { // https://github.com/gorhill/httpswitchboard/issues/239 // Convert url to lower case: // `match-case` option not supported, but then, I saw only one // occurrence of it in all the supported lists (bulgaria list). - var url = requestURL.toLowerCase(); + var url = context.requestURL.toLowerCase(); // The logic here is simple: // @@ -2044,27 +1833,15 @@ FilterContainer.prototype.matchString = function(pageDetails, requestURL, reques // filters are tested *only* if there is a (unlikely) hit on a block // filter. - var requestHostname = µb.URI.hostnameFromURI(requestURL); - - // Evaluate dynamic filters first. "Block" dynamic filters are always - // "important", they override everything else. - var bf = this.matchDynamicFilters( - pageDetails.rootHostname, - requestType, - isFirstParty(pageDetails.rootDomain, requestHostname) - ); - if ( bf !== '' && bf.slice(0, 2) !== '@@' ) { - return bf; - } - - var party = isFirstParty(pageDetails.pageDomain, requestHostname) ? FirstParty : ThirdParty ; + var requestHostname = context.requestHostname; + var party = isFirstParty(context.pageDomain, requestHostname) ? FirstParty : ThirdParty; // This will be used by hostname-based filters - pageHostname = pageDetails.pageHostname || ''; + pageHostname = context.pageHostname || ''; - var type = typeNameToTypeValue[requestType]; + var type = typeNameToTypeValue[context.requestType]; var categories = this.categories; - var buckets = this.buckets; + var bucket; // Tokenize only once this.tokenize(url); @@ -2074,13 +1851,29 @@ FilterContainer.prototype.matchString = function(pageDetails, requestURL, reques // The purpose of the `important` option is to reverse the order of // evaluation. Normally, it is "evaluate block then evaluate allow", with // the `important` property it is "evaluate allow then evaluate block". - buckets[0] = categories[this.makeCategoryKey(BlockAnyTypeAnyParty | Important)]; - buckets[1] = categories[this.makeCategoryKey(BlockAnyType | Important | party)]; - buckets[2] = categories[this.makeCategoryKey(BlockAnyParty | Important | type)]; - buckets[3] = categories[this.makeCategoryKey(BlockAction | Important | type | party)]; - bf = this.matchTokens(url); - if ( bf !== false ) { - return bf.toString() + '$important'; + if ( bucket = categories[this.makeCategoryKey(BlockAnyTypeAnyParty | Important)] ) { + bf = this.matchTokens(bucket, url); + if ( bf !== false ) { + return 'sb:' + bf.toString() + '$important'; + } + } + if ( bucket = categories[this.makeCategoryKey(BlockAnyType | Important | party)] ) { + bf = this.matchTokens(bucket, url); + if ( bf !== false ) { + return 'sb:' + bf.toString() + '$important'; + } + } + if ( bucket = categories[this.makeCategoryKey(BlockAnyParty | Important | type)] ) { + bf = this.matchTokens(bucket, url); + if ( bf !== false ) { + return 'sb:' + bf.toString() + '$important'; + } + } + if ( bucket = categories[this.makeCategoryKey(BlockAction | Important | type | party)] ) { + bf = this.matchTokens(bucket, url); + if ( bf !== false ) { + return 'sb:' + bf.toString() + '$important'; + } } // Test hostname-based block filters @@ -2091,11 +1884,24 @@ FilterContainer.prototype.matchString = function(pageDetails, requestURL, reques // Test against block filters if ( bf === false ) { - buckets[0] = categories[this.makeCategoryKey(BlockAnyTypeAnyParty)]; - buckets[1] = categories[this.makeCategoryKey(BlockAnyType | party)]; - buckets[2] = categories[this.makeCategoryKey(BlockAnyParty | type)]; - buckets[3] = categories[this.makeCategoryKey(BlockAction | type | party)]; - bf = this.matchTokens(url); + if ( bucket = categories[this.makeCategoryKey(BlockAnyTypeAnyParty)] ) { + bf = this.matchTokens(bucket, url); + } + } + if ( bf === false ) { + if ( bucket = categories[this.makeCategoryKey(BlockAnyType | party)] ) { + bf = this.matchTokens(bucket, url); + } + } + if ( bf === false ) { + if ( bucket = categories[this.makeCategoryKey(BlockAnyParty | type)] ) { + bf = this.matchTokens(bucket, url); + } + } + if ( bf === false ) { + if ( bucket = categories[this.makeCategoryKey(BlockAction | type | party)] ) { + bf = this.matchTokens(bucket, url); + } } // If there is no block filter, no need to test against allow filters @@ -2104,16 +1910,33 @@ FilterContainer.prototype.matchString = function(pageDetails, requestURL, reques } // Test against allow filters - buckets[0] = categories[this.makeCategoryKey(AllowAnyTypeAnyParty)]; - buckets[1] = categories[this.makeCategoryKey(AllowAnyType | party)]; - buckets[2] = categories[this.makeCategoryKey(AllowAnyParty | type)]; - buckets[3] = categories[this.makeCategoryKey(AllowAction | type | party)]; - var af = this.matchTokens(url); - if ( af !== false ) { - return '@@' + af.toString(); + var af; + if ( bucket = categories[this.makeCategoryKey(AllowAnyTypeAnyParty)] ) { + af = this.matchTokens(bucket, url); + if ( af !== false ) { + return 'sa:' + af.toString(); + } + } + if ( bucket = categories[this.makeCategoryKey(AllowAnyType | party)] ) { + af = this.matchTokens(bucket, url); + if ( af !== false ) { + return 'sa:' + af.toString(); + } + } + if ( bucket = categories[this.makeCategoryKey(AllowAnyParty | type)] ) { + af = this.matchTokens(bucket, url); + if ( af !== false ) { + return 'sa:' + af.toString(); + } + } + if ( bucket = categories[this.makeCategoryKey(AllowAction | type | party)] ) { + af = this.matchTokens(bucket, url); + if ( af !== false ) { + return 'sa:' + af.toString(); + } } - return bf.toString(); + return 'sb:' + bf.toString(); }; /******************************************************************************/ diff --git a/src/js/stats.js b/src/js/stats.js index 9eb3b18ae..df5a92452 100644 --- a/src/js/stats.js +++ b/src/js/stats.js @@ -21,12 +21,13 @@ /* jshint bitwise: false */ /* global uDom */ -'use strict'; /******************************************************************************/ (function() { +'use strict'; + /******************************************************************************/ var messager = vAPI.messaging.channel('stats.js'); @@ -77,8 +78,8 @@ var renderURL = function(url, filter) { if ( pos > 0 ) { reText = reText.slice(0, pos); } - if ( reText.slice(0, 2) === '@@' ) { - reText = reText.slice(2); + if ( reText.charAt(0) === 's' ) { + reText = reText.slice(3); } if ( reText === '*' ) { reText = '\\*'; diff --git a/src/js/storage.js b/src/js/storage.js index 694750897..b6727229b 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -149,7 +149,7 @@ return; } µb.mergeFilterText(content); - µb.netFilteringEngine.freeze(); + µb.staticNetFilteringEngine.freeze(); µb.cosmeticFilteringEngine.freeze(); µb.destroySelfie(); µb.toSelfieAsync(); @@ -278,7 +278,7 @@ } var loadBlacklistsEnd = function() { - µb.netFilteringEngine.freeze(); + µb.staticNetFilteringEngine.freeze(); µb.cosmeticFilteringEngine.freeze(); vAPI.storage.set({ 'remoteBlacklists': µb.remoteBlacklists }); vAPI.messaging.broadcast({ what: 'loadUbiquitousBlacklistCompleted' }); @@ -296,7 +296,7 @@ var loadBlacklistsStart = function(lists) { µb.remoteBlacklists = lists; - µb.netFilteringEngine.reset(); + µb.staticNetFilteringEngine.reset(); µb.cosmeticFilteringEngine.reset(); µb.destroySelfie(); var locations = Object.keys(lists); @@ -328,17 +328,17 @@ µBlock.mergeFilterList = function(details) { // console.log('µBlock > mergeFilterList from "%s": "%s..."', details.path, details.content.slice(0, 40)); - var netFilteringEngine = this.netFilteringEngine; + var staticNetFilteringEngine = this.staticNetFilteringEngine; var cosmeticFilteringEngine = this.cosmeticFilteringEngine; - var duplicateCount = netFilteringEngine.duplicateCount + cosmeticFilteringEngine.duplicateCount; - var acceptedCount = netFilteringEngine.acceptedCount + cosmeticFilteringEngine.acceptedCount; + var duplicateCount = staticNetFilteringEngine.duplicateCount + cosmeticFilteringEngine.duplicateCount; + var acceptedCount = staticNetFilteringEngine.acceptedCount + cosmeticFilteringEngine.acceptedCount; this.mergeFilterText(details.content); // For convenience, store the number of entries for this // blacklist, user might be happy to know this information. - duplicateCount = netFilteringEngine.duplicateCount + cosmeticFilteringEngine.duplicateCount - duplicateCount; - acceptedCount = netFilteringEngine.acceptedCount + cosmeticFilteringEngine.acceptedCount - acceptedCount; + duplicateCount = staticNetFilteringEngine.duplicateCount + cosmeticFilteringEngine.duplicateCount - duplicateCount; + acceptedCount = staticNetFilteringEngine.acceptedCount + cosmeticFilteringEngine.acceptedCount - acceptedCount; var filterListMeta = this.remoteBlacklists[details.path]; @@ -363,7 +363,7 @@ // Useful references: // https://adblockplus.org/en/filter-cheatsheet // https://adblockplus.org/en/filters - var netFilteringEngine = this.netFilteringEngine; + var staticNetFilteringEngine = this.staticNetFilteringEngine; var cosmeticFilteringEngine = this.cosmeticFilteringEngine; var parseCosmeticFilters = this.userSettings.parseAllABPHideFilters; @@ -433,7 +433,7 @@ continue; } - netFilteringEngine.add(matches[0]); + staticNetFilteringEngine.add(matches[0]); } }; @@ -520,7 +520,7 @@ magic: this.selfieMagic, publicSuffixList: publicSuffixList.toSelfie(), filterLists: this.remoteBlacklists, - netFilteringEngine: this.netFilteringEngine.toSelfie(), + staticNetFilteringEngine: this.staticNetFilteringEngine.toSelfie(), cosmeticFilteringEngine: this.cosmeticFilteringEngine.toSelfie() }; vAPI.storage.set({ selfie: selfie }); @@ -565,7 +565,7 @@ } // console.log('µBlock.fromSelfie> selfie looks good'); µb.remoteBlacklists = selfie.filterLists; - µb.netFilteringEngine.fromSelfie(selfie.netFilteringEngine); + µb.staticNetFilteringEngine.fromSelfie(selfie.staticNetFilteringEngine); µb.cosmeticFilteringEngine.fromSelfie(selfie.cosmeticFilteringEngine); callback(true); }; @@ -649,12 +649,21 @@ // Important: block remote fetching for when loading assets at launch // time. µb.assets.allowRemoteFetch = false; - µb.assets.autoUpdate = settings.autoUpdate; - µb.netFilteringEngine.dynamicFiltersFromSelfie(settings.dynamicFilteringSelfie); µb.fromSelfie(onSelfieReady); µb.mirrors.toggle(settings.experimentalEnabled); µb.contextMenu.toggle(settings.contextMenuEnabled); + + if ( typeof settings.dynamicFilteringSelfie === 'string' ) { + if ( settings.dynamicFilteringString === '' && settings.dynamicFilteringSelfie !== '' ) { + µb.dynamicNetFilteringEngine.fromObsoleteSelfie(settings.dynamicFilteringSelfie); + µb.userSettings.dynamicFilteringString = µb.dynamicNetFilteringEngine.toString(); + µb.XAL.keyvalSetOne('dynamicFilteringString', µb.userSettings.dynamicFilteringString); + } + delete µb.userSettings.dynamicFilteringSelfie; + µb.XAL.keyvalRemoveOne('dynamicFilteringSelfie'); + } + µb.dynamicNetFilteringEngine.fromString(µb.userSettings.dynamicFilteringString); }; this.loadUserSettings(onUserSettingsReady); diff --git a/src/js/tab.js b/src/js/tab.js index 3b3a2f791..048548b90 100644 --- a/src/js/tab.js +++ b/src/js/tab.js @@ -55,6 +55,8 @@ vAPI.tabs.onClosed = function(tabId) { // https://github.com/gorhill/uBlock/issues/297 vAPI.tabs.onPopup = function(details) { + //console.debug('vAPI.tabs.onPopup: url="%s"', details.url); + var pageStore = µBlock.pageStoreFromTabId(details.sourceTabId); if ( !pageStore ) { return; @@ -65,7 +67,7 @@ vAPI.tabs.onPopup = function(details) { // https://github.com/gorhill/uBlock/issues/323 // If popup URL is whitelisted, do not block it if ( µBlock.getNetFilteringSwitch(requestURL) ) { - result = µBlock.netFilteringEngine.matchStringExactType(pageStore, requestURL, 'popup'); + result = µBlock.staticNetFilteringEngine.matchStringExactType(pageStore, requestURL, 'popup'); } // https://github.com/gorhill/uBlock/issues/91 diff --git a/src/js/traffic.js b/src/js/traffic.js index 72d7f59fb..b4c86fb07 100644 --- a/src/js/traffic.js +++ b/src/js/traffic.js @@ -80,7 +80,6 @@ var onBeforeRequest = function(details) { // https://github.com/gorhill/uBlock/issues/114 var requestContext = pageStore; - var frameStore; var frameId = details.frameId; if ( frameId > 0 ) { @@ -89,7 +88,12 @@ var onBeforeRequest = function(details) { } } - var result = pageStore.filterRequest(requestContext, requestType, requestURL); + // Setup context and evaluate + requestContext.requestURL = requestURL; + requestContext.requestHostname = µb.URI.hostnameFromURI(requestURL); + requestContext.requestType = requestType; + + var result = pageStore.filterRequest(requestContext); // Not blocked if ( pageStore.boolFromResult(result) === false ) { @@ -208,11 +212,12 @@ var onBeforeSendHeaders = function(details) { var pageDetails = { pageHostname: referrerHostname, pageDomain: µburi.domainFromHostname(referrerHostname), + firstParty: false }; pageDetails.rootHostname = pageDetails.pageHostname; pageDetails.rootDomain = pageDetails.pageDomain; //console.debug('Referrer="%s"', referrer); - var result = µb.netFilteringEngine.matchStringExactType(pageDetails, requestURL, 'popup'); + var result = µb.staticNetFilteringEngine.matchStringExactType(pageDetails, requestURL, 'popup'); // Not blocked? if ( result === '' || result.slice(0, 2) === '@@' ) { @@ -251,22 +256,14 @@ var onHeadersReceived = function(details) { // https://github.com/gorhill/uBlock/issues/384 pageStore.skipLocalMirroring = headerValue(details.responseHeaders, 'content-security-policy'); - - var result = ''; - if ( pageStore.getNetFilteringSwitch() ) { - result = µb.netFilteringEngine.matchStringExactType(pageStore, details.url, 'inline-script'); - } - - // Not blocked? - if ( result === '' || result.slice(0, 2) === '@@' ) { + pageStore.requestURL = details.url; + pageStore.requestHostname = µb.URI.hostnameFromURI(details.url); + pageStore.requestType = 'inline-script'; + var result = pageStore.filterRequest(pageStore); + if ( result === '' ) { return; } - // Record request - if ( result !== '' ) { - pageStore.recordResult('script', details.url, result); - } - // Blocked pageStore.perLoadBlockedRequestCount++; µb.localSettings.blockedRequestCount++; diff --git a/src/js/ublock.js b/src/js/ublock.js index ea1fc337f..82bbe9592 100644 --- a/src/js/ublock.js +++ b/src/js/ublock.js @@ -309,23 +309,88 @@ var matchWhitelistDirective = function(url, hostname, directive) { µBlock.toggleDynamicFilter = function(details) { var changed = false; if ( details.block ) { - changed = this.netFilteringEngine.dynamicFilterBlock(details.hostname, details.requestType, details.firstParty); + changed = this.dynamicNetFilteringEngine.blockCell(details.srcHostname, details.desHostname, details.requestType); } else { - changed = this.netFilteringEngine.dynamicFilterUnblock(details.hostname, details.requestType, details.firstParty); + changed = this.dynamicNetFilteringEngine.unsetCell(details.srcHostname, details.desHostname, details.requestType); } if ( !changed ) { return; } - this.userSettings.dynamicFilteringSelfie = this.netFilteringEngine.selfieFromDynamicFilters(); - this.XAL.keyvalSetOne('dynamicFilteringSelfie', this.userSettings.dynamicFilteringSelfie); + this.userSettings.dynamicFilteringString = this.dynamicNetFilteringEngine.toString(); + this.XAL.keyvalSetOne('dynamicFilteringString', this.userSettings.dynamicFilteringString); // https://github.com/gorhill/uBlock/issues/420 - if ( details.requestType === 'sub_frame' && !details.block ) { + if ( details.requestType === '3p-frame' && !details.block ) { this.cosmeticFilteringEngine.removeFromSelectorCache(details.hostname, 'net'); } }; /******************************************************************************/ +µBlock.isFirstParty = function(firstPartyDomain, hostname) { + if ( hostname.slice(0 - firstPartyDomain.length) !== firstPartyDomain ) { + return false; + } + // Be sure to not confuse 'example.com' with 'anotherexample.com' + var c = hostname.charAt(hostname.length - firstPartyDomain.length - 1); + return c === '.' || c === ''; +}; + +/******************************************************************************/ + +// The core logic to evaluate requests through dynamic/static filtering +// is here. + +µBlock.filterRequest = function(context) { + // Given that: + // - Dynamic filtering override static filtering + // - Evaluating dynamic filtering is much faster than static filtering + // We evaluate dynamic filtering first, and hopefully we can skip + // evaluation of static filtering. + // Dynamic filtering evaluation is ordered from most-specific to least- + // specific. + var df = this.dynamicNetFilteringEngine; + var rootHostname = context.rootHostname; + var requestHostname = context.requestHostname; + var requestType = context.requestType; + + // Dynamic filters: + // 1. specific source, specific destination, any type, allow/block + // 2. any source, specific destination, any type, allow/block + df.evaluateCellZY(rootHostname, requestHostname, '*'); + if ( df.mustBlockOrAllow() ) { + return df.toFilterString(); + } + + // Dynamic filters: + // 3. specific source, any destination, specific type, allow/block + // 4. any source, any destination, specific type, allow/block + if ( requestType === 'script' ) { + df.evaluateCellZY(rootHostname, requestHostname, this.isFirstParty(rootHostname, requestHostname) ? '1p-script' : '3p-script'); + if ( df.mustBlockOrAllow() ) { + return df.toFilterString(); + } + } + + if ( requestType === 'sub_frame' ) { + if ( this.isFirstParty(rootHostname, requestHostname) === false ) { + df.evaluateCellZY(rootHostname, requestHostname, '3p-frame'); + if ( df.mustBlockOrAllow() ) { + return df.toFilterString(); + } + } + } + + df.evaluateCellZY(rootHostname, requestHostname, requestType); + if ( df.mustBlockOrAllow() ) { + return df.toFilterString(); + } + + // 5. Static filtering never override dynamic filtering + return this.staticNetFilteringEngine.matchString(context); +}; + +/******************************************************************************/ + })(); \ No newline at end of file diff --git a/src/js/xal.js b/src/js/xal.js index 6d3366b50..a8bf273a6 100644 --- a/src/js/xal.js +++ b/src/js/xal.js @@ -48,6 +48,12 @@ exports.keyvalSetMany = function(dict, callback) { /******************************************************************************/ +exports.keyvalRemoveOne = function(key, callback) { + vAPI.storage.remove(key, callback || noopFunc); +}; + +/******************************************************************************/ + exports.keyvalRemoveAll = function(callback) { vAPI.storage.clear(callback || noopFunc); }; diff --git a/src/popup.html b/src/popup.html index e411eb228..952232fd6 100644 --- a/src/popup.html +++ b/src/popup.html @@ -11,9 +11,17 @@

v

+
+
images
+
inline scripts
+
1st-party scripts
+
3rd-party scripts
+
3rd-party frames
+
+

-

+

  @@ -24,65 +32,6 @@

?

-
-
-
-
-
- ? -
- - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<script>
-
<iframe>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<script>
-
<iframe>
-
-