/******************************************************************************* 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 { matchesStackTraceFn } from './stack-trace.js'; import { proxyApplyFn } from './proxy-apply.js'; import { registerScriptlet } from './base.js'; import { safeSelf } from './safe-self.js'; /******************************************************************************/ function objectFindOwnerFn( root, path, prune = false ) { const safe = safeSelf(); let owner = root; let chain = path; for (;;) { if ( typeof owner !== 'object' || owner === null ) { return false; } const pos = chain.indexOf('.'); if ( pos === -1 ) { if ( prune === false ) { return safe.Object_hasOwn(owner, chain); } let modified = false; if ( chain === '*' ) { for ( const key in owner ) { if ( safe.Object_hasOwn(owner, key) === false ) { continue; } delete owner[key]; modified = true; } } else if ( safe.Object_hasOwn(owner, chain) ) { delete owner[chain]; modified = true; } return modified; } const prop = chain.slice(0, pos); const next = chain.slice(pos + 1); let found = false; if ( prop === '[-]' && Array.isArray(owner) ) { let i = owner.length; while ( i-- ) { if ( objectFindOwnerFn(owner[i], next) === false ) { continue; } owner.splice(i, 1); found = true; } return found; } if ( prop === '{-}' && owner instanceof Object ) { for ( const key of Object.keys(owner) ) { if ( objectFindOwnerFn(owner[key], next) === false ) { continue; } delete owner[key]; found = true; } return found; } if ( prop === '[]' && Array.isArray(owner) || prop === '{}' && owner instanceof Object || prop === '*' && owner instanceof Object ) { for ( const key of Object.keys(owner) ) { if (objectFindOwnerFn(owner[key], next, prune) === false ) { continue; } found = true; } return found; } if ( safe.Object_hasOwn(owner, prop) === false ) { return false; } owner = owner[prop]; chain = chain.slice(pos + 1); } } registerScriptlet(objectFindOwnerFn, { name: 'object-find-owner.fn', dependencies: [ safeSelf, ], }); /******************************************************************************/ // When no "prune paths" argument is provided, the scriptlet is // used for logging purpose and the "needle paths" argument is // used to filter logging output. // // https://github.com/uBlockOrigin/uBlock-issues/issues/1545 // - Add support for "remove everything if needle matches" case export function objectPruneFn( obj, rawPrunePaths, rawNeedlePaths, stackNeedleDetails = { matchAll: true }, extraArgs = {} ) { if ( typeof rawPrunePaths !== 'string' ) { return; } const safe = safeSelf(); const prunePaths = rawPrunePaths !== '' ? safe.String_split.call(rawPrunePaths, / +/) : []; const needlePaths = prunePaths.length !== 0 && rawNeedlePaths !== '' ? safe.String_split.call(rawNeedlePaths, / +/) : []; if ( stackNeedleDetails.matchAll !== true ) { if ( matchesStackTraceFn(stackNeedleDetails, extraArgs.logstack) === false ) { return; } } if ( objectPruneFn.mustProcess === undefined ) { objectPruneFn.mustProcess = (root, needlePaths) => { for ( const needlePath of needlePaths ) { if ( objectFindOwnerFn(root, needlePath) === false ) { return false; } } return true; }; } if ( prunePaths.length === 0 ) { return; } let outcome = 'nomatch'; if ( objectPruneFn.mustProcess(obj, needlePaths) ) { for ( const path of prunePaths ) { if ( objectFindOwnerFn(obj, path, true) ) { outcome = 'match'; } } } if ( outcome === 'match' ) { return obj; } } registerScriptlet(objectPruneFn, { name: 'object-prune.fn', dependencies: [ matchesStackTraceFn, objectFindOwnerFn, safeSelf, ], }); /******************************************************************************/ function trustedPruneInboundObject( entryPoint = '', argPos = '', rawPrunePaths = '', rawNeedlePaths = '' ) { if ( entryPoint === '' ) { return; } let context = globalThis; let prop = entryPoint; for (;;) { const pos = prop.indexOf('.'); if ( pos === -1 ) { break; } context = context[prop.slice(0, pos)]; if ( context instanceof Object === false ) { return; } prop = prop.slice(pos+1); } if ( typeof context[prop] !== 'function' ) { return; } const argIndex = parseInt(argPos); if ( isNaN(argIndex) ) { return; } if ( argIndex < 1 ) { return; } const safe = safeSelf(); const extraArgs = safe.getExtraArgs(Array.from(arguments), 4); const needlePaths = []; if ( rawPrunePaths !== '' ) { needlePaths.push(...safe.String_split.call(rawPrunePaths, / +/)); } if ( rawNeedlePaths !== '' ) { needlePaths.push(...safe.String_split.call(rawNeedlePaths, / +/)); } const stackNeedle = safe.initPattern(extraArgs.stackToMatch || '', { canNegate: true }); const mustProcess = root => { for ( const needlePath of needlePaths ) { if ( objectFindOwnerFn(root, needlePath) === false ) { return false; } } return true; }; context[prop] = new Proxy(context[prop], { apply: function(target, thisArg, args) { const targetArg = argIndex <= args.length ? args[argIndex-1] : undefined; if ( targetArg instanceof Object && mustProcess(targetArg) ) { let objBefore = targetArg; if ( extraArgs.dontOverwrite ) { try { objBefore = safe.JSON_parse(safe.JSON_stringify(targetArg)); } catch { objBefore = undefined; } } if ( objBefore !== undefined ) { const objAfter = objectPruneFn( objBefore, rawPrunePaths, rawNeedlePaths, stackNeedle, extraArgs ); args[argIndex-1] = objAfter || objBefore; } } return Reflect.apply(target, thisArg, args); }, }); } registerScriptlet(trustedPruneInboundObject, { name: 'trusted-prune-inbound-object.js', requiresTrust: true, dependencies: [ objectFindOwnerFn, objectPruneFn, safeSelf, ], }); /******************************************************************************/ function trustedPruneOutboundObject( propChain = '', rawPrunePaths = '', rawNeedlePaths = '' ) { if ( propChain === '' ) { return; } const safe = safeSelf(); const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); proxyApplyFn(propChain, function(context) { const objBefore = context.reflect(); if ( objBefore instanceof Object === false ) { return objBefore; } const objAfter = objectPruneFn( objBefore, rawPrunePaths, rawNeedlePaths, { matchAll: true }, extraArgs ); return objAfter || objBefore; }); } registerScriptlet(trustedPruneOutboundObject, { name: 'trusted-prune-outbound-object.js', requiresTrust: true, dependencies: [ objectPruneFn, proxyApplyFn, safeSelf, ], }); /******************************************************************************/