Improve `no-xhr-if` scriptlet

Now support AdGuard's `randomize` parameter. If `true`,
the scriplet will generate a random 10-character string
to be returned as the response.

Reference:
https://github.com/AdguardTeam/Scriptlets/blob/master/wiki/about-scriptlets.md#prevent-xhr

AdGuard's `prevent-xhr` also support `length:n-m` form,
but since I do not see it being used, for now it's not
supported in uBO's `no-xhr-if`.

Additionally, the scriptlet will now honor `responseType` and
return the proper response type accordingly.
This commit is contained in:
Raymond Hill 2023-08-13 13:23:41 -04:00
parent 21fe1c2df8
commit 418087de9c
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
1 changed files with 52 additions and 43 deletions

View File

@ -1929,52 +1929,29 @@ builtinScriptlets.push({
],
fn: noXhrIf,
dependencies: [
'safe-self.fn',
'match-object-properties.fn',
'parse-properties-to-match.fn',
],
});
function noXhrIf(
arg1 = ''
propsToMatch = '',
randomize = ''
) {
if ( typeof arg1 !== 'string' ) { return; }
const safe = safeSelf();
if ( typeof propsToMatch !== 'string' ) { return; }
const xhrInstances = new WeakMap();
const needles = [];
for ( const condition of arg1.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;
}
needles.push({ key, re: safe.patternToRegex(value) });
}
const log = needles.length === 0 ? console.log.bind(console) : undefined;
const propNeedles = parsePropertiesToMatch(propsToMatch, 'url');
const log = propNeedles.size === 0 ? console.log.bind(console) : undefined;
self.XMLHttpRequest = class extends self.XMLHttpRequest {
open(...args) {
open(method, url, ...args) {
if ( log !== undefined ) {
log(`uBO: xhr.open(${args.join(', ')})`);
log(`uBO: xhr.open(${method}, ${url}, ${args.join(', ')})`);
} else {
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);
}
const haystack = { method, url };
if ( matchObjectProperties(propNeedles, haystack) ) {
xhrInstances.set(this, haystack);
}
}
return super.open(...args);
return super.open(method, url, ...args);
}
send(...args) {
const haystack = xhrInstances.get(this);
@ -1982,13 +1959,45 @@ function noXhrIf(
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 },
readyState: { value: 4 },
responseURL: { value: haystack.url },
status: { value: 200 },
statusText: { value: 'OK' },
});
let response = '';
let responseText = '';
let responseXML = null;
switch ( this.responseType ) {
case 'arraybuffer':
response = new ArrayBuffer(0);
break;
case 'blob':
response = new Blob([]);
break;
case 'document': {
const parser = new DOMParser();
const doc = parser.parseFromString('', 'text/html');
response = doc;
responseXML = doc;
break;
}
case 'json':
response = {};
responseText = '{}';
break;
default:
if ( randomize !== 'true' ) { break; }
do {
response += Math.random().toString(36).slice(-2);
} while ( response.length < 10 );
response = response.slice(-10);
responseText = response;
break;
}
Object.defineProperties(this, {
response: { value: response },
responseText: { value: responseText },
responseXML: { value: responseXML },
});
this.dispatchEvent(new Event('readystatechange'));
this.dispatchEvent(new Event('load'));