Skip to content

Commit 82a7d11

Browse files
committed
Add trusted-replace-fetch-response scriptlet
This scriplet requires a trusted source. Related issue: uBlockOrigin/uBlock-issues#2742 See AdGuard's documentation for usage: https://github.com/AdguardTeam/Scriptlets/blob/master/wiki/about-trusted-scriptlets.md#trusted-replace-fetch-response
1 parent 9f91335 commit 82a7d11

File tree

1 file changed

+120
-32
lines changed

1 file changed

+120
-32
lines changed

assets/resources/scriptlets.js

Lines changed: 120 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ function safeSelf() {
5353
'RegExp_exec': self.RegExp.prototype.exec,
5454
'addEventListener': self.EventTarget.prototype.addEventListener,
5555
'removeEventListener': self.EventTarget.prototype.removeEventListener,
56+
'fetch': self.fetch,
57+
'jsonParse': self.JSON.parse.bind(self.JSON),
58+
'jsonStringify': self.JSON.stringify.bind(self.JSON),
5659
'log': console.log.bind(console),
5760
'uboLog': function(...args) {
5861
if ( args.length === 0 ) { return; }
@@ -103,10 +106,15 @@ builtinScriptlets.push({
103106
function patternToRegex(pattern, flags = undefined) {
104107
if ( pattern === '' ) { return /^/; }
105108
const match = /^\/(.+)\/([gimsu]*)$/.exec(pattern);
106-
if ( match !== null ) {
109+
if ( match === null ) {
110+
return new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags);
111+
}
112+
try {
107113
return new RegExp(match[1], match[2] || flags);
108114
}
109-
return new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags);
115+
catch(ex) {
116+
}
117+
return /^/;
110118
}
111119

112120
/******************************************************************************/
@@ -210,11 +218,14 @@ function runAtHtmlElement(fn) {
210218
/******************************************************************************/
211219

212220
builtinScriptlets.push({
213-
name: 'get-extra-args-entries.fn',
214-
fn: getExtraArgsEntries,
221+
name: 'get-extra-args.fn',
222+
fn: getExtraArgs,
223+
dependencies: [
224+
'get-extra-args-entries.fn',
225+
],
215226
});
216-
function getExtraArgsEntries(args, offset) {
217-
return args.slice(offset).reduce((out, v, i, a) => {
227+
function getExtraArgs(args, offset = 0) {
228+
const entries = args.slice(offset).reduce((out, v, i, a) => {
218229
if ( (i & 1) === 0 ) {
219230
const rawValue = a[i+1];
220231
const value = /^\d+$/.test(rawValue)
@@ -224,28 +235,7 @@ function getExtraArgsEntries(args, offset) {
224235
}
225236
return out;
226237
}, []);
227-
}
228-
229-
builtinScriptlets.push({
230-
name: 'get-extra-args-map.fn',
231-
fn: getExtraArgsMap,
232-
dependencies: [
233-
'get-extra-args-entries.fn',
234-
],
235-
});
236-
function getExtraArgsMap(args, offset = 0) {
237-
return new Map(getExtraArgsEntries(args, offset));
238-
}
239-
240-
builtinScriptlets.push({
241-
name: 'get-extra-args.fn',
242-
fn: getExtraArgs,
243-
dependencies: [
244-
'get-extra-args-entries.fn',
245-
],
246-
});
247-
function getExtraArgs(args, offset = 0) {
248-
return Object.fromEntries(getExtraArgsEntries(args, offset));
238+
return Object.fromEntries(entries);
249239
}
250240

251241
/******************************************************************************/
@@ -447,7 +437,7 @@ function setConstantCore(
447437
if ( Math.abs(cValue) > 0x7FFF ) { return; }
448438
} else if ( trusted ) {
449439
if ( cValue.startsWith('{') && cValue.endsWith('}') ) {
450-
try { cValue = JSON.parse(cValue).value; } catch(ex) { return; }
440+
try { cValue = safe.jsonParse(cValue).value; } catch(ex) { return; }
451441
}
452442
} else {
453443
return;
@@ -2415,7 +2405,7 @@ function xmlPrune(
24152405
log(`xmlPrune: ${item.constructor.name}.${item.nodeName} removed`);
24162406
}
24172407
} catch(ex) {
2418-
if ( log ) { safeSelf().uboLog(ex); }
2408+
log(ex);
24192409
}
24202410
return xmlDoc;
24212411
};
@@ -2438,13 +2428,12 @@ function xmlPrune(
24382428
if ( arg instanceof Request ) { return arg.url; }
24392429
return String(arg);
24402430
};
2441-
const realFetch = self.fetch;
24422431
self.fetch = new Proxy(self.fetch, {
24432432
apply: function(target, thisArg, args) {
24442433
if ( reUrl.test(urlFromArg(args[0])) === false ) {
24452434
return Reflect.apply(target, thisArg, args);
24462435
}
2447-
return realFetch(...args).then(realResponse =>
2436+
return safe.fetch(...args).then(realResponse =>
24482437
realResponse.text().then(text =>
24492438
new Response(pruneFromText(text), {
24502439
status: realResponse.status,
@@ -3329,4 +3318,103 @@ function trustedSetLocalStorageItem(key = '', value = '') {
33293318
setLocalStorageItemCore('local', true, key, value);
33303319
}
33313320

3321+
/*******************************************************************************
3322+
*
3323+
* trusted-replace-fetch-response.js
3324+
*
3325+
* Replaces response text content of fetch requests if all given parameters
3326+
* match.
3327+
*
3328+
* Reference:
3329+
* https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/trusted-replace-fetch-response.js
3330+
*
3331+
**/
3332+
3333+
builtinScriptlets.push({
3334+
name: 'trusted-replace-fetch-response.js',
3335+
requiresTrust: true,
3336+
fn: trustedReplaceFetchResponse,
3337+
dependencies: [
3338+
'get-extra-args.fn',
3339+
'pattern-to-regex.fn',
3340+
'safe-self.fn',
3341+
'should-log.fn',
3342+
],
3343+
});
3344+
function trustedReplaceFetchResponse(
3345+
pattern = '',
3346+
replacement = '',
3347+
propsToMatch = ''
3348+
) {
3349+
const safe = safeSelf();
3350+
const extraArgs = getExtraArgs(Array.from(arguments), 3);
3351+
const logLevel = shouldLog({
3352+
log: pattern === '' || extraArgs.log,
3353+
});
3354+
const log = logLevel ? ((...args) => { safe.uboLog(...args); }) : (( ) => { });
3355+
if ( pattern === '*' ) { pattern = '.*'; }
3356+
const rePattern = patternToRegex(pattern);
3357+
const propNeedles = new Map();
3358+
for ( const needle of propsToMatch.split(/\s+/) ) {
3359+
const [ prop, value ] = needle.split(':');
3360+
if ( prop === '' ) { continue; }
3361+
propNeedles.set(prop, patternToRegex(value));
3362+
}
3363+
self.fetch = new Proxy(self.fetch, {
3364+
apply: function(target, thisArg, args) {
3365+
if ( logLevel === true ) {
3366+
log('trusted-replace-fetch-response:', JSON.stringify(Array.from(args)).slice(1,-1));
3367+
}
3368+
const fetchPromise = Reflect.apply(target, thisArg, args);
3369+
if ( pattern === '' ) { return fetchPromise; }
3370+
let skip = false;
3371+
if ( propNeedles.size !== 0 ) {
3372+
const fetchDetails = {};
3373+
if ( args[0] instanceof self.Request ) {
3374+
Object.assign(fetchDetails, args[0]);
3375+
} else {
3376+
Object.assign(fetchDetails, { url: args[0] });
3377+
}
3378+
if ( args[1] instanceof Object ) {
3379+
Object.assign(fetchDetails, args[1]);
3380+
}
3381+
for ( const prop of Object.keys(fetchDetails) ) {
3382+
let value = fetchDetails[prop];
3383+
if ( typeof value !== 'string' ) {
3384+
try { value = JSON.stringify(value); }
3385+
catch(ex) { }
3386+
}
3387+
if ( typeof value !== 'string' ) { continue; }
3388+
const needle = propNeedles.get(prop);
3389+
if ( needle === undefined ) { continue; }
3390+
if ( needle.test(value) ) { continue; }
3391+
skip = true;
3392+
break;
3393+
}
3394+
}
3395+
if ( skip ) { return fetchPromise; }
3396+
return fetchPromise.then(responseBefore => {
3397+
return responseBefore.text().then(textBefore => {
3398+
const textAfter = textBefore.replace(rePattern, replacement);
3399+
const responseAfter = new Response(textAfter, {
3400+
status: responseBefore.status,
3401+
statusText: responseBefore.statusText,
3402+
headers: responseBefore.headers,
3403+
});
3404+
Object.defineProperties(responseAfter, {
3405+
ok: { value: responseBefore.ok },
3406+
redirected: { value: responseBefore.redirected },
3407+
type: { value: responseBefore.type },
3408+
url: { value: responseBefore.url },
3409+
});
3410+
return responseAfter;
3411+
}).catch(reason => {
3412+
log('trusted-replace-fetch-response:', reason);
3413+
return responseBefore;
3414+
});
3415+
});
3416+
}
3417+
});
3418+
}
3419+
33323420
/******************************************************************************/

0 commit comments

Comments
 (0)