Add `trusted-click-element` scriptlet

Implemented as per AdGuard API documentation:
https://github.com/AdguardTeam/Scriptlets/blob/master/wiki/about-trusted-scriptlets.md#trusted-click-element

The current implementation in uBO does not support the `extraMatch`
argument. If the extraMatch argument is not empty, the scriptlet
will abort and do nothing.

As for the rest, it complies with the documentation.
This commit is contained in:
Raymond Hill 2023-10-15 11:08:15 -04:00
parent 9a809a8e7c
commit 7af88b025d
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
1 changed files with 112 additions and 3 deletions

View File

@ -224,9 +224,9 @@ function runAt(fn, when) {
builtinScriptlets.push({
name: 'run-at-html-element.fn',
fn: runAtHtmlElement,
fn: runAtHtmlElementFn,
});
function runAtHtmlElement(fn) {
function runAtHtmlElementFn(fn) {
if ( document.documentElement ) {
fn();
return;
@ -1160,7 +1160,7 @@ builtinScriptlets.push({
// Issues to mind before changing anything:
// https://github.com/uBlockOrigin/uBlock-issues/issues/2154
function abortCurrentScript(...args) {
runAtHtmlElement(( ) => {
runAtHtmlElementFn(( ) => {
abortCurrentScriptCore(...args);
});
}
@ -3882,4 +3882,113 @@ function trustedReplaceXhrResponse(
};
}
/*******************************************************************************
*
* trusted-click-element.js
*
* Reference API:
* https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/trusted-click-element.js
*
**/
builtinScriptlets.push({
name: 'trusted-click-element.js',
requiresTrust: true,
fn: trustedClickElement,
world: 'ISOLATED',
dependencies: [
'run-at-html-element.fn',
'safe-self.fn',
],
});
function trustedClickElement(
selectors = '',
extraMatch = '', // not yet supported
delay = ''
) {
if ( extraMatch !== '' ) { return; }
const selectorList = selectors.split(/\s*,\s*/)
.filter(s => {
try {
void document.querySelector(s);
} catch(_) {
return false;
}
return true;
});
if ( selectorList.length === 0 ) { return; }
const clickDelay = parseInt(delay, 10) || 1;
const t0 = Date.now();
const tbye = t0 + 10000;
let tnext = t0;
const terminate = ( ) => {
selectorList.length = 0;
next.stop();
observe.stop();
};
const next = notFound => {
if ( selectorList.length === 0 ) { return terminate(); }
const tnow = Date.now();
if ( tnow >= tbye ) { return terminate(); }
if ( notFound ) { observe(); }
const delay = Math.max(notFound ? tbye - tnow : tnext - tnow, 1);
next.timer = setTimeout(( ) => {
next.timer = undefined;
process();
}, delay);
};
next.stop = ( ) => {
if ( next.timer === undefined ) { return; }
clearTimeout(next.timer);
next.timer = undefined;
};
const observe = ( ) => {
if ( observe.observer !== undefined ) { return; }
observe.observer = new MutationObserver(( ) => {
if ( observe.timer !== undefined ) { return; }
observe.timer = setTimeout(( ) => {
observe.timer = undefined;
process();
}, 20);
});
observe.observer.observe(document, {
attributes: true,
childList: true,
subtree: true,
});
};
observe.stop = ( ) => {
if ( observe.timer !== undefined ) {
clearTimeout(observe.timer);
observe.timer = undefined;
}
if ( observe.observer ) {
observe.observer.disconnect();
observe.observer = undefined;
}
};
const process = ( ) => {
next.stop();
if ( Date.now() < tnext ) { return next(); }
const selector = selectorList.shift();
if ( selector === undefined ) { return terminate(); }
const elem = document.querySelector(selector);
if ( elem === null ) {
selectorList.unshift(selector);
return next(true);
}
elem.click();
tnext += clickDelay;
next();
};
runAtHtmlElementFn(process);
}
/******************************************************************************/