uBlock/src/js/ublock.js

528 lines
16 KiB
JavaScript
Raw Normal View History

2014-06-24 00:42:43 +02:00
/*******************************************************************************
2016-05-03 14:22:48 +02:00
uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-2016 Raymond Hill
2014-06-24 00:42:43 +02:00
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
*/
2014-12-12 23:59:47 +01:00
/******************************************************************************/
(function(){
'use strict';
2014-06-24 00:42:43 +02:00
/******************************************************************************/
2015-04-07 03:26:05 +02:00
// https://github.com/chrisaljoudi/uBlock/issues/405
// Be more flexible with whitelist syntax
// Any special regexp char will be escaped
var whitelistDirectiveEscape = /[-\/\\^$+?.()|[\]{}]/g;
// All `*` will be expanded into `.*`
var whitelistDirectiveEscapeAsterisk = /\*/g;
// Probably manually entered whitelist directive
var isHandcraftedWhitelistDirective = function(directive) {
2014-12-14 23:21:59 +01:00
return directive.indexOf('/') !== -1 &&
directive.indexOf('*') !== -1;
};
var matchWhitelistDirective = function(url, hostname, directive) {
// Directive is a plain hostname
if ( directive.indexOf('/') === -1 ) {
return hostname.endsWith(directive) &&
(hostname.length === directive.length ||
hostname.charAt(hostname.length - directive.length - 1) === '.');
2014-12-14 23:21:59 +01:00
}
// Match URL exactly
if ( directive.indexOf('*') === -1 ) {
return url === directive;
2014-08-02 17:40:27 +02:00
}
2016-05-03 14:22:48 +02:00
// TODO: Revisit implementation to avoid creating a regex each time.
2014-12-14 23:21:59 +01:00
// Regex escape code inspired from:
// "Is there a RegExp.escape function in Javascript?"
// http://stackoverflow.com/a/3561711
var reStr = directive.replace(whitelistDirectiveEscape, '\\$&')
.replace(whitelistDirectiveEscapeAsterisk, '.*');
var re = new RegExp(reStr);
return re.test(url);
};
/******************************************************************************/
2014-08-02 17:40:27 +02:00
µBlock.getNetFilteringSwitch = function(url) {
2014-12-14 23:21:59 +01:00
var netWhitelist = this.netWhitelist;
2015-03-23 20:19:17 +01:00
var buckets, i, pos;
var targetHostname = this.URI.hostnameFromURI(url);
2014-12-14 23:21:59 +01:00
var key = targetHostname;
for (;;) {
2014-12-14 23:21:59 +01:00
if ( netWhitelist.hasOwnProperty(key) ) {
buckets = netWhitelist[key];
i = buckets.length;
while ( i-- ) {
2015-03-23 20:19:17 +01:00
if ( matchWhitelistDirective(url, targetHostname, buckets[i]) ) {
// console.log('"%s" matche url "%s"', buckets[i], url);
return false;
}
}
}
2014-12-14 23:21:59 +01:00
pos = key.indexOf('.');
if ( pos === -1 ) {
break;
2014-06-24 00:42:43 +02:00
}
2014-12-14 23:21:59 +01:00
key = key.slice(pos + 1);
2014-06-24 00:42:43 +02:00
}
return true;
};
/******************************************************************************/
2014-08-02 17:40:27 +02:00
µBlock.toggleNetFilteringSwitch = function(url, scope, newState) {
var currentState = this.getNetFilteringSwitch(url);
2014-06-24 00:42:43 +02:00
if ( newState === undefined ) {
newState = !currentState;
}
if ( newState === currentState ) {
return currentState;
}
2014-08-02 17:40:27 +02:00
var netWhitelist = this.netWhitelist;
2014-12-14 23:21:59 +01:00
var pos = url.indexOf('#');
var targetURL = pos !== -1 ? url.slice(0, pos) : url;
var targetHostname = this.URI.hostnameFromURI(targetURL);
var key = targetHostname;
var directive = scope === 'page' ? targetURL : targetHostname;
2014-06-24 00:42:43 +02:00
2014-12-14 23:21:59 +01:00
// Add to directive list
if ( newState === false ) {
2014-12-14 23:21:59 +01:00
if ( netWhitelist.hasOwnProperty(key) === false ) {
2014-12-22 20:48:17 +01:00
netWhitelist[key] = [];
}
2014-12-22 20:48:17 +01:00
netWhitelist[key].push(directive);
2014-08-02 17:40:27 +02:00
this.saveWhitelist();
2014-06-24 00:42:43 +02:00
return true;
}
2014-12-14 23:21:59 +01:00
// Remove from directive list whatever causes current URL to be whitelisted
2014-12-22 20:48:17 +01:00
var buckets, i;
for (;;) {
2014-12-14 23:21:59 +01:00
if ( netWhitelist.hasOwnProperty(key) ) {
buckets = netWhitelist[key];
i = buckets.length;
while ( i-- ) {
directive = buckets[i];
2014-12-14 23:21:59 +01:00
if ( !matchWhitelistDirective(targetURL, targetHostname, directive) ) {
continue;
}
buckets.splice(i, 1);
// If it is a directive which can't be created easily through
// the user interface, keep it around as a commented out
// directive
if ( isHandcraftedWhitelistDirective(directive) ) {
netWhitelist['#'].push('# ' + directive);
}
}
if ( buckets.length === 0 ) {
2014-12-14 23:21:59 +01:00
delete netWhitelist[key];
}
2014-06-24 00:42:43 +02:00
}
2014-12-14 23:21:59 +01:00
pos = key.indexOf('.');
if ( pos === -1 ) {
break;
}
2014-12-14 23:21:59 +01:00
key = key.slice(pos + 1);
2014-08-02 17:40:27 +02:00
}
this.saveWhitelist();
return true;
2014-06-24 00:42:43 +02:00
};
/******************************************************************************/
2014-12-08 18:37:35 +01:00
µBlock.stringFromWhitelist = function(whitelist) {
2014-08-02 17:40:27 +02:00
var r = {};
var i, bucket;
for ( var key in whitelist ) {
if ( whitelist.hasOwnProperty(key) === false ) {
2014-08-02 17:40:27 +02:00
continue;
}
bucket = whitelist[key];
i = bucket.length;
while ( i-- ) {
2014-08-02 17:40:27 +02:00
r[bucket[i]] = true;
2014-06-24 01:23:36 +02:00
}
}
2014-08-02 17:40:27 +02:00
return Object.keys(r).sort(function(a,b){return a.localeCompare(b);}).join('\n');
2014-06-24 01:23:36 +02:00
};
/******************************************************************************/
2014-08-02 17:40:27 +02:00
µBlock.whitelistFromString = function(s) {
var whitelist = {
'#': []
};
var reInvalidHostname = /[^a-z0-9.\-\[\]:]/;
2016-05-03 14:22:48 +02:00
var reHostnameExtractor = /([a-z0-9\[][a-z0-9.\-]*[a-z0-9\]])(?::[\d*]+)?\/(?:[^\x00-\x20\/]|$)[^\x00-\x20]*$/;
2014-08-02 17:40:27 +02:00
var lines = s.split(/[\n\r]+/);
var line, matches, key, directive;
2014-08-02 17:40:27 +02:00
for ( var i = 0; i < lines.length; i++ ) {
line = lines[i].trim();
2015-05-04 23:10:55 +02:00
// https://github.com/gorhill/uBlock/issues/171
// Skip empty lines
if ( line === '' ) {
continue;
}
// Don't throw out commented out lines: user might want to fix them
if ( line.startsWith('#') ) {
key = '#';
directive = line;
}
// Plain hostname
else if ( line.indexOf('/') === -1 ) {
if ( reInvalidHostname.test(line) ) {
key = '#';
directive = '# ' + line;
} else {
key = directive = line;
}
2014-08-02 17:40:27 +02:00
}
// URL, possibly wildcarded: there MUST be at least one hostname
// label (or else it would be just impossible to make an efficient
// dict.
else {
matches = reHostnameExtractor.exec(line);
if ( !matches || matches.length !== 2 ) {
key = '#';
directive = '# ' + line;
} else {
key = matches[1];
directive = line;
}
}
2015-05-04 23:10:55 +02:00
// https://github.com/gorhill/uBlock/issues/171
// Skip empty keys
if ( key === '' ) {
continue;
}
// Be sure this stays fixed:
2015-04-07 03:26:05 +02:00
// https://github.com/chrisaljoudi/uBlock/issues/185
if ( whitelist.hasOwnProperty(key) === false ) {
whitelist[key] = [];
2014-08-02 17:40:27 +02:00
}
whitelist[key].push(directive);
2014-08-02 17:40:27 +02:00
}
return whitelist;
2014-06-24 00:42:43 +02:00
};
/******************************************************************************/
µBlock.changeUserSettings = function(name, value) {
var us = this.userSettings;
// Return all settings if none specified.
2014-06-25 00:29:55 +02:00
if ( name === undefined ) {
us = JSON.parse(JSON.stringify(us));
us.noCosmeticFiltering = this.hnSwitches.evaluate('no-cosmetic-filtering', '*') === 1;
us.noLargeMedia = this.hnSwitches.evaluate('no-large-media', '*') === 1;
us.noRemoteFonts = this.hnSwitches.evaluate('no-remote-fonts', '*') === 1;
return us;
2014-06-25 00:29:55 +02:00
}
2014-06-24 00:42:43 +02:00
if ( typeof name !== 'string' || name === '' ) {
return;
}
if ( value === undefined ) {
return us[name];
2014-06-24 00:42:43 +02:00
}
// Pre-change
switch ( name ) {
case 'largeMediaSize':
if ( typeof value !== 'number' ) {
value = parseInt(value, 10) || 0;
}
value = Math.ceil(Math.max(value, 0));
break;
2015-05-21 20:15:17 +02:00
default:
break;
2014-06-24 00:42:43 +02:00
}
// Change -- but only if the user setting actually exists.
var mustSave = us.hasOwnProperty(name) &&
value !== us[name];
if ( mustSave ) {
us[name] = value;
}
2014-06-24 00:42:43 +02:00
// Post-change
switch ( name ) {
2015-05-21 20:15:17 +02:00
case 'collapseBlocked':
if ( value === false ) {
this.cosmeticFilteringEngine.removeFromSelectorCache('*', 'net');
}
break;
case 'contextMenuEnabled':
this.contextMenu.update(null);
2015-05-21 20:15:17 +02:00
break;
case 'hyperlinkAuditingDisabled':
vAPI.browserSettings.set({ 'hyperlinkAuditing': !value });
break;
case 'noCosmeticFiltering':
if ( this.hnSwitches.toggle('no-cosmetic-filtering', '*', value ? 1 : 0) ) {
this.saveHostnameSwitches();
}
break;
case 'noLargeMedia':
if ( this.hnSwitches.toggle('no-large-media', '*', value ? 1 : 0) ) {
this.saveHostnameSwitches();
}
break;
case 'noRemoteFonts':
if ( this.hnSwitches.toggle('no-remote-fonts', '*', value ? 1 : 0) ) {
this.saveHostnameSwitches();
}
break;
2015-06-01 21:03:22 +02:00
case 'prefetchingDisabled':
vAPI.browserSettings.set({ 'prefetching': !value });
2015-06-01 21:03:22 +02:00
break;
case 'webrtcIPAddressHidden':
vAPI.browserSettings.set({ 'webrtcIPAddress': !value });
break;
2015-05-21 20:15:17 +02:00
default:
break;
2014-06-24 00:42:43 +02:00
}
if ( mustSave ) {
this.saveUserSettings();
}
2014-06-24 00:42:43 +02:00
};
/******************************************************************************/
2014-09-28 18:05:46 +02:00
µBlock.elementPickerExec = function(tabId, targetElement) {
if ( vAPI.isBehindTheSceneTabId(tabId) ) {
return;
}
this.epickerTarget = targetElement || '';
2015-06-26 06:08:41 +02:00
this.scriptlets.inject(tabId, 'element-picker');
if ( typeof vAPI.tabs.select === 'function' ) {
vAPI.tabs.select(tabId);
}
2014-09-28 18:05:46 +02:00
};
/******************************************************************************/
2014-10-06 20:02:44 +02:00
µBlock.toggleFirewallRule = function(details) {
2016-05-28 15:18:36 +02:00
var requestType = details.requestType;
if ( details.action !== 0 ) {
2016-05-28 15:18:36 +02:00
this.sessionFirewall.setCellZ(details.srcHostname, details.desHostname, requestType, details.action);
2014-10-06 20:02:44 +02:00
} else {
2016-05-28 15:18:36 +02:00
this.sessionFirewall.unsetCell(details.srcHostname, details.desHostname, requestType);
2014-10-06 20:02:44 +02:00
}
2014-12-17 16:32:50 +01:00
2015-04-07 03:26:05 +02:00
// https://github.com/chrisaljoudi/uBlock/issues/731#issuecomment-73937469
if ( details.persist ) {
if ( details.action !== 0 ) {
2016-05-28 15:18:36 +02:00
this.permanentFirewall.setCellZ(details.srcHostname, details.desHostname, requestType, details.action);
} else {
2016-05-28 15:18:36 +02:00
this.permanentFirewall.unsetCell(details.srcHostname, details.desHostname, requestType, details.action);
}
this.savePermanentFirewallRules();
}
2016-05-28 15:18:36 +02:00
// https://github.com/gorhill/uBlock/issues/1662
// Flush all cached `net` cosmetic filters if we are dealing with a
// collapsable type: any of the cached entries could be a resource on the
// target page.
var srcHostname = details.srcHostname;
if (
(srcHostname !== '*') &&
(requestType === '*' || requestType === 'image' || requestType === '3p' || requestType === '3p-frame')
) {
srcHostname = '*';
}
2015-04-07 03:26:05 +02:00
// https://github.com/chrisaljoudi/uBlock/issues/420
2016-05-28 15:18:36 +02:00
this.cosmeticFilteringEngine.removeFromSelectorCache(srcHostname, 'net');
};
/******************************************************************************/
2015-05-21 20:15:17 +02:00
µ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');
2015-05-22 14:05:55 +02:00
if ( !details.persist ) {
return;
}
changed = this.permanentURLFiltering.setRule(
details.context,
details.url,
details.type,
details.action
);
if ( changed ) {
this.savePermanentFirewallRules();
}
2015-05-21 20:15:17 +02:00
};
/******************************************************************************/
2015-01-01 16:55:00 +01:00
µBlock.isBlockResult = function(result) {
return typeof result === 'string' && result.charAt(1) === 'b';
};
/******************************************************************************/
µBlock.isAllowResult = function(result) {
return typeof result !== 'string' || result.charAt(1) !== 'b';
};
2015-01-01 16:55:00 +01:00
/******************************************************************************/
2015-03-27 18:00:55 +01:00
µBlock.toggleHostnameSwitch = function(details) {
2015-04-07 19:27:46 +02:00
if ( this.hnSwitches.toggleZ(details.name, details.hostname, !!details.deep, details.state) ) {
2015-03-27 18:00:55 +01:00
this.saveHostnameSwitches();
}
// Take action if needed
switch ( details.name ) {
case 'no-cosmetic-filtering':
2015-06-26 06:08:41 +02:00
this.scriptlets.injectDeep(
details.tabId,
details.state ? 'cosmetic-off' : 'cosmetic-on'
);
break;
case 'no-large-media':
if ( details.state === false ) {
var pageStore = this.pageStoreFromTabId(details.tabId);
if ( pageStore !== null ) {
pageStore.temporarilyAllowLargeMediaElements();
}
}
break;
}
};
/******************************************************************************/
µBlock.logCosmeticFilters = (function() {
var tabIdToTimerMap = {};
var injectNow = function(tabId) {
delete tabIdToTimerMap[tabId];
µBlock.scriptlets.injectDeep(tabId, 'cosmetic-logger');
};
var injectAsync = function(tabId) {
if ( tabIdToTimerMap.hasOwnProperty(tabId) ) {
return;
}
tabIdToTimerMap[tabId] = vAPI.setTimeout(
injectNow.bind(null, tabId),
100
);
};
return injectAsync;
})();
/******************************************************************************/
2015-06-26 06:08:41 +02:00
µBlock.scriptlets = (function() {
var pendingEntries = Object.create(null);
var Entry = function(tabId, scriptlet, callback) {
this.tabId = tabId;
this.scriptlet = scriptlet;
this.callback = callback;
this.timer = vAPI.setTimeout(this.service.bind(this), 1000);
};
Entry.prototype.service = function(response) {
if ( this.timer !== null ) {
clearTimeout(this.timer);
}
delete pendingEntries[makeKey(this.tabId, this.scriptlet)];
this.callback(response);
};
var makeKey = function(tabId, scriptlet) {
return tabId + ' ' + scriptlet;
};
var report = function(tabId, scriptlet, response) {
var key = makeKey(tabId, scriptlet);
var entry = pendingEntries[key];
if ( entry === undefined ) {
return;
}
entry.service(response);
};
var inject = function(tabId, scriptlet, callback) {
if ( typeof callback === 'function' ) {
if ( vAPI.isBehindTheSceneTabId(tabId) ) {
callback();
return;
}
var key = makeKey(tabId, scriptlet);
if ( pendingEntries[key] !== undefined ) {
callback();
return;
}
pendingEntries[key] = new Entry(tabId, scriptlet, callback);
}
vAPI.tabs.injectScript(tabId, { file: 'js/scriptlets/' + scriptlet + '.js' });
};
// TODO: think about a callback mechanism.
var injectDeep = function(tabId, scriptlet) {
vAPI.tabs.injectScript(tabId, {
file: 'js/scriptlets/' + scriptlet + '.js',
allFrames: true
});
};
return {
inject: inject,
injectDeep: injectDeep,
report: report
};
})();
/******************************************************************************/
2015-03-07 19:20:18 +01:00
})();