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 @@