/******************************************************************************* 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 jsonPruneFetchResponse( 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(jsonPruneFetchResponse, { name: 'json-prune-fetch-response.js', dependencies: [ matchObjectPropertiesFn, objectPruneFn, parsePropertiesToMatchFn, safeSelf, ], }); /******************************************************************************/ function jsonPruneXhrResponse( 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(jsonPruneXhrResponse, { name: 'json-prune-xhr-response.js', dependencies: [ matchObjectPropertiesFn, objectPruneFn, parsePropertiesToMatchFn, safeSelf, ], }); /******************************************************************************/ // 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, ], }); /******************************************************************************/