diff options
Diffstat (limited to 'data/extensions/uBlock0@raymondhill.net/js/resources/json-edit.js')
-rw-r--r-- | data/extensions/uBlock0@raymondhill.net/js/resources/json-edit.js | 1097 |
1 files changed, 1097 insertions, 0 deletions
diff --git a/data/extensions/uBlock0@raymondhill.net/js/resources/json-edit.js b/data/extensions/uBlock0@raymondhill.net/js/resources/json-edit.js new file mode 100644 index 0000000..f8ea191 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/resources/json-edit.js @@ -0,0 +1,1097 @@ +/******************************************************************************* + + 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 { JSONPath } from './shared.js'; +import { proxyApplyFn } from './proxy-apply.js'; +import { registerScriptlet } from './base.js'; +import { safeSelf } from './safe-self.js'; + +/******************************************************************************/ +/******************************************************************************/ + +function editOutboundObjectFn( + trusted = false, + propChain = '', + jsonq = '', +) { + if ( propChain === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix( + `${trusted ? 'trusted-' : ''}edit-outbound-object`, + propChain, + jsonq + ); + const jsonp = JSONPath.create(jsonq); + if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) { + return safe.uboLog(logPrefix, 'Bad JSONPath query'); + } + proxyApplyFn(propChain, function(context) { + const obj = context.reflect(); + if ( jsonp.apply(obj) === 0 ) { return obj; } + safe.uboLog(logPrefix, 'Edited'); + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `After edit:\n${safe.JSON_stringify(obj, null, 2)}`); + } + return obj; + }); +} +registerScriptlet(editOutboundObjectFn, { + name: 'edit-outbound-object.fn', + dependencies: [ + JSONPath, + proxyApplyFn, + safeSelf, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet edit-outbound-object-.js + * + * @description + * Prune properties from an object returned by a specific method. + * Properties can only be removed. + * + * @param propChain + * Property chain of the method to trap. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * */ + +function editOutboundObject(propChain = '', jsonq = '') { + editOutboundObjectFn(false, propChain, jsonq); +} +registerScriptlet(editOutboundObject, { + name: 'edit-outbound-object.js', + dependencies: [ + editOutboundObjectFn, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet trusted-edit-outbound-object.js + * + * @description + * Edit properties of an object returned by a specific method. + * Properties can be assigned new values. + * + * @param propChain + * Property chain of the method to trap. + * +* @param jsonq + * A uBO-flavored JSONPath query. + * + * */ + +function trustedEditOutboundObject(propChain = '', jsonq = '') { + editOutboundObjectFn(true, propChain, jsonq); +} +registerScriptlet(trustedEditOutboundObject, { + name: 'trusted-edit-outbound-object.js', + requiresTrust: true, + dependencies: [ + editOutboundObjectFn, + ], +}); + +/******************************************************************************/ +/******************************************************************************/ +/** + * @scriptlet json-edit.js + * + * @description + * Edit object generated through JSON.parse(). + * Properties can only be removed. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * */ + +function jsonEdit(jsonq = '') { + editOutboundObjectFn(false, 'JSON.parse', jsonq); +} +registerScriptlet(jsonEdit, { + name: 'json-edit.js', + dependencies: [ + editOutboundObjectFn, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet trusted-json-edit.js + * + * @description + * Edit object generated through JSON.parse(). + * Properties can be assigned new values. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * */ + +function trustedJsonEdit(jsonq = '') { + editOutboundObjectFn(true, 'JSON.parse', jsonq); +} +registerScriptlet(trustedJsonEdit, { + name: 'trusted-json-edit.js', + requiresTrust: true, + dependencies: [ + editOutboundObjectFn, + ], +}); + +/******************************************************************************/ +/******************************************************************************/ + +function editInboundObjectFn( + trusted = false, + propChain = '', + argPosRaw = '', + jsonq = '', +) { + if ( propChain === '' ) { return; } + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix( + `${trusted ? 'trusted-' : ''}edit-inbound-object`, + propChain, + jsonq + ); + const jsonp = JSONPath.create(jsonq); + if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) { + return safe.uboLog(logPrefix, 'Bad JSONPath query'); + } + const argPos = parseInt(argPosRaw, 10); + if ( isNaN(argPos) ) { return; } + const getArgPos = args => { + if ( argPos >= 0 ) { + if ( args.length <= argPos ) { return; } + return argPos; + } + if ( args.length < -argPos ) { return; } + return args.length + argPos; + }; + const editObj = obj => { + let clone; + try { + clone = safe.JSON_parse(safe.JSON_stringify(obj)); + } catch { + } + if ( typeof clone !== 'object' || clone === null ) { return; } + if ( jsonp.apply(clone) === 0 ) { return; } + safe.uboLog(logPrefix, 'Edited'); + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `After edit:\n${safe.JSON_stringify(clone, null, 2)}`); + } + return clone; + }; + proxyApplyFn(propChain, function(context) { + const i = getArgPos(context.args); + if ( i !== undefined ) { + const obj = editObj(context.args[i]); + if ( obj ) { + context.args[i] = obj; + } + } + return context.reflect(); + }); +} +registerScriptlet(editInboundObjectFn, { + name: 'edit-inbound-object.fn', + dependencies: [ + JSONPath, + proxyApplyFn, + safeSelf, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet edit-inbound-object.js + * + * @description + * Prune properties from an object passed as argument to a specific method. + * Properties can only be removed. + * + * @param propChain + * Property chain of the method to trap. + * + * @param argPos + * 0-based position of the argument. Use negative integer for position relative + * to the end. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * */ + +function editInboundObject(propChain = '', argPos = '', jsonq = '') { + editInboundObjectFn(false, propChain, argPos, jsonq); +} +registerScriptlet(editInboundObject, { + name: 'edit-inbound-object.js', + dependencies: [ + editInboundObjectFn, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet trusted-edit-inbound-object.js + * + * @description + * Edit properties of an object passed as argument to a specific method. + * Properties can be assigned new values. + * + * @param propChain + * Property chain of the method to trap. + * + * @param argPos + * 0-based position of the argument. Use negative integer for position relative + * to the end. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * */ + +function trustedEditInboundObject(propChain = '', argPos = '', jsonq = '') { + editInboundObjectFn(true, propChain, argPos, jsonq); +} +registerScriptlet(trustedEditInboundObject, { + name: 'trusted-edit-inbound-object.js', + requiresTrust: true, + dependencies: [ + editInboundObjectFn, + ], +}); + +/******************************************************************************/ +/******************************************************************************/ + +function jsonEditXhrResponseFn(trusted, jsonq = '') { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix( + `${trusted ? 'trusted-' : ''}json-edit-xhr-response`, + jsonq + ); + const xhrInstances = new WeakMap(); + const jsonp = JSONPath.create(jsonq); + if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) { + return safe.uboLog(logPrefix, 'Bad JSONPath query'); + } + const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); + const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url'); + self.XMLHttpRequest = class extends self.XMLHttpRequest { + open(method, url, ...args) { + const xhrDetails = { method, url }; + const matched = propNeedles.size === 0 || + matchObjectPropertiesFn(propNeedles, xhrDetails); + if ( matched ) { + if ( safe.logLevel > 1 && Array.isArray(matched) ) { + safe.uboLog(logPrefix, `Matched "propsToMatch":\n\t${matched.join('\n\t')}`); + } + 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 obj; + if ( typeof innerResponse === 'object' ) { + obj = innerResponse; + } else if ( typeof innerResponse === 'string' ) { + try { obj = safe.JSON_parse(innerResponse); } catch { } + } + if ( typeof obj !== 'object' || obj === null || jsonp.apply(obj) === 0 ) { + return (xhrDetails.response = innerResponse); + } + safe.uboLog(logPrefix, 'Edited'); + const outerResponse = typeof innerResponse === 'string' + ? JSONPath.toJSON(obj, safe.JSON_stringify) + : obj; + return (xhrDetails.response = outerResponse); + } + get responseText() { + const response = this.response; + return typeof response !== 'string' + ? super.responseText + : response; + } + }; +} +registerScriptlet(jsonEditXhrResponseFn, { + name: 'json-edit-xhr-response.fn', + dependencies: [ + JSONPath, + matchObjectPropertiesFn, + parsePropertiesToMatchFn, + safeSelf, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet json-edit-xhr-response.js + * + * @description + * Edit the object fetched through a XHR instance. + * Properties can only be removed. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when xhr.open() is + * called. + * + * */ + +function jsonEditXhrResponse(jsonq = '', ...args) { + jsonEditXhrResponseFn(false, jsonq, ...args); +} +registerScriptlet(jsonEditXhrResponse, { + name: 'json-edit-xhr-response.js', + dependencies: [ + jsonEditXhrResponseFn, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet trusted-json-edit-xhr-response.js + * + * @description + * Edit the object fetched through a XHR instance. + * Properties can be assigned new values. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when xhr.open() is + * called. + * + * */ + +function trustedJsonEditXhrResponse(jsonq = '', ...args) { + jsonEditXhrResponseFn(true, jsonq, ...args); +} +registerScriptlet(trustedJsonEditXhrResponse, { + name: 'trusted-json-edit-xhr-response.js', + requiresTrust: true, + dependencies: [ + jsonEditXhrResponseFn, + ], +}); + +/******************************************************************************/ +/******************************************************************************/ + +function jsonEditXhrRequestFn(trusted, jsonq = '') { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix( + `${trusted ? 'trusted-' : ''}json-edit-xhr-request`, + jsonq + ); + const xhrInstances = new WeakMap(); + const jsonp = JSONPath.create(jsonq); + if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) { + return safe.uboLog(logPrefix, 'Bad JSONPath query'); + } + const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); + const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url'); + self.XMLHttpRequest = class extends self.XMLHttpRequest { + open(method, url, ...args) { + const xhrDetails = { method, url }; + const matched = propNeedles.size === 0 || + matchObjectPropertiesFn(propNeedles, xhrDetails); + if ( matched ) { + if ( safe.logLevel > 1 && Array.isArray(matched) ) { + safe.uboLog(logPrefix, `Matched "propsToMatch":\n\t${matched.join('\n\t')}`); + } + xhrInstances.set(this, xhrDetails); + } + return super.open(method, url, ...args); + } + send(body) { + const xhrDetails = xhrInstances.get(this); + if ( xhrDetails ) { + body = this.#filterBody(body) || body; + } + super.send(body); + } + #filterBody(body) { + if ( typeof body !== 'string' ) { return; } + let data; + try { data = safe.JSON_parse(body); } + catch { } + if ( data instanceof Object === false ) { return; } + const n = jsonp.apply(data); + if ( n === 0 ) { return; } + body = safe.JSON_stringify(data); + safe.uboLog(logPrefix, 'Edited'); + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `After edit:\n${body}`); + } + return body; + } + }; +} +registerScriptlet(jsonEditXhrRequestFn, { + name: 'json-edit-xhr-request.fn', + dependencies: [ + JSONPath, + matchObjectPropertiesFn, + parsePropertiesToMatchFn, + safeSelf, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet json-edit-xhr-request.js + * + * @description + * Edit the object sent as the body in a XHR instance. + * Properties can only be removed. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when xhr.open() is + * called. + * + * */ + +function jsonEditXhrRequest(jsonq = '', ...args) { + jsonEditXhrRequestFn(false, jsonq, ...args); +} +registerScriptlet(jsonEditXhrRequest, { + name: 'json-edit-xhr-request.js', + dependencies: [ + jsonEditXhrRequestFn, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet trusted-json-edit-xhr-request.js + * + * @description + * Edit the object sent as the body in a XHR instance. + * Properties can be assigned new values. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when xhr.open() is + * called. + * + * */ + +function trustedJsonEditXhrRequest(jsonq = '', ...args) { + jsonEditXhrRequestFn(true, jsonq, ...args); +} +registerScriptlet(trustedJsonEditXhrRequest, { + name: 'trusted-json-edit-xhr-request.js', + requiresTrust: true, + dependencies: [ + jsonEditXhrRequestFn, + ], +}); + +/******************************************************************************/ +/******************************************************************************/ + +function jsonEditFetchResponseFn(trusted, jsonq = '') { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix( + `${trusted ? 'trusted-' : ''}json-edit-fetch-response`, + jsonq + ); + const jsonp = JSONPath.create(jsonq); + if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) { + return safe.uboLog(logPrefix, 'Bad JSONPath query'); + } + const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); + const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url'); + proxyApplyFn('fetch', function(context) { + const args = context.callArgs; + const fetchPromise = context.reflect(); + 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(obj => { + if ( typeof obj !== 'object' ) { return responseBefore; } + if ( jsonp.apply(obj) === 0 ) { return responseBefore; } + safe.uboLog(logPrefix, 'Edited'); + const responseAfter = Response.json(obj, { + 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; + }); + }); +} +registerScriptlet(jsonEditFetchResponseFn, { + name: 'json-edit-fetch-response.fn', + dependencies: [ + JSONPath, + matchObjectPropertiesFn, + parsePropertiesToMatchFn, + proxyApplyFn, + safeSelf, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet json-edit-fetch-response.js + * + * @description + * Edit the object fetched through the fetch API. + * Properties can only be removed. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when xhr.open() is + * called. + * + * */ + +function jsonEditFetchResponse(jsonq = '', ...args) { + jsonEditFetchResponseFn(false, jsonq, ...args); +} +registerScriptlet(jsonEditFetchResponse, { + name: 'json-edit-fetch-response.js', + dependencies: [ + jsonEditFetchResponseFn, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet trusted-json-edit-fetch-response.js + * + * @description + * Edit the object fetched through the fetch API. The trusted version allows + * Properties can be assigned new values. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when xhr.open() is + * called. + * + * */ + +function trustedJsonEditFetchResponse(jsonq = '', ...args) { + jsonEditFetchResponseFn(true, jsonq, ...args); +} +registerScriptlet(trustedJsonEditFetchResponse, { + name: 'trusted-json-edit-fetch-response.js', + requiresTrust: true, + dependencies: [ + jsonEditFetchResponseFn, + ], +}); + +/******************************************************************************/ +/******************************************************************************/ + +function jsonEditFetchRequestFn(trusted, jsonq = '') { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix( + `${trusted ? 'trusted-' : ''}json-edit-fetch-request`, + jsonq + ); + const jsonp = JSONPath.create(jsonq); + if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) { + return safe.uboLog(logPrefix, 'Bad JSONPath query'); + } + const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); + const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url'); + const filterBody = body => { + if ( typeof body !== 'string' ) { return; } + let data; + try { data = safe.JSON_parse(body); } + catch { } + if ( data instanceof Object === false ) { return; } + const n = jsonp.apply(data); + if ( n === 0 ) { return; } + return safe.JSON_stringify(data); + } + const proxyHandler = context => { + const args = context.callArgs; + const [ resource, options ] = args; + const bodyBefore = options?.body; + if ( Boolean(bodyBefore) === false ) { return context.reflect(); } + const bodyAfter = filterBody(bodyBefore); + if ( bodyAfter === undefined || bodyAfter === bodyBefore ) { + return context.reflect(); + } + if ( propNeedles.size !== 0 ) { + const objs = [ + resource instanceof Object ? resource : { url: `${resource}` } + ]; + if ( objs[0] instanceof Request ) { + try { + objs[0] = safe.Request_clone.call(objs[0]); + } catch(ex) { + safe.uboErr(logPrefix, 'Error:', ex); + } + } + const matched = matchObjectPropertiesFn(propNeedles, ...objs); + if ( matched === undefined ) { return context.reflect(); } + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `Matched "propsToMatch":\n\t${matched.join('\n\t')}`); + } + } + safe.uboLog(logPrefix, 'Edited'); + if ( safe.logLevel > 1 ) { + safe.uboLog(logPrefix, `After edit:\n${bodyAfter}`); + } + options.body = bodyAfter; + return context.reflect(); + }; + proxyApplyFn('fetch', proxyHandler); + proxyApplyFn('Request', proxyHandler); +} +registerScriptlet(jsonEditFetchRequestFn, { + name: 'json-edit-fetch-request.fn', + dependencies: [ + JSONPath, + matchObjectPropertiesFn, + parsePropertiesToMatchFn, + proxyApplyFn, + safeSelf, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet json-edit-fetch-request.js + * + * @description + * Edit the request body sent through the fetch API. + * Properties can only be removed. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when fetch() is called. + * + * */ + +function jsonEditFetchRequest(jsonq = '', ...args) { + jsonEditFetchRequestFn(false, jsonq, ...args); +} +registerScriptlet(jsonEditFetchRequest, { + name: 'json-edit-fetch-request.js', + dependencies: [ + jsonEditFetchRequestFn, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet trusted-json-edit-fetch-request.js + * + * @description + * Edit the request body sent through the fetch API. + * Properties can be assigned new values. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when fetch() is called. + * + * */ + +function trustedJsonEditFetchRequest(jsonq = '', ...args) { + jsonEditFetchRequestFn(true, jsonq, ...args); +} +registerScriptlet(trustedJsonEditFetchRequest, { + name: 'trusted-json-edit-fetch-request.js', + requiresTrust: true, + dependencies: [ + jsonEditFetchRequestFn, + ], +}); + +/******************************************************************************/ +/******************************************************************************/ + +function jsonlEditFn(jsonp, text = '') { + const safe = safeSelf(); + const lineSeparator = /\r?\n/.exec(text)?.[0] || '\n'; + const linesBefore = text.split('\n'); + const linesAfter = []; + for ( const lineBefore of linesBefore ) { + let obj; + try { obj = safe.JSON_parse(lineBefore); } catch { } + if ( typeof obj !== 'object' || obj === null ) { + linesAfter.push(lineBefore); + continue; + } + if ( jsonp.apply(obj) === 0 ) { + linesAfter.push(lineBefore); + continue; + } + const lineAfter = safe.JSON_stringify(obj); + linesAfter.push(lineAfter); + } + return linesAfter.join(lineSeparator); +} +registerScriptlet(jsonlEditFn, { + name: 'jsonl-edit.fn', + dependencies: [ + JSONPath, + safeSelf, + ], +}); + +/******************************************************************************/ + +function jsonlEditXhrResponseFn(trusted, jsonq = '') { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix( + `${trusted ? 'trusted-' : ''}jsonl-edit-xhr-response`, + jsonq + ); + const xhrInstances = new WeakMap(); + const jsonp = JSONPath.create(jsonq); + if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) { + return safe.uboLog(logPrefix, 'Bad JSONPath query'); + } + const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); + const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url'); + self.XMLHttpRequest = class extends self.XMLHttpRequest { + open(method, url, ...args) { + const xhrDetails = { method, url }; + const matched = propNeedles.size === 0 || + matchObjectPropertiesFn(propNeedles, xhrDetails); + if ( matched ) { + if ( safe.logLevel > 1 && Array.isArray(matched) ) { + safe.uboLog(logPrefix, `Matched "propsToMatch":\n\t${matched.join('\n\t')}`); + } + 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; + } + if ( typeof innerResponse !== 'string' ) { + return (xhrDetails.response = innerResponse); + } + const outerResponse = jsonlEditFn(jsonp, innerResponse); + if ( outerResponse !== innerResponse ) { + safe.uboLog(logPrefix, 'Pruned'); + } + return (xhrDetails.response = outerResponse); + } + get responseText() { + const response = this.response; + return typeof response !== 'string' + ? super.responseText + : response; + } + }; +} +registerScriptlet(jsonlEditXhrResponseFn, { + name: 'jsonl-edit-xhr-response.fn', + dependencies: [ + JSONPath, + jsonlEditFn, + matchObjectPropertiesFn, + parsePropertiesToMatchFn, + safeSelf, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet jsonl-edit-xhr-response.js + * + * @description + * Edit the objects found in a JSONL resource fetched through a XHR instance. + * Properties can only be removed. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when xhr.open() is + * called. + * + * */ + +function jsonlEditXhrResponse(jsonq = '', ...args) { + jsonlEditXhrResponseFn(false, jsonq, ...args); +} +registerScriptlet(jsonlEditXhrResponse, { + name: 'jsonl-edit-xhr-response.js', + dependencies: [ + jsonlEditXhrResponseFn, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet trusted-jsonl-edit-xhr-response.js + * + * @description + * Edit the objects found in a JSONL resource fetched through a XHR instance. + * Properties can be assigned new values. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when xhr.open() is + * called. + * + * */ + +function trustedJsonlEditXhrResponse(jsonq = '', ...args) { + jsonlEditXhrResponseFn(true, jsonq, ...args); +} +registerScriptlet(trustedJsonlEditXhrResponse, { + name: 'trusted-jsonl-edit-xhr-response.js', + requiresTrust: true, + dependencies: [ + jsonlEditXhrResponseFn, + ], +}); + +/******************************************************************************/ +/******************************************************************************/ + +function jsonlEditFetchResponseFn(trusted, jsonq = '') { + const safe = safeSelf(); + const logPrefix = safe.makeLogPrefix( + `${trusted ? 'trusted-' : ''}jsonl-edit-fetch-response`, + jsonq + ); + const jsonp = JSONPath.create(jsonq); + if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) { + return safe.uboLog(logPrefix, 'Bad JSONPath query'); + } + const extraArgs = safe.getExtraArgs(Array.from(arguments), 2); + const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url'); + const logall = jsonq === ''; + proxyApplyFn('fetch', function(context) { + const args = context.callArgs; + const fetchPromise = context.reflect(); + 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.text().then(textBefore => { + if ( typeof textBefore !== 'string' ) { return textBefore; } + if ( logall ) { + safe.uboLog(logPrefix, textBefore); + return responseBefore; + } + const textAfter = jsonlEditFn(jsonp, textBefore); + if ( textAfter === textBefore ) { return responseBefore; } + safe.uboLog(logPrefix, 'Pruned'); + const responseAfter = new Response(textAfter, { + 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; + }); + }); +} +registerScriptlet(jsonlEditFetchResponseFn, { + name: 'jsonl-edit-fetch-response.fn', + dependencies: [ + JSONPath, + jsonlEditFn, + matchObjectPropertiesFn, + parsePropertiesToMatchFn, + proxyApplyFn, + safeSelf, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet jsonl-edit-fetch-response.js + * + * @description + * Edit the objects found in a JSONL resource fetched through the fetch API. + * Properties can only be removed. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when xhr.open() is + * called. + * + * */ + +function jsonlEditFetchResponse(jsonq = '', ...args) { + jsonlEditFetchResponseFn(false, jsonq, ...args); +} +registerScriptlet(jsonlEditFetchResponse, { + name: 'jsonl-edit-fetch-response.js', + dependencies: [ + jsonlEditFetchResponseFn, + ], +}); + +/******************************************************************************/ +/** + * @scriptlet trusted-jsonl-edit-fetch-response.js + * + * @description + * Edit the objects found in a JSONL resource fetched through the fetch API. + * Properties can be assigned new values. + * + * @param jsonq + * A uBO-flavored JSONPath query. + * + * @param [propsToMatch, value] + * An optional vararg detailing the arguments to match when xhr.open() is + * called. + * + * */ + +function trustedJsonlEditFetchResponse(jsonq = '', ...args) { + jsonlEditFetchResponseFn(true, jsonq, ...args); +} +registerScriptlet(trustedJsonlEditFetchResponse, { + name: 'trusted-jsonl-edit-fetch-response.js', + requiresTrust: true, + dependencies: [ + jsonlEditFetchResponseFn, + ], +}); + +/******************************************************************************/ |