Skip to content

Commit fdc1a4e

Browse files
committed
Improve JSONPath; add [trusted-]edit-element-object scriptlets
Improve JSONPath: Add ability to call a method on looked-up instances, jsonpath query example (to be further documented): $.["onload","onerror"]=call(["${obj}","removeAttribute","${key}"]) New scriptlet `edit-element-object`: @description Prune properties of one or more elements matching a specific selector. Properties can only be removed. @param selector The selector used to locate the elements on which the JSONPath query will be applied. @param jsonq A uBO-flavored JSONPath query. @param timeout The maximum time (in milliseconds) to wait for matching elements before stopping. Defaults to 0, which executes a single lookup attempt without waiting. New scriptlet `trusted-edit-element-object`: @description Edit properties of one or more elements matching a specific selector. Properties can be assigned new values. @param selector The selector used to locate the elements on which the JSONPath query will be applied. @param jsonq A uBO-flavored JSONPath query. @param timeout The maximum time (in milliseconds) to wait for matching elements before stopping. Defaults to 0, which executes a single lookup attempt without waiting.
1 parent 5512f70 commit fdc1a4e

7 files changed

Lines changed: 282 additions & 131 deletions

File tree

src/js/jsonpath.js

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@
4949
* - ^=: stringified value starts with
5050
* - $=: stringified value ends with
5151
* - *=: stringified value includes
52-
* - =/.../: regular expression matches
53-
*
52+
* - =/.../: true if the regular expression matches the stringified value
53+
* - =repl(...): [to be documented] i.e. {"regex":"...","flags":"i","replacement":"..."}
54+
* - =call(...): [to be documented] i.e. ["${obj}","setAttribute","${key}"]
55+
* *
5456
* - Examples (from "JSONPath examples" at reference link)
5557
* - .store.book[*].author
5658
* - ..author
@@ -102,9 +104,10 @@ export class JSONPath {
102104
if ( r.i !== query.length ) {
103105
let val;
104106
if ( query.startsWith('=', r.i) ) {
105-
if ( /^=repl\(.+\)$/.test(query.slice(r.i)) ) {
106-
r.modify = 'repl';
107-
val = query.slice(r.i+6, -1);
107+
const match = this.#reRval.exec(query.slice(r.i));
108+
if ( match ) {
109+
r.modify = match[1];
110+
val = match[2];
108111
} else {
109112
val = query.slice(r.i+1);
110113
}
@@ -162,6 +165,7 @@ export class JSONPath {
162165
#reUnquotedIdentifier = /^[A-Za-z_][\w]*|^\*/;
163166
#reExpr = /^([!=^$*]=|[<>]=?)(.+?)\]/;
164167
#reIndice = /^-?\d+/;
168+
#reRval = /^=([a-z]+)\((.+)\)$/;
165169
#root;
166170
#compiled;
167171
#compile(query, i) {
@@ -314,14 +318,6 @@ export class JSONPath {
314318
out.push(q);
315319
}
316320
}
317-
#normalizeKey(owner, key) {
318-
if ( typeof key === 'number' ) {
319-
if ( Array.isArray(owner) ) {
320-
return key >= 0 ? key : owner.length + key;
321-
}
322-
}
323-
return key;
324-
}
325321
#getDescendants(v, recursive) {
326322
const iterator = {
327323
next() {
@@ -461,11 +457,14 @@ export class JSONPath {
461457
}
462458
#evaluateExpr(step, owner, key) {
463459
if ( owner === undefined || owner === null ) { return; }
460+
let k;
464461
if ( typeof key === 'number' ) {
465462
if ( Array.isArray(owner) === false ) { return; }
463+
k = key >= 0 ? key : owner.length + key;
464+
} else {
465+
k = key;
466466
}
467-
const k = this.#normalizeKey(owner, key);
468-
const hasOwn = Object.hasOwn(owner, k);
467+
const hasOwn = owner[k] !== undefined || Object.hasOwn(owner, k);
469468
if ( step.op !== undefined && hasOwn === false ) { return; }
470469
const target = step.not !== true;
471470
const v = owner[k];
@@ -504,6 +503,18 @@ export class JSONPath {
504503
}
505504
break;
506505
}
506+
case 'call': {
507+
const entries = rval.slice();
508+
if ( entries.length < 2 ) { break; }
509+
entries.forEach((a, i, aa) => {
510+
if ( a === '${obj}' ) { aa[i] = obj; }
511+
else if ( a === '${key}' ) { aa[i] = key; }
512+
else if ( a === '${val}' ) { aa[i] = obj[key]; }
513+
});
514+
const instance = entries[0] ?? self;
515+
instance[entries[1]](...entries.slice(2));
516+
break;
517+
}
507518
case 'repl': {
508519
const lval = obj[key];
509520
if ( typeof lval !== 'string' ) { return; }

src/js/resources/attribute.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
2121
*/
2222

23+
import { offIdleFn, onIdleFn } from './utils.js';
2324
import { registerScriptlet } from './base.js';
2425
import { runAt } from './run-at.js';
2526
import { safeSelf } from './safe-self.js';
@@ -241,14 +242,14 @@ export function removeAttr(
241242
let timerId;
242243
const rmattrAsync = ( ) => {
243244
if ( timerId !== undefined ) { return; }
244-
timerId = safe.onIdle(( ) => {
245+
timerId = onIdleFn(( ) => {
245246
timerId = undefined;
246247
rmattr();
247248
}, { timeout: 17 });
248249
};
249250
const rmattr = ( ) => {
250251
if ( timerId !== undefined ) {
251-
safe.offIdle(timerId);
252+
offIdleFn(timerId);
252253
timerId = undefined;
253254
}
254255
try {
@@ -298,6 +299,8 @@ registerScriptlet(removeAttr, {
298299
'ra.js',
299300
],
300301
dependencies: [
302+
offIdleFn,
303+
onIdleFn,
301304
runAt,
302305
safeSelf,
303306
],

src/js/resources/href-sanitizer.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
2121
*/
2222

23+
import { onIdleFn } from './utils.js';
2324
import { registerScriptlet } from './base.js';
2425
import { runAt } from './run-at.js';
2526
import { safeSelf } from './safe-self.js';
@@ -141,7 +142,7 @@ function hrefSanitizer(
141142
if ( shouldSanitize ) { break; }
142143
}
143144
if ( shouldSanitize === false ) { return; }
144-
timer = safe.onIdle(( ) => {
145+
timer = onIdleFn(( ) => {
145146
timer = undefined;
146147
sanitize();
147148
});
@@ -163,6 +164,7 @@ registerScriptlet(hrefSanitizer, {
163164
'urlskip.js',
164165
],
165166
dependencies: [
167+
onIdleFn,
166168
runAt,
167169
safeSelf,
168170
urlSkip,

src/js/resources/json-edit.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import {
2424
collateFetchArgumentsFn,
25+
lookupElementsFn,
2526
matchObjectPropertiesFn,
2627
parsePropertiesToMatchFn,
2728
} from './utils.js';
@@ -413,6 +414,115 @@ registerScriptlet(trustedEditThisObject, {
413414
/******************************************************************************/
414415
/******************************************************************************/
415416

417+
function editElementObjectFn(
418+
trusted = false,
419+
selector = '',
420+
jsonq = '',
421+
timeout = ''
422+
) {
423+
if ( selector === '' ) { return; }
424+
const safe = safeSelf();
425+
const logPrefix = safe.makeLogPrefix(
426+
`${trusted ? 'trusted-' : ''}edit-element-object`,
427+
selector, jsonq, timeout
428+
);
429+
const jsonp = JSONPath.create(jsonq);
430+
if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) {
431+
return safe.uboLog(logPrefix, 'Bad JSONPath query');
432+
}
433+
const process = elems => {
434+
for ( const elem of elems ) {
435+
const r = jsonp.apply(elem);
436+
if ( r === undefined ) { continue; }
437+
safe.uboLog(logPrefix, 'Edited');
438+
}
439+
};
440+
const result = lookupElementsFn(selector,
441+
timeout !== '' ? Date.now() + parseInt(timeout, 10) : 0
442+
);
443+
if ( Array.isArray(result) ) {
444+
process(result);
445+
} else if ( result instanceof Promise ) {
446+
result.then(elems => process(elems));
447+
}
448+
}
449+
registerScriptlet(editElementObjectFn, {
450+
name: 'edit-element-object.fn',
451+
dependencies: [
452+
JSONPath,
453+
lookupElementsFn,
454+
safeSelf,
455+
],
456+
});
457+
458+
/******************************************************************************/
459+
/**
460+
* @scriptlet edit-element-object.js
461+
*
462+
* @description
463+
* Prune properties of one or more elements matching a specific selector.
464+
* Properties can only be removed.
465+
*
466+
* @param selector
467+
* The selector used to locate the elements on which the JSONPath query will
468+
* be applied.
469+
*
470+
* @param jsonq
471+
* A uBO-flavored JSONPath query.
472+
*
473+
* @param timeout
474+
* The maximum time (in milliseconds) to wait for matching elements before
475+
* stopping. Defaults to 0, which executes a single lookup attempt without
476+
* waiting.
477+
*
478+
* */
479+
480+
function editElementObject(...args) {
481+
editElementObjectFn(false, ...args);
482+
}
483+
registerScriptlet(editElementObject, {
484+
name: 'edit-element-object.js',
485+
dependencies: [
486+
editElementObjectFn,
487+
],
488+
});
489+
490+
/******************************************************************************/
491+
/**
492+
* @scriptlet trusted-element-this-object.js
493+
*
494+
* @description
495+
* Edit properties of one or more elements matching a specific selector.
496+
* Properties can be assigned new values.
497+
*
498+
* @param selector
499+
* The selector used to locate the elements on which the JSONPath query will
500+
* be applied.
501+
*
502+
* @param jsonq
503+
* A uBO-flavored JSONPath query.
504+
*
505+
* @param timeout
506+
* The maximum time (in milliseconds) to wait for matching elements before
507+
* stopping. Defaults to 0, which executes a single lookup attempt without
508+
* waiting.
509+
*
510+
* */
511+
512+
function trustedEditElementObject(...args) {
513+
editElementObjectFn(true, ...args);
514+
}
515+
registerScriptlet(trustedEditElementObject, {
516+
name: 'trusted-edit-element-object.js',
517+
requiresTrust: true,
518+
dependencies: [
519+
editElementObjectFn,
520+
],
521+
});
522+
523+
/******************************************************************************/
524+
/******************************************************************************/
525+
416526
function jsonEditXhrResponseFn(trusted, jsonq = '') {
417527
const safe = safeSelf();
418528
const logPrefix = safe.makeLogPrefix(

src/js/resources/safe-self.js

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -147,18 +147,6 @@ export function safeSelf() {
147147
}, []);
148148
return this.Object_fromEntries(entries);
149149
},
150-
onIdle(fn, options) {
151-
if ( self.requestIdleCallback ) {
152-
return self.requestIdleCallback(fn, options);
153-
}
154-
return self.requestAnimationFrame(fn);
155-
},
156-
offIdle(id) {
157-
if ( self.requestIdleCallback ) {
158-
return self.cancelIdleCallback(id);
159-
}
160-
return self.cancelAnimationFrame(id);
161-
}
162150
};
163151
scriptletGlobals.safeSelf = safe;
164152
if ( scriptletGlobals.bcSecret === undefined ) { return safe; }

0 commit comments

Comments
 (0)