From b7b53eef14d342c27806b5f94edcb2c7c2a88be1 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Wed, 9 Nov 2022 11:25:18 -0500 Subject: [PATCH] [mv3] Add support for no-xhr-if/no-fetch-if scriptlets --- platform/mv3/make-rulesets.js | 10 +- .../mv3/scriptlets/no-addeventlistener-if.js | 1 + platform/mv3/scriptlets/no-fetch-if.js | 143 +++++++++++++++++ platform/mv3/scriptlets/no-windowopen-if.js | 1 + platform/mv3/scriptlets/no-xhr-if.js | 145 ++++++++++++++++++ 5 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 platform/mv3/scriptlets/no-fetch-if.js create mode 100644 platform/mv3/scriptlets/no-xhr-if.js diff --git a/platform/mv3/make-rulesets.js b/platform/mv3/make-rulesets.js index 35dfc3d74..b57727046 100644 --- a/platform/mv3/make-rulesets.js +++ b/platform/mv3/make-rulesets.js @@ -942,7 +942,10 @@ async function processDomainScriptletFilters(assetDetails, domainBased, original continue; } const normalized = parseScriptletFilter(rawFilter, originalScriptletMap); - if ( normalized === undefined ) { continue; } + if ( normalized === undefined ) { + log(`Discarded unsupported scriptlet filter: ${rawFilter}`, true); + continue; + } let argsDetails = scriptletDetails.get(normalized.token); if ( argsDetails === undefined ) { argsDetails = new Map(); @@ -1046,7 +1049,10 @@ async function processEntityScriptletFilters(assetDetails, entityBased, original continue; } const normalized = parseScriptletFilter(rawFilter, originalScriptletMap, '.entity'); - if ( normalized === undefined ) { continue; } + if ( normalized === undefined ) { + log(`Discarded unsupported scriptlet filter: ${rawFilter}`, true); + continue; + } let argsDetails = scriptletMap.get(normalized.token); if ( argsDetails === undefined ) { argsDetails = new Map(); diff --git a/platform/mv3/scriptlets/no-addeventlistener-if.js b/platform/mv3/scriptlets/no-addeventlistener-if.js index a92857ae9..e8da01169 100644 --- a/platform/mv3/scriptlets/no-addeventlistener-if.js +++ b/platform/mv3/scriptlets/no-addeventlistener-if.js @@ -30,6 +30,7 @@ /// name no-addeventlistener-if /// alias noaelif +/// alias addEventListener-defuser /// alias aeld /******************************************************************************/ diff --git a/platform/mv3/scriptlets/no-fetch-if.js b/platform/mv3/scriptlets/no-fetch-if.js new file mode 100644 index 000000000..f7b8915c0 --- /dev/null +++ b/platform/mv3/scriptlets/no-fetch-if.js @@ -0,0 +1,143 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2019-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 + + The scriptlets below are meant to be injected only into a + web page context. +*/ + +/* jshint esversion:11 */ + +'use strict'; + +/******************************************************************************/ + +/// name no-fetch-if + +/******************************************************************************/ + +// Important! +// Isolate from global scope +(function uBOL_noFetchIf() { + +/******************************************************************************/ + +// $rulesetId$ + +const argsList = self.$argsList$; + +const hostnamesMap = new Map(self.$hostnamesMap$); + +/******************************************************************************/ + +const scriptlet = ( + conditions = '' +) => { + const needles = []; + for ( const condition of conditions.split(/\s+/) ) { + if ( condition === '' ) { continue; } + const pos = condition.indexOf(':'); + let key, value; + if ( pos !== -1 ) { + key = condition.slice(0, pos); + value = condition.slice(pos + 1); + } else { + key = 'url'; + value = condition; + } + if ( value === '' ) { + value = '^'; + } else if ( value.startsWith('/') && value.endsWith('/') ) { + value = value.slice(1, -1); + } else { + value = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + needles.push({ key, re: new RegExp(value) }); + } + self.fetch = new Proxy(self.fetch, { + apply: function(target, thisArg, args) { + let proceed = true; + try { + let details; + if ( args[0] instanceof self.Request ) { + details = args[0]; + } else { + details = Object.assign({ url: args[0] }, args[1]); + } + const props = new Map(); + for ( const prop in details ) { + let v = details[prop]; + if ( typeof v !== 'string' ) { + try { v = JSON.stringify(v); } + catch(ex) { } + } + if ( typeof v !== 'string' ) { continue; } + props.set(prop, v); + } + proceed = needles.length === 0; + for ( const { key, re } of needles ) { + if ( + props.has(key) === false || + re.test(props.get(key)) === false + ) { + proceed = true; + break; + } + } + } catch(ex) { + } + return proceed + ? Reflect.apply(target, thisArg, args) + : Promise.resolve(new Response()); + } + }); +}; + +/******************************************************************************/ + +let hn; +try { hn = document.location.hostname; } catch(ex) { } +while ( hn ) { + if ( hostnamesMap.has(hn) ) { + let argsIndices = hostnamesMap.get(hn); + if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; } + for ( const argsIndex of argsIndices ) { + const details = argsList[argsIndex]; + if ( details.n && details.n.includes(hn) ) { continue; } + try { scriptlet(...details.a); } catch(ex) {} + } + } + if ( hn === '*' ) { break; } + const pos = hn.indexOf('.'); + if ( pos !== -1 ) { + hn = hn.slice(pos + 1); + } else { + hn = '*'; + } +} + +argsList.length = 0; +hostnamesMap.clear(); + +/******************************************************************************/ + +})(); + +/******************************************************************************/ + diff --git a/platform/mv3/scriptlets/no-windowopen-if.js b/platform/mv3/scriptlets/no-windowopen-if.js index e1b30241c..53bb879bc 100644 --- a/platform/mv3/scriptlets/no-windowopen-if.js +++ b/platform/mv3/scriptlets/no-windowopen-if.js @@ -31,6 +31,7 @@ /// name no-windowopen-if /// alias no-windowOpen-if /// alias nowoif +/// alias window.open-defuser /******************************************************************************/ diff --git a/platform/mv3/scriptlets/no-xhr-if.js b/platform/mv3/scriptlets/no-xhr-if.js new file mode 100644 index 000000000..a2cc895d2 --- /dev/null +++ b/platform/mv3/scriptlets/no-xhr-if.js @@ -0,0 +1,145 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + Copyright (C) 2019-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 + + The scriptlets below are meant to be injected only into a + web page context. +*/ + +/* jshint esversion:11 */ + +'use strict'; + +/******************************************************************************/ + +/// name no-xhr-if + +/******************************************************************************/ + +// Important! +// Isolate from global scope +(function uBOL_noXhrIf() { + +/******************************************************************************/ + +// $rulesetId$ + +const argsList = self.$argsList$; + +const hostnamesMap = new Map(self.$hostnamesMap$); + +/******************************************************************************/ + +const scriptlet = ( + conditions = '' +) => { + const xhrInstances = new WeakMap(); + const needles = []; + for ( const condition of conditions.split(/\s+/) ) { + if ( condition === '' ) { continue; } + const pos = condition.indexOf(':'); + let key, value; + if ( pos !== -1 ) { + key = condition.slice(0, pos); + value = condition.slice(pos + 1); + } else { + key = 'url'; + value = condition; + } + if ( value === '' ) { + value = '^'; + } else if ( value.startsWith('/') && value.endsWith('/') ) { + value = value.slice(1, -1); + } else { + value = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + needles.push({ key, re: new RegExp(value) }); + } + self.XMLHttpRequest = class extends self.XMLHttpRequest { + open(...args) { + const argNames = [ 'method', 'url' ]; + const haystack = new Map(); + for ( let i = 0; i < args.length && i < argNames.length; i++ ) { + haystack.set(argNames[i], args[i]); + } + if ( haystack.size !== 0 ) { + let matches = true; + for ( const { key, re } of needles ) { + matches = re.test(haystack.get(key) || ''); + if ( matches === false ) { break; } + } + if ( matches ) { + xhrInstances.set(this, haystack); + } + } + return super.open(...args); + } + send(...args) { + const haystack = xhrInstances.get(this); + if ( haystack === undefined ) { + return super.send(...args); + } + Object.defineProperties(this, { + readyState: { value: 4, writable: false }, + response: { value: '', writable: false }, + responseText: { value: '', writable: false }, + responseURL: { value: haystack.get('url'), writable: false }, + responseXML: { value: '', writable: false }, + status: { value: 200, writable: false }, + statusText: { value: 'OK', writable: false }, + }); + this.dispatchEvent(new Event('readystatechange')); + this.dispatchEvent(new Event('load')); + this.dispatchEvent(new Event('loadend')); + } + }; +}; + +/******************************************************************************/ + +let hn; +try { hn = document.location.hostname; } catch(ex) { } +while ( hn ) { + if ( hostnamesMap.has(hn) ) { + let argsIndices = hostnamesMap.get(hn); + if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; } + for ( const argsIndex of argsIndices ) { + const details = argsList[argsIndex]; + if ( details.n && details.n.includes(hn) ) { continue; } + try { scriptlet(...details.a); } catch(ex) {} + } + } + if ( hn === '*' ) { break; } + const pos = hn.indexOf('.'); + if ( pos !== -1 ) { + hn = hn.slice(pos + 1); + } else { + hn = '*'; + } +} + +argsList.length = 0; +hostnamesMap.clear(); + +/******************************************************************************/ + +})(); + +/******************************************************************************/ +