From 45e62c939ffd90dd07f5ca8941a45eb9454e5e7f Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sat, 20 Jan 2024 10:33:36 -0500 Subject: [PATCH] Add support for `extraMatch` in `trusted-click-element` scriptlet Related issue: https://github.com/uBlockOrigin/uAssets/issues/20744#issuecomment-1900710708 Reference documentation: https://github.com/AdguardTeam/Scriptlets/blob/master/wiki/about-trusted-scriptlets.md#-%EF%B8%8F-trusted-click-element Except that in uBO's implementation, if a regex is given as value to match, it will be tested against an assembled "key=value" string. --- assets/resources/scriptlets.js | 128 ++++++++++++++++++++++++++++----- 1 file changed, 111 insertions(+), 17 deletions(-) diff --git a/assets/resources/scriptlets.js b/assets/resources/scriptlets.js index bc66ca1c4..c87e5d945 100644 --- a/assets/resources/scriptlets.js +++ b/assets/resources/scriptlets.js @@ -79,6 +79,9 @@ function safeSelf() { if ( `${args[0]}` === '' ) { return; } this.log('[uBO]', ...args); }, + escapeRegexChars(s) { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + }, initPattern(pattern, options = {}) { if ( pattern === '' ) { return { matchAll: true }; @@ -99,8 +102,7 @@ function safeSelf() { } if ( options.flags !== undefined ) { return { - re: new this.RegExp(pattern.replace( - /[.*+?^${}()|[\]\\]/g, '\\$&'), + re: new this.RegExp(this.escapeRegexChars(pattern), options.flags ), expect, @@ -119,7 +121,7 @@ function safeSelf() { if ( pattern === '' ) { return /^/; } const match = /^\/(.+)\/([gimsu]*)$/.exec(pattern); if ( match === null ) { - const reStr = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const reStr = this.escapeRegexChars(pattern); return new RegExp(verbatim ? `^${reStr}$` : reStr, flags); } try { @@ -835,9 +837,63 @@ function objectFindOwnerFn( /******************************************************************************/ +builtinScriptlets.push({ + name: 'get-all-cookies.fn', + fn: getAllCookiesFn, +}); +function getAllCookiesFn() { + return document.cookie.split(/\s*;\s*/).map(s => { + const pos = s.indexOf('='); + if ( pos === 0 ) { return; } + if ( pos === -1 ) { return `${s.trim()}=`; } + const key = s.slice(0, pos).trim(); + const value = s.slice(pos+1).trim(); + return { key, value }; + }).filter(s => s !== undefined); +} + +/******************************************************************************/ + +builtinScriptlets.push({ + name: 'get-all-local-storage.fn', + fn: getAllLocalStorageFn, +}); +function getAllLocalStorageFn(which = 'localStorage') { + const storage = self[which]; + const out = []; + for ( let i = 0; i < storage.length; i++ ) { + const key = storage.key(i); + const value = storage.getItem(key); + return { key, value }; + } + return out; +} + +/******************************************************************************/ + +builtinScriptlets.push({ + name: 'get-cookie.fn', + fn: getCookieFn, +}); +function getCookieFn( + name = '' +) { + for ( const s of document.cookie.split(/\s*;\s*/) ) { + const pos = s.indexOf('='); + if ( pos === -1 ) { continue; } + if ( s.slice(0, pos) !== name ) { continue; } + return s.slice(pos+1).trim(); + } +} + +/******************************************************************************/ + builtinScriptlets.push({ name: 'set-cookie.fn', fn: setCookieFn, + dependencies: [ + 'get-cookie.fn', + ], }); function setCookieFn( trusted = false, @@ -847,16 +903,7 @@ function setCookieFn( path = '', options = {}, ) { - const getCookieValue = name => { - for ( const s of document.cookie.split(/\s*;\s*/) ) { - const pos = s.indexOf('='); - if ( pos === -1 ) { continue; } - if ( s.slice(0, pos) !== name ) { continue; } - return s.slice(pos+1); - } - }; - - const cookieBefore = getCookieValue(name); + const cookieBefore = getCookieFn(name); if ( cookieBefore !== undefined && options.dontOverwrite ) { return; } if ( cookieBefore === value && options.reload ) { return; } @@ -884,7 +931,7 @@ function setCookieFn( } catch(_) { } - if ( options.reload && getCookieValue(name) === value ) { + if ( options.reload && getCookieFn(name) === value ) { window.location.reload(); } } @@ -4029,6 +4076,9 @@ function trustedSetSessionStorageItem(key = '', value = '') { builtinScriptlets.push({ name: 'trusted-replace-fetch-response.js', requiresTrust: true, + aliases: [ + 'trusted-rpfr.js', + ], fn: trustedReplaceFetchResponse, dependencies: [ 'replace-fetch-response.fn', @@ -4140,23 +4190,67 @@ builtinScriptlets.push({ fn: trustedClickElement, world: 'ISOLATED', dependencies: [ + 'get-all-cookies.fn', + 'get-all-local-storage.fn', 'run-at-html-element.fn', 'safe-self.fn', ], }); function trustedClickElement( selectors = '', - extraMatch = '', // not yet supported + extraMatch = '', delay = '' ) { - if ( extraMatch !== '' ) { return; } - const safe = safeSelf(); const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); const uboLog = extraArgs.log !== undefined ? ((...args) => { safe.uboLog(...args); }) : (( ) => { }); + if ( extraMatch !== '' ) { + const assertions = extraMatch.split(',').map(s => { + const pos1 = s.indexOf(':'); + const s1 = pos1 !== -1 ? s.slice(0, pos1) : s; + const not = s1.startsWith('!'); + const type = not ? s1.slice(1) : s1; + const s2 = pos1 !== -1 ? s.slice(pos1+1).trim() : ''; + if ( s2 === '' ) { return; } + const out = { not, type }; + const match = /^\/(.+)\/(i?)$/.exec(s2); + if ( match !== null ) { + out.re = new RegExp(match[1], match[2] || undefined); + return out; + } + const pos2 = s2.indexOf('='); + const key = pos2 !== -1 ? s2.slice(0, pos2).trim() : s2; + const value = pos2 !== -1 ? s2.slice(pos2+1).trim() : ''; + out.re = new RegExp(`^${this.escapeRegexChars(key)}=${this.escapeRegexChars(value)}`); + return out; + }).filter(details => details !== undefined); + const allCookies = assertions.some(o => o.type === 'cookie') + ? getAllCookiesFn() + : []; + const allStorageItems = assertions.some(o => o.type === 'localStorage') + ? getAllLocalStorageFn() + : []; + const hasNeedle = (haystack, needle) => { + for ( const { key, value } of haystack ) { + if ( needle.test(`${key}=${value}`) ) { return true; } + } + return false; + }; + for ( const { not, type, re } of assertions ) { + switch ( type ) { + case 'cookie': + if ( hasNeedle(allCookies, re) === not ) { return; } + break; + case 'localStorage': + if ( hasNeedle(allStorageItems, re) === not ) { return; } + break; + } + } + } + const querySelectorEx = (selector, context = document) => { const pos = selector.indexOf(' >>> '); if ( pos === -1 ) { return context.querySelector(selector); }