Code review of whitelisting-related code

- Use `Map()` instead of `{}` for internal data
  structure
- Export as array of directives instead of as
  a string
This commit is contained in:
Raymond Hill 2019-06-25 11:57:14 -04:00
parent 8e7384ba84
commit 9065bbdd48
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
6 changed files with 132 additions and 100 deletions

View File

@ -116,7 +116,7 @@ const µBlock = (function() { // jshint ignore:line
// https://github.com/chrisaljoudi/uBlock/issues/180 // https://github.com/chrisaljoudi/uBlock/issues/180
// Whitelist directives need to be loaded once the PSL is available // Whitelist directives need to be loaded once the PSL is available
netWhitelist: {}, netWhitelist: new Map(),
netWhitelistModifyTime: 0, netWhitelistModifyTime: 0,
netWhitelistDefault: [ netWhitelistDefault: [
'about-scheme', 'about-scheme',

View File

@ -762,9 +762,9 @@ const µb = µBlock;
// Settings // Settings
var getLocalData = function(callback) { const getLocalData = function(callback) {
var onStorageInfoReady = function(bytesInUse) { const onStorageInfoReady = function(bytesInUse) {
var o = µb.restoreBackupSettings; const o = µb.restoreBackupSettings;
callback({ callback({
storageUsed: bytesInUse, storageUsed: bytesInUse,
lastRestoreFile: o.lastRestoreFile, lastRestoreFile: o.lastRestoreFile,
@ -779,13 +779,15 @@ var getLocalData = function(callback) {
µb.getBytesInUse(onStorageInfoReady); µb.getBytesInUse(onStorageInfoReady);
}; };
var backupUserData = function(callback) { const backupUserData = function(callback) {
var userData = { const userData = {
timeStamp: Date.now(), timeStamp: Date.now(),
version: vAPI.app.version, version: vAPI.app.version,
userSettings: µb.userSettings, userSettings: µb.userSettings,
selectedFilterLists: µb.selectedFilterLists, selectedFilterLists: µb.selectedFilterLists,
hiddenSettings: µb.hiddenSettings, hiddenSettings: µb.hiddenSettings,
whitelist: µb.arrayFromWhitelist(µb.netWhitelist),
// String representation eventually to be deprecated
netWhitelist: µb.stringFromWhitelist(µb.netWhitelist), netWhitelist: µb.stringFromWhitelist(µb.netWhitelist),
dynamicFilteringString: µb.permanentFirewall.toString(), dynamicFilteringString: µb.permanentFirewall.toString(),
urlFilteringString: µb.permanentURLFiltering.toString(), urlFilteringString: µb.permanentURLFiltering.toString(),
@ -793,9 +795,9 @@ var backupUserData = function(callback) {
userFilters: '' userFilters: ''
}; };
var onUserFiltersReady = function(details) { const onUserFiltersReady = function(details) {
userData.userFilters = details.content; userData.userFilters = details.content;
var filename = vAPI.i18n('aboutBackupFilename') const filename = vAPI.i18n('aboutBackupFilename')
.replace('{{datetime}}', µb.dateNowToSensibleString()) .replace('{{datetime}}', µb.dateNowToSensibleString())
.replace(/ +/g, '_'); .replace(/ +/g, '_');
µb.restoreBackupSettings.lastBackupFile = filename; µb.restoreBackupSettings.lastBackupFile = filename;
@ -829,9 +831,19 @@ const restoreUserData = function(request) {
userData.hiddenSettingsString || '' userData.hiddenSettingsString || ''
); );
} }
// Whitelist directives can be represented as an array or as a
// (eventually to be deprecated) string.
let whitelist = userData.whitelist;
if (
Array.isArray(whitelist) === false &&
typeof userData.netWhitelist === 'string' &&
userData.netWhitelist !== ''
) {
whitelist = userData.netWhitelist.split('\n');
}
vAPI.storage.set({ vAPI.storage.set({
hiddenSettings: hiddenSettings, hiddenSettings: hiddenSettings,
netWhitelist: userData.netWhitelist || '', netWhitelist: whitelist || [],
dynamicFilteringString: userData.dynamicFilteringString || '', dynamicFilteringString: userData.dynamicFilteringString || '',
urlFilteringString: userData.urlFilteringString || '', urlFilteringString: userData.urlFilteringString || '',
hostnameSwitchesString: userData.hostnameSwitchesString || '', hostnameSwitchesString: userData.hostnameSwitchesString || '',

View File

@ -50,7 +50,10 @@ const handleImportFilePicker = function() {
if ( typeof userData.userSettings !== 'object' ) { if ( typeof userData.userSettings !== 'object' ) {
throw 'Invalid'; throw 'Invalid';
} }
if ( typeof userData.netWhitelist !== 'string' ) { if (
Array.isArray(userData.whitelist) === false &&
typeof userData.netWhitelist !== 'string'
) {
throw 'Invalid'; throw 'Invalid';
} }
if ( if (

View File

@ -239,7 +239,10 @@ const onVersionReady = function(lastVersion) {
// gorhill 2014-12-15: not anymore // gorhill 2014-12-15: not anymore
const onNetWhitelistReady = function(netWhitelistRaw) { const onNetWhitelistReady = function(netWhitelistRaw) {
µb.netWhitelist = µb.whitelistFromString(netWhitelistRaw); if ( typeof netWhitelistRaw === 'string' ) {
netWhitelistRaw = netWhitelistRaw.split('\n');
}
µb.netWhitelist = µb.whitelistFromArray(netWhitelistRaw);
µb.netWhitelistModifyTime = Date.now(); µb.netWhitelistModifyTime = Date.now();
}; };
@ -358,7 +361,7 @@ const createDefaultProps = function() {
'lastRestoreTime': 0, 'lastRestoreTime': 0,
'lastBackupFile': '', 'lastBackupFile': '',
'lastBackupTime': 0, 'lastBackupTime': 0,
'netWhitelist': µb.netWhitelistDefault.join('\n'), 'netWhitelist': µb.netWhitelistDefault,
'selfieMagic': 0, 'selfieMagic': 0,
'version': '0.0.0.0' 'version': '0.0.0.0'
}; };

View File

@ -238,7 +238,7 @@
µBlock.saveWhitelist = function() { µBlock.saveWhitelist = function() {
vAPI.storage.set({ vAPI.storage.set({
netWhitelist: this.stringFromWhitelist(this.netWhitelist) netWhitelist: this.arrayFromWhitelist(this.netWhitelist)
}); });
this.netWhitelistModifyTime = Date.now(); this.netWhitelistModifyTime = Date.now();
}; };
@ -1194,8 +1194,11 @@
binNotEmpty = true; binNotEmpty = true;
} }
if ( typeof data.netWhitelist === 'string' ) { if ( Array.isArray(data.whitelist) ) {
bin.netWhitelist = data.netWhitelist; bin.netWhitelist = data.whitelist;
binNotEmpty = true;
} else if ( typeof data.netWhitelist === 'string' ) {
bin.netWhitelist = data.netWhitelist.split('\n');
binNotEmpty = true; binNotEmpty = true;
} }

View File

@ -24,29 +24,30 @@
/******************************************************************************/ /******************************************************************************/
/******************************************************************************/ /******************************************************************************/
(function(){ // *****************************************************************************
// start of local namespace
/******************************************************************************/ {
// https://github.com/chrisaljoudi/uBlock/issues/405 // https://github.com/chrisaljoudi/uBlock/issues/405
// Be more flexible with whitelist syntax // Be more flexible with whitelist syntax
// Any special regexp char will be escaped // Any special regexp char will be escaped
var whitelistDirectiveEscape = /[-\/\\^$+?.()|[\]{}]/g; const whitelistDirectiveEscape = /[-\/\\^$+?.()|[\]{}]/g;
// All `*` will be expanded into `.*` // All `*` will be expanded into `.*`
var whitelistDirectiveEscapeAsterisk = /\*/g; const whitelistDirectiveEscapeAsterisk = /\*/g;
// Remember encountered regexps for reuse. // Remember encountered regexps for reuse.
var directiveToRegexpMap = new Map(); const directiveToRegexpMap = new Map();
// Probably manually entered whitelist directive // Probably manually entered whitelist directive
var isHandcraftedWhitelistDirective = function(directive) { const isHandcraftedWhitelistDirective = function(directive) {
return directive.startsWith('/') && directive.endsWith('/') || return directive.startsWith('/') && directive.endsWith('/') ||
directive.indexOf('/') !== -1 && directive.indexOf('*') !== -1; directive.indexOf('/') !== -1 && directive.indexOf('*') !== -1;
}; };
var matchDirective = function(url, hostname, directive) { const matchDirective = function(url, hostname, directive) {
// Directive is a plain hostname. // Directive is a plain hostname.
if ( directive.indexOf('/') === -1 ) { if ( directive.indexOf('/') === -1 ) {
return hostname.endsWith(directive) && return hostname.endsWith(directive) &&
@ -54,13 +55,16 @@ var matchDirective = function(url, hostname, directive) {
hostname.charAt(hostname.length - directive.length - 1) === '.'); hostname.charAt(hostname.length - directive.length - 1) === '.');
} }
// Match URL exactly. // Match URL exactly.
if ( directive.startsWith('/') === false && directive.indexOf('*') === -1 ) { if (
directive.startsWith('/') === false &&
directive.indexOf('*') === -1
) {
return url === directive; return url === directive;
} }
// Transpose into a regular expression. // Transpose into a regular expression.
var re = directiveToRegexpMap.get(directive); let re = directiveToRegexpMap.get(directive);
if ( re === undefined ) { if ( re === undefined ) {
var reStr; let reStr;
if ( directive.startsWith('/') && directive.endsWith('/') ) { if ( directive.startsWith('/') && directive.endsWith('/') ) {
reStr = directive.slice(1, -1); reStr = directive.slice(1, -1);
} else { } else {
@ -73,9 +77,9 @@ var matchDirective = function(url, hostname, directive) {
return re.test(url); return re.test(url);
}; };
var matchBucket = function(url, hostname, bucket, start) { const matchBucket = function(url, hostname, bucket, start) {
if ( bucket ) { if ( bucket ) {
for ( var i = start || 0, n = bucket.length; i < n; i++ ) { for ( let i = start || 0, n = bucket.length; i < n; i++ ) {
if ( matchDirective(url, hostname, bucket[i]) ) { if ( matchDirective(url, hostname, bucket[i]) ) {
return i; return i;
} }
@ -84,23 +88,20 @@ var matchBucket = function(url, hostname, bucket, start) {
return -1; return -1;
}; };
// https://www.youtube.com/watch?v=RL2W_XK-UJ4&list=PLhPp-QAUKF_hRMjWsYvvdazGw0qIjtSXJ
/******************************************************************************/ /******************************************************************************/
µBlock.getNetFilteringSwitch = function(url) { µBlock.getNetFilteringSwitch = function(url) {
var targetHostname = this.URI.hostnameFromURI(url), const hostname = this.URI.hostnameFromURI(url);
key = targetHostname, let key = hostname;
pos;
for (;;) { for (;;) {
if ( matchBucket(url, targetHostname, this.netWhitelist[key]) !== -1 ) { if ( matchBucket(url, hostname, this.netWhitelist.get(key)) !== -1 ) {
return false; return false;
} }
pos = key.indexOf('.'); const pos = key.indexOf('.');
if ( pos === -1 ) { break; } if ( pos === -1 ) { break; }
key = key.slice(pos + 1); key = key.slice(pos + 1);
} }
if ( matchBucket(url, targetHostname, this.netWhitelist['//']) !== -1 ) { if ( matchBucket(url, hostname, this.netWhitelist.get('//')) !== -1 ) {
return false; return false;
} }
return true; return true;
@ -109,7 +110,7 @@ var matchBucket = function(url, hostname, bucket, start) {
/******************************************************************************/ /******************************************************************************/
µBlock.toggleNetFilteringSwitch = function(url, scope, newState) { µBlock.toggleNetFilteringSwitch = function(url, scope, newState) {
var currentState = this.getNetFilteringSwitch(url); const currentState = this.getNetFilteringSwitch(url);
if ( newState === undefined ) { if ( newState === undefined ) {
newState = !currentState; newState = !currentState;
} }
@ -117,58 +118,59 @@ var matchBucket = function(url, hostname, bucket, start) {
return currentState; return currentState;
} }
var netWhitelist = this.netWhitelist, const netWhitelist = this.netWhitelist;
pos = url.indexOf('#'), const pos = url.indexOf('#');
targetURL = pos !== -1 ? url.slice(0, pos) : url, let targetURL = pos !== -1 ? url.slice(0, pos) : url;
targetHostname = this.URI.hostnameFromURI(targetURL), const targetHostname = this.URI.hostnameFromURI(targetURL);
key = targetHostname, let key = targetHostname;
directive = scope === 'page' ? targetURL : targetHostname; let directive = scope === 'page' ? targetURL : targetHostname;
// Add to directive list // Add to directive list
if ( newState === false ) { if ( newState === false ) {
if ( netWhitelist[key] === undefined ) { let bucket = netWhitelist.get(key);
netWhitelist[key] = []; if ( bucket === undefined ) {
bucket = [];
netWhitelist.set(key, bucket);
} }
netWhitelist[key].push(directive); bucket.push(directive);
this.saveWhitelist(); this.saveWhitelist();
return true; return true;
} }
// Remove from directive list whatever causes current URL to be whitelisted // Remove all directives which cause current URL to be whitelisted
var bucket, i;
for (;;) { for (;;) {
bucket = netWhitelist[key]; const bucket = netWhitelist.get(key);
if ( bucket !== undefined ) { if ( bucket !== undefined ) {
i = undefined; let i;
for (;;) { for (;;) {
i = matchBucket(targetURL, targetHostname, bucket, i); i = matchBucket(targetURL, targetHostname, bucket, i);
if ( i === -1 ) { break; } if ( i === -1 ) { break; }
directive = bucket.splice(i, 1)[0]; directive = bucket.splice(i, 1)[0];
if ( isHandcraftedWhitelistDirective(directive) ) { if ( isHandcraftedWhitelistDirective(directive) ) {
netWhitelist['#'].push('# ' + directive); netWhitelist.get('#').push(`# ${directive}`);
} }
} }
if ( bucket.length === 0 ) { if ( bucket.length === 0 ) {
delete netWhitelist[key]; netWhitelist.delete(key);
} }
} }
pos = key.indexOf('.'); const pos = key.indexOf('.');
if ( pos === -1 ) { break; } if ( pos === -1 ) { break; }
key = key.slice(pos + 1); key = key.slice(pos + 1);
} }
bucket = netWhitelist['//']; const bucket = netWhitelist.get('//');
if ( bucket !== undefined ) { if ( bucket !== undefined ) {
i = undefined; let i;
for (;;) { for (;;) {
i = matchBucket(targetURL, targetHostname, bucket, i); i = matchBucket(targetURL, targetHostname, bucket, i);
if ( i === -1 ) { break; } if ( i === -1 ) { break; }
directive = bucket.splice(i, 1)[0]; directive = bucket.splice(i, 1)[0];
if ( isHandcraftedWhitelistDirective(directive) ) { if ( isHandcraftedWhitelistDirective(directive) ) {
netWhitelist['#'].push('# ' + directive); netWhitelist.get('#').push(`# ${directive}`);
} }
} }
if ( bucket.length === 0 ) { if ( bucket.length === 0 ) {
delete netWhitelist['//']; netWhitelist.delete('//');
} }
} }
this.saveWhitelist(); this.saveWhitelist();
@ -179,8 +181,7 @@ var matchBucket = function(url, hostname, bucket, start) {
µBlock.arrayFromWhitelist = function(whitelist) { µBlock.arrayFromWhitelist = function(whitelist) {
const out = new Set(); const out = new Set();
for ( const key in whitelist ) { for ( const bucket of whitelist.values() ) {
const bucket = whitelist[key];
for ( const directive of bucket ) { for ( const directive of bucket ) {
out.add(directive); out.add(directive);
} }
@ -194,25 +195,23 @@ var matchBucket = function(url, hostname, bucket, start) {
/******************************************************************************/ /******************************************************************************/
µBlock.whitelistFromString = function(s) { µBlock.whitelistFromArray = function(lines) {
var whitelist = Object.create(null), const whitelist = new Map();
lineIter = new this.LineIterator(s),
line, matches, key, directive, re;
// Comment bucket must always be ready to be used. // Comment bucket must always be ready to be used.
whitelist['#'] = []; whitelist.set('#', []);
// New set of directives, scrap cached data. // New set of directives, scrap cached data.
directiveToRegexpMap.clear(); directiveToRegexpMap.clear();
while ( !lineIter.eot() ) { for ( let line of lines ) {
line = lineIter.next().trim(); line = line.trim();
// https://github.com/gorhill/uBlock/issues/171 // https://github.com/gorhill/uBlock/issues/171
// Skip empty lines // Skip empty lines
if ( line === '' ) { if ( line === '' ) { continue; }
continue;
} let key, directive;
// Don't throw out commented out lines: user might want to fix them // Don't throw out commented out lines: user might want to fix them
if ( line.startsWith('#') ) { if ( line.startsWith('#') ) {
@ -229,11 +228,15 @@ var matchBucket = function(url, hostname, bucket, start) {
} }
} }
// Regex-based (ensure it is valid) // Regex-based (ensure it is valid)
else if ( line.length > 2 && line.startsWith('/') && line.endsWith('/') ) { else if (
line.length > 2 &&
line.startsWith('/') &&
line.endsWith('/')
) {
key = '//'; key = '//';
directive = line; directive = line;
try { try {
re = new RegExp(directive.slice(1, -1)); const re = new RegExp(directive.slice(1, -1));
directiveToRegexpMap.set(directive, re); directiveToRegexpMap.set(directive, re);
} catch(ex) { } catch(ex) {
key = '#'; key = '#';
@ -244,7 +247,7 @@ var matchBucket = function(url, hostname, bucket, start) {
// label (or else it would be just impossible to make an efficient // label (or else it would be just impossible to make an efficient
// dict. // dict.
else { else {
matches = this.reWhitelistHostnameExtractor.exec(line); const matches = this.reWhitelistHostnameExtractor.exec(line);
if ( !matches || matches.length !== 2 ) { if ( !matches || matches.length !== 2 ) {
key = '#'; key = '#';
directive = '# ' + line; directive = '# ' + line;
@ -260,21 +263,28 @@ var matchBucket = function(url, hostname, bucket, start) {
// Be sure this stays fixed: // Be sure this stays fixed:
// https://github.com/chrisaljoudi/uBlock/issues/185 // https://github.com/chrisaljoudi/uBlock/issues/185
if ( whitelist[key] === undefined ) { let bucket = whitelist.get(key);
whitelist[key] = []; if ( bucket === undefined ) {
bucket = [];
whitelist.set(key, bucket);
} }
whitelist[key].push(directive); bucket.push(directive);
} }
return whitelist; return whitelist;
}; };
µBlock.whitelistFromString = function(s) {
return this.whitelistFromArray(s.split('\n'));
};
// https://github.com/gorhill/uBlock/issues/3717 // https://github.com/gorhill/uBlock/issues/3717
µBlock.reWhitelistBadHostname = /[^a-z0-9.\-_\[\]:]/; µBlock.reWhitelistBadHostname = /[^a-z0-9.\-_\[\]:]/;
µBlock.reWhitelistHostnameExtractor = /([a-z0-9.\-_\[\]]+)(?::[\d*]+)?\/(?:[^\x00-\x20\/]|$)[^\x00-\x20]*$/; µBlock.reWhitelistHostnameExtractor = /([a-z0-9.\-_\[\]]+)(?::[\d*]+)?\/(?:[^\x00-\x20\/]|$)[^\x00-\x20]*$/;
/******************************************************************************/ // end of local namespace
// *****************************************************************************
})(); }
/******************************************************************************/ /******************************************************************************/
/******************************************************************************/ /******************************************************************************/
@ -391,7 +401,7 @@ var matchBucket = function(url, hostname, bucket, start) {
// https://www.reddit.com/r/uBlockOrigin/comments/8524cf/my_custom_scriptlets_doesnt_work_what_am_i_doing/ // https://www.reddit.com/r/uBlockOrigin/comments/8524cf/my_custom_scriptlets_doesnt_work_what_am_i_doing/
µBlock.changeHiddenSettings = function(hs) { µBlock.changeHiddenSettings = function(hs) {
var mustReloadResources = const mustReloadResources =
hs.userResourcesLocation !== this.hiddenSettings.userResourcesLocation; hs.userResourcesLocation !== this.hiddenSettings.userResourcesLocation;
this.hiddenSettings = hs; this.hiddenSettings = hs;
this.saveHiddenSettings(); this.saveHiddenSettings();
@ -522,7 +532,7 @@ var matchBucket = function(url, hostname, bucket, start) {
); );
break; break;
case 'no-large-media': case 'no-large-media':
var pageStore = this.pageStoreFromTabId(details.tabId); const pageStore = this.pageStoreFromTabId(details.tabId);
if ( pageStore !== null ) { if ( pageStore !== null ) {
pageStore.temporarilyAllowLargeMediaElements(!details.state); pageStore.temporarilyAllowLargeMediaElements(!details.state);
} }
@ -560,43 +570,44 @@ var matchBucket = function(url, hostname, bucket, start) {
/******************************************************************************/ /******************************************************************************/
µBlock.scriptlets = (function() { µBlock.scriptlets = (function() {
var pendingEntries = new Map(); const pendingEntries = new Map();
var Entry = function(tabId, scriptlet, callback) { const Entry = class {
this.tabId = tabId; constructor(tabId, scriptlet, callback) {
this.scriptlet = scriptlet; this.tabId = tabId;
this.callback = callback; this.scriptlet = scriptlet;
this.timer = vAPI.setTimeout(this.service.bind(this), 1000); this.callback = callback;
}; this.timer = vAPI.setTimeout(this.service.bind(this), 1000);
}
Entry.prototype.service = function(response) { service(response) {
if ( this.timer !== null ) { if ( this.timer !== null ) {
clearTimeout(this.timer); clearTimeout(this.timer);
this.timer = null; this.timer = null;
}
pendingEntries.delete(makeKey(this.tabId, this.scriptlet));
this.callback(response);
} }
pendingEntries.delete(makeKey(this.tabId, this.scriptlet));
this.callback(response);
}; };
var makeKey = function(tabId, scriptlet) { const makeKey = function(tabId, scriptlet) {
return tabId + ' ' + scriptlet; return tabId + ' ' + scriptlet;
}; };
var report = function(tabId, scriptlet, response) { const report = function(tabId, scriptlet, response) {
var key = makeKey(tabId, scriptlet); const key = makeKey(tabId, scriptlet);
var entry = pendingEntries.get(key); const entry = pendingEntries.get(key);
if ( entry === undefined ) { return; } if ( entry === undefined ) { return; }
entry.service(response); entry.service(response);
}; };
var inject = function(tabId, scriptlet, callback) { const inject = function(tabId, scriptlet, callback) {
if ( typeof callback === 'function' ) { if ( typeof callback === 'function' ) {
if ( vAPI.isBehindTheSceneTabId(tabId) ) { if ( vAPI.isBehindTheSceneTabId(tabId) ) {
callback(); callback();
return; return;
} }
var key = makeKey(tabId, scriptlet), const key = makeKey(tabId, scriptlet);
entry = pendingEntries.get(key); const entry = pendingEntries.get(key);
if ( entry !== undefined ) { if ( entry !== undefined ) {
if ( callback !== entry.callback ) { if ( callback !== entry.callback ) {
callback(); callback();
@ -611,7 +622,7 @@ var matchBucket = function(url, hostname, bucket, start) {
}; };
// TODO: think about a callback mechanism. // TODO: think about a callback mechanism.
var injectDeep = function(tabId, scriptlet) { const injectDeep = function(tabId, scriptlet) {
vAPI.tabs.injectScript(tabId, { vAPI.tabs.injectScript(tabId, {
file: '/js/scriptlets/' + scriptlet + '.js', file: '/js/scriptlets/' + scriptlet + '.js',
allFrames: true allFrames: true