another round of work re dynamic filtering

This commit is contained in:
gorhill 2014-12-31 17:26:17 -05:00
parent ceaf920cff
commit 3c941f8309
18 changed files with 397 additions and 87 deletions

View file

@ -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__",

View file

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

View file

@ -12,10 +12,11 @@
<div>
<p data-i18n="1pFormatHint"></p>
<p><button id="importUserFiltersFromFile" data-i18n="1pImport"></button> &emsp; <button id="exportUserFiltersToFile" data-i18n="1pExport"></button></p>
<input id="importFilePicker" type="file" accept="text/plain" class="hiddenFileInput">
<textarea class="userFilters" id="userFilters" dir="auto" spellcheck="false"></textarea>
<p><button id="userFiltersApply" disabled="true" data-i18n="1pApplyChanges"></button></p>
<textarea class="userFilters" id="userFilters" dir="auto" spellcheck="false"></textarea>
<p><button id="importUserFiltersFromFile" data-i18n="1pImport"></button> &emsp;
<button id="exportUserFiltersToFile" data-i18n="1pExport"></button>
<input id="importFilePicker" type="file" accept="text/plain" class="hiddenFileInput"></p>
</div>
<script src="js/vapi-common.js"></script>

View file

@ -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 <b>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."

View file

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

13
src/css/dyna-rules.css Normal file
View file

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

View file

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

View file

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

View file

@ -13,6 +13,7 @@
<span data-i18n="extName"></span>
<a class="tabButton" href="#3p-filters.html" data-i18n="3pPageName"></a>
<a class="tabButton" href="#1p-filters.html" data-i18n="1pPageName"></a>
<a class="tabButton" href="#dyna-rules.html" data-i18n="rulesPageName"></a>
<a class="tabButton" href="#whitelist.html" data-i18n="whitelistPageName"></a>
<a class="tabButton" href="#settings.html" data-i18n="settingsPageName"></a>
<a class="tabButton" href="#stats.html" data-i18n="statsPageName"></a>

31
src/dyna-rules.html Normal file
View file

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>µMatrix — Dynamic filtering rules</title>
<link rel="stylesheet" type="text/css" href="css/common.css">
<link rel="stylesheet" type="text/css" href="css/dashboard-common.css">
<link rel="stylesheet" type="text/css" href="css/dyna-rules.css">
</head>
<body>
<div>
<p data-i18n="rulesFormatHint"></p>
<p><button id="rulesApply" disabled="true" data-i18n="1pApplyChanges"></button></p>
<textarea id="rulesEditor" spellcheck="false"></textarea>
<p><button id="importButton" type="button" data-i18n="1pImport"></button> &emsp;
<button id="exportButton" type="button" data-i18n="1pExport"></button>
<input class="hidden" id="importFilePicker" type="file" accept="text/plain" style="display: none;"></p>
<span class="hidden" data-i18n="rulesDefaultFileName" style="display: none;"></span>
</div>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/dashboard-common.js"></script>
<script src="js/dyna-rules.js"></script>
</body>
</html>

155
src/js/dyna-rules.js Normal file
View file

@ -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);
});
/******************************************************************************/
})();

View file

@ -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);
}
};

View file

@ -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() {

View file

@ -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));
};
/******************************************************************************/

View file

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

View file

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

View file

@ -12,12 +12,12 @@
<h4 title="popupTipDashboard">v<span id="version"></span></h4>
<div>
<div id="dynamicFilteringContainer">
<div><span>images</span><span data-src="/" data-des="*" data-type="image"> </span><span data-src="." data-des="*" data-type="image"></span></div>
<div><span>inline scripts</span><span data-src="/" data-des="*" data-type="inline-script"> </span><span data-src="." data-des="*" data-type="inline-script"> </span></div>
<div><span>1st-party scripts</span><span data-src="/" data-des="*" data-type="1p-script"> </span><span data-src="." data-des="*" data-type="1p-script"> </span></div>
<div><span>3rd-party scripts</span><span data-src="/" data-des="*" data-type="3p-script"> </span><span data-src="." data-des="*" data-type="3p-script"> </span></div>
<div><span>3rd-party frames</span><span data-src="/" data-des="*" data-type="3p-frame"> </span><span data-src="." data-des="*" data-type="3p-frame"> </span></div>
<div id="privacyInfo"><b>?</b> distinct domains touched</div>
<div><span data-i18n="popupImageRulePrompt"></span><span data-src="/" data-des="*" data-type="image"> </span><span data-src="." data-des="*" data-type="image"></span></div>
<div><span data-i18n="popupInlineScriptRulePrompt"></span><span data-src="/" data-des="*" data-type="inline-script"> </span><span data-src="." data-des="*" data-type="inline-script"> </span></div>
<div><span data-i18n="popup1pScriptRulePrompt"></span><span data-src="/" data-des="*" data-type="1p-script"> </span><span data-src="." data-des="*" data-type="1p-script"> </span></div>
<div><span data-i18n="popup3pScriptRulePrompt"></span><span data-src="/" data-des="*" data-type="3p-script"> </span><span data-src="." data-des="*" data-type="3p-script"> </span></div>
<div><span data-i18n="popup3pFrameRulePrompt"></span><span data-src="/" data-des="*" data-type="3p-frame"> </span><span data-src="." data-des="*" data-type="3p-frame"> </span></div>
<div id="privacyInfo"></div>
</div>
</div><!-- DO NOT REMOVE --><div>
<p id="switch" data-i18n-tip="popupPowerSwitchInfo"><span class="fa">&#xf011;</span></p>

View file

@ -12,10 +12,11 @@
<div>
<p data-i18n="whitelistPrompt"></p>
<p><button id="importWhitelistFromFile" data-i18n="whitelistImport"></button> &emsp; <button id="exportWhitelistToFile" data-i18n="whitelistExport"></button></p>
<input id="importFilePicker" type="file" accept="text/plain" class="hiddenFileInput">
<textarea id="whitelist" dir="auto" spellcheck="false"></textarea>
<p><button id="whitelistApply" type="button" disabled="true" data-i18n="whitelistApply"></button></p>
<textarea id="whitelist" dir="auto" spellcheck="false"></textarea>
<p><button id="importWhitelistFromFile" data-i18n="whitelistImport"></button> &emsp;
<button id="exportWhitelistToFile" data-i18n="whitelistExport"></button>
<input id="importFilePicker" type="file" accept="text/plain" class="hiddenFileInput"></p>
</div>
<script src="lib/punycode.min.js"></script>