summaryrefslogtreecommitdiff
path: root/data/extensions/uBlock0@raymondhill.net/js/scriptlet-filtering.js
diff options
context:
space:
mode:
Diffstat (limited to 'data/extensions/uBlock0@raymondhill.net/js/scriptlet-filtering.js')
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/scriptlet-filtering.js394
1 files changed, 394 insertions, 0 deletions
diff --git a/data/extensions/uBlock0@raymondhill.net/js/scriptlet-filtering.js b/data/extensions/uBlock0@raymondhill.net/js/scriptlet-filtering.js
new file mode 100644
index 0000000..7980a40
--- /dev/null
+++ b/data/extensions/uBlock0@raymondhill.net/js/scriptlet-filtering.js
@@ -0,0 +1,394 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2017-present 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/uBlock
+*/
+
+/******************************************************************************/
+
+import {
+ domainFromHostname,
+ hostnameFromURI,
+} from './uri-utils.js';
+
+import { MRUCache } from './mrucache.js';
+import { ScriptletFilteringEngine } from './scriptlet-filtering-core.js';
+
+import logger from './logger.js';
+import { onBroadcast } from './broadcast.js';
+import { redirectEngine as reng } from './redirect-engine.js';
+import { sessionFirewall } from './filtering-engines.js';
+import µb from './background.js';
+
+/******************************************************************************/
+
+const contentScriptRegisterer = {
+ id: 1,
+ hostnameToDetails: new Map(),
+ register(hostname, code) {
+ if ( browser.contentScripts === undefined ) { return false; }
+ if ( hostname === '' ) { return false; }
+ const details = this.hostnameToDetails.get(hostname);
+ if ( details !== undefined ) {
+ if ( code === details.code ) {
+ return details.handle instanceof Promise === false;
+ }
+ this.unregisterHandle(details.handle);
+ this.hostnameToDetails.delete(hostname);
+ }
+ const id = this.id++;
+ const promise = browser.contentScripts.register({
+ js: [ { code } ],
+ allFrames: true,
+ matches: [ `*://*.${hostname}/*` ],
+ matchAboutBlank: true,
+ runAt: 'document_start',
+ }).then(handle => {
+ const details = this.hostnameToDetails.get(hostname);
+ if ( details === undefined ) { return; }
+ if ( details.id !== id ) { return; }
+ details.handle = handle;
+ }).catch(( ) => {
+ this.hostnameToDetails.delete(hostname);
+ });
+ this.hostnameToDetails.set(hostname, { id, handle: promise, code });
+ return false;
+ },
+ unregister(hostname) {
+ if ( hostname === '' ) { return; }
+ if ( this.hostnameToDetails.size === 0 ) { return; }
+ const details = this.hostnameToDetails.get(hostname);
+ if ( details === undefined ) { return; }
+ this.hostnameToDetails.delete(hostname);
+ this.unregisterHandle(details.handle);
+ },
+ flush(hostname) {
+ if ( hostname === '' ) { return; }
+ if ( hostname === '*' ) { return this.reset(); }
+ for ( const hn of this.hostnameToDetails.keys() ) {
+ if ( hn.endsWith(hostname) === false ) { continue; }
+ const pos = hn.length - hostname.length;
+ if ( pos !== 0 && hn.charCodeAt(pos-1) !== 0x2E /* . */ ) { continue; }
+ this.unregister(hn);
+ }
+ },
+ reset() {
+ if ( this.hostnameToDetails.size === 0 ) { return; }
+ for ( const details of this.hostnameToDetails.values() ) {
+ this.unregisterHandle(details.handle);
+ }
+ this.hostnameToDetails.clear();
+ },
+ unregisterHandle(handle) {
+ if ( handle instanceof Promise ) {
+ handle.then(handle => {
+ if ( handle ) { handle.unregister(); }
+ });
+ } else {
+ handle.unregister();
+ }
+ },
+};
+
+/******************************************************************************/
+
+const isolatedWorldInjector = (( ) => {
+ const parts = [
+ '(',
+ function(details) {
+ if ( self.uBO_isolatedScriptlets === 'done' ) { return; }
+ const doc = document;
+ if ( doc.location === null ) { return; }
+ const hostname = doc.location.hostname;
+ if ( hostname !== '' && details.hostname !== hostname ) { return; }
+ const isolatedScriptlets = function(){};
+ isolatedScriptlets();
+ self.uBO_isolatedScriptlets = 'done';
+ return 0;
+ }.toString(),
+ ')(',
+ 'json-slot',
+ ');',
+ ];
+ const jsonSlot = parts.indexOf('json-slot');
+ return {
+ assemble(hostname, details) {
+ parts[jsonSlot] = JSON.stringify({ hostname });
+ const code = parts.join('');
+ // Manually substitute noop function with scriptlet wrapper
+ // function, so as to not suffer instances of special
+ // replacement characters `$`,`\` when using String.replace()
+ // with scriptlet code.
+ const match = /function\(\)\{\}/.exec(code);
+ return code.slice(0, match.index) +
+ details.isolatedWorld +
+ code.slice(match.index + match[0].length);
+ },
+ };
+})();
+
+const onScriptletMessageInjector = (( ) => {
+ const parts = [
+ '(',
+ function(name) {
+ if ( self.uBO_bcSecret ) { return; }
+ try {
+ const bcSecret = new self.BroadcastChannel(name);
+ bcSecret.onmessage = ev => {
+ const msg = ev.data;
+ switch ( typeof msg ) {
+ case 'string':
+ if ( msg !== 'areyouready?' ) { break; }
+ bcSecret.postMessage('iamready!');
+ break;
+ case 'object':
+ if ( self.vAPI && self.vAPI.messaging ) {
+ self.vAPI.messaging.send('contentscript', msg);
+ } else {
+ console.log(`[uBO][${msg.type}]${msg.text}`);
+ }
+ break;
+ }
+ };
+ bcSecret.postMessage('iamready!');
+ self.uBO_bcSecret = bcSecret;
+ } catch {
+ }
+ }.toString(),
+ ')(',
+ 'bcSecret-slot',
+ ');',
+ ];
+ const bcSecretSlot = parts.indexOf('bcSecret-slot');
+ return {
+ assemble(details) {
+ parts[bcSecretSlot] = JSON.stringify(details.bcSecret);
+ return parts.join('\n');
+ },
+ };
+})();
+
+/******************************************************************************/
+
+export class ScriptletFilteringEngineEx extends ScriptletFilteringEngine {
+ constructor() {
+ super();
+ this.warOrigin = vAPI.getURL('/web_accessible_resources');
+ this.warSecret = undefined;
+ this.scriptletCache = new MRUCache(32);
+ this.isDevBuild = undefined;
+ this.logLevel = 1;
+ this.bc = onBroadcast(msg => {
+ switch ( msg.what ) {
+ case 'filteringBehaviorChanged': {
+ const direction = msg.direction || 0;
+ if ( direction > 0 ) { return; }
+ if ( direction >= 0 && msg.hostname ) {
+ return contentScriptRegisterer.flush(msg.hostname);
+ }
+ contentScriptRegisterer.reset();
+ break;
+ }
+ case 'hiddenSettingsChanged':
+ this.isDevBuild = undefined;
+ /* fall through */
+ case 'loggerEnabled':
+ case 'loggerDisabled':
+ this.clearCache();
+ break;
+ case 'loggerLevelChanged':
+ this.logLevel = msg.level;
+ vAPI.tabs.query({
+ discarded: false,
+ url: [ 'http://*/*', 'https://*/*' ],
+ }).then(tabs => {
+ for ( const tab of tabs ) {
+ const { status } = tab;
+ if ( status !== 'loading' && status !== 'complete' ) { continue; }
+ vAPI.tabs.executeScript(tab.id, {
+ allFrames: true,
+ file: `/js/scriptlets/scriptlet-loglevel-${this.logLevel}.js`,
+ matchAboutBlank: true,
+ });
+ }
+ });
+ this.clearCache();
+ break;
+ }
+ });
+ }
+
+ reset() {
+ super.reset();
+ this.warSecret = vAPI.warSecret.long(this.warSecret);
+ this.clearCache();
+ }
+
+ freeze() {
+ super.freeze();
+ this.warSecret = vAPI.warSecret.long(this.warSecret);
+ this.clearCache();
+ }
+
+ clearCache() {
+ this.scriptletCache.reset();
+ contentScriptRegisterer.reset();
+ }
+
+ retrieve(request) {
+ const { hostname } = request;
+
+ // https://github.com/gorhill/uBlock/issues/2835
+ // Do not inject scriptlets if the site is under an `allow` rule.
+ if ( µb.userSettings.advancedUserEnabled ) {
+ if ( sessionFirewall.evaluateCellZY(hostname, hostname, '*') === 2 ) {
+ return;
+ }
+ }
+
+ if ( this.scriptletCache.resetTime < reng.modifyTime ) {
+ this.clearCache();
+ }
+
+ let scriptletDetails = this.scriptletCache.lookup(hostname);
+ if ( scriptletDetails !== undefined ) {
+ return scriptletDetails || undefined;
+ }
+
+ if ( this.isDevBuild === undefined ) {
+ this.isDevBuild = vAPI.webextFlavor.soup.has('devbuild') ||
+ µb.hiddenSettings.filterAuthorMode;
+ }
+
+ if ( this.warSecret === undefined ) {
+ this.warSecret = vAPI.warSecret.long();
+ }
+
+ const bcSecret = vAPI.generateSecret(3);
+
+ const options = {
+ scriptletGlobals: {
+ warOrigin: this.warOrigin,
+ warSecret: this.warSecret,
+ },
+ debug: this.isDevBuild,
+ debugScriptlets: µb.hiddenSettings.debugScriptlets,
+ };
+ if ( logger.enabled ) {
+ options.scriptletGlobals.bcSecret = bcSecret;
+ options.scriptletGlobals.logLevel = this.logLevel;
+ }
+
+ scriptletDetails = super.retrieve(request, options);
+
+ if ( scriptletDetails === undefined ) {
+ if ( request.nocache !== true ) {
+ this.scriptletCache.add(hostname, null);
+ }
+ return;
+ }
+
+ const contentScript = [];
+ if ( scriptletDetails.mainWorld ) {
+ contentScript.push(vAPI.scriptletsInjector(hostname, scriptletDetails));
+ }
+ if ( scriptletDetails.isolatedWorld ) {
+ contentScript.push(isolatedWorldInjector.assemble(hostname, scriptletDetails));
+ }
+
+ const cachedScriptletDetails = {
+ bcSecret,
+ code: contentScript.join('\n\n'),
+ filters: scriptletDetails.filters,
+ };
+
+ if ( hostname !== '' && request.nocache !== true ) {
+ this.scriptletCache.add(hostname, cachedScriptletDetails);
+ }
+
+ return cachedScriptletDetails;
+ }
+
+ injectNow(details) {
+ if ( typeof details.frameId !== 'number' ) { return; }
+
+ const hostname = hostnameFromURI(details.url);
+ const domain = domainFromHostname(hostname);
+
+ const scriptletDetails = this.retrieve({
+ tabId: details.tabId,
+ frameId: details.frameId,
+ url: details.url,
+ hostname,
+ domain,
+ ancestors: details.ancestors,
+ });
+ if ( scriptletDetails === undefined ) {
+ contentScriptRegisterer.unregister(hostname);
+ return;
+ }
+ if ( Boolean(scriptletDetails.code) === false ) {
+ return scriptletDetails;
+ }
+
+ const contentScript = [ scriptletDetails.code ];
+ if ( logger.enabled ) {
+ contentScript.unshift(
+ onScriptletMessageInjector.assemble(scriptletDetails)
+ );
+ }
+ if ( µb.hiddenSettings.debugScriptletInjector ) {
+ contentScript.unshift('debugger');
+ }
+ const code = contentScript.join('\n\n');
+
+ const isAlreadyInjected = contentScriptRegisterer.register(hostname, code);
+ if ( isAlreadyInjected !== true ) {
+ vAPI.tabs.executeScript(details.tabId, {
+ code,
+ frameId: details.frameId,
+ matchAboutBlank: true,
+ runAt: 'document_start',
+ });
+ }
+ return scriptletDetails;
+ }
+
+ toLogger(request, details) {
+ if ( details === undefined ) { return; }
+ if ( logger.enabled !== true ) { return; }
+ if ( Array.isArray(details.filters) === false ) { return; }
+ µb.filteringContext
+ .duplicate()
+ .fromTabId(request.tabId)
+ .setRealm('extended')
+ .setType('scriptlet')
+ .setURL(request.url)
+ .setDocOriginFromURL(request.url)
+ .setFilter(details.filters.map(a => ({ source: 'extended', raw: a })))
+ .toLogger();
+ }
+}
+
+/******************************************************************************/
+
+const scriptletFilteringEngine = new ScriptletFilteringEngineEx();
+
+export default scriptletFilteringEngine;
+
+/******************************************************************************/