diff --git a/doc/img/filtering-overview-plain.svg b/doc/img/filtering-overview-plain.svg index 9bf6278ec..53cd343cf 100644 --- a/doc/img/filtering-overview-plain.svg +++ b/doc/img/filtering-overview-plain.svg @@ -9,7 +9,7 @@ xmlns="http://www.w3.org/2000/svg" version="1.1" width="732" - height="1540.3398" + height="1740.3398" id="svg2"> @@ -517,6 +517,66 @@ id="path11883" style="fill:#000000;fill-rule:evenodd;stroke:#000000;stroke-width:1pt" /> + + + + + + + + + + + + + + + @@ -603,15 +663,15 @@ rx="0.5" ry="0.49999997" x="240" - y="-97.637817" + y="102.36218" id="rect3783-4" style="fill:#ffeeaa;fill-opacity:1;fill-rule:nonzero;stroke:none" /> whitelisted? local dynamicfiltering rule? allow no noop no global dynamicfiltering rule? allow noop no static filtering? - advanced usermode? yes no no filter URL of page block block exception filter block filter remote server yes your browser + + + url filtering rule? + + allow + + block + no + + + noop + diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index fbdc76c64..be29dbf22 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "uBlock Origin", - "version": "0.9.7.5", + "version": "0.9.7.6", "default_locale": "en", "description": "__MSG_extShortDesc__", diff --git a/src/background.html b/src/background.html index 65b6ea016..786cd81c2 100644 --- a/src/background.html +++ b/src/background.html @@ -17,6 +17,7 @@ + @@ -28,7 +29,6 @@ - diff --git a/src/css/logger-ui.css b/src/css/logger-ui.css index 00b8f9b62..bb620aa20 100644 --- a/src/css/logger-ui.css +++ b/src/css/logger-ui.css @@ -106,25 +106,25 @@ body.f table tr.f { display: none; } -#content table tr.cat_info { +#content tr.cat_info { color: #00f; } -#content table tr.blocked { +#content tr.blocked { background-color: rgba(192, 0, 0, 0.1); } -body.colorBlind #content table tr.blocked { +body.colorBlind #content tr.blocked { background-color: rgba(0, 19, 110, 0.1); } -#content table tr.allowed { +#content tr.allowed { background-color: rgba(0, 160, 0, 0.1); } -body.colorBlind #content table tr.allowed { +body.colorBlind #content tr.allowed { background-color: rgba(255, 194, 57, 0.1) } -#content table tr.cosmetic { +#content tr.cb { background-color: rgba(255, 255, 0, 0.1); } -#content table tr.maindoc { +#content tr.maindoc { background-color: #666; color: white; text-align: center; @@ -140,13 +140,13 @@ body #content td { word-break: break-all; word-wrap: break-word; } -#content table tr td { +#content tr td { border-top: 1px solid #ccc; } -#content table tr td:first-of-type { +#content tr td:first-of-type { border-left: none; } -#content table tr td:last-of-type { +#content tr td:last-of-type { border-right: none; } body.compactView #content td { @@ -155,56 +155,63 @@ body.compactView #content td { white-space: nowrap; } -#content table tr td:nth-of-type(1) { +#content tr td:nth-of-type(1) { text-align: right; white-space: nowrap; } -#content table tr td:nth-of-type(2) { +#content tr td:nth-of-type(2) { text-align: center; white-space: nowrap; } -#content table tr.tab_bts > td:nth-of-type(2):before { +#content tr.tab_bts > td:nth-of-type(2):before { content: '\f070'; font: 1em FontAwesome; } -#content table tr.tab:not(.canMtx) { +#content tr.tab:not(.canMtx) { opacity: 0.3; } -#content table tr.tab:not(.canMtx):hover { +#content tr.tab:not(.canMtx):hover { opacity: 0.7; } -#content table tr.tab:not(.canMtx) > td:nth-of-type(2):before { +#content tr.tab:not(.canMtx) > td:nth-of-type(2):before { content: '\f00d'; font: 1em FontAwesome; } -body:not(.popupOn) #content table tr.canMtx td:nth-of-type(2) { +body:not(.popupOn) #content tr.canMtx td:nth-of-type(2) { cursor: zoom-in; } -body:not(.popupOn) #content table tr.canMtx td:nth-of-type(2):hover { +body:not(.popupOn) #content tr.canMtx td:nth-of-type(2):hover { background: #ccc; } -#content table tr.cat_net td:nth-of-type(3), -#content table tr.cat_cosmetic td:nth-of-type(3) { +#content tr.cat_net td:nth-of-type(3), +#content tr.cat_cosmetic td:nth-of-type(3) { font: 12px monospace; text-align: center; white-space: nowrap; } -#content table tr.cat_net td:nth-of-type(6) > span > b { +#content tr.cat_net td:nth-of-type(3) { + cursor: pointer; + position: relative; + } +#content tr.cat_net td:nth-of-type(3):hover { + background: #ccc; + } +#content tr.cat_net td:nth-of-type(6) > span > b { font-weight: bold; } -#content table tr td:nth-of-type(6) b { +#content tr td:nth-of-type(6) b { font-weight: normal; } -#content table tr.blocked td:nth-of-type(6) b { +#content tr.blocked td:nth-of-type(6) b { background-color: rgba(192, 0, 0, 0.2); } -body.colorBlind #content table tr.blocked td:nth-of-type(6) b { +body.colorBlind #content tr.blocked td:nth-of-type(6) b { background-color: rgba(0, 19, 110, 0.2); } -#content table tr.allowed td:nth-of-type(6) b { +#content tr.allowed td:nth-of-type(6) b { background-color: rgba(0, 160, 0, 0.2); } -body.colorBlind #content table tr.allowed td:nth-of-type(6) b { +body.colorBlind #content tr.allowed td:nth-of-type(6) b { background-color: rgba(255, 194, 57, 0.2); } @@ -255,3 +262,150 @@ body[dir="rtl"] #popupContainer > div { #popupContainer.hide > iframe { display: none; } + +#urlFilteringMenu { + background-color: rgba(0, 0, 0, 0.5); + border: 0; + bottom: 0; + left: 0; + margin: 0; + position: fixed; + right: 0; + top: 0; + z-index: 100; + } +#urlFilteringMenu .dialog { + background-color: white; + border: 1px solid gray; + padding: 0.2em; + position: fixed; + } +#urlFilteringMenu .dialog > div:first-child { + padding: 0.2em 0.2em 0.4em 0.2em; + } +#urlFilteringMenu .dialog > div:first-child > * { + display: inline-block; + vertical-align: middle; + } +#urlFilteringMenu .save { + background-color: #ffe; + border: 1px solid #ddc; + border-radius: 4px; + color: #888; + cursor: pointer; + font-size: 1.8em; + margin-right: 0.1em; + padding: 0.1em 0.5em; + visibility: hidden; + } +body.dirty #urlFilteringMenu .save { + visibility: visible; + } +#urlFilteringMenu .save:hover { + color: black; + } +#urlFilteringMenu select { + font: inherit; + } +#urlFilteringMenu .entries { + font-size: 13px; + max-height: 12em; + max-width: 70vw; + overflow-y: auto; + } +#urlFilteringMenu .entries > div { + background-color: #e6e6e6; + border: 0; + line-height: 2em; + margin: 0; + margin-top: 1px; + overflow: hidden; + padding: 0; + white-space: nowrap; + width: 100%; + } +#urlFilteringMenu .entries > div:first-child { + margin-top: 0; + } +#urlFilteringMenu .entries > div:hover { + background-color: #f0f0f0; + } +#urlFilteringMenu .entries > div > .action { + background-color: transparent; + border: 0; + border-right: 1px solid white; + cursor: pointer; + display: inline-block; + height: 100%; + width: 3.8em; + } +#urlFilteringMenu .entries > div > .action.allow { + background-color: rgba(0, 160, 0, 0.3); + } +body.colorBlind #urlFilteringMenu .entries > div > .action.allow { + background-color: rgba(255, 194, 57, 0.4); + } +#urlFilteringMenu .entries > div > .action.noop { + background-color: rgba(108, 108, 108, 0.3); + } +body.colorBlind #urlFilteringMenu .entries > div > .action.noop { + background-color: rgba(96, 96, 96, 0.4); + } +#urlFilteringMenu .entries > div > .action.block { + background-color: rgba(192, 0, 0, 0.3); + } +body.colorBlind #urlFilteringMenu .entries > div > .action.block { + background-color: rgba(0, 19, 110, 0.4); + } +#urlFilteringMenu .entries > div > .action.allow.own { + background-color: rgba(0, 160, 0, 1); + } +body.colorBlind #urlFilteringMenu .entries > div > .action.allow.own { + background-color: rgba(255, 194, 57, 1); + } +#urlFilteringMenu .entries > div > .action.noop.own { + background-color: rgba(108, 108, 108, 1); + } +#urlFilteringMenu .entries > div > .action.block.own { + background-color: rgba(192, 0, 0, 1); + } +body.colorBlind #urlFilteringMenu .entries > div > .action.block.own { + background-color: rgba(0, 19, 110, 1); + } +#urlFilteringMenu .entries > div > .action > span { + background-color: transparent; + border: 0; + display: inline-block; + height: 100%; + opacity: 0.2; + visibility: hidden; + width: 33.33%; + } +#urlFilteringMenu .entries > div > .action > span:before { + content: '\00A0'; + } +#urlFilteringMenu .entries > div > .action:not(.own):hover > span { + opacity: 0.2; + visibility: visible; + } +#urlFilteringMenu .entries > div > .action:not(.own):hover > span:hover { + opacity: 0.75; + } +#urlFilteringMenu .entries > div > .action > .allow { + background-color: rgb(0, 160, 0); + } +body.colorBlind #urlFilteringMenu .entries > div > .action > .allow { + background-color: rgb(255, 194, 57); + } +#urlFilteringMenu .entries > div > .action > .noop { + background-color: rgb(108, 108, 108); + } +#urlFilteringMenu .entries > div > .action > .block { + background-color: rgb(192, 0, 0); + } +body.colorBlind #urlFilteringMenu .entries > div > .action > .block { + background-color: rgb(0, 19, 110); + } +#urlFilteringMenu .entries > div > .url { + padding: 0 0.25em; + } diff --git a/src/js/dyna-rules.js b/src/js/dyna-rules.js index ce8dcec61..04830611d 100644 --- a/src/js/dyna-rules.js +++ b/src/js/dyna-rules.js @@ -41,7 +41,9 @@ var renderRules = function(details) { var rules, rule, i; // Switches always displayed first -- just like in uMatrix + // Merge url rules and switches: they just look the same rules = details.hnSwitches.split(/\n+/).sort(); + for ( i = 0; i < rules.length; i++ ) { rule = rules[i]; liLeft = liTemplate.clone().text(rule); @@ -120,7 +122,7 @@ function handleImportFilePicker() { .replace(/\n/g, ' * noop\n'); } var request = { - 'what': 'setSessionFirewallRules', + 'what': 'setSessionRules', 'rules': rulesFromHTML('#diff .right li') + '\n' + result }; messager.send(request, renderRules); @@ -183,7 +185,7 @@ var rulesFromHTML = function(selector) { var revertHandler = function() { var request = { - 'what': 'setSessionFirewallRules', + 'what': 'setSessionRules', 'rules': rulesFromHTML('#diff .left li') }; messager.send(request, renderRules); @@ -193,7 +195,7 @@ var revertHandler = function() { var commitHandler = function() { var request = { - 'what': 'setPermanentFirewallRules', + 'what': 'setPermanentRules', 'rules': rulesFromHTML('#diff .right li') }; messager.send(request, renderRules); @@ -217,7 +219,7 @@ var editStopHandler = function() { var parent = uDom(this).ancestors('#diff'); parent.toggleClass('edit', false); var request = { - 'what': 'setSessionFirewallRules', + 'what': 'setSessionRules', 'rules': uDom('#diff .right textarea').val() }; messager.send(request, renderRules); @@ -245,7 +247,7 @@ uDom.onLoad(function() { uDom('#editStopButton').on('click', editStopHandler); uDom('#editCancelButton').on('click', editCancelHandler); - messager.send({ what: 'getFirewallRules' }, renderRules); + messager.send({ what: 'getRules' }, renderRules); }); /******************************************************************************/ diff --git a/src/js/dynamic-net-filtering.js b/src/js/dynamic-net-filtering.js index 16e1d6840..b44fc2131 100644 --- a/src/js/dynamic-net-filtering.js +++ b/src/js/dynamic-net-filtering.js @@ -440,19 +440,21 @@ Matrix.prototype.mustAbort = function() { /******************************************************************************/ Matrix.prototype.toFilterString = function() { + if ( this.r === 0 ) { + return ''; + } if ( this.type === '' ) { return ''; } + var body = this.z + ' ' + this.y + ' ' + this.type; if ( this.r === 1 ) { - return 'db:' + this.z + ' ' + this.y + ' ' + this.type + ' block'; + return 'db:' + body + ' block'; } if ( this.r === 2 ) { - return 'da:' + this.z + ' ' + this.y + ' ' + this.type + ' allow'; + return 'da:' + body + ' allow'; } - if ( this.r === 3 ) { - return 'dn:' + this.z + ' ' + this.y + ' ' + this.type + ' noop'; - } - return ''; + /* this.r === 3 */ + return 'dn:' + body + ' noop'; }; /******************************************************************************/ @@ -529,6 +531,11 @@ Matrix.prototype.fromString = function(text, append) { continue; } + // URL net filtering rules + if ( line.indexOf('://') !== -1 ) { + continue; + } + // Valid rule syntax: // srcHostname desHostname type state @@ -542,6 +549,12 @@ Matrix.prototype.fromString = function(text, append) { continue; } + // Ignore special rules: + // hostname-based switch rules + if ( fields[0].slice(-1) === ':' ) { + continue; + } + srcHostname = punycode.toASCII(fields[0]); desHostname = punycode.toASCII(fields[1]); diff --git a/src/js/logger-ui.js b/src/js/logger-ui.js index c9fd05387..e3619d73a 100644 --- a/src/js/logger-ui.js +++ b/src/js/logger-ui.js @@ -50,6 +50,7 @@ var noTabId = ''; var allTabIds = {}; var allTabIdsToken; var hiddenTemplate = document.querySelector('#hiddenTemplate > span'); +var reRFC3986 = /^([^:\/?#]+:)?(\/\/[^\/?#]*)?([^?#]*)(\?[^#]*)?(#.*)?/; var prettyRequestTypes = { 'main_frame': 'doc', @@ -58,6 +59,13 @@ var prettyRequestTypes = { 'xmlhttprequest': 'xhr' }; +var uglyRequestTypes = { + 'doc': 'main_frame', + 'css': 'stylesheet', + 'frame': 'sub_frame', + 'xhr': 'xmlhttprequest' +}; + var timeOptions = { hour: '2-digit', minute: '2-digit', @@ -84,40 +92,65 @@ var classNameFromTabId = function(tabId) { /******************************************************************************/ +var retextFromStaticFilteringResult = function(result) { + var retext = result.slice(3); + var pos = retext.indexOf('$'); + if ( pos > 0 ) { + retext = retext.slice(0, pos); + } + if ( retext === '*' ) { + return '^.*$'; + } + if ( retext.charAt(0) === '/' && retext.slice(-1) === '/' ) { + return retext.slice(1, -1); + } + return retext + .replace(/\./g, '\\.') + .replace(/\?/g, '\\?') + .replace('||', '') + .replace(/\^/g, '.') + .replace(/^\|/g, '^') + .replace(/\|$/g, '$') + .replace(/\*/g, '.*') + ; +}; + +/******************************************************************************/ + +var retextFromURLFilteringResult = function(result) { + var beg = result.indexOf(' '); + var end = result.indexOf(' ', beg + 1); + var url = result.slice(beg + 1, end); + if ( url === '*' ) { + return '^.*$'; + } + return '^' + url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +}; + +/******************************************************************************/ + // Emphasize hostname in URL, as this is what matters in uMatrix's rules. var nodeFromURL = function(url, filter) { - if ( filter.charAt(0) !== 's' ) { + var filterType = filter.charAt(0); + if ( filterType !== 's' && filterType !== 'l' ) { return document.createTextNode(url); } - // make a regex out of the filter - var reText = filter.slice(3); - var pos = reText.indexOf('$'); - if ( pos > 0 ) { - reText = reText.slice(0, pos); + var retext = ''; + if ( filterType === 's' ) { + retext = retextFromStaticFilteringResult(filter); + } else if ( filterType === 'l' ) { + retext = retextFromURLFilteringResult(filter); } - if ( reText === '*' ) { - reText = '\\*'; - } else if ( reText.charAt(0) === '/' && reText.slice(-1) === '/' ) { - reText = reText.slice(1, -1); - } else { - reText = reText - .replace(/\./g, '\\.') - .replace(/\?/g, '\\?') - .replace('||', '') - .replace(/\^/g, '.') - .replace(/^\|/g, '^') - .replace(/\|$/g, '$') - .replace(/\*/g, '.*') - ; + if ( retext === '' ) { + return document.createTextNode(url); } - var re = new RegExp(reText, 'gi'); + var re = new RegExp(retext, 'gi'); var matches = re.exec(url); if ( matches === null || matches[0].length === 0 ) { return document.createTextNode(url); } - var node = renderedURLTemplate.cloneNode(true); node.childNodes[0].textContent = url.slice(0, matches.index); node.childNodes[1].textContent = url.slice(matches.index, re.lastIndex); @@ -153,6 +186,7 @@ var createRow = function(layout) { var tr = trJunkyard.pop(); if ( tr ) { tr.className = ''; + tr.removeAttribute('data-context'); } else { tr = document.createElement('tr'); } @@ -212,6 +246,7 @@ var renderNetLogEntry = function(tr, entry) { var filter = entry.d0; var type = entry.d1; var url = entry.d2; + var td; tr.classList.add('canMtx'); @@ -221,19 +256,26 @@ var renderNetLogEntry = function(tr, entry) { createGap(entry.tab, url); } - // Cosmetic filter? - if ( filter.charAt(0) === 'c' ) { - tr.classList.add('cosmetic'); + // Root hostname + if ( entry.d3 ) { + tr.setAttribute('data-context', entry.d3); } + // Cosmetic filter? + var filterCat = filter.slice(0, 3); + if ( filterCat.charAt(2) === ':' ) { + tr.classList.add(filterCat.slice(0, 2)); + } + + td = tr.cells[2]; if ( filter.charAt(1) === 'b' ) { tr.classList.add('blocked'); - tr.cells[2].textContent = ' --'; + td.textContent = '--'; } else if ( filter.charAt(1) === 'a' ) { tr.classList.add('allowed'); - tr.cells[2].textContent = ' ++'; + td.textContent = '++'; } else { - tr.cells[2].textContent = ''; + td.textContent = ''; } var filterText = filter.slice(3); @@ -241,8 +283,8 @@ var renderNetLogEntry = function(tr, entry) { filterText = '@@' + filterText; } - tr.cells[3].textContent = filterText + '\t'; - tr.cells[4].textContent = (prettyRequestTypes[type] || type) + '\t'; + tr.cells[3].textContent = filterText; + tr.cells[4].textContent = (prettyRequestTypes[type] || type); tr.cells[5].appendChild(nodeFromURL(url, filter)); }; @@ -530,6 +572,236 @@ var onMaxEntriesChanged = function() { truncateLog(maxEntries); }; +/******************************************************************************/ +/******************************************************************************/ + +var urlFilteringMenu = (function() { + var menu = document.querySelector('#urlFilteringMenu'); + var menuDialog = menu.querySelector('.dialog'); + var selectContext = menuDialog.querySelector('.context'); + var selectType = menuDialog.querySelector('.type'); + var menuEntries = menu.querySelector('.entries'); + var menuURLs = []; + + var removeAllChildren = function(node) { + while ( node.firstChild ) { + node.removeChild(node.firstChild); + } + }; + + var uglyTypeFromSelector = function() { + var prettyType = selectType.value; + return uglyRequestTypes[prettyType] || prettyType; + }; + + var onColorsReady = function(response) { + document.body.classList.toggle('dirty', response.dirty); + var colorEntries = response.colors; + var colorEntry, node; + for ( var url in colorEntries ) { + if ( colorEntries.hasOwnProperty(url) === false ) { + continue; + } + colorEntry = colorEntries[url]; + node = menu.querySelector('.entries [data-url="' + url + '"]'); + if ( node === null ) { + continue; + } + node.classList.toggle('allow', colorEntry.r === 2); + node.classList.toggle('noop', colorEntry.r === 3); + node.classList.toggle('block', colorEntry.r === 1); + node.classList.toggle('own', colorEntry.own); + } + }; + + var colorize = function() { + messager.send({ + what: 'getURLFilteringData', + context: selectContext.value, + urls: menuURLs, + type: uglyTypeFromSelector() + }, onColorsReady); + }; + + var onClick = function(ev) { + var target = ev.target; + + // click outside the url filtering menu + if ( target.id === 'urlFilteringMenu' ) { + toggleOff(); + return; + } + + ev.stopPropagation(); + + // Save url filtering rule(s) + if ( target.classList.contains('save') ) { + messager.send({ + what: 'saveURLFilteringRules', + context: selectContext.value, + urls: menuURLs, + type: uglyTypeFromSelector() + }, colorize); + return; + } + + // Remove url filtering rule + if ( target.classList.contains('action') ) { + messager.send({ + what: 'setURLFilteringRule', + context: selectContext.value, + url: target.getAttribute('data-url'), + type: uglyTypeFromSelector(), + action: 0 + }, colorize); + return; + } + + // add "allow" url filtering rule + if ( target.classList.contains('allow') ) { + messager.send({ + what: 'setURLFilteringRule', + context: selectContext.value, + url: target.parentNode.getAttribute('data-url'), + type: uglyTypeFromSelector(), + action: 2 + }, colorize); + return; + } + + // add "block" url filtering rule + if ( target.classList.contains('noop') ) { + messager.send({ + what: 'setURLFilteringRule', + context: selectContext.value, + url: target.parentNode.getAttribute('data-url'), + type: uglyTypeFromSelector(), + action: 3 + }, colorize); + return; + } + + // add "block" url filtering rule + if ( target.classList.contains('block') ) { + messager.send({ + what: 'setURLFilteringRule', + context: selectContext.value, + url: target.parentNode.getAttribute('data-url'), + type: uglyTypeFromSelector(), + action: 1 + }, colorize); + return; + } + }; + + var toggleOn = function(ev) { + var td = ev.target; + var tr = td.parentElement; + var cells = tr.cells; + + var context = tr.getAttribute('data-context'); + if ( !context ) { + return; + } + + var type = cells[4].textContent.trim(); + if ( !type ) { + return; + } + + var pos, option; + + // Fill context selector + removeAllChildren(selectContext); + for (;;) { + option = document.createElement('option'); + option.textContent = context; + option.setAttribute('value', context); + pos = context.indexOf('.'); + selectContext.appendChild(option); + if ( pos === -1 ) { + break; + } + context = context.slice(pos + 1); + } + option = document.createElement('option'); + option.textContent = '*'; + option.setAttribute('value', '*'); + selectContext.appendChild(option); + + // Fill type selector + selectType.options[0].textContent = type; + selectType.options[0].setAttribute('value', type); + selectType.selectedIndex = 0; + + // Extract data needed to build URL filtering menu + var candidateURL = cells[5].textContent; + var matches = reRFC3986.exec(candidateURL); + if ( matches === null || !matches[1] || !matches[2] ) { + return; + } + + // Shortest URL which for a valid URL filtering rule + var candidateRootURL = matches[1] + matches[2]; + menuURLs.push(candidateRootURL); + var candidatePath = matches[3] || ''; + pos = candidatePath.charAt(0) === '/' ? 1 : 0; + while ( pos < candidatePath.length ) { + pos = candidatePath.indexOf('/', pos + 1); + if ( pos === -1 ) { + pos = candidatePath.length; + } + menuURLs.push(candidateRootURL + candidatePath.slice(0, pos)); + } + var candidateQuery = matches[4] || ''; + if ( candidateQuery !== '') { + menuURLs.push(candidateRootURL + candidatePath + candidateQuery); + } + + // Fill menu + var menuEntryTemplate = document.querySelector('#templates .urlFilteringMenuEntry'); + + // Adding URL filtering rules + var i = menuURLs.length; + var url, menuEntry; + while ( i-- ) { + url = menuURLs[i]; + menuEntry = menuEntryTemplate.cloneNode(true); + menuEntry.children[0].setAttribute('data-url', url); + menuEntry.children[1].textContent = url; + menuEntries.appendChild(menuEntry); + } + + colorize(); + + var rect = td.getBoundingClientRect(); + menuDialog.style.setProperty('left', rect.left + 'px'); + menuDialog.style.setProperty('top', rect.bottom + 'px'); + document.body.appendChild(menu); + + menu.addEventListener('click', onClick, true); + selectContext.addEventListener('change', colorize); + selectType.addEventListener('change', colorize); + }; + + var toggleOff = function() { + if ( menu.parentNode === null ) { + return; + } + removeAllChildren(menuEntries); + selectContext.removeEventListener('change', colorize); + selectType.removeEventListener('change', colorize); + menu.removeEventListener('click', onClick, true); + menu.parentNode.removeChild(menu); + menuURLs = []; + }; + + return { + toggleOn: toggleOn + }; +})(); + +/******************************************************************************/ /******************************************************************************/ var rowFilterer = (function() { @@ -823,6 +1095,7 @@ uDom.onLoad(function() { uDom('#clear').on('click', clearBuffer); uDom('#maxEntries').on('change', onMaxEntriesChanged); uDom('#content table').on('click', 'tr.canMtx > td:nth-of-type(2)', popupManager.toggleOn); + uDom('#content').on('click', 'tr.cat_net > td:nth-of-type(3)', urlFilteringMenu.toggleOn); }); /******************************************************************************/ diff --git a/src/js/logger.js b/src/js/logger.js index 2da5757d2..91bc04fba 100644 --- a/src/js/logger.js +++ b/src/js/logger.js @@ -161,6 +161,7 @@ var janitor = function() { logBuffer !== null && logBuffer.lastReadTime < (Date.now() - logBufferObsoleteAfter) ) { + api.writeOne = writeOneNoop; logBuffer = logBuffer.dispose(); } if ( logBuffer !== null ) { @@ -170,16 +171,18 @@ var janitor = function() { /******************************************************************************/ +var writeOneNoop = function() { +}; + var writeOne = function() { - if ( logBuffer !== null ) { - logBuffer.writeOne(arguments); - } + logBuffer.writeOne(arguments); }; /******************************************************************************/ var readAll = function() { if ( logBuffer === null ) { + api.writeOne = writeOne; logBuffer = new LogBuffer(); vAPI.setTimeout(janitor, logBufferObsoleteAfter); } @@ -188,18 +191,20 @@ var readAll = function() { /******************************************************************************/ -var isObserved = function() { +var isEnabled = function() { return logBuffer !== null; }; /******************************************************************************/ -return { - writeOne: writeOne, +var api = { + writeOne: writeOneNoop, readAll: readAll, - isObserved: isObserved + isEnabled: isEnabled }; +return api; + /******************************************************************************/ /******************************************************************************/ diff --git a/src/js/messaging.js b/src/js/messaging.js index 56ebe6b8a..a39d800d3 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -183,30 +183,30 @@ var getHostnameDict = function(hostnameToCountMap) { var getFirewallRules = function(srcHostname, desHostnames) { var r = {}; - var dFiltering = µb.sessionFirewall; - r['/ * *'] = dFiltering.evaluateCellZY('*', '*', '*').toFilterString(); - r['/ * image'] = dFiltering.evaluateCellZY('*', '*', 'image').toFilterString(); - r['/ * 3p'] = dFiltering.evaluateCellZY('*', '*', '3p').toFilterString(); - r['/ * inline-script'] = dFiltering.evaluateCellZY('*', '*', 'inline-script').toFilterString(); - r['/ * 1p-script'] = dFiltering.evaluateCellZY('*', '*', '1p-script').toFilterString(); - r['/ * 3p-script'] = dFiltering.evaluateCellZY('*', '*', '3p-script').toFilterString(); - r['/ * 3p-frame'] = dFiltering.evaluateCellZY('*', '*', '3p-frame').toFilterString(); + var df = µb.sessionFirewall; + r['/ * *'] = df.evaluateCellZY('*', '*', '*').toFilterString(); + r['/ * image'] = df.evaluateCellZY('*', '*', 'image').toFilterString(); + r['/ * 3p'] = df.evaluateCellZY('*', '*', '3p').toFilterString(); + r['/ * inline-script'] = df.evaluateCellZY('*', '*', 'inline-script').toFilterString(); + r['/ * 1p-script'] = df.evaluateCellZY('*', '*', '1p-script').toFilterString(); + r['/ * 3p-script'] = df.evaluateCellZY('*', '*', '3p-script').toFilterString(); + r['/ * 3p-frame'] = df.evaluateCellZY('*', '*', '3p-frame').toFilterString(); if ( typeof srcHostname !== 'string' ) { return r; } - r['. * *'] = dFiltering.evaluateCellZY(srcHostname, '*', '*').toFilterString(); - r['. * image'] = dFiltering.evaluateCellZY(srcHostname, '*', 'image').toFilterString(); - r['. * 3p'] = dFiltering.evaluateCellZY(srcHostname, '*', '3p').toFilterString(); - r['. * inline-script'] = dFiltering.evaluateCellZY(srcHostname, '*', 'inline-script').toFilterString(); - r['. * 1p-script'] = dFiltering.evaluateCellZY(srcHostname, '*', '1p-script').toFilterString(); - r['. * 3p-script'] = dFiltering.evaluateCellZY(srcHostname, '*', '3p-script').toFilterString(); - r['. * 3p-frame'] = dFiltering.evaluateCellZY(srcHostname, '*', '3p-frame').toFilterString(); + r['. * *'] = df.evaluateCellZY(srcHostname, '*', '*').toFilterString(); + r['. * image'] = df.evaluateCellZY(srcHostname, '*', 'image').toFilterString(); + r['. * 3p'] = df.evaluateCellZY(srcHostname, '*', '3p').toFilterString(); + r['. * inline-script'] = df.evaluateCellZY(srcHostname, '*', 'inline-script').toFilterString(); + r['. * 1p-script'] = df.evaluateCellZY(srcHostname, '*', '1p-script').toFilterString(); + r['. * 3p-script'] = df.evaluateCellZY(srcHostname, '*', '3p-script').toFilterString(); + r['. * 3p-frame'] = df.evaluateCellZY(srcHostname, '*', '3p-frame').toFilterString(); for ( var desHostname in desHostnames ) { if ( desHostnames.hasOwnProperty(desHostname) ) { - r['/ ' + desHostname + ' *'] = dFiltering.evaluateCellZY('*', desHostname, '*').toFilterString(); - r['. ' + desHostname + ' *'] = dFiltering.evaluateCellZY(srcHostname, desHostname, '*').toFilterString(); + r['/ ' + desHostname + ' *'] = df.evaluateCellZY('*', desHostname, '*').toFilterString(); + r['. ' + desHostname + ' *'] = df.evaluateCellZY(srcHostname, desHostname, '*').toFilterString(); } } return r; @@ -853,18 +853,19 @@ var µb = µBlock; var getRules = function() { return { - permanentRules: µb.permanentFirewall.toString(), - sessionRules: µb.sessionFirewall.toString(), + permanentRules: µb.permanentFirewall.toString() + '\n' + µb.permanentURLFiltering.toString(), + sessionRules: µb.sessionFirewall.toString() + '\n' + µb.sessionURLFiltering.toString(), hnSwitches: µb.hnSwitches.toString() }; }; -// Untangle rules and switches. +// Untangle firewall rules, url rules and switches. var untangle = function(s) { var textEnd = s.length; var lineBeg = 0, lineEnd; var line; - var rules = []; + var firewallRules = []; + var urlRules = []; var switches = []; while ( lineBeg < textEnd ) { @@ -878,16 +879,18 @@ var untangle = function(s) { line = s.slice(lineBeg, lineEnd).trim(); lineBeg = lineEnd + 1; - // Switches always contain a ':' - if ( line.indexOf(':') === -1 ) { - rules.push(line); + if ( line.indexOf('://') !== -1 ) { + urlRules.push(line); + } else if ( line.indexOf(':') === -1 ) { + firewallRules.push(line); } else { switches.push(line); } } return { - rules: rules.join('\n'), + firewallRules: firewallRules.join('\n'), + urlRules: urlRules.join('\n'), switches: switches.join('\n') }; }; @@ -906,24 +909,27 @@ var onMessage = function(request, sender, callback) { var response; switch ( request.what ) { - case 'getFirewallRules': + case 'getRules': response = getRules(); break; - case 'setSessionFirewallRules': + case 'setSessionRules': // https://github.com/chrisaljoudi/uBlock/issues/772 µb.cosmeticFilteringEngine.removeFromSelectorCache('*'); r = untangle(request.rules); - µb.sessionFirewall.fromString(r.rules); + µb.sessionFirewall.fromString(r.firewallRules); + µb.sessionURLFiltering.fromString(r.urlRules); µb.hnSwitches.fromString(r.switches); µb.saveHostnameSwitches(); response = getRules(); break; - case 'setPermanentFirewallRules': + case 'setPermanentRules': r = untangle(request.rules); - µb.permanentFirewall.fromString(r.rules); + µb.permanentFirewall.fromString(r.firewallRules); µb.savePermanentFirewallRules(); + µb.permanentURLFiltering.fromString(r.urlRules); + µb.savePermanentURLFilteringRules(); µb.hnSwitches.fromString(r.switches); µb.saveHostnameSwitches(); response = getRules(); @@ -1031,6 +1037,7 @@ var backupUserData = function(callback) { filterLists: {}, netWhitelist: µb.stringFromWhitelist(µb.netWhitelist), dynamicFilteringString: µb.permanentFirewall.toString(), + urlFilteringString: µb.permanentURLFiltering.toString(), hostnameSwitchesString: µb.hnSwitches.toString(), userFilters: '' }; @@ -1067,7 +1074,7 @@ var backupUserData = function(callback) { var restoreUserData = function(request) { var userData = request.userData; - var countdown = 7; + var countdown = 8; var onCountdown = function() { countdown -= 1; if ( countdown === 0 ) { @@ -1088,6 +1095,7 @@ var restoreUserData = function(request) { var s = userData.dynamicFilteringString || userData.userSettings.dynamicFilteringString || ''; µb.keyvalSetOne('dynamicFilteringString', s, onCountdown); + µb.keyvalSetOne('urlFilteringString', userData.urlFilteringString || '', onCountdown); µb.keyvalSetOne('hostnameSwitchesString', userData.hostnameSwitchesString || '', onCountdown); µb.assets.put('assets/user/filters.txt', userData.userFilters, onCountdown); vAPI.storage.set({ @@ -1173,6 +1181,66 @@ var µb = µBlock; /******************************************************************************/ +var getURLFilteringData = function(details) { + var colors = {}; + var response = { + dirty: false, + colors: colors + }; + var suf = µb.sessionURLFiltering; + var puf = µb.permanentURLFiltering; + var urls = details.urls, + context = details.context, + type = details.type; + var url, colorEntry; + var i = urls.length; + while ( i-- ) { + url = urls[i]; + colorEntry = colors[url] = { r: 0, own: false }; + if ( suf.evaluateZ(context, url, type).r !== 0 ) { + colorEntry.r = suf.r; + colorEntry.own = suf.context === context && suf.url === url && suf.type === type; + } + if ( response.dirty ) { + continue; + } + puf.evaluateZ(context, url, type); + response.dirty = colorEntry.own !== (puf.context === context && puf.url === url && puf.type === type); + } + return response; +}; + +/******************************************************************************/ + +var saveTemporaryURLFilteringRules = function(details) { + var changed = false; + var suf = µb.sessionURLFiltering; + var puf = µb.permanentURLFiltering; + var urls = details.urls, + context = details.context, + type = details.type; + var url, sOwn, pOwn; + var i = urls.length; + while ( i-- ) { + url = urls[i]; + suf.evaluateZ(context, url, type); + sOwn = suf.context === context && suf.url === url && suf.type === type; + puf.evaluateZ(context, url, type); + pOwn = puf.context === context && puf.url === url && puf.type === type; + if ( sOwn && !pOwn ) { + puf.setRule(context, url, type, suf.r); + changed = true; + } + if ( !sOwn && pOwn ) { + puf.removeRule(context, url, type); + changed = true; + } + } + return changed; +}; + +/******************************************************************************/ + var onMessage = function(request, sender, callback) { // Async switch ( request.what ) { @@ -1207,6 +1275,20 @@ var onMessage = function(request, sender, callback) { }; break; + case 'saveURLFilteringRules': + if ( saveTemporaryURLFilteringRules(request) ) { + µb.savePermanentURLFilteringRules(); + } + break; + + case 'setURLFilteringRule': + µb.toggleURLFilteringRule(request); + break; + + case 'getURLFilteringData': + response = getURLFilteringData(request); + break; + default: return vAPI.messaging.UNHANDLED; } diff --git a/src/js/pagestore.js b/src/js/pagestore.js index 690f3da9e..0d19baa58 100644 --- a/src/js/pagestore.js +++ b/src/js/pagestore.js @@ -1,7 +1,7 @@ /******************************************************************************* - µBlock - a browser extension to block requests. - Copyright (C) 2014 Raymond Hill + uBlock - a browser extension to block requests. + Copyright (C) 2014-2015 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 @@ -498,26 +498,21 @@ PageStore.prototype.filterRequest = function(context) { return entry.result; } - var result = ''; + µb.sessionURLFiltering.evaluateZ(context.rootHostname, context.requestURL, context.requestType); + var result = µb.sessionURLFiltering.toFilterString(); // 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. - if ( µb.userSettings.advancedUserEnabled ) { - var df = µb.sessionFirewall.evaluateCellZY( - context.rootHostname, - context.requestHostname, - context.requestType - ); - if ( df.mustBlockOrAllow() ) { - result = df.toFilterString(); - } + if ( result === '' && µb.userSettings.advancedUserEnabled ) { + µb.sessionFirewall.evaluateCellZY( context.rootHostname, context.requestHostname, context.requestType); + result = µb.sessionFirewall.toFilterString(); } // Static filtering never override dynamic filtering - if ( result === '' ) { + if ( result === '' || result.charAt(1) === 'n' ) { result = µb.staticNetFilteringEngine.matchString(context); } @@ -541,26 +536,21 @@ PageStore.prototype.filterRequestNoCache = function(context) { return ''; } - var result = ''; + µb.sessionURLFiltering.evaluateZ(context.rootHostname, context.requestURL, context.requestType); + var result = µb.sessionURLFiltering.toFilterString(); // 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. - if ( µb.userSettings.advancedUserEnabled ) { - var df = µb.sessionFirewall.evaluateCellZY( - context.rootHostname, - context.requestHostname, - context.requestType - ); - if ( df.mustBlockOrAllow() ) { - result = df.toFilterString(); - } + if ( result === '' && µb.userSettings.advancedUserEnabled ) { + µb.sessionFirewall.evaluateCellZY(context.rootHostname, context.requestHostname, context.requestType); + result = µb.sessionFirewall.toFilterString(); } // Static filtering never override dynamic filtering - if ( result === '' ) { + if ( result === '' || result.charAt(1) === 'n' ) { result = µb.staticNetFilteringEngine.matchString(context); } diff --git a/src/js/start.js b/src/js/start.js index 911f7e3b1..913621eee 100644 --- a/src/js/start.js +++ b/src/js/start.js @@ -1,6 +1,6 @@ /******************************************************************************* - µBlock - a browser extension to block requests. + uBlock - a browser extension to block requests. Copyright (C) 2014-2015 Raymond Hill This program is free software: you can redistribute it and/or modify @@ -142,11 +142,12 @@ var onUserSettingsReady = function(fetched) { // https://github.com/chrisaljoudi/uBlock/issues/540 // Disabling local mirroring for the time being userSettings.experimentalEnabled = false; - µb.mirrors.toggle(false /* userSettings.experimentalEnabled */); µb.contextMenu.toggle(userSettings.contextMenuEnabled); µb.permanentFirewall.fromString(fetched.dynamicFilteringString); µb.sessionFirewall.assign(µb.permanentFirewall); + µb.permanentURLFiltering.fromString(fetched.urlFilteringString); + µb.sessionURLFiltering.assign(µb.permanentURLFiltering); µb.hnSwitches.fromString(fetched.hostnameSwitchesString); // Remove obsolete setting @@ -226,6 +227,7 @@ return function() { var fetchableProps = { 'compiledMagic': '', 'dynamicFilteringString': '', + 'urlFilteringString': '', 'hostnameSwitchesString': '', 'lastRestoreFile': '', 'lastRestoreTime': 0, diff --git a/src/js/storage.js b/src/js/storage.js index 3823b2285..f8d94074e 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -1,6 +1,6 @@ /******************************************************************************* - µBlock - a browser extension to block requests. + uBlock - a browser extension to block requests. Copyright (C) 2014-2015 Raymond Hill This program is free software: you can redistribute it and/or modify @@ -83,6 +83,12 @@ /******************************************************************************/ +µBlock.savePermanentURLFilteringRules = function() { + this.keyvalSetOne('urlFilteringString', this.permanentURLFiltering.toString()); +}; + +/******************************************************************************/ + µBlock.saveHostnameSwitches = function() { this.keyvalSetOne('hostnameSwitchesString', this.hnSwitches.toString()); }; diff --git a/src/js/tab.js b/src/js/tab.js index 7650b3d38..05e705209 100644 --- a/src/js/tab.js +++ b/src/js/tab.js @@ -482,7 +482,8 @@ vAPI.tabs.onPopup = function(details) { 'net', result, 'popup', - targetURL + targetURL, + openerHostname ); // Not blocked diff --git a/src/js/traffic.js b/src/js/traffic.js index 1c98f76c6..700ae60ed 100644 --- a/src/js/traffic.js +++ b/src/js/traffic.js @@ -92,7 +92,7 @@ var onBeforeRequest = function(details) { // Possible outcomes: blocked, allowed-passthru, allowed-mirror pageStore.logRequest(requestContext, result); - µb.logger.writeOne(tabId, 'net', result, requestType, requestURL); + µb.logger.writeOne(tabId, 'net', result, requestType, requestURL, requestContext.rootHostname); // Not blocked if ( µb.isAllowResult(result) ) { @@ -187,7 +187,7 @@ var onBeforeRootFrameRequest = function(details) { if ( pageStore ) { pageStore.logRequest(context, result); } - µb.logger.writeOne(tabId, 'net', result, 'main_frame', requestURL); + µb.logger.writeOne(tabId, 'net', result, 'main_frame', requestURL, context.rootHostname); // Not blocked if ( µb.isAllowResult(result) ) { @@ -279,7 +279,7 @@ var onBeforeBehindTheSceneRequest = function(details) { } pageStore.logRequest(context, result); - µb.logger.writeOne(vAPI.noTabId, 'net', result, details.type, details.url); + µb.logger.writeOne(vAPI.noTabId, 'net', result, details.type, details.url, context.rootHostname); // Not blocked if ( µb.isAllowResult(result) ) { @@ -328,7 +328,7 @@ var onHeadersReceived = function(details) { var result = pageStore.filterRequestNoCache(context); pageStore.logRequest(context, result); - µb.logger.writeOne(tabId, 'net', result, 'inline-script', details.url); + µb.logger.writeOne(tabId, 'net', result, 'inline-script', details.url, context.rootHostname); // Don't block if ( µb.isAllowResult(result) ) { @@ -378,7 +378,7 @@ var onRootFrameHeadersReceived = function(details) { var result = pageStore.filterRequestNoCache(context); pageStore.logRequest(context, result); - µb.logger.writeOne(tabId, 'net', result, 'inline-script', details.url); + µb.logger.writeOne(tabId, 'net', result, 'inline-script', details.url, context.rootHostname); // Don't block if ( µb.isAllowResult(result) ) { diff --git a/src/js/ublock.js b/src/js/ublock.js index c8a4f31f8..9801572d6 100644 --- a/src/js/ublock.js +++ b/src/js/ublock.js @@ -254,8 +254,8 @@ var matchWhitelistDirective = function(url, hostname, directive) { // Pre-change switch ( name ) { - default: - break; + default: + break; } // Change @@ -263,23 +263,18 @@ var matchWhitelistDirective = function(url, hostname, directive) { // Post-change switch ( name ) { - case 'collapseBlocked': - if ( value === false ) { - this.cosmeticFilteringEngine.removeFromSelectorCache('*', 'net'); - } - break; - case 'contextMenuEnabled': - this.contextMenu.toggle(value); - break; - case 'experimentalEnabled': - if ( typeof this.mirrors === 'object' ) { - // https://github.com/chrisaljoudi/uBlock/issues/540 - // Disabling local mirroring for the time being - this.mirrors.toggle(false /* value */); - } - break; - default: - break; + case 'collapseBlocked': + if ( value === false ) { + this.cosmeticFilteringEngine.removeFromSelectorCache('*', 'net'); + } + break; + case 'contextMenuEnabled': + this.contextMenu.toggle(value); + break; + case 'experimentalEnabled': + break; + default: + break; } this.saveUserSettings(); @@ -317,6 +312,23 @@ var matchWhitelistDirective = function(url, hostname, directive) { /******************************************************************************/ +µBlock.toggleURLFilteringRule = function(details) { + var changed = this.sessionURLFiltering.setRule( + details.context, + details.url, + details.type, + details.action + ); + + if ( !changed ) { + return; + } + + this.cosmeticFilteringEngine.removeFromSelectorCache(details.context, 'net'); +}; + +/******************************************************************************/ + µBlock.isBlockResult = function(result) { return typeof result === 'string' && result.charAt(1) === 'b'; }; diff --git a/src/js/url-net-filtering.js b/src/js/url-net-filtering.js new file mode 100644 index 000000000..fb8be6429 --- /dev/null +++ b/src/js/url-net-filtering.js @@ -0,0 +1,391 @@ +/******************************************************************************* + + uBlock - a browser extension to black/white list requests. + Copyright (C) 2015 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 µBlock */ + +/******************************************************************************/ + +// The purpose of log filtering is to create ad hoc filtering rules, to +// diagnose and assist in the creation of custom filters. + +µBlock.URLNetFiltering = (function() { + +'use strict'; + +/******************************************************************************* + +buckets: map of [origin + urlkey] + bucket: array of rule entry, sorted from shorter to longer url + +rule entry: { url, action } + + +*******************************************************************************/ + +/******************************************************************************/ + +var actionToNameMap = { + 1: 'block', + 2: 'allow', + 3: 'noop' +}; + +var nameToActionMap = { + 'block': 1, + 'allow': 2, + 'noop': 3 +}; + +/******************************************************************************/ + +var RuleEntry = function(url, action) { + this.url = url; + this.action = action; +}; + +/******************************************************************************/ + +var indexOfURL = function(urls, url) { + // TODO: binary search -- maybe, depends on common use cases + var urlLen = url.length; + var entry; + // urls must be ordered by increasing length. + for ( var i = 0; i< urls.length; i++ ) { + entry = urls[i]; + if ( entry.url.length > urlLen ) { + break; + } + if ( entry.url === url ) { + return i; + } + } + return -1; +}; + +/******************************************************************************/ + +var indexOfMatch = function(urls, url) { + // TODO: binary search -- maybe, depends on common use cases + var urlLen = url.length; + var i = urls.length; + var entry; + while ( i-- ) { + entry = urls[i]; + if ( entry.url.length > urlLen ) { + continue; + } + if ( url.lastIndexOf(entry.url, 0) === 0 ) { + return i; + } + } + return -1; +}; + +/******************************************************************************/ + +var indexFromLength = function(urls, len) { + // TODO: binary search -- maybe, depends on common use cases + // urls must be ordered by increasing length. + for ( var i = 0; i< urls.length; i++ ) { + if ( urls[i].url.length > len ) { + return i; + } + } + return -1; +}; + +/******************************************************************************/ + +var addRuleEntry = function(urls, url, action) { + var entry = new RuleEntry(url, action); + var i = indexFromLength(urls, url.length); + if ( i === -1 ) { + urls.push(entry); + } else { + urls.splice(i, 0, entry); + } +}; + +/******************************************************************************/ + +var urlKeyFromURL = function(url) { + var match = reURLKey.exec(url); + return match !== null ? match[0] : ''; +}; + +var reURLKey = /^[a-z]+:\/\/[^\/?#]+/; + +/******************************************************************************/ + +var URLNetFiltering = function() { + this.reset(); +}; + +/******************************************************************************/ + +// rules: +// hostname + urlkey => urls +// urls = collection of urls to match + +URLNetFiltering.prototype.reset = function() { + this.rules = Object.create(null); + // registers, filled with result of last evaluation + this.context = ''; + this.url = ''; + this.type = ''; + this.r = 0; +}; + +/******************************************************************************/ + +URLNetFiltering.prototype.assign = function(other) { + var thisRules = this.rules; + var otherRules = other.rules; + var k; + + // Remove rules not in other + for ( k in thisRules ) { + if ( otherRules[k] === undefined ) { + delete thisRules[k]; + } + } + + // Add/change rules in other + for ( k in otherRules ) { + thisRules[k] = otherRules[k].slice(); + } +}; + +/******************************************************************************/ + +URLNetFiltering.prototype.setRule = function(srcHostname, url, type, action) { + if ( action === 0 ) { + return this.removeRule(srcHostname, url, type); + } + + var urlKey = urlKeyFromURL(url); + if ( urlKey === '' ) { + return false; + } + + var bucketKey = srcHostname + ' ' + urlKey + ' ' + type; + var urls = this.rules[bucketKey]; + if ( urls === undefined ) { + urls = this.rules[bucketKey] = []; + } + + var entry; + var i = indexOfURL(urls, url); + if ( i !== -1 ) { + entry = urls[i]; + if ( entry.action === action ) { + return false; + } + entry.action = action; + return true; + } + + addRuleEntry(urls, url, action); + return true; +}; + +/******************************************************************************/ + +URLNetFiltering.prototype.removeRule = function(srcHostname, url, type) { + var urlKey = urlKeyFromURL(url); + if ( urlKey === '' ) { + return false; + } + + var bucketKey = srcHostname + ' ' + urlKey + ' ' + type; + var urls = this.rules[bucketKey]; + if ( urls === undefined ) { + return false; + } + + var i = indexOfURL(urls, url); + if ( i === -1 ) { + return false; + } + + urls.splice(i, 1); + if ( urls.length === 0 ) { + delete this.rules[bucketKey]; + } + + return true; +}; + +/******************************************************************************/ + +URLNetFiltering.prototype.evaluateZ = function(context, target, type) { + var urlKey = urlKeyFromURL(target); + if ( urlKey === '' ) { + this.r = 0; + return this; + } + + var urls, pos, i, entry, prefixKey; + + for (;;) { + this.context = context; + prefixKey = context + ' ' + urlKey; + if ( urls = this.rules[prefixKey + ' ' + type] ) { + i = indexOfMatch(urls, target); + if ( i !== -1 ) { + entry = urls[i]; + this.url = entry.url; + this.type = type; + this.r = entry.action; + return this; + } + } + if ( urls = this.rules[prefixKey + ' *'] ) { + i = indexOfMatch(urls, target); + if ( i !== -1 ) { + entry = urls[i]; + this.url = entry.url; + this.type = '*'; + this.r = entry.action; + return this; + } + } + if ( context === '*' ) { + break; + } + pos = context.indexOf('.'); + context = pos !== -1 ? context.slice(pos + 1) : '*'; + } + + this.r = 0; + return this; +}; + +/******************************************************************************/ + +URLNetFiltering.prototype.mustBlockOrAllow = function() { + return this.r === 1 || this.r === 2; +}; + +/******************************************************************************/ + +URLNetFiltering.prototype.toFilterString = function() { + if ( this.r === 0 ) { + return ''; + } + var body = this.context + ' ' + this.url + ' ' + this.type; + if ( this.r === 1 ) { + return 'lb:' + body + ' block'; + } + if ( this.r === 2 ) { + return 'la:' + body + ' allow'; + } + /* this.r === 3 */ + return 'ln:' + body + ' noop'; +}; + +/******************************************************************************/ + +// "url-filtering:" hostname url action + +URLNetFiltering.prototype.toString = function() { + var out = []; + var pos, hn, type, urls, i, entry; + for ( var bucketKey in this.rules ) { + pos = bucketKey.indexOf(' '); + hn = bucketKey.slice(0, pos); + pos = bucketKey.lastIndexOf(' '); + type = bucketKey.slice(pos + 1); + urls = this.rules[bucketKey]; + for ( i = 0; i < urls.length; i++ ) { + entry = urls[i]; + out.push( + hn + ' ' + + entry.url + ' ' + + type + ' ' + + actionToNameMap[entry.action] + ); + } + } + return out.sort().join('\n'); +}; + +/******************************************************************************/ + +URLNetFiltering.prototype.fromString = function(text) { + var textEnd = text.length; + var lineBeg = 0, lineEnd; + var line, fields; + + this.reset(); + + 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; + + if ( line === '' ) { + continue; + } + + // Coarse test + if ( line.indexOf('://') === -1 ) { + continue; + } + + fields = line.split(/\s+/); + if ( fields.length !== 4 ) { + continue; + } + + // Finer test + if ( fields[1].indexOf('://') === -1 ) { + continue; + } + + if ( nameToActionMap.hasOwnProperty(fields[3]) === false ) { + continue; + } + + this.setRule(fields[0], fields[1], fields[2], nameToActionMap[fields[3]]); + } +}; + +/******************************************************************************/ + +return URLNetFiltering; + +/******************************************************************************/ + +})(); + +/******************************************************************************/ + +µBlock.sessionURLFiltering = new µBlock.URLNetFiltering(); +µBlock.permanentURLFiltering = new µBlock.URLNetFiltering(); + +/******************************************************************************/ diff --git a/src/logger-ui.html b/src/logger-ui.html index 8ac913c27..85cfe30cb 100644 --- a/src/logger-ui.html +++ b/src/logger-ui.html @@ -38,10 +38,21 @@
-
+