Refactor scriptlets injection code

Builtin scriptlets are no longer parsed as text-based resources,
they are imported as JS functions, and `toString()` is used to
obtain text-based representation of a scriptlet.

Scriptlet parameters are now passed as function call arguments
rather than by replacing text-based occurrences of `{{i}}`. The
arguments are always string values (see below for exception).

Support for argument as Object has been added. This opens the
door to have scriptlets using named arguments rather than
positional arguments, and hence easier to extend functionality
of existing scriptlets. Example:

    example.com##+js(scriplet, { "prop": "adblock", "value": false, "log": true })

Compatibility with user-provided scriptlets has been preserved.

User-provided scriptlets can benefit some of the changes:

Use the form `function(..){..}` instead of `(function(..){..})();`
in order to received scriptlet arguments as part of function call
-- instead of using `{{i}}`.

If using the form `function(..){..}`, you can choose to receive
an Object as argument -- just be sure that your scriptlet's
parameter is valid JSON notation.
This commit is contained in:
Raymond Hill 2023-03-24 14:05:18 -04:00
parent 56b8201196
commit 18a84d2819
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
5 changed files with 531 additions and 315 deletions

View File

@ -2,7 +2,7 @@
"browser": true, "browser": true,
"devel": true, "devel": true,
"eqeqeq": true, "eqeqeq": true,
"esversion": 9, "esversion": 11,
"globals": { "globals": {
"chrome": false, // global variable in Chromium, Chrome, Opera "chrome": false, // global variable in Chromium, Chrome, Opera
"globalThis": false, "globalThis": false,

File diff suppressed because it is too large Load Diff

View File

@ -325,13 +325,20 @@ RedirectEngine.prototype.loadBuiltinResources = function(fetcher) {
this.aliases = new Map(); this.aliases = new Map();
const fetches = [ const fetches = [
fetcher( import('/assets/resources/scriptlets.js').then(module => {
'/assets/resources/scriptlets.js' for ( const scriptlet of module.builtinScriptlets ) {
).then(result => { const { name, aliases, fn } = scriptlet;
const content = result.content; const entry = RedirectEntry.fromContent(
if ( typeof content !== 'string' ) { return; } mimeFromName(name),
if ( content.length === 0 ) { return; } fn.toString()
this.resourcesFromString(content); );
this.resources.set(name, entry);
if ( Array.isArray(aliases) === false ) { continue; }
for ( const alias of aliases ) {
this.aliases.set(alias, name);
}
}
this.modifyTime = Date.now();
}), }),
]; ];
@ -426,7 +433,7 @@ RedirectEngine.prototype.getResourceDetails = function() {
/******************************************************************************/ /******************************************************************************/
const RESOURCES_SELFIE_VERSION = 6; const RESOURCES_SELFIE_VERSION = 7;
const RESOURCES_SELFIE_NAME = 'compiled/redirectEngine/resources'; const RESOURCES_SELFIE_NAME = 'compiled/redirectEngine/resources';
RedirectEngine.prototype.selfieFromResources = function(storage) { RedirectEngine.prototype.selfieFromResources = function(storage) {

View File

@ -149,7 +149,7 @@ const lookupScriptlet = function(rawToken, reng, toInject) {
let content = scriptletCache.lookup(rawToken); let content = scriptletCache.lookup(rawToken);
if ( content === undefined ) { if ( content === undefined ) {
const pos = rawToken.indexOf(','); const pos = rawToken.indexOf(',');
let token, args; let token, args = '';
if ( pos === -1 ) { if ( pos === -1 ) {
token = rawToken; token = rawToken;
} else { } else {
@ -165,10 +165,7 @@ const lookupScriptlet = function(rawToken, reng, toInject) {
} }
content = reng.resourceContentFromName(token, 'text/javascript'); content = reng.resourceContentFromName(token, 'text/javascript');
if ( !content ) { return; } if ( !content ) { return; }
if ( args ) {
content = patchScriptlet(content, args); content = patchScriptlet(content, args);
if ( !content ) { return; }
}
content = content =
'try {\n' + 'try {\n' +
content + '\n' + content + '\n' +
@ -180,6 +177,14 @@ const lookupScriptlet = function(rawToken, reng, toInject) {
// Fill-in scriptlet argument placeholders. // Fill-in scriptlet argument placeholders.
const patchScriptlet = function(content, args) { const patchScriptlet = function(content, args) {
if ( content.startsWith('function') ) {
content = `(${content})({{args}});`;
}
if ( args.startsWith('{') && args.endsWith('}') ) {
return content.replace('{{args}}', args);
}
const arglist = [];
if ( args !== '' ) {
let s = args; let s = args;
let len = s.length; let len = s.length;
let beg = 0, pos = 0; let beg = 0, pos = 0;
@ -193,13 +198,15 @@ const patchScriptlet = function(content, args) {
continue; continue;
} }
if ( pos === -1 ) { pos = len; } if ( pos === -1 ) { pos = len; }
content = content.replace( arglist.push(s.slice(beg, pos).trim().replace(reEscapeScriptArg, '\\$&'));
`{{${i}}}`,
s.slice(beg, pos).trim().replace(reEscapeScriptArg, '\\$&')
);
beg = pos = pos + 1; beg = pos = pos + 1;
i++; i++;
} }
}
for ( let i = 0; i < arglist.length; i++ ) {
content = content.replace(`{{${i+1}}}`, arglist[i]);
}
content = content.replace('{{args}}', arglist.map(a => `'${a}'`).join(', '));
return content; return content;
}; };

View File

@ -118,6 +118,7 @@ export const NODE_TYPE_EXT_PATTERN_HTML = iota++;
export const NODE_TYPE_EXT_PATTERN_RESPONSEHEADER = iota++; export const NODE_TYPE_EXT_PATTERN_RESPONSEHEADER = iota++;
export const NODE_TYPE_EXT_PATTERN_SCRIPTLET = iota++; export const NODE_TYPE_EXT_PATTERN_SCRIPTLET = iota++;
export const NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN = iota++; export const NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN = iota++;
export const NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARGS = iota++;
export const NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG = iota++; export const NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG = iota++;
export const NODE_TYPE_NET_RAW = iota++; export const NODE_TYPE_NET_RAW = iota++;
export const NODE_TYPE_NET_EXCEPTION = iota++; export const NODE_TYPE_NET_EXCEPTION = iota++;
@ -276,6 +277,7 @@ export const nodeNameFromNodeType = new Map([
[ NODE_TYPE_EXT_PATTERN_RESPONSEHEADER, 'extPatternResponseheader' ], [ NODE_TYPE_EXT_PATTERN_RESPONSEHEADER, 'extPatternResponseheader' ],
[ NODE_TYPE_EXT_PATTERN_SCRIPTLET, 'extPatternScriptlet' ], [ NODE_TYPE_EXT_PATTERN_SCRIPTLET, 'extPatternScriptlet' ],
[ NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN, 'extPatternScriptletToken' ], [ NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN, 'extPatternScriptletToken' ],
[ NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARGS, 'extPatternScriptletArgs' ],
[ NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG, 'extPatternScriptletArg' ], [ NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG, 'extPatternScriptletArg' ],
[ NODE_TYPE_NET_RAW, 'netRaw' ], [ NODE_TYPE_NET_RAW, 'netRaw' ],
[ NODE_TYPE_NET_EXCEPTION, 'netException' ], [ NODE_TYPE_NET_EXCEPTION, 'netException' ],
@ -748,6 +750,7 @@ export class AstFilterParser {
this.reHostnamePatternPart = /^[^\x00-\x24\x26-\x29\x2B\x2C\x2F\x3A-\x40\x5B-\x5E\x60\x7B-\x7F]+/; this.reHostnamePatternPart = /^[^\x00-\x24\x26-\x29\x2B\x2C\x2F\x3A-\x40\x5B-\x5E\x60\x7B-\x7F]+/;
this.reHostnameLabel = /[^.]+/g; this.reHostnameLabel = /[^.]+/g;
this.reResponseheaderPattern = /^\^responseheader\(.*\)$/; this.reResponseheaderPattern = /^\^responseheader\(.*\)$/;
this.rePatternScriptletJsonArgs = /^\{.*\}$/;
// TODO: mind maxTokenLength // TODO: mind maxTokenLength
this.reGoodRegexToken = /[^\x01%0-9A-Za-z][%0-9A-Za-z]{7,}|[^\x01%0-9A-Za-z][%0-9A-Za-z]{1,6}[^\x01%0-9A-Za-z]/; this.reGoodRegexToken = /[^\x01%0-9A-Za-z][%0-9A-Za-z]{7,}|[^\x01%0-9A-Za-z][%0-9A-Za-z]{1,6}[^\x01%0-9A-Za-z]/;
this.reBadCSP = /(?:=|;)\s*report-(?:to|uri)\b/; this.reBadCSP = /(?:=|;)\s*report-(?:to|uri)\b/;
@ -2118,23 +2121,19 @@ export class AstFilterParser {
let prev = head, next = 0; let prev = head, next = 0;
const s = this.getNodeString(parent); const s = this.getNodeString(parent);
const argsEnd = s.length; const argsEnd = s.length;
let argCount = 0; // token
let argBeg = 0, argEnd = 0, argBodyBeg = 0, argBodyEnd = 0; let argEnd = this.indexOfNextScriptletArgSeparator(s, 0);
let rawArg = ''; let rawArg = s.slice(0, argEnd);
while ( argBeg < argsEnd ) { let argBodyBeg = this.leftWhitespaceCount(rawArg);
argEnd = this.indexOfNextScriptletArgSeparator(s, argBeg); if ( argBodyBeg !== 0 ) {
rawArg = s.slice(argBeg, argEnd);
argBodyBeg = argBeg + this.leftWhitespaceCount(rawArg);
if ( argBodyBeg !== argBodyEnd ) {
next = this.allocTypedNode( next = this.allocTypedNode(
NODE_TYPE_EXT_DECORATION, NODE_TYPE_EXT_DECORATION,
parentBeg + argBodyEnd, parentBeg,
parentBeg + argBodyBeg parentBeg + argBodyBeg
); );
prev = this.linkRight(prev, next); prev = this.linkRight(prev, next);
} }
argBodyEnd = argEnd - this.rightWhitespaceCount(rawArg); let argBodyEnd = argEnd - this.rightWhitespaceCount(rawArg);
if ( argCount === 0 ) {
rawArg = s.slice(argBodyBeg, argBodyEnd); rawArg = s.slice(argBodyBeg, argBodyEnd);
const tokenEnd = rawArg.endsWith('.js') const tokenEnd = rawArg.endsWith('.js')
? argBodyEnd - 3 ? argBodyEnd - 3
@ -2145,6 +2144,7 @@ export class AstFilterParser {
parentBeg + tokenEnd parentBeg + tokenEnd
); );
prev = this.linkRight(prev, next); prev = this.linkRight(prev, next);
// ignore pointless `.js`
if ( tokenEnd !== argBodyEnd ) { if ( tokenEnd !== argBodyEnd ) {
next = this.allocTypedNode( next = this.allocTypedNode(
NODE_TYPE_IGNORE, NODE_TYPE_IGNORE,
@ -2153,18 +2153,27 @@ export class AstFilterParser {
); );
prev = this.linkRight(prev, next); prev = this.linkRight(prev, next);
} }
} else { // all args
argBodyBeg = argEnd + 1;
const rawArgs = s.slice(argBodyBeg, argsEnd);
argBodyBeg += this.leftWhitespaceCount(rawArgs);
next = this.allocTypedNode( next = this.allocTypedNode(
NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG, NODE_TYPE_EXT_DECORATION,
parentBeg + argBodyEnd,
parentBeg + argBodyBeg
);
prev = this.linkRight(prev, next);
argBodyEnd = argsEnd - this.rightWhitespaceCount(rawArgs);
if ( argBodyBeg !== argBodyEnd ) {
next = this.allocTypedNode(
NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARGS,
parentBeg + argBodyBeg, parentBeg + argBodyBeg,
parentBeg + argBodyEnd parentBeg + argBodyEnd
); );
this.linkDown(next, this.parseExtPatternScriptletArglist(next));
prev = this.linkRight(prev, next); prev = this.linkRight(prev, next);
} }
argBeg = argEnd + 1; if ( argBodyEnd !== argsEnd ) {
argCount += 1;
}
if ( argsEnd !== argBodyEnd ) {
next = this.allocTypedNode( next = this.allocTypedNode(
NODE_TYPE_EXT_DECORATION, NODE_TYPE_EXT_DECORATION,
parentBeg + argBodyEnd, parentBeg + argBodyEnd,
@ -2175,6 +2184,57 @@ export class AstFilterParser {
return this.throwHeadNode(head); return this.throwHeadNode(head);
} }
parseExtPatternScriptletArglist(parent) {
const parentBeg = this.nodes[parent+NODE_BEG_INDEX];
const parentEnd = this.nodes[parent+NODE_END_INDEX];
if ( parentEnd === parentBeg ) { return 0; }
const s = this.getNodeString(parent);
let next = 0, prev = 0;
// json-based arg?
const match = this.rePatternScriptletJsonArgs.exec(s);
if ( match !== null ) {
next = this.allocTypedNode(
NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG,
parentBeg,
parentEnd
);
try {
void JSON.parse(s);
} catch(ex) {
this.addNodeFlags(next, NODE_FLAG_ERROR);
this.addFlags(AST_FLAG_HAS_ERROR);
}
return next;
}
// positional args
const argsEnd = s.length;
let argBodyBeg = 0, argBodyEnd = 0, argEnd = 0;
let t = '';
while ( argBodyBeg < argsEnd ) {
argEnd = this.indexOfNextScriptletArgSeparator(s, argBodyBeg);
t = s.slice(argBodyBeg, argEnd);
argBodyEnd = argEnd - this.rightWhitespaceCount(t);
next = this.allocTypedNode(
NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG,
parentBeg + argBodyBeg,
parentBeg + argBodyEnd
);
prev = this.linkRight(prev, next);
if ( argEnd === argsEnd ) { break; }
t = s.slice(argEnd + 1);
argBodyBeg = argEnd + 1 + this.leftWhitespaceCount(t);
if ( argBodyEnd !== argBodyBeg ) {
next = this.allocTypedNode(
NODE_TYPE_EXT_DECORATION,
parentBeg + argBodyEnd,
parentBeg + argBodyBeg
);
prev = this.linkRight(prev, next);
}
}
return next;
}
indexOfNextScriptletArgSeparator(pattern, beg = 0) { indexOfNextScriptletArgSeparator(pattern, beg = 0) {
const patternEnd = pattern.length; const patternEnd = pattern.length;
if ( beg >= patternEnd ) { return patternEnd; } if ( beg >= patternEnd ) { return patternEnd; }