From 3c941f8309c56287092b2422c1197f0fbd30f687 Mon Sep 17 00:00:00 2001 From: gorhill Date: Wed, 31 Dec 2014 17:26:17 -0500 Subject: [PATCH] another round of work re dynamic filtering --- platform/chromium/manifest.json | 2 +- platform/chromium/vapi-background.js | 2 +- src/1p-filters.html | 7 +- src/_locales/en/messages.json | 66 ++++++++++-- src/css/1p-filters.css | 2 +- src/css/dyna-rules.css | 13 +++ src/css/popup.css | 17 +-- src/css/whitelist.css | 2 +- src/dashboard.html | 1 + src/dyna-rules.html | 31 ++++++ src/js/dyna-rules.js | 155 +++++++++++++++++++++++++++ src/js/dynamic-net-filtering.js | 72 +++++-------- src/js/messaging.js | 49 +++++++++ src/js/popup.js | 36 +++++-- src/js/storage.js | 7 ++ src/js/ublock.js | 3 +- src/popup.html | 12 +-- src/whitelist.html | 7 +- 18 files changed, 397 insertions(+), 87 deletions(-) create mode 100644 src/css/dyna-rules.css create mode 100644 src/dyna-rules.html create mode 100644 src/js/dyna-rules.js diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index 303490272..75f0875b0 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "µBlock", - "version": "0.8.2.3", + "version": "0.8.5.0", "default_locale": "en", "description": "__MSG_extShortDesc__", diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index 208d3eb94..66223a843 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -425,7 +425,7 @@ vAPI.onLoadAllCompleted = function() { vAPI.tabs.injectScript(tabId, { file: 'js/contentscript-start.js', allFrames: true, - runAt: 'document_idle' + runAt: 'document_start' }, function(){ scriptEnd(tabId); }); }; var bindToTabs = function(tabs) { diff --git a/src/1p-filters.html b/src/1p-filters.html index 8425d6e2d..606d36d63 100644 --- a/src/1p-filters.html +++ b/src/1p-filters.html @@ -12,10 +12,11 @@

-

- -

+ +

  + +

diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 1a0f41ae5..7039432f5 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -13,27 +13,31 @@ }, "settingsPageName":{ "message":"Settings", - "description":"appears as tab name in dashboard." + "description":"appears as tab name in dashboard" }, "3pPageName":{ "message":"3rd-party filters", - "description":"appears as tab name in dashboard." + "description":"appears as tab name in dashboard" }, "1pPageName":{ - "message":"Your filters", - "description":"appears as tab name in dashboard." + "message":"My filters", + "description":"appears as tab name in dashboard" + }, + "rulesPageName": { + "message": "My rules", + "description": "appears as tab name in dashboard" }, "whitelistPageName":{ "message":"Whitelist", - "description":"appears as tab name in dashboard." + "description":"appears as tab name in dashboard" }, "statsPageName":{ "message":"Statistics", - "description":"appears as tab name in dashboard." + "description":"appears as tab name in dashboard" }, "aboutPageName":{ "message":"About", - "description":"appears as tab name in dashboard." + "description":"appears as tab name in dashboard" }, "popupPowerSwitchInfo":{ "message":"Click: disable\/enable µBlock for this site.\n\nCtrl+click: disable µBlock only on this page.", @@ -147,6 +151,30 @@ "message":"3rd-party frames are blocked<\/b> everywhere by default", "description":"" }, + "popupImageRulePrompt":{ + "message":"images", + "description":"" + }, + "popupInlineScriptRulePrompt":{ + "message":"inline scripts", + "description":"" + }, + "popup1pScriptRulePrompt":{ + "message":"1st-party scripts", + "description":"" + }, + "popup3pScriptRulePrompt":{ + "message":"3rd-party scripts", + "description":"" + }, + "popup3pFrameRulePrompt":{ + "message":"3rd-party frames", + "description":"" + }, + "popupHitDomainCountPrompt":{ + "message":"Connected to {{count}} distinct domain(s)", + "description":"appear in dynamic filtering pane" + }, "pickerCreate":{ "message":"Create", "description":"English: Create" @@ -295,6 +323,30 @@ "message":"Apply changes", "description":"English: Apply changes" }, + "rulesEdit": { + "message": "Edit", + "description": "Will enable manual-edit mode (textarea)" + }, + "rulesEditSave": { + "message": "Save", + "description": "Will save manually-edited content and exit manual-edit mode" + }, + "rulesEditDiscard": { + "message": "Discard", + "description": "Will discard manually-edited content and exit manual-edit mode" + }, + "rulesImport": { + "message": "Import from file...", + "description": "" + }, + "rulesExport": { + "message": "Export to file...", + "description": "" + }, + "rulesDefaultFileName": { + "message": "my-ublock-dynamic-rules.txt", + "description": "default file name to use" + }, "whitelistPrompt":{ "message":"Your list of host names for which µBlock will be disabled. One entry per line. Invalid host names will be silently ignored.", "description":"English: Your list of host names for which µBlock will be disabled. One host name per line. Invalid host names will be silently ignored." diff --git a/src/css/1p-filters.css b/src/css/1p-filters.css index 8413e6bdf..055622106 100644 --- a/src/css/1p-filters.css +++ b/src/css/1p-filters.css @@ -5,7 +5,7 @@ div > p:last-child { margin-bottom: 0; } .userFilters { - font-size: smaller; + font-size: small; width: 48em; height: 40em; white-space: pre; diff --git a/src/css/dyna-rules.css b/src/css/dyna-rules.css new file mode 100644 index 000000000..5273afe1d --- /dev/null +++ b/src/css/dyna-rules.css @@ -0,0 +1,13 @@ +div > p:first-child { + margin-top: 0; + } +div > p:last-child { + margin-bottom: 0; + } +#rulesEditor { + font-size: small; + width: 48em; + height: 40em; + white-space: pre; + text-align: left; + } diff --git a/src/css/popup.css b/src/css/popup.css index b41bd6edd..e47b021fc 100644 --- a/src/css/popup.css +++ b/src/css/popup.css @@ -168,6 +168,9 @@ body.dynamicFilteringEnabled #dynamicFilteringContainer > div:hover { padding: 4px 0; text-align: center; } +#dynamicFilteringContainer > div.isDomain { + margin-top: 2px; + } #dynamicFilteringContainer > div > span { background-color: transparent; border: none; @@ -204,28 +207,28 @@ body.dynamicFilteringEnabled #dynamicFilteringContainer > div > span { #dynamicFilteringContainer > div.isDomain > span:nth-of-type(1) { font-weight: bold; } -body.dynamicFilteringEnabled #dynamicFilteringContainer > div > span:nth-of-type(3) { +#dynamicFilteringContainer > div > span:nth-of-type(3) { color: #666; pointer-events: auto; } -#dynamicFilteringContainer span.aRule { +#dynamicFilteringContainer > div > span.aRule { background-color: rgba(0, 160, 0, 0.3); } -#dynamicFilteringContainer span.bRule { +#dynamicFilteringContainer > div > span.bRule { background-color: rgba(192, 0, 0, 0.3); } -#dynamicFilteringContainer span.nRule { +#dynamicFilteringContainer > div > span.nRule { background-color: rgba(96, 96, 96, 0.3); } -#dynamicFilteringContainer span.aRule.ownRule { +#dynamicFilteringContainer > div > span.aRule.ownRule { background-color: rgba(0, 160, 0, 1); color: white; } -#dynamicFilteringContainer span.bRule.ownRule { +#dynamicFilteringContainer > div > span.bRule.ownRule { background-color: rgba(192, 0, 0, 1); color: white; } -#dynamicFilteringContainer span.nRule.ownRule { +#dynamicFilteringContainer > div > span.nRule.ownRule { background-color: rgba(108, 108, 108, 1); color: white; } diff --git a/src/css/whitelist.css b/src/css/whitelist.css index a9d8cd4f5..8f646a6a9 100644 --- a/src/css/whitelist.css +++ b/src/css/whitelist.css @@ -5,7 +5,7 @@ div > p:last-child { margin-bottom: 0; } #whitelist { - font-size: smaller; + font-size: small; width: 48em; height: 40em; white-space: pre; diff --git a/src/dashboard.html b/src/dashboard.html index bc53db07b..9a172dd44 100644 --- a/src/dashboard.html +++ b/src/dashboard.html @@ -13,6 +13,7 @@ + diff --git a/src/dyna-rules.html b/src/dyna-rules.html new file mode 100644 index 000000000..80f54c527 --- /dev/null +++ b/src/dyna-rules.html @@ -0,0 +1,31 @@ + + + + +µMatrix — Dynamic filtering rules + + + + + + + +
+

+

+ +

  + +

+ +
+ + + + + + + + + + diff --git a/src/js/dyna-rules.js b/src/js/dyna-rules.js new file mode 100644 index 000000000..52370b204 --- /dev/null +++ b/src/js/dyna-rules.js @@ -0,0 +1,155 @@ +/******************************************************************************* + + µMatrix - a Chromium browser extension to block 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/uMatrix +*/ + +/* global chrome, messaging, uDom */ + +/******************************************************************************/ + +(function() { + +'use strict'; + +/******************************************************************************/ + +var messager = vAPI.messaging.channel('dyna-rules.js'); + +/******************************************************************************/ + +// Switches before, rules after + +var normalizeRawRules = function(s) { + return s.replace(/[ \t]+/g, ' ') + .split(/\s*\n+\s*/) + .sort(directiveSort) + .join('\n') + .trim(); +}; + +/******************************************************************************/ + +// This is to give a visual hint that the content of user blacklist has changed. + +function rulesChanged() { + uDom('#rulesApply').prop( + 'disabled', + normalizeRawRules(uDom('#rulesEditor').val()) === cachedRawRules + ); +} + +var cachedRawRules = ''; + +/******************************************************************************/ + +// Switches before, rules after + +var directiveSort = function(a, b) { + var aIsSwitch = a.indexOf(':') !== -1; + var bIsSwitch = b.indexOf(':') !== -1; + if ( aIsSwitch === bIsSwitch ) { + return a.localeCompare(b); + } + return aIsSwitch ? -1 : 1; +}; + +/******************************************************************************/ + +var processRules = function(rawRules) { + cachedRawRules = normalizeRawRules(rawRules); + uDom('#rulesEditor').val(cachedRawRules); +}; + +/******************************************************************************/ + +var rulesApplyHandler = function() { + var onWritten = function(response) { + processRules(response); + rulesChanged(); + }; + var request = { + what: 'setDynamicRules', + rawRules: uDom('#rulesEditor').val() + }; + messager.send(request, onWritten); +}; + +/******************************************************************************/ + +function handleImportFilePicker() { + var fileReaderOnLoadHandler = function() { + if ( typeof this.result !== 'string' || this.result === '' ) { + return; + } + var request = { + 'what': 'setDynamicRules', + 'rawRules': uDom('#rulesEditor').val() + }; + messager.send(request, processRules); + }; + var file = this.files[0]; + if ( file === undefined || file.name === '' ) { + return; + } + if ( file.type.indexOf('text') !== 0 ) { + return; + } + var fr = new FileReader(); + fr.onload = fileReaderOnLoadHandler; + fr.readAsText(file); +} + +/******************************************************************************/ + +var startImportFilePicker = function() { + var input = document.getElementById('importFilePicker'); + // Reset to empty string, this will ensure an change event is properly + // triggered if the user pick a file, even if it is the same as the last + // one picked. + input.value = ''; + input.click(); +}; + +/******************************************************************************/ + +function exportUserRulesToFile() { + chrome.downloads.download({ + 'url': 'data:text/plain,' + encodeURIComponent(rulesFromHTML('#diff .left li')), + 'filename': uDom('[data-i18n="userRulesDefaultFileName"]').text(), + 'saveAs': true + }); +} + +/******************************************************************************/ + +uDom.onLoad(function() { + // Handle user interaction + uDom('#importButton').on('click', startImportFilePicker); + uDom('#importFilePicker').on('change', handleImportFilePicker); + uDom('#exportButton').on('click', exportUserRulesToFile); + uDom('#rulesEditor').on('input', rulesChanged); + uDom('#rulesApply').on('click', rulesApplyHandler); + + messager.send({ what: 'getDynamicRules' }, processRules); +}); + +/******************************************************************************/ + +})(); + diff --git a/src/js/dynamic-net-filtering.js b/src/js/dynamic-net-filtering.js index 5b104a750..4f316968e 100644 --- a/src/js/dynamic-net-filtering.js +++ b/src/js/dynamic-net-filtering.js @@ -47,13 +47,13 @@ var typeBitOffsets = { 'image': 10 }; -var stateToNameMap = { +var actionToNameMap = { '1': 'block', '2': 'allow', '3': 'noop' }; -var nameToStateMap = { +var nameToActionMap = { 'block': 1, 'allow': 2, 'noop': 3 @@ -187,6 +187,7 @@ Matrix.prototype.clearRegisters = function() { this.type = ''; this.y = ''; this.z = ''; + return this; }; /******************************************************************************/ @@ -320,7 +321,7 @@ Matrix.prototype.toString = function() { punycode.toUnicode(srcHostname) + ' ' + punycode.toUnicode(desHostname) + ' ' + type + ' ' + - stateToNameMap[val] + actionToNameMap[val] ); } } @@ -332,11 +333,12 @@ Matrix.prototype.toString = function() { 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; + var line, pos, fields; + var srcHostname, desHostname, type, action; + + if ( append !== true ) { + this.reset(); + } while ( lineBeg < textEnd ) { lineEnd = text.indexOf('\n', lineBeg); @@ -357,59 +359,33 @@ Matrix.prototype.fromString = function(text, append) { 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]] + // 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` + // state = [`block`, `allow`, `noop`] // Lines with invalid syntax silently ignored + fields = line.split(/\s+/); + if ( fields.length !== 4 ) { + continue; + } + 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 = '*'; + type = fields[2]; + if ( typeBitOffsets.hasOwnProperty(type) === false ) { + continue; } - fieldVal = fields[3]; - - if ( fieldVal !== undefined ) { - // Unknown state: reject - if ( nameToStateMap.hasOwnProperty(fieldVal) === false ) { - continue; - } - state = nameToStateMap[fieldVal]; - } else { - state = 2; + action = nameToActionMap[fields[3]]; + if ( typeof action !== 'number' || action < 0 || action > 3 ) { + continue; } - this.setCell(srcHostname, desHostname, type, state); + this.setCell(srcHostname, desHostname, type, action); } }; diff --git a/src/js/messaging.js b/src/js/messaging.js index 0cf8b7ee4..53c1ab9ff 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -640,6 +640,55 @@ vAPI.messaging.listen('1p-filters.js', onMessage); /******************************************************************************/ /******************************************************************************/ +// dyna-rules.js + +(function() { + +'use strict'; + +/******************************************************************************/ + +var µb = µBlock; + +/******************************************************************************/ + +var onMessage = function(request, sender, callback) { + // Async + switch ( request.what ) { + default: + break; + } + + // Sync + var response; + + switch ( request.what ) { + case 'getDynamicRules': + response = µb.dynamicNetFilteringEngine.toString(); + break; + + case 'setDynamicRules': + µb.dynamicNetFilteringEngine.fromString(request.rawRules); + µb.saveDynamicRules(); + response = µb.dynamicNetFilteringEngine.toString(); + break; + + default: + return vAPI.messaging.UNHANDLED; + } + + callback(response); +}; + +vAPI.messaging.listen('dyna-rules.js', onMessage); + +/******************************************************************************/ + +})(); + +/******************************************************************************/ +/******************************************************************************/ + // whitelist.js (function() { diff --git a/src/js/popup.js b/src/js/popup.js index 56dc07cdf..0504196a8 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -49,6 +49,7 @@ var threePlus = '+++'; var threeMinus = '−−−'; var sixSpace = '\u2007\u2007\u2007\u2007\u2007\u2007'; var dynaHotspots = null; +var hostnameToSortableTokenMap = {}; /******************************************************************************/ @@ -59,11 +60,30 @@ var messager = vAPI.messaging.channel('popup.js'); /******************************************************************************/ var cachePopupData = function(data) { - if ( data ) { - stats = data; - scopeToSrcHostnameMap['.'] = data.pageHostname || ''; + stats = {}; + scopeToSrcHostnameMap['.'] = ''; + hostnameToSortableTokenMap = {}; + if ( typeof data !== 'object' ) { + return stats; + } + stats = data; + scopeToSrcHostnameMap['.'] = stats.pageHostname || ''; + var hostnameDict = stats.hostnameDict; + if ( typeof hostnameDict === 'object' ) { + var domain, prefix; + for ( var hostname in hostnameDict ) { + if ( hostnameDict.hasOwnProperty(hostname) === false ) { + continue; + } + domain = hostnameDict[hostname].domain; + if ( domain === stats.pageDomain ) { + domain = '\u0020'; + } + prefix = hostname.slice(0, 0 - domain.length); + hostnameToSortableTokenMap[hostname] = domain + prefix.split('.').reverse().join('.'); + } } - return data; + return stats; }; /******************************************************************************/ @@ -80,11 +100,11 @@ var formatNumber = function(count) { var rulekeyCompare = function(a, b) { var ha = a.slice(2, a.indexOf(' ', 2)); if ( !reIP.test(ha) ) { - ha = ha.split('.').reverse().join('.').replace(reRulekeyCompareNoise, '~'); + ha = hostnameToSortableTokenMap[ha] || ''; } var hb = b.slice(2, b.indexOf(' ', 2)); if ( !reIP.test(hb) ) { - hb = hb.split('.').reverse().join('.').replace(reRulekeyCompareNoise, '~'); + hb = hostnameToSortableTokenMap[hb] || ''; } return ha.localeCompare(hb); }; @@ -142,7 +162,7 @@ var syncDynamicFilterCell = function(scope, des, type, result) { var matches = reSrcHostnameFromResult.exec(result); if ( matches !== null ) { ownRule = matches[2] === des && - matches[1] === scopeToSrcHostnameMap[scope]; + matches[1] === scopeToSrcHostnameMap[scope]; } cell.toggleClass('ownRule', ownRule); @@ -194,7 +214,7 @@ var syncAllDynamicFilters = function() { syncDynamicFilterCell(key.charAt(0), key.slice(2, key.indexOf(' ', 2)), '*', rules[key]); } - uDom('#privacyInfo > b').text(Object.keys(touchedDomains).length); + uDom('#privacyInfo').text(vAPI.i18n('popupHitDomainCountPrompt').replace('{{count}}', Object.keys(touchedDomains).length)); }; /******************************************************************************/ diff --git a/src/js/storage.js b/src/js/storage.js index b6727229b..d90a1d10e 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -86,6 +86,13 @@ /******************************************************************************/ +µBlock.saveDynamicRules = function() { + this.userSettings.dynamicFilteringString = this.dynamicNetFilteringEngine.toString(); + this.XAL.keyvalSetOne('dynamicFilteringString', this.userSettings.dynamicFilteringString); +}; + +/******************************************************************************/ + µBlock.saveWhitelist = function() { var bin = { 'netWhitelist': this.stringFromWhitelist(this.netWhitelist) diff --git a/src/js/ublock.js b/src/js/ublock.js index c7e402555..75c63ab20 100644 --- a/src/js/ublock.js +++ b/src/js/ublock.js @@ -350,7 +350,8 @@ var matchWhitelistDirective = function(url, hostname, directive) { // evaluation of static filtering. // Dynamic filtering evaluation is ordered from most-specific to least- // specific. - var df = this.dynamicNetFilteringEngine; + var df = this.dynamicNetFilteringEngine.clearRegisters(); + var rootHostname = context.rootHostname; var requestHostname = context.requestHostname; var requestType = context.requestType; diff --git a/src/popup.html b/src/popup.html index 257e548de..1fb6725e1 100644 --- a/src/popup.html +++ b/src/popup.html @@ -12,12 +12,12 @@

v

-
images
-
inline scripts
-
1st-party scripts
-
3rd-party scripts
-
3rd-party frames
-
? distinct domains touched
+
+
+
+
+
+

diff --git a/src/whitelist.html b/src/whitelist.html index 2a5789f41..843b5ace4 100644 --- a/src/whitelist.html +++ b/src/whitelist.html @@ -12,10 +12,11 @@

-

- -

+ +

  + +