mirror of
https://github.com/gorhill/uBlock.git
synced 2024-11-16 15:33:38 +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,
|
"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
@ -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) {
|
||||||
|
@ -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,26 +177,36 @@ 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) {
|
||||||
let s = args;
|
if ( content.startsWith('function') ) {
|
||||||
let len = s.length;
|
content = `(${content})({{args}});`;
|
||||||
let beg = 0, pos = 0;
|
|
||||||
let i = 1;
|
|
||||||
while ( beg < len ) {
|
|
||||||
pos = s.indexOf(',', pos);
|
|
||||||
// Escaped comma? If so, skip.
|
|
||||||
if ( pos > 0 && s.charCodeAt(pos - 1) === 0x5C /* '\\' */ ) {
|
|
||||||
s = s.slice(0, pos - 1) + s.slice(pos);
|
|
||||||
len -= 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ( pos === -1 ) { pos = len; }
|
|
||||||
content = content.replace(
|
|
||||||
`{{${i}}}`,
|
|
||||||
s.slice(beg, pos).trim().replace(reEscapeScriptArg, '\\$&')
|
|
||||||
);
|
|
||||||
beg = pos = pos + 1;
|
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
|
let i = 1;
|
||||||
|
while ( beg < len ) {
|
||||||
|
pos = s.indexOf(',', pos);
|
||||||
|
// Escaped comma? If so, skip.
|
||||||
|
if ( pos > 0 && s.charCodeAt(pos - 1) === 0x5C /* '\\' */ ) {
|
||||||
|
s = s.slice(0, pos - 1) + s.slice(pos);
|
||||||
|
len -= 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ( pos === -1 ) { pos = len; }
|
||||||
|
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;
|
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_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,53 +2121,59 @@ 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);
|
next = this.allocTypedNode(
|
||||||
argBodyBeg = argBeg + this.leftWhitespaceCount(rawArg);
|
NODE_TYPE_EXT_DECORATION,
|
||||||
if ( argBodyBeg !== argBodyEnd ) {
|
parentBeg,
|
||||||
next = this.allocTypedNode(
|
parentBeg + argBodyBeg
|
||||||
NODE_TYPE_EXT_DECORATION,
|
);
|
||||||
parentBeg + argBodyEnd,
|
prev = this.linkRight(prev, next);
|
||||||
parentBeg + argBodyBeg
|
|
||||||
);
|
|
||||||
prev = this.linkRight(prev, next);
|
|
||||||
}
|
|
||||||
argBodyEnd = argEnd - this.rightWhitespaceCount(rawArg);
|
|
||||||
if ( argCount === 0 ) {
|
|
||||||
rawArg = s.slice(argBodyBeg, argBodyEnd);
|
|
||||||
const tokenEnd = rawArg.endsWith('.js')
|
|
||||||
? argBodyEnd - 3
|
|
||||||
: argBodyEnd;
|
|
||||||
next = this.allocTypedNode(
|
|
||||||
NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN,
|
|
||||||
parentBeg + argBodyBeg,
|
|
||||||
parentBeg + tokenEnd
|
|
||||||
);
|
|
||||||
prev = this.linkRight(prev, next);
|
|
||||||
if ( tokenEnd !== argBodyEnd ) {
|
|
||||||
next = this.allocTypedNode(
|
|
||||||
NODE_TYPE_IGNORE,
|
|
||||||
parentBeg + argBodyEnd - 3,
|
|
||||||
parentBeg + argBodyEnd
|
|
||||||
);
|
|
||||||
prev = this.linkRight(prev, next);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
next = this.allocTypedNode(
|
|
||||||
NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG,
|
|
||||||
parentBeg + argBodyBeg,
|
|
||||||
parentBeg + argBodyEnd
|
|
||||||
);
|
|
||||||
prev = this.linkRight(prev, next);
|
|
||||||
}
|
|
||||||
argBeg = argEnd + 1;
|
|
||||||
argCount += 1;
|
|
||||||
}
|
}
|
||||||
if ( argsEnd !== argBodyEnd ) {
|
let argBodyEnd = argEnd - this.rightWhitespaceCount(rawArg);
|
||||||
|
rawArg = s.slice(argBodyBeg, argBodyEnd);
|
||||||
|
const tokenEnd = rawArg.endsWith('.js')
|
||||||
|
? argBodyEnd - 3
|
||||||
|
: argBodyEnd;
|
||||||
|
next = this.allocTypedNode(
|
||||||
|
NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN,
|
||||||
|
parentBeg + argBodyBeg,
|
||||||
|
parentBeg + tokenEnd
|
||||||
|
);
|
||||||
|
prev = this.linkRight(prev, next);
|
||||||
|
// ignore pointless `.js`
|
||||||
|
if ( tokenEnd !== argBodyEnd ) {
|
||||||
|
next = this.allocTypedNode(
|
||||||
|
NODE_TYPE_IGNORE,
|
||||||
|
parentBeg + argBodyEnd - 3,
|
||||||
|
parentBeg + argBodyEnd
|
||||||
|
);
|
||||||
|
prev = this.linkRight(prev, next);
|
||||||
|
}
|
||||||
|
// all args
|
||||||
|
argBodyBeg = argEnd + 1;
|
||||||
|
const rawArgs = s.slice(argBodyBeg, argsEnd);
|
||||||
|
argBodyBeg += this.leftWhitespaceCount(rawArgs);
|
||||||
|
next = this.allocTypedNode(
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
if ( argBodyEnd !== argsEnd ) {
|
||||||
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; }
|
||||||
|
Loading…
Reference in New Issue
Block a user