close
Skip to content

Commit 99e6228

Browse files
committed
Add edit-object-on-[getter|setter] scriptlets
Related issue: uBlockOrigin/uBlock-issues#4019 (comment) --- @Scriptlet edit-object-on-getter.js @description Prune properties from an object on read access. Properties can only be removed. @PARAM propChain Property chain of the property to trap. @PARAM jsonq A uBO-flavored JSONPath query. --- @Scriptlet edit-object-on-setter.js @description Prune properties from an object on write access. Properties can only be removed. @PARAM propChain Property chain of the property to trap. @PARAM jsonq A uBO-flavored JSONPath query. --- The `trusted-` versions of the above scriptlets allows to assign new values to looked up properties.
1 parent ef32a02 commit 99e6228

3 files changed

Lines changed: 285 additions & 1 deletion

File tree

‎src/js/jsonpath.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ export class JSONPath {
141141
if ( i === 0 ) { this.#root = null; return; }
142142
while ( i-- ) {
143143
const { obj, key } = this.#resolvePath(paths[i]);
144+
if ( obj === undefined ) { continue; }
144145
if ( rval !== undefined ) {
145146
this.#modifyVal(obj, key);
146147
} else if ( Array.isArray(obj) && typeof key === 'number' ) {
@@ -294,6 +295,7 @@ export class JSONPath {
294295
case this.#CURRENT:
295296
if ( step.op ) {
296297
const { obj, key } = this.#resolvePath(pathin);
298+
if ( obj === undefined ) { return []; }
297299
const outcome = this.#evaluateExpr(step, obj, key);
298300
if ( outcome !== true ) { break; }
299301
}
@@ -321,6 +323,7 @@ export class JSONPath {
321323
const listout = [];
322324
for ( const pathin of listin ) {
323325
const { value: owner } = this.#resolvePath(pathin);
326+
if ( owner === undefined ) { continue; }
324327
if ( step.steps ) {
325328
this.#getMatchesFromExpr(pathin, step, owner, listout);
326329
continue;
@@ -377,7 +380,7 @@ export class JSONPath {
377380
}
378381
#getMatchesFromExpr(pathin, step, owner, out) {
379382
const recursive = step.mv === this.#DESCENDANTS;
380-
const v2 = this.#compiled.v2 || Array.isArray(owner);
383+
const v2 = this.#compiled.v2 || recursive || Array.isArray(owner);
381384
for ( const { path } of this.#getDescendants(owner, recursive) ) {
382385
const q = v2 ? [ ...pathin, ...path ] : pathin;
383386
const r = this.#evaluate(step.steps, q);
@@ -532,6 +535,7 @@ export class JSONPath {
532535
let obj = this.#root
533536
for ( let i = 0, n = path.length-1; i < n; i++ ) {
534537
obj = obj[path[i]];
538+
if ( obj instanceof Object === false ) { return {}; }
535539
}
536540
return { obj, key, value: obj[key] };
537541
}

‎src/js/resources/json-edit.js‎

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
matchObjectPropertiesFn,
2727
parsePropertiesToMatchFn,
2828
sleepFn,
29+
trapPropertyFn,
2930
} from './utils.js';
3031

3132
import { JSONPath } from './shared.js';
@@ -415,6 +416,209 @@ registerScriptlet(trustedEditThisObject, {
415416
/******************************************************************************/
416417
/******************************************************************************/
417418

419+
function editObjectOnGetterFn(
420+
trusted = false,
421+
propChain = '',
422+
jsonq = '',
423+
) {
424+
if ( propChain === '' ) { return; }
425+
const safe = safeSelf();
426+
const logPrefix = safe.makeLogPrefix(
427+
`${trusted ? 'trusted-' : ''}edit-object-on-getter`,
428+
propChain,
429+
jsonq
430+
);
431+
const jsonp = JSONPath.create(jsonq);
432+
if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) {
433+
return safe.uboLog(logPrefix, 'Bad JSONPath query');
434+
}
435+
const editObj = objBefore => {
436+
const objAfter = jsonp.apply(objBefore);
437+
if ( objAfter === undefined ) { return; }
438+
safe.uboLog(logPrefix, 'Edited');
439+
if ( safe.logLevel <= 1 ) { return; }
440+
safe.uboLog(logPrefix, `After edit:\n${safe.JSON_stringify(objAfter, null, 2)}`);
441+
};
442+
let currentValue = trapPropertyFn(propChain, {
443+
get: function() {
444+
if ( currentValue instanceof Object ) {
445+
editObj(currentValue);
446+
}
447+
return currentValue;
448+
},
449+
set: function(a) {
450+
currentValue = a;
451+
}
452+
});
453+
}
454+
registerScriptlet(editObjectOnGetterFn, {
455+
name: 'edit-object-on-getter.fn',
456+
dependencies: [
457+
JSONPath,
458+
safeSelf,
459+
trapPropertyFn,
460+
],
461+
});
462+
463+
/******************************************************************************/
464+
/**
465+
* @scriptlet edit-object-on-getter.js
466+
*
467+
* @description
468+
* Prune properties from an object on read access.
469+
* Properties can only be removed.
470+
*
471+
* @param propChain
472+
* Property chain of the property to trap.
473+
*
474+
* @param jsonq
475+
* A uBO-flavored JSONPath query.
476+
*
477+
* */
478+
479+
function editObjectOnGetter(...args) {
480+
editObjectOnGetterFn(false, ...args);
481+
}
482+
registerScriptlet(editObjectOnGetter, {
483+
name: 'edit-object-on-getter.js',
484+
dependencies: [
485+
editObjectOnGetterFn,
486+
],
487+
});
488+
489+
/******************************************************************************/
490+
/**
491+
* @scriptlet trusted-edit-object-on-getter.js
492+
*
493+
* @description
494+
* Edit properties of an object on read access.
495+
* Properties can be assigned new values.
496+
*
497+
* @param propChain
498+
* Property chain of the property to trap.
499+
*
500+
* @param jsonq
501+
* A uBO-flavored JSONPath query.
502+
*
503+
* */
504+
505+
function trustedEditObjectOnGetter(...args) {
506+
editObjectOnGetterFn(true, ...args);
507+
}
508+
registerScriptlet(trustedEditObjectOnGetter, {
509+
name: 'trusted-edit-object-on-getter.js',
510+
requiresTrust: true,
511+
dependencies: [
512+
editObjectOnGetterFn,
513+
],
514+
});
515+
516+
/******************************************************************************/
517+
/******************************************************************************/
518+
519+
function editObjectOnSetterFn(
520+
trusted = false,
521+
propChain = '',
522+
jsonq = '',
523+
) {
524+
if ( propChain === '' ) { return; }
525+
const safe = safeSelf();
526+
const logPrefix = safe.makeLogPrefix(
527+
`${trusted ? 'trusted-' : ''}edit-object-on-setter`,
528+
propChain,
529+
jsonq
530+
);
531+
const jsonp = JSONPath.create(jsonq);
532+
if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) {
533+
return safe.uboLog(logPrefix, 'Bad JSONPath query');
534+
}
535+
const editObj = objBefore => {
536+
const objAfter = jsonp.apply(objBefore);
537+
if ( objAfter === undefined ) { return; }
538+
safe.uboLog(logPrefix, 'Edited');
539+
if ( safe.logLevel <= 1 ) { return; }
540+
safe.uboLog(logPrefix, `After edit:\n${safe.JSON_stringify(objAfter, null, 2)}`);
541+
};
542+
let currentValue = trapPropertyFn(propChain, {
543+
get: function() {
544+
return currentValue;
545+
},
546+
set: function(a) {
547+
currentValue = a;
548+
if ( currentValue instanceof Object ) {
549+
editObj(currentValue);
550+
}
551+
}
552+
});
553+
if ( currentValue instanceof Object ) {
554+
editObj(currentValue);
555+
}
556+
}
557+
registerScriptlet(editObjectOnSetterFn, {
558+
name: 'edit-object-on-setter.fn',
559+
dependencies: [
560+
JSONPath,
561+
safeSelf,
562+
trapPropertyFn,
563+
],
564+
});
565+
566+
/******************************************************************************/
567+
/**
568+
* @scriptlet edit-object-on-setter.js
569+
*
570+
* @description
571+
* Prune properties from an object on write access.
572+
* Properties can only be removed.
573+
*
574+
* @param propChain
575+
* Property chain of the property to trap.
576+
*
577+
* @param jsonq
578+
* A uBO-flavored JSONPath query.
579+
*
580+
* */
581+
582+
function editObjectOnSetter(...args) {
583+
editObjectOnSetterFn(false, ...args);
584+
}
585+
registerScriptlet(editObjectOnSetter, {
586+
name: 'edit-object-on-setter.js',
587+
dependencies: [
588+
editObjectOnSetterFn,
589+
],
590+
});
591+
592+
/******************************************************************************/
593+
/**
594+
* @scriptlet trusted-edit-object-on-setter.js
595+
*
596+
* @description
597+
* Edit properties of an object on write access.
598+
* Properties can be assigned new values.
599+
*
600+
* @param propChain
601+
* Property chain of the property to trap.
602+
*
603+
* @param jsonq
604+
* A uBO-flavored JSONPath query.
605+
*
606+
* */
607+
608+
function trustedEditObjectOnSetter(...args) {
609+
editObjectOnSetterFn(true, ...args);
610+
}
611+
registerScriptlet(trustedEditObjectOnSetter, {
612+
name: 'trusted-edit-object-on-setter.js',
613+
requiresTrust: true,
614+
dependencies: [
615+
editObjectOnSetterFn,
616+
],
617+
});
618+
619+
/******************************************************************************/
620+
/******************************************************************************/
621+
418622
async function editElementObjectFn(
419623
trusted = false,
420624
selector = '',

‎src/js/resources/utils.js‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,82 @@ registerScriptlet(getExceptionTokenFn, {
6262

6363
/******************************************************************************/
6464

65+
export function trapPropertyFn(propChain, handler) {
66+
if ( propChain === '' ) { return; }
67+
let owner = self;
68+
let prop = propChain;
69+
for (;;) {
70+
const pos = prop.indexOf('.');
71+
if ( pos === -1 ) { break; }
72+
owner = owner[prop.slice(0, pos)];
73+
if ( owner instanceof Object === false ) { return; }
74+
prop = prop.slice(pos + 1);
75+
}
76+
const safe = safeSelf();
77+
if ( trapPropertyFn.db === undefined ) {
78+
trapPropertyFn.db = new WeakMap();
79+
trapPropertyFn.entryFromContext = (owner, prop) => {
80+
const handlers = trapPropertyFn.db.get(owner);
81+
return handlers?.get(prop);
82+
};
83+
trapPropertyFn.getter = (owner, prop) => {
84+
const entry = trapPropertyFn.entryFromContext(owner, prop);
85+
if ( entry === undefined ) { return; }
86+
let r = entry.value;
87+
for ( const desc of entry.stack ) {
88+
try { r = desc.get(); } catch { }
89+
}
90+
return r;
91+
};
92+
trapPropertyFn.setter = (owner, prop, value) => {
93+
const entry = trapPropertyFn.entryFromContext(owner, prop);
94+
if ( entry === undefined ) { return; }
95+
entry.value = value;
96+
for ( const desc of entry.stack ) {
97+
try { desc.set(value); } catch { }
98+
}
99+
};
100+
}
101+
const { db } = trapPropertyFn;
102+
const handlers = db.get(owner) || new Map();
103+
if ( handlers.size === 0 ) {
104+
db.set(owner, handlers);
105+
}
106+
const entry = handlers.get(prop) || {
107+
value: owner[prop],
108+
stack: [],
109+
};
110+
entry.stack.push(handler);
111+
if ( entry.stack.length > 1 ) { return entry.value; }
112+
handlers.set(prop, entry);
113+
const desc = safe.Object_getOwnPropertyDescriptor(owner, prop);
114+
if ( desc instanceof safe.Object ) {
115+
if ( desc.get || desc.set ) {
116+
entry.stack.push(desc);
117+
}
118+
}
119+
try {
120+
safe.Object_defineProperty(owner, prop, {
121+
get() {
122+
return trapPropertyFn.getter(this, prop);
123+
},
124+
set(value) {
125+
trapPropertyFn.setter(this, prop, value);
126+
}
127+
});
128+
} catch {
129+
}
130+
return entry.value;
131+
}
132+
registerScriptlet(trapPropertyFn, {
133+
name: 'trap-property-access.fn',
134+
dependencies: [
135+
safeSelf,
136+
],
137+
});
138+
139+
/******************************************************************************/
140+
65141
export function collateFetchArgumentsFn(resource, options) {
66142
const safe = safeSelf();
67143
const props = [

0 commit comments

Comments
 (0)