Skip to content

Commit

Permalink
AG-22709 Add evaldata-prune scriptlet. #322
Browse files Browse the repository at this point in the history
Squashed commit of the following:

commit 07d47b6
Merge: 969881b 7e485bb
Author: Adam Wróblewski <adam@adguard.com>
Date:   Tue Jun 6 13:04:49 2023 +0200

    Merge branch 'master' into feature/AG-22709

commit 969881b
Author: Adam Wróblewski <adam@adguard.com>
Date:   Mon Jun 5 16:02:24 2023 +0200

    Fix JSDoc return value in description
    Add related uBO scriptlet to description

commit 8911c2e
Author: Adam Wróblewski <adam@adguard.com>
Date:   Thu Jun 1 09:09:22 2023 +0200

    Move jsonPruner and isPruningNeeded to helpers
    Fix description

commit 318084d
Author: Adam Wróblewski <adam@adguard.com>
Date:   Wed May 31 17:50:55 2023 +0200

    Move toRegExp before the comment
    Add @added unreleased information

commit 27e2c8b
Merge: 5990342 fdade01
Author: Adam Wróblewski <adam@adguard.com>
Date:   Wed May 31 17:36:01 2023 +0200

    Merge branch 'master' into feature/AG-22709

commit 5990342
Author: Adam Wróblewski <adam@adguard.com>
Date:   Wed May 31 16:58:27 2023 +0200

    Add evaldata-prune scriptlet
  • Loading branch information
AdamWr committed Jun 6, 2023
1 parent 7e485bb commit deff2c7
Show file tree
Hide file tree
Showing 7 changed files with 465 additions and 92 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- new `evaldata-prune` scriptlet [#322](https://github.com/AdguardTeam/Scriptlets/issues/322)
- new `trusted-replace-node-text` scriptlet [#319](https://github.com/AdguardTeam/Scriptlets/issues/319)
- new `remove-node-text` scriptlet [#318](https://github.com/AdguardTeam/Scriptlets/issues/318)
- ability for `prevent-element-src-loading` scriptlet to
Expand Down
1 change: 1 addition & 0 deletions src/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export * from './script-source-utils';
export * from './open-shadow-dom-utils';
export * from './prevent-utils';
export * from './prevent-window-open-utils';
export * from './prune-utils';
export * from './regexp-utils';
export * from './response-utils';
export * from './request-utils';
Expand Down
108 changes: 108 additions & 0 deletions src/helpers/prune-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { hit } from './hit';
import { getWildcardPropertyInChain } from './get-wildcard-property-in-chain';
import { logMessage } from './log-message';
import { toRegExp } from './string-utils';

/**
* Checks if prunning is required
*
* @param {Object} source required, scriptlet properties
* @param {Object} root object which should be pruned or logged
* @param {Array} prunePaths array with string of space-separated properties to remove
* @param {Array} requiredPaths array with string of space-separated properties
* which must be all present for the pruning to occur
* @returns {boolean|undefined} true if prunning is required
*/
export function isPruningNeeded(source, root, prunePaths, requiredPaths) {
if (!root) {
return false;
}

let shouldProcess;

// Only log hostname and matched JSON payload if only second argument is present
if (prunePaths.length === 0 && requiredPaths.length > 0) {
const rootString = JSON.stringify(root);
const matchRegex = toRegExp(requiredPaths.join(''));
const shouldLog = matchRegex.test(rootString);
if (shouldLog) {
logMessage(source, `${window.location.hostname}\n${JSON.stringify(root, null, 2)}`, true);
if (root && typeof root === 'object') {
logMessage(source, root, true, false);
}
shouldProcess = false;
return shouldProcess;
}
}

const wildcardSymbols = ['.*.', '*.', '.*', '.[].', '[].', '.[]'];

for (let i = 0; i < requiredPaths.length; i += 1) {
const requiredPath = requiredPaths[i];
const lastNestedPropName = requiredPath.split('.').pop();
const hasWildcard = wildcardSymbols.some((symbol) => requiredPath.includes(symbol));

// if the path has wildcard, getPropertyInChain should 'look through' chain props
const details = getWildcardPropertyInChain(root, requiredPath, hasWildcard);

// start value of 'shouldProcess' due to checking below
shouldProcess = !hasWildcard;

for (let i = 0; i < details.length; i += 1) {
if (hasWildcard) {
// if there is a wildcard,
// at least one (||) of props chain should be present in object
shouldProcess = !(details[i].base[lastNestedPropName] === undefined)
|| shouldProcess;
} else {
// otherwise each one (&&) of them should be there
shouldProcess = !(details[i].base[lastNestedPropName] === undefined)
&& shouldProcess;
}
}
}

return shouldProcess;
}

/**
* Prunes properties of 'root' object
*
* @param {Object} source required, scriptlet properties
* @param {Object} root object which should be pruned or logged
* @param {Array} prunePaths array with string of space-separated properties to remove
* @param {Array} requiredPaths array with string of space-separated properties
* which must be all present for the pruning to occur
* @returns {Object} pruned root
*/
export const jsonPruner = (source, root, prunePaths, requiredPaths) => {
if (prunePaths.length === 0 && requiredPaths.length === 0) {
logMessage(source, `${window.location.hostname}\n${JSON.stringify(root, null, 2)}`, true);
if (root && typeof root === 'object') {
logMessage(source, root, true, false);
}
return root;
}

try {
if (isPruningNeeded(source, root, prunePaths, requiredPaths) === false) {
return root;
}

// if pruning is needed, we check every input pathToRemove
// and delete it if root has it
prunePaths.forEach((path) => {
const ownerObjArr = getWildcardPropertyInChain(root, path, true);
ownerObjArr.forEach((ownerObj) => {
if (ownerObj !== undefined && ownerObj.base) {
delete ownerObj.base[ownerObj.prop];
hit(source);
}
});
});
} catch (e) {
logMessage(source, e);
}

return root;
};
143 changes: 143 additions & 0 deletions src/scriptlets/evaldata-prune.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
hit,
matchStackTrace,
getWildcardPropertyInChain,
logMessage,
toRegExp,
isPruningNeeded,
jsonPruner,
// following helpers are needed for helpers above
getNativeRegexpTest,
shouldAbortInlineOrInjectedScript,
} from '../helpers/index';

/* eslint-disable max-len */
/**
* @scriptlet evaldata-prune
*
* @description
* Removes specified properties from the result of calling eval (if payloads contains `Object`) and returns to the caller.
*
* Related UBO scriptlet:
* https://github.com/gorhill/uBlock/commit/c8de9041917b61035171e454df886706f27fc4f3
*
* ### Syntax
*
* ```text
* example.org#%#//scriptlet('evaldata-prune'[, propsToRemove [, obligatoryProps [, stack]]])
* ```
*
* - `propsToRemove` — optional, string of space-separated properties to remove
* - `obligatoryProps` — optional, string of space-separated properties
* which must be all present for the pruning to occur
* - `stack` — optional, string or regular expression that must match the current function call stack trace;
* if regular expression is invalid it will be skipped
*
* > Note please that you can use wildcard `*` for chain property name,
* > e.g. `ad.*.src` instead of `ad.0.src ad.1.src ad.2.src`.
*
* ### Examples
*
* 1. Removes property `example` from the payload of the eval call
*
* ```adblock
* example.org#%#//scriptlet('evaldata-prune', 'example')
* ```
*
* For instance, the following call will return `{ one: 1}`
*
* ```html
* eval({ one: 1, example: true })
* ```
*
* 2. If there are no specified properties in the payload of eval call, pruning will NOT occur
*
* ```adblock
* example.org#%#//scriptlet('evaldata-prune', 'one', 'obligatoryProp')
* ```
*
* For instance, the following call will return `{ one: 1, two: 2}`
*
* ```html
* JSON.parse('{"one":1,"two":2}')
* ```
*
* 3. A property in a list of properties can be a chain of properties
*
* ```adblock
* example.org#%#//scriptlet('evaldata-prune', 'a.b', 'ads.url.first')
* ```
*
* 4. Removes property `content.ad` from the payload of eval call if its error stack trace contains `test.js`
*
* ```adblock
* example.org#%#//scriptlet('evaldata-prune', 'content.ad', '', 'test.js')
* ```
*
* 5. A property in a list of properties can be a chain of properties with wildcard in it
*
* ```adblock
* example.org#%#//scriptlet('evaldata-prune', 'content.*.media.src', 'content.*.media.ad')
* ```
*
* 6. Call with no arguments will log the current hostname and object payload at the console
*
* ```adblock
* example.org#%#//scriptlet('evaldata-prune')
* ```
*
* 7. Call with only second argument will log the current hostname and matched object payload at the console
*
* ```adblock
* example.org#%#//scriptlet('evaldata-prune', '', '"id":"117458"')
* ```
*
* @added unreleased.
*/
/* eslint-enable max-len */
export function evalDataPrune(source, propsToRemove, requiredInitialProps, stack) {
if (!!stack && !matchStackTrace(stack, new Error().stack)) {
return;
}
const prunePaths = propsToRemove !== undefined && propsToRemove !== ''
? propsToRemove.split(/ +/)
: [];
const requiredPaths = requiredInitialProps !== undefined && requiredInitialProps !== ''
? requiredInitialProps.split(/ +/)
: [];

const evalWrapper = (target, thisArg, args) => {
let data = Reflect.apply(target, thisArg, args);
if (typeof data === 'object') {
data = jsonPruner(source, data, prunePaths, requiredPaths);
}
return data;
};

const evalHandler = {
apply: evalWrapper,
};
// eslint-disable-next-line no-eval
window.eval = new Proxy(window.eval, evalHandler);
}

evalDataPrune.names = [
'evaldata-prune',
// aliases are needed for matching the related scriptlet converted into our syntax
'evaldata-prune.js',
'ubo-evaldata-prune.js',
'ubo-evaldata-prune',
];

evalDataPrune.injections = [
hit,
matchStackTrace,
getWildcardPropertyInChain,
logMessage,
toRegExp,
isPruningNeeded,
jsonPruner,
// following helpers are needed for helpers above
getNativeRegexpTest,
shouldAbortInlineOrInjectedScript,
];
98 changes: 6 additions & 92 deletions src/scriptlets/json-prune.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
matchStackTrace,
getWildcardPropertyInChain,
logMessage,
isPruningNeeded,
jsonPruner,
// following helpers are needed for helpers above
toRegExp,
getNativeRegexpTest,
Expand Down Expand Up @@ -108,102 +110,12 @@ export function jsonPrune(source, propsToRemove, requiredInitialProps, stack) {
? requiredInitialProps.split(/ +/)
: [];

function isPruningNeeded(root) {
if (!root) {
return false;
}

let shouldProcess;

// Only log hostname and matched JSON payload if only second argument is present
if (prunePaths.length === 0 && requiredPaths.length > 0) {
const rootString = JSON.stringify(root);
const matchRegex = toRegExp(requiredPaths.join(''));
const shouldLog = matchRegex.test(rootString);
if (shouldLog) {
logMessage(source, `${window.location.hostname}\n${JSON.stringify(root, null, 2)}`, true);
if (root && typeof root === 'object') {
logMessage(source, root, true, false);
}
shouldProcess = false;
return shouldProcess;
}
}

for (let i = 0; i < requiredPaths.length; i += 1) {
const requiredPath = requiredPaths[i];
const lastNestedPropName = requiredPath.split('.').pop();

const wildcardSymbols = ['.*.', '*.', '.*', '.[].', '[].', '.[]'];
const hasWildcard = wildcardSymbols.some((symbol) => requiredPath.includes(symbol));

// if the path has wildcard, getPropertyInChain should 'look through' chain props
const details = getWildcardPropertyInChain(root, requiredPath, hasWildcard);

// start value of 'shouldProcess' due to checking below
shouldProcess = !hasWildcard;

for (let i = 0; i < details.length; i += 1) {
if (hasWildcard) {
// if there is a wildcard,
// at least one (||) of props chain should be present in object
shouldProcess = !(details[i].base[lastNestedPropName] === undefined)
|| shouldProcess;
} else {
// otherwise each one (&&) of them should be there
shouldProcess = !(details[i].base[lastNestedPropName] === undefined)
&& shouldProcess;
}
}
}

return shouldProcess;
}

/**
* Prunes properties of 'root' object
*
* @param {Object} root
* @returns {Object} pruned root
*/
const jsonPruner = (root) => {
if (prunePaths.length === 0 && requiredPaths.length === 0) {
logMessage(source, `${window.location.hostname}\n${JSON.stringify(root, null, 2)}`, true);
if (root && typeof root === 'object') {
logMessage(source, root, true, false);
}
return root;
}

try {
if (isPruningNeeded(root) === false) {
return root;
}

// if pruning is needed, we check every input pathToRemove
// and delete it if root has it
prunePaths.forEach((path) => {
const ownerObjArr = getWildcardPropertyInChain(root, path, true);
ownerObjArr.forEach((ownerObj) => {
if (ownerObj !== undefined && ownerObj.base) {
delete ownerObj.base[ownerObj.prop];
hit(source);
}
});
});
} catch (e) {
logMessage(source, e);
}

return root;
};

const nativeJSONParse = JSON.parse;
const jsonParseWrapper = (...args) => {
// dealing with stringified json in args, which should be parsed.
// so we call nativeJSONParse as JSON.parse which is bound to JSON object
const root = nativeJSONParse.apply(JSON, args);
return jsonPruner(root);
return jsonPruner(source, root, prunePaths, requiredPaths);
};

// JSON.parse mocking
Expand All @@ -215,7 +127,7 @@ export function jsonPrune(source, propsToRemove, requiredInitialProps, stack) {
const responseJsonWrapper = function () {
const promise = nativeResponseJson.apply(this);
return promise.then((obj) => {
return jsonPruner(obj);
return jsonPruner(source, obj, prunePaths, requiredPaths);
});
};

Expand All @@ -242,6 +154,8 @@ jsonPrune.injections = [
matchStackTrace,
getWildcardPropertyInChain,
logMessage,
isPruningNeeded,
jsonPruner,
// following helpers are needed for helpers above
toRegExp,
getNativeRegexpTest,
Expand Down
Loading

0 comments on commit deff2c7

Please sign in to comment.