mirror of
https://github.com/gorhill/uBlock.git
synced 2024-11-23 19:03:02 +01:00
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:
parent
56b8201196
commit
18a84d2819
@ -2,7 +2,7 @@
|
||||
"browser": true,
|
||||
"devel": true,
|
||||
"eqeqeq": true,
|
||||
"esversion": 9,
|
||||
"esversion": 11,
|
||||
"globals": {
|
||||
"chrome": false, // global variable in Chromium, Chrome, Opera
|
||||
"globalThis": false,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -325,13 +325,20 @@ RedirectEngine.prototype.loadBuiltinResources = function(fetcher) {
|
||||
this.aliases = new Map();
|
||||
|
||||
const fetches = [
|
||||
fetcher(
|
||||
'/assets/resources/scriptlets.js'
|
||||
).then(result => {
|
||||
const content = result.content;
|
||||
if ( typeof content !== 'string' ) { return; }
|
||||
if ( content.length === 0 ) { return; }
|
||||
this.resourcesFromString(content);
|
||||
import('/assets/resources/scriptlets.js').then(module => {
|
||||
for ( const scriptlet of module.builtinScriptlets ) {
|
||||
const { name, aliases, fn } = scriptlet;
|
||||
const entry = RedirectEntry.fromContent(
|
||||
mimeFromName(name),
|
||||
fn.toString()
|
||||
);
|
||||
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';
|
||||
|
||||
RedirectEngine.prototype.selfieFromResources = function(storage) {
|
||||
|
@ -149,7 +149,7 @@ const lookupScriptlet = function(rawToken, reng, toInject) {
|
||||
let content = scriptletCache.lookup(rawToken);
|
||||
if ( content === undefined ) {
|
||||
const pos = rawToken.indexOf(',');
|
||||
let token, args;
|
||||
let token, args = '';
|
||||
if ( pos === -1 ) {
|
||||
token = rawToken;
|
||||
} else {
|
||||
@ -165,10 +165,7 @@ const lookupScriptlet = function(rawToken, reng, toInject) {
|
||||
}
|
||||
content = reng.resourceContentFromName(token, 'text/javascript');
|
||||
if ( !content ) { return; }
|
||||
if ( args ) {
|
||||
content = patchScriptlet(content, args);
|
||||
if ( !content ) { return; }
|
||||
}
|
||||
content =
|
||||
'try {\n' +
|
||||
content + '\n' +
|
||||
@ -180,6 +177,14 @@ const lookupScriptlet = function(rawToken, reng, toInject) {
|
||||
|
||||
// Fill-in scriptlet argument placeholders.
|
||||
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 len = s.length;
|
||||
let beg = 0, pos = 0;
|
||||
@ -193,13 +198,15 @@ const patchScriptlet = function(content, args) {
|
||||
continue;
|
||||
}
|
||||
if ( pos === -1 ) { pos = len; }
|
||||
content = content.replace(
|
||||
`{{${i}}}`,
|
||||
s.slice(beg, pos).trim().replace(reEscapeScriptArg, '\\$&')
|
||||
);
|
||||
arglist.push(s.slice(beg, pos).trim().replace(reEscapeScriptArg, '\\$&'));
|
||||
beg = pos = pos + 1;
|
||||
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;
|
||||
};
|
||||
|
||||
|
@ -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_SCRIPTLET = 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_NET_RAW = 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_SCRIPTLET, 'extPatternScriptlet' ],
|
||||
[ NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN, 'extPatternScriptletToken' ],
|
||||
[ NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARGS, 'extPatternScriptletArgs' ],
|
||||
[ NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG, 'extPatternScriptletArg' ],
|
||||
[ NODE_TYPE_NET_RAW, 'netRaw' ],
|
||||
[ 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.reHostnameLabel = /[^.]+/g;
|
||||
this.reResponseheaderPattern = /^\^responseheader\(.*\)$/;
|
||||
this.rePatternScriptletJsonArgs = /^\{.*\}$/;
|
||||
// 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.reBadCSP = /(?:=|;)\s*report-(?:to|uri)\b/;
|
||||
@ -2118,23 +2121,19 @@ export class AstFilterParser {
|
||||
let prev = head, next = 0;
|
||||
const s = this.getNodeString(parent);
|
||||
const argsEnd = s.length;
|
||||
let argCount = 0;
|
||||
let argBeg = 0, argEnd = 0, argBodyBeg = 0, argBodyEnd = 0;
|
||||
let rawArg = '';
|
||||
while ( argBeg < argsEnd ) {
|
||||
argEnd = this.indexOfNextScriptletArgSeparator(s, argBeg);
|
||||
rawArg = s.slice(argBeg, argEnd);
|
||||
argBodyBeg = argBeg + this.leftWhitespaceCount(rawArg);
|
||||
if ( argBodyBeg !== argBodyEnd ) {
|
||||
// token
|
||||
let argEnd = this.indexOfNextScriptletArgSeparator(s, 0);
|
||||
let rawArg = s.slice(0, argEnd);
|
||||
let argBodyBeg = this.leftWhitespaceCount(rawArg);
|
||||
if ( argBodyBeg !== 0 ) {
|
||||
next = this.allocTypedNode(
|
||||
NODE_TYPE_EXT_DECORATION,
|
||||
parentBeg + argBodyEnd,
|
||||
parentBeg,
|
||||
parentBeg + argBodyBeg
|
||||
);
|
||||
prev = this.linkRight(prev, next);
|
||||
}
|
||||
argBodyEnd = argEnd - this.rightWhitespaceCount(rawArg);
|
||||
if ( argCount === 0 ) {
|
||||
let argBodyEnd = argEnd - this.rightWhitespaceCount(rawArg);
|
||||
rawArg = s.slice(argBodyBeg, argBodyEnd);
|
||||
const tokenEnd = rawArg.endsWith('.js')
|
||||
? argBodyEnd - 3
|
||||
@ -2145,6 +2144,7 @@ export class AstFilterParser {
|
||||
parentBeg + tokenEnd
|
||||
);
|
||||
prev = this.linkRight(prev, next);
|
||||
// ignore pointless `.js`
|
||||
if ( tokenEnd !== argBodyEnd ) {
|
||||
next = this.allocTypedNode(
|
||||
NODE_TYPE_IGNORE,
|
||||
@ -2153,18 +2153,27 @@ export class AstFilterParser {
|
||||
);
|
||||
prev = this.linkRight(prev, next);
|
||||
}
|
||||
} else {
|
||||
// all args
|
||||
argBodyBeg = argEnd + 1;
|
||||
const rawArgs = s.slice(argBodyBeg, argsEnd);
|
||||
argBodyBeg += this.leftWhitespaceCount(rawArgs);
|
||||
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 + argBodyEnd
|
||||
);
|
||||
this.linkDown(next, this.parseExtPatternScriptletArglist(next));
|
||||
prev = this.linkRight(prev, next);
|
||||
}
|
||||
argBeg = argEnd + 1;
|
||||
argCount += 1;
|
||||
}
|
||||
if ( argsEnd !== argBodyEnd ) {
|
||||
if ( argBodyEnd !== argsEnd ) {
|
||||
next = this.allocTypedNode(
|
||||
NODE_TYPE_EXT_DECORATION,
|
||||
parentBeg + argBodyEnd,
|
||||
@ -2175,6 +2184,57 @@ export class AstFilterParser {
|
||||
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) {
|
||||
const patternEnd = pattern.length;
|
||||
if ( beg >= patternEnd ) { return patternEnd; }
|
||||
|
Loading…
Reference in New Issue
Block a user