uBlock/src/js/resources/json-prune.js
Raymond Hill 95a3be9d56
Add jsonl-prune-xhr-response/jsonl-prune-fetch-response scriptlets
As discussed internally with filter list volunteers.
2025-03-22 16:01:43 -04:00

300 lines
11 KiB
JavaScript

/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
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
*/
import {
matchObjectPropertiesFn,
parsePropertiesToMatchFn,
} from './utils.js';
import { objectPruneFn } from './object-prune.js';
import { proxyApplyFn } from './proxy-apply.js';
import { registerScriptlet } from './base.js';
import { safeSelf } from './safe-self.js';
/******************************************************************************/
function jsonPrune(
rawPrunePaths = '',
rawNeedlePaths = '',
stackNeedle = ''
) {
const safe = safeSelf();
const logPrefix = safe.makeLogPrefix('json-prune', rawPrunePaths, rawNeedlePaths, stackNeedle);
const stackNeedleDetails = safe.initPattern(stackNeedle, { canNegate: true });
const extraArgs = safe.getExtraArgs(Array.from(arguments), 3);
JSON.parse = new Proxy(JSON.parse, {
apply: function(target, thisArg, args) {
const objBefore = Reflect.apply(target, thisArg, args);
if ( rawPrunePaths === '' ) {
safe.uboLog(logPrefix, safe.JSON_stringify(objBefore, null, 2));
}
const objAfter = objectPruneFn(
objBefore,
rawPrunePaths,
rawNeedlePaths,
stackNeedleDetails,
extraArgs
);
if ( objAfter === undefined ) { return objBefore; }
safe.uboLog(logPrefix, 'Pruned');
if ( safe.logLevel > 1 ) {
safe.uboLog(logPrefix, `After pruning:\n${safe.JSON_stringify(objAfter, null, 2)}`);
}
return objAfter;
},
});
}
registerScriptlet(jsonPrune, {
name: 'json-prune.js',
dependencies: [
objectPruneFn,
safeSelf,
],
});
/******************************************************************************/
function jsonPruneFetchResponseFn(
rawPrunePaths = '',
rawNeedlePaths = ''
) {
const safe = safeSelf();
const logPrefix = safe.makeLogPrefix('json-prune-fetch-response', rawPrunePaths, rawNeedlePaths);
const extraArgs = safe.getExtraArgs(Array.from(arguments), 2);
const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url');
const stackNeedle = safe.initPattern(extraArgs.stackToMatch || '', { canNegate: true });
const logall = rawPrunePaths === '';
const applyHandler = function(target, thisArg, args) {
const fetchPromise = Reflect.apply(target, thisArg, args);
if ( propNeedles.size !== 0 ) {
const objs = [ args[0] instanceof Object ? args[0] : { url: args[0] } ];
if ( objs[0] instanceof Request ) {
try {
objs[0] = safe.Request_clone.call(objs[0]);
} catch(ex) {
safe.uboErr(logPrefix, 'Error:', ex);
}
}
if ( args[1] instanceof Object ) {
objs.push(args[1]);
}
const matched = matchObjectPropertiesFn(propNeedles, ...objs);
if ( matched === undefined ) { return fetchPromise; }
if ( safe.logLevel > 1 ) {
safe.uboLog(logPrefix, `Matched "propsToMatch":\n\t${matched.join('\n\t')}`);
}
}
return fetchPromise.then(responseBefore => {
const response = responseBefore.clone();
return response.json().then(objBefore => {
if ( typeof objBefore !== 'object' ) { return responseBefore; }
if ( logall ) {
safe.uboLog(logPrefix, safe.JSON_stringify(objBefore, null, 2));
return responseBefore;
}
const objAfter = objectPruneFn(
objBefore,
rawPrunePaths,
rawNeedlePaths,
stackNeedle,
extraArgs
);
if ( typeof objAfter !== 'object' ) { return responseBefore; }
safe.uboLog(logPrefix, 'Pruned');
const responseAfter = Response.json(objAfter, {
status: responseBefore.status,
statusText: responseBefore.statusText,
headers: responseBefore.headers,
});
Object.defineProperties(responseAfter, {
ok: { value: responseBefore.ok },
redirected: { value: responseBefore.redirected },
type: { value: responseBefore.type },
url: { value: responseBefore.url },
});
return responseAfter;
}).catch(reason => {
safe.uboErr(logPrefix, 'Error:', reason);
return responseBefore;
});
}).catch(reason => {
safe.uboErr(logPrefix, 'Error:', reason);
return fetchPromise;
});
};
self.fetch = new Proxy(self.fetch, {
apply: applyHandler
});
}
registerScriptlet(jsonPruneFetchResponseFn, {
name: 'json-prune-fetch-response.fn',
dependencies: [
matchObjectPropertiesFn,
objectPruneFn,
parsePropertiesToMatchFn,
safeSelf,
],
});
/******************************************************************************/
function jsonPruneFetchResponse(...args) {
jsonPruneFetchResponseFn(...args);
}
registerScriptlet(jsonPruneFetchResponse, {
name: 'json-prune-fetch-response.js',
dependencies: [
jsonPruneFetchResponseFn,
],
});
/******************************************************************************/
function jsonPruneXhrResponseFn(
rawPrunePaths = '',
rawNeedlePaths = ''
) {
const safe = safeSelf();
const logPrefix = safe.makeLogPrefix('json-prune-xhr-response', rawPrunePaths, rawNeedlePaths);
const xhrInstances = new WeakMap();
const extraArgs = safe.getExtraArgs(Array.from(arguments), 2);
const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url');
const stackNeedle = safe.initPattern(extraArgs.stackToMatch || '', { canNegate: true });
self.XMLHttpRequest = class extends self.XMLHttpRequest {
open(method, url, ...args) {
const xhrDetails = { method, url };
let outcome = 'match';
if ( propNeedles.size !== 0 ) {
if ( matchObjectPropertiesFn(propNeedles, xhrDetails) === undefined ) {
outcome = 'nomatch';
}
}
if ( outcome === 'match' ) {
if ( safe.logLevel > 1 ) {
safe.uboLog(logPrefix, `Matched optional "propsToMatch", "${extraArgs.propsToMatch}"`);
}
xhrInstances.set(this, xhrDetails);
}
return super.open(method, url, ...args);
}
get response() {
const innerResponse = super.response;
const xhrDetails = xhrInstances.get(this);
if ( xhrDetails === undefined ) {
return innerResponse;
}
const responseLength = typeof innerResponse === 'string'
? innerResponse.length
: undefined;
if ( xhrDetails.lastResponseLength !== responseLength ) {
xhrDetails.response = undefined;
xhrDetails.lastResponseLength = responseLength;
}
if ( xhrDetails.response !== undefined ) {
return xhrDetails.response;
}
let objBefore;
if ( typeof innerResponse === 'object' ) {
objBefore = innerResponse;
} else if ( typeof innerResponse === 'string' ) {
try {
objBefore = safe.JSON_parse(innerResponse);
} catch {
}
}
if ( typeof objBefore !== 'object' ) {
return (xhrDetails.response = innerResponse);
}
const objAfter = objectPruneFn(
objBefore,
rawPrunePaths,
rawNeedlePaths,
stackNeedle,
extraArgs
);
let outerResponse;
if ( typeof objAfter === 'object' ) {
outerResponse = typeof innerResponse === 'string'
? safe.JSON_stringify(objAfter)
: objAfter;
safe.uboLog(logPrefix, 'Pruned');
} else {
outerResponse = innerResponse;
}
return (xhrDetails.response = outerResponse);
}
get responseText() {
const response = this.response;
return typeof response !== 'string'
? super.responseText
: response;
}
};
}
registerScriptlet(jsonPruneXhrResponseFn, {
name: 'json-prune-xhr-response.fn',
dependencies: [
matchObjectPropertiesFn,
objectPruneFn,
parsePropertiesToMatchFn,
safeSelf,
],
});
/******************************************************************************/
function jsonPruneXhrResponse(...args) {
jsonPruneXhrResponseFn(...args);
}
registerScriptlet(jsonPruneXhrResponse, {
name: 'json-prune-xhr-response.js',
dependencies: [
jsonPruneXhrResponseFn,
],
});
/******************************************************************************/
// There is still code out there which uses `eval` in lieu of `JSON.parse`.
function evaldataPrune(
rawPrunePaths = '',
rawNeedlePaths = ''
) {
proxyApplyFn('eval', function(context) {
const before = context.reflect();
if ( typeof before !== 'object' ) { return before; }
if ( before === null ) { return null; }
const after = objectPruneFn(before, rawPrunePaths, rawNeedlePaths);
return after || before;
});
}
registerScriptlet(evaldataPrune, {
name: 'evaldata-prune.js',
dependencies: [
objectPruneFn,
proxyApplyFn,
],
});
/******************************************************************************/