uBlock/src/js/ublock.js

663 lines
20 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.
2018-09-03 20:06:49 +02:00
Copyright (C) 2014-present 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
*/
2016-11-05 19:48:42 +01:00
'use strict';
/******************************************************************************/
2014-12-12 23:59:47 +01:00
/******************************************************************************/
{
// *****************************************************************************
// start of local namespace
2014-12-12 23:59:47 +01: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
const whitelistDirectiveEscape = /[-\/\\^$+?.()|[\]{}]/g;
// All `*` will be expanded into `.*`
const whitelistDirectiveEscapeAsterisk = /\*/g;
// Remember encountered regexps for reuse.
const directiveToRegexpMap = new Map();
// Probably manually entered whitelist directive
const isHandcraftedWhitelistDirective = function(directive) {
return directive.startsWith('/') && directive.endsWith('/') ||
directive.indexOf('/') !== -1 && directive.indexOf('*') !== -1;
2014-12-14 23:21:59 +01:00
};
const matchDirective = 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.startsWith('/') === false &&
directive.indexOf('*') === -1
) {
2014-12-14 23:21:59 +01:00
return url === directive;
2014-08-02 17:40:27 +02:00
}
// Transpose into a regular expression.
let re = directiveToRegexpMap.get(directive);
if ( re === undefined ) {
let reStr;
if ( directive.startsWith('/') && directive.endsWith('/') ) {
reStr = directive.slice(1, -1);
} else {
reStr = directive.replace(whitelistDirectiveEscape, '\\$&')
.replace(whitelistDirectiveEscapeAsterisk, '.*');
}
re = new RegExp(reStr);
directiveToRegexpMap.set(directive, re);
}
2014-12-14 23:21:59 +01:00
return re.test(url);
};
const matchBucket = function(url, hostname, bucket, start) {
if ( bucket ) {
for ( let i = start || 0, n = bucket.length; i < n; i++ ) {
if ( matchDirective(url, hostname, bucket[i]) ) {
return i;
}
}
}
return -1;
};
/******************************************************************************/
2014-08-02 17:40:27 +02:00
µBlock.getNetFilteringSwitch = function(url) {
const hostname = this.URI.hostnameFromURI(url);
let key = hostname;
for (;;) {
if ( matchBucket(url, hostname, this.netWhitelist.get(key)) !== -1 ) {
return false;
}
const pos = key.indexOf('.');
if ( pos === -1 ) { break; }
2014-12-14 23:21:59 +01:00
key = key.slice(pos + 1);
2014-06-24 00:42:43 +02:00
}
if ( matchBucket(url, hostname, this.netWhitelist.get('//')) !== -1 ) {
return false;
}
2014-06-24 00:42:43 +02:00
return true;
};
/******************************************************************************/
2014-08-02 17:40:27 +02:00
µBlock.toggleNetFilteringSwitch = function(url, scope, newState) {
const 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
const netWhitelist = this.netWhitelist;
const pos = url.indexOf('#');
let targetURL = pos !== -1 ? url.slice(0, pos) : url;
const targetHostname = this.URI.hostnameFromURI(targetURL);
let key = targetHostname;
let 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 ) {
let bucket = netWhitelist.get(key);
if ( bucket === undefined ) {
bucket = [];
netWhitelist.set(key, bucket);
}
bucket.push(directive);
2014-08-02 17:40:27 +02:00
this.saveWhitelist();
2014-06-24 00:42:43 +02:00
return true;
}
// Remove all directives which cause current URL to be whitelisted
for (;;) {
const bucket = netWhitelist.get(key);
if ( bucket !== undefined ) {
let i;
for (;;) {
i = matchBucket(targetURL, targetHostname, bucket, i);
if ( i === -1 ) { break; }
directive = bucket.splice(i, 1)[0];
if ( isHandcraftedWhitelistDirective(directive) ) {
netWhitelist.get('#').push(`# ${directive}`);
}
}
if ( bucket.length === 0 ) {
netWhitelist.delete(key);
}
2014-06-24 00:42:43 +02:00
}
const 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
}
const bucket = netWhitelist.get('//');
if ( bucket !== undefined ) {
let i;
for (;;) {
i = matchBucket(targetURL, targetHostname, bucket, i);
if ( i === -1 ) { break; }
directive = bucket.splice(i, 1)[0];
if ( isHandcraftedWhitelistDirective(directive) ) {
netWhitelist.get('#').push(`# ${directive}`);
}
}
if ( bucket.length === 0 ) {
netWhitelist.delete('//');
}
}
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.arrayFromWhitelist = function(whitelist) {
const out = new Set();
for ( const bucket of whitelist.values() ) {
for ( const directive of bucket ) {
out.add(directive);
2014-06-24 01:23:36 +02:00
}
}
return Array.from(out).sort((a, b) => a.localeCompare(b));
};
µBlock.stringFromWhitelist = function(whitelist) {
return this.arrayFromWhitelist(whitelist).join('\n');
2014-06-24 01:23:36 +02:00
};
/******************************************************************************/
µBlock.whitelistFromArray = function(lines) {
const whitelist = new Map();
// Comment bucket must always be ready to be used.
whitelist.set('#', []);
// New set of directives, scrap cached data.
directiveToRegexpMap.clear();
for ( let line of lines ) {
line = line.trim();
2016-12-26 17:35:37 +01:00
2015-05-04 23:10:55 +02:00
// https://github.com/gorhill/uBlock/issues/171
// Skip empty lines
if ( line === '' ) { continue; }
let key, directive;
2015-05-04 23:10:55 +02:00
// 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 ( this.reWhitelistBadHostname.test(line) ) {
key = '#';
directive = '# ' + line;
} else {
key = directive = line;
}
2014-08-02 17:40:27 +02:00
}
// Regex-based (ensure it is valid)
else if (
line.length > 2 &&
line.startsWith('/') &&
line.endsWith('/')
) {
key = '//';
directive = line;
try {
const re = new RegExp(directive.slice(1, -1));
directiveToRegexpMap.set(directive, re);
} catch(ex) {
key = '#';
directive = '# ' + line;
}
}
// URL, possibly wildcarded: there MUST be at least one hostname
// label (or else it would be just impossible to make an efficient
// dict.
else {
const matches = this.reWhitelistHostnameExtractor.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; }
2015-05-04 23:10:55 +02:00
// Be sure this stays fixed:
2015-04-07 03:26:05 +02:00
// https://github.com/chrisaljoudi/uBlock/issues/185
let bucket = whitelist.get(key);
if ( bucket === undefined ) {
bucket = [];
whitelist.set(key, bucket);
2014-08-02 17:40:27 +02:00
}
bucket.push(directive);
2014-08-02 17:40:27 +02:00
}
return whitelist;
2014-06-24 00:42:43 +02:00
};
µBlock.whitelistFromString = function(s) {
return this.whitelistFromArray(s.split('\n'));
};
// https://github.com/gorhill/uBlock/issues/3717
µBlock.reWhitelistBadHostname = /[^a-z0-9.\-_\[\]:]/;
µBlock.reWhitelistHostnameExtractor = /([a-z0-9.\-_\[\]]+)(?::[\d*]+)?\/(?:[^\x00-\x20\/]|$)[^\x00-\x20]*$/;
2016-12-26 17:35:37 +01:00
// end of local namespace
// *****************************************************************************
2014-06-24 00:42:43 +02:00
}
/******************************************************************************/
/******************************************************************************/
2014-06-24 00:42:43 +02:00
µBlock.changeUserSettings = function(name, value) {
2018-09-03 20:06:49 +02:00
let 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));
2018-09-03 20:06:49 +02:00
us.noCosmeticFiltering = this.sessionSwitches.evaluate('no-cosmetic-filtering', '*') === 1;
us.noLargeMedia = this.sessionSwitches.evaluate('no-large-media', '*') === 1;
us.noRemoteFonts = this.sessionSwitches.evaluate('no-remote-fonts', '*') === 1;
us.noScripting = this.sessionSwitches.evaluate('no-scripting', '*') === 1;
us.noCSPReports = this.sessionSwitches.evaluate('no-csp-reports', '*') === 1;
return us;
2014-06-25 00:29:55 +02:00
}
2018-09-03 20:06:49 +02:00
if ( typeof name !== 'string' || name === '' ) { return; }
2014-06-24 00:42:43 +02:00
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.
2018-09-03 20:06:49 +02:00
let mustSave = us.hasOwnProperty(name) && value !== us[name];
if ( mustSave ) {
us[name] = value;
}
2014-06-24 00:42:43 +02:00
// Post-change
switch ( name ) {
2016-11-05 19:48:42 +01:00
case 'advancedUserEnabled':
if ( value === true ) {
us.dynamicFilteringEnabled = true;
}
break;
case 'autoUpdate':
this.scheduleAssetUpdater(value ? 7 * 60 * 1000 : 0);
break;
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':
if ( this.privacySettingsSupported ) {
vAPI.browserSettings.set({ 'hyperlinkAuditing': !value });
}
break;
case 'noCosmeticFiltering':
case 'noLargeMedia':
case 'noRemoteFonts':
case 'noScripting':
2017-10-19 15:35:28 +02:00
case 'noCSPReports':
2018-09-03 20:06:49 +02:00
let switchName;
switch ( name ) {
case 'noCosmeticFiltering':
switchName = 'no-cosmetic-filtering'; break;
case 'noLargeMedia':
switchName = 'no-large-media'; break;
case 'noRemoteFonts':
switchName = 'no-remote-fonts'; break;
case 'noScripting':
switchName = 'no-scripting'; break;
case 'noCSPReports':
switchName = 'no-csp-reports'; break;
default:
break;
}
if ( switchName === undefined ) { break; }
let switchState = value ? 1 : 0;
this.sessionSwitches.toggle(switchName, '*', switchState);
if ( this.permanentSwitches.toggle(switchName, '*', switchState) ) {
2017-10-19 15:35:28 +02:00
this.saveHostnameSwitches();
}
break;
2015-06-01 21:03:22 +02:00
case 'prefetchingDisabled':
if ( this.privacySettingsSupported ) {
vAPI.browserSettings.set({ 'prefetching': !value });
}
2015-06-01 21:03:22 +02:00
break;
case 'webrtcIPAddressHidden':
if ( this.privacySettingsSupported ) {
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
};
/******************************************************************************/
// https://www.reddit.com/r/uBlockOrigin/comments/8524cf/my_custom_scriptlets_doesnt_work_what_am_i_doing/
µBlock.changeHiddenSettings = function(hs) {
const mustReloadResources =
hs.userResourcesLocation !== this.hiddenSettings.userResourcesLocation;
this.hiddenSettings = hs;
this.saveHiddenSettings();
if ( mustReloadResources ) {
this.redirectEngine.invalidateResourcesSelfie();
this.loadRedirectResources();
}
};
/******************************************************************************/
2014-06-24 00:42:43 +02:00
2017-05-27 17:51:24 +02:00
µBlock.elementPickerExec = function(tabId, targetElement, zap) {
if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; }
this.epickerTarget = targetElement || '';
2017-05-27 17:51:24 +02:00
this.epickerZap = zap || false;
// https://github.com/uBlockOrigin/uBlock-issues/issues/40
// The element picker needs this library
vAPI.tabs.injectScript(
tabId,
{
file: '/lib/diff/swatinem_diff.js',
runAt: 'document_end'
}
);
// https://github.com/uBlockOrigin/uBlock-issues/issues/168
// Force activate the target tab once the element picker has been
// injected.
vAPI.tabs.injectScript(
tabId,
{
file: '/js/scriptlets/element-picker.js',
runAt: 'document_end'
},
( ) => {
vAPI.tabs.select(tabId);
}
);
2014-09-28 18:05:46 +02:00
};
/******************************************************************************/
2014-10-06 20:02:44 +02:00
2016-09-27 14:31:12 +02:00
// https://github.com/gorhill/uBlock/issues/2033
// Always set own rules, trying to be fancy to avoid setting seemingly
// (but not really) redundant rules led to this issue.
µBlock.toggleFirewallRule = function(details) {
let { srcHostname, desHostname, requestType, action } = details;
if ( action !== 0 ) {
this.sessionFirewall.setCell(
srcHostname,
desHostname,
requestType,
action
);
2014-10-06 20:02:44 +02:00
} else {
this.sessionFirewall.unsetCell(
srcHostname,
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 ( action !== 0 ) {
this.permanentFirewall.setCell(
srcHostname,
desHostname,
requestType,
action
);
} else {
this.permanentFirewall.unsetCell(
srcHostname,
desHostname,
requestType,
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
2016-05-28 15:19:35 +02:00
// collapsible type: any of the cached entries could be a resource on the
2016-05-28 15:18:36 +02:00
// target page.
if (
(srcHostname !== '*') &&
(
requestType === '*' ||
requestType === 'image' ||
requestType === '3p' ||
requestType === '3p-frame'
)
2016-05-28 15:18:36 +02:00
) {
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) {
2018-09-03 20:06:49 +02:00
let changed = this.sessionURLFiltering.setRule(
2015-05-21 20:15:17 +02:00
details.context,
details.url,
details.type,
details.action
);
2018-09-03 20:06:49 +02:00
if ( changed === false ) { return; }
2015-05-21 20:15:17 +02:00
this.cosmeticFilteringEngine.removeFromSelectorCache(details.context, 'net');
2015-05-22 14:05:55 +02:00
2018-09-03 20:06:49 +02:00
if ( details.persist !== true ) { return; }
2015-05-22 14:05:55 +02:00
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-03-27 18:00:55 +01:00
µBlock.toggleHostnameSwitch = function(details) {
2018-09-03 20:06:49 +02:00
let changed = this.sessionSwitches.toggleZ(
details.name,
details.hostname,
!!details.deep,
details.state
);
if ( changed === false ) { return; }
// 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':
const pageStore = this.pageStoreFromTabId(details.tabId);
2017-09-12 17:43:43 +02:00
if ( pageStore !== null ) {
pageStore.temporarilyAllowLargeMediaElements(!details.state);
}
break;
}
2018-09-03 20:06:49 +02:00
if ( details.persist !== true ) { return; }
changed = this.permanentSwitches.toggleZ(
details.name,
details.hostname,
!!details.deep,
details.state
);
if ( changed ) {
this.saveHostnameSwitches();
}
};
/******************************************************************************/
// https://github.com/NanoMeow/QuickReports/issues/6#issuecomment-414516623
// Inject as early as possible to make the cosmetic logger code less
// sensitive to the removal of DOM nodes which may match injected
// cosmetic filters.
µBlock.logCosmeticFilters = function(tabId, frameId) {
vAPI.tabs.injectScript(tabId, {
file: '/js/scriptlets/cosmetic-logger.js',
frameId: frameId,
runAt: 'document_start'
});
};
/******************************************************************************/
2015-06-26 06:08:41 +02:00
µBlock.scriptlets = (function() {
const pendingEntries = new Map();
const Entry = class {
constructor(tabId, scriptlet, callback) {
this.tabId = tabId;
this.scriptlet = scriptlet;
this.callback = callback;
this.timer = vAPI.setTimeout(this.service.bind(this), 1000);
}
service(response) {
if ( this.timer !== null ) {
clearTimeout(this.timer);
this.timer = null;
}
pendingEntries.delete(makeKey(this.tabId, this.scriptlet));
this.callback(response);
2015-06-26 06:08:41 +02:00
}
};
const makeKey = function(tabId, scriptlet) {
2015-06-26 06:08:41 +02:00
return tabId + ' ' + scriptlet;
};
const report = function(tabId, scriptlet, response) {
const key = makeKey(tabId, scriptlet);
const entry = pendingEntries.get(key);
2017-10-25 17:37:58 +02:00
if ( entry === undefined ) { return; }
2015-06-26 06:08:41 +02:00
entry.service(response);
};
const inject = function(tabId, scriptlet, callback) {
2015-06-26 06:08:41 +02:00
if ( typeof callback === 'function' ) {
if ( vAPI.isBehindTheSceneTabId(tabId) ) {
callback();
return;
}
const key = makeKey(tabId, scriptlet);
const entry = pendingEntries.get(key);
2017-10-25 17:37:58 +02:00
if ( entry !== undefined ) {
if ( callback !== entry.callback ) {
callback();
}
2015-06-26 06:08:41 +02:00
return;
}
2017-10-25 17:37:58 +02:00
pendingEntries.set(key, new Entry(tabId, scriptlet, callback));
2015-06-26 06:08:41 +02:00
}
2017-10-25 17:37:58 +02:00
vAPI.tabs.injectScript(tabId, {
file: '/js/scriptlets/' + scriptlet + '.js'
2017-10-25 17:37:58 +02:00
});
2015-06-26 06:08:41 +02:00
};
// TODO: think about a callback mechanism.
const injectDeep = function(tabId, scriptlet) {
2015-06-26 06:08:41 +02:00
vAPI.tabs.injectScript(tabId, {
file: '/js/scriptlets/' + scriptlet + '.js',
2015-06-26 06:08:41 +02:00
allFrames: true
});
};
return {
inject: inject,
injectDeep: injectDeep,
report: report
};
})();
/******************************************************************************/