diff --git a/assets/resources/scriptlets.js b/assets/resources/scriptlets.js index 67eec324d..4ea602c88 100644 --- a/assets/resources/scriptlets.js +++ b/assets/resources/scriptlets.js @@ -68,14 +68,13 @@ builtinScriptlets.push({ name: 'pattern-to-regex.fn', fn: patternToRegex, }); -function patternToRegex(pattern) { - if ( pattern === '' ) { - return /^/; +function patternToRegex(pattern, flags = undefined) { + if ( pattern === '' ) { return /^/; } + const match = /^\/(.+)\/([gimsu]*)$/.exec(pattern); + if ( match !== null ) { + return new RegExp(match[1], match[2] || flags); } - if ( pattern.startsWith('/') && pattern.endsWith('/') ) { - return new RegExp(pattern.slice(1, -1)); - } - return new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); + return new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags); } /******************************************************************************/ @@ -394,7 +393,7 @@ function abortOnStackTrace( for ( let line of err.stack.split(/[\n\r]+/) ) { if ( line.includes(exceptionToken) ) { continue; } line = line.trim(); - let match = safe.RegExp_exec.call(reLine, line); + const match = safe.RegExp_exec.call(reLine, line); if ( match === null ) { continue; } let url = match[2]; if ( url.startsWith('(') ) { url = url.slice(1); } @@ -2122,6 +2121,9 @@ function callNothrow( builtinScriptlets.push({ name: 'spoof-css.js', fn: spoofCSS, + dependencies: [ + 'safe-self.fn', + ], }); function spoofCSS( selector, @@ -2137,19 +2139,30 @@ function spoofCSS( if ( typeof args[i+1] !== 'string' ) { break; } propToValueMap.set(toCamelCase(args[i+0]), args[i+1]); } + const safe = safeSelf(); + const canDebug = scriptletGlobals.has('canDebug'); + const shouldDebug = canDebug && propToValueMap.get('debug') || 0; + const shouldLog = canDebug && propToValueMap.has('log') || 0; + const proxiedStyles = new WeakSet(); + const spoofStyle = (prop, real) => { + const normalProp = toCamelCase(prop); + const shouldSpoof = propToValueMap.has(normalProp); + const value = shouldSpoof ? propToValueMap.get(normalProp) : real; + if ( shouldLog === 2 || shouldSpoof && shouldLog === 1 ) { + safe.uboLog(prop, value); + } + return value; + }; self.getComputedStyle = new Proxy(self.getComputedStyle, { apply: function(target, thisArg, args) { - if ( propToValueMap.has('debug') ) { debugger; } // jshint ignore: line + if ( shouldDebug !== 0 ) { debugger; } // jshint ignore: line const style = Reflect.apply(target, thisArg, args); const targetElements = new WeakSet(document.querySelectorAll(selector)); if ( targetElements.has(args[0]) === false ) { return style; } + proxiedStyles.add(target); const proxiedStyle = new Proxy(style, { get(target, prop, receiver) { - const normalProp = toCamelCase(prop); - const value = propToValueMap.has(normalProp) - ? propToValueMap.get(normalProp) - : Reflect.get(target, prop, receiver); - return value; + return spoofStyle(prop, Reflect.get(target, prop, receiver)); }, }); return proxiedStyle; @@ -2161,9 +2174,23 @@ function spoofCSS( return Reflect.get(target, prop, receiver); }, }); + CSSStyleDeclaration.prototype.getPropertyValue = new Proxy(CSSStyleDeclaration.prototype.getPropertyValue, { + apply: function(target, thisArg, args) { + if ( shouldDebug !== 0 ) { debugger; } // jshint ignore: line + const value = Reflect.apply(target, thisArg, args); + if ( proxiedStyles.has(thisArg) === false ) { return value; } + return spoofStyle(args[0], value); + }, + get(target, prop, receiver) { + if ( prop === 'toString' ) { + return target.toString.bind(target); + } + return Reflect.get(target, prop, receiver); + }, + }); Element.prototype.getBoundingClientRect = new Proxy(Element.prototype.getBoundingClientRect, { apply: function(target, thisArg, args) { - if ( propToValueMap.has('debug') ) { debugger; } // jshint ignore: line + if ( shouldDebug !== 0 ) { debugger; } // jshint ignore: line const rect = Reflect.apply(target, thisArg, args); const targetElements = new WeakSet(document.querySelectorAll(selector)); if ( targetElements.has(thisArg) === false ) { return rect; } @@ -2186,3 +2213,62 @@ function spoofCSS( } /******************************************************************************/ + +builtinScriptlets.push({ + name: 'sed.js', + requiresTrust: true, + fn: sed, + dependencies: [ + 'pattern-to-regex.fn', + 'safe-self.fn', + ], +}); +function sed( + nodeName = '', + pattern = '', + replacement = '' +) { + const reNodeName = patternToRegex(nodeName, 'i'); + const rePattern = patternToRegex(pattern, 'gms'); + const extraArgs = new Map( + Array.from(arguments).slice(3).reduce((out, v, i, a) => { + if ( (i & 1) === 0 ) { out.push([ a[i], a[i+1] || undefined ]); } + return out; + }, []) + ); + const shouldLog = scriptletGlobals.has('canDebug') && extraArgs.get('log') || 0; + const reCondition = patternToRegex(extraArgs.get('condition') || '', 'gms'); + let sedCount = extraArgs.has('sedCount') ? parseInt(extraArgs.get('sedCount')) : 0; + let tryCount = extraArgs.has('tryCount') ? parseInt(extraArgs.get('tryCount')) : 0; + const safe = safeSelf(); + const handler = mutations => { + for ( const mutation of mutations ) { + for ( const node of mutation.addedNodes ) { + if ( reNodeName.test(node.nodeName) === false ) { continue; } + const before = node.textContent; + if ( safe.RegExp_test.call(rePattern, before) === false ) { continue; } + if ( safe.RegExp_test.call(reCondition, before) === false ) { continue; } + if ( shouldLog !== 0 ) { safe.uboLog('sed.js before:\n', before); } + const after = before.replace(rePattern, replacement); + if ( shouldLog !== 0 ) { safe.uboLog('sed.js after:\n', after); } + node.textContent = after; + if ( sedCount !== 0 && (sedCount -= 1) === 0 ) { + observer.disconnect(); + if ( shouldLog !== 0 ) { safe.uboLog('sed.js: quitting'); } + return; + } + } + } + if ( tryCount !== 0 && (tryCount -= 1) === 0 ) { + observer.disconnect(); + if ( shouldLog !== 0 ) { safe.uboLog('sed.js: quitting'); } + } + }; + const observer = new MutationObserver(handler); + observer.observe(document.documentElement, { + childList: true, + subtree: true, + }); +} + +/******************************************************************************/ diff --git a/src/js/redirect-engine.js b/src/js/redirect-engine.js index dee269734..a2237817d 100644 --- a/src/js/redirect-engine.js +++ b/src/js/redirect-engine.js @@ -86,6 +86,7 @@ class RedirectEntry { this.data = ''; this.warURL = undefined; this.params = undefined; + this.requiresTrust = false; this.dependencies = []; } @@ -149,21 +150,16 @@ class RedirectEntry { return this.data; } - static fromContent(mime, content, dependencies = []) { + static fromDetails(details) { const r = new RedirectEntry(); - r.mime = mime; - r.data = content; - r.dependencies.push(...dependencies); - return r; - } - - static fromSelfie(selfie) { - const r = new RedirectEntry(); - r.mime = selfie.mime; - r.data = selfie.data; - r.warURL = selfie.warURL; - r.params = selfie.params; - r.dependencies = selfie.dependencies || []; + r.mime = details.mime; + r.data = details.data; + r.requiresTrust = details.requiresTrust === true; + r.warURL = details.warURL !== undefined && details.warURL || undefined; + r.params = details.params !== undefined && details.params || undefined; + if ( Array.isArray(details.dependencies) ) { + r.dependencies.push(...details.dependencies); + } return r; } } @@ -213,6 +209,11 @@ class RedirectEngine { return this.resources.get(this.aliases.get(token) || token) !== undefined; } + tokenRequiresTrust(token) { + const entry = this.resources.get(this.aliases.get(token) || token); + return entry && entry.requiresTrust === true || false; + } + async toSelfie() { } @@ -288,10 +289,10 @@ class RedirectEngine { // No more data, add the resource. const name = this.aliases.get(fields[0]) || fields[0]; const mime = fields[1]; - const content = orphanizeString( + const data = orphanizeString( fields.slice(2).join(encoded ? '' : '\n') ); - this.resources.set(name, RedirectEntry.fromContent(mime, content)); + this.resources.set(name, RedirectEntry.fromDetails({ mime, data })); if ( Array.isArray(details) ) { for ( const { prop, value } of details ) { if ( prop !== 'alias' ) { continue; } @@ -314,11 +315,12 @@ class RedirectEngine { 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(), - scriptlet.dependencies, - ); + const entry = RedirectEntry.fromDetails({ + mime: mimeFromName(name), + data: fn.toString(), + dependencies: scriptlet.dependencies, + requiresTrust: scriptlet.requiresTrust === true, + }); this.resources.set(name, entry); if ( Array.isArray(aliases) === false ) { continue; } for ( const alias of aliases ) { @@ -331,7 +333,7 @@ class RedirectEngine { const store = (name, data = undefined) => { const details = redirectableResources.get(name); - const entry = RedirectEntry.fromSelfie({ + const entry = RedirectEntry.fromDetails({ mime: mimeFromName(name), data, warURL: `/web_accessible_resources/${name}`, @@ -444,7 +446,7 @@ class RedirectEngine { this.aliases = new Map(selfie.aliases); this.resources = new Map(); for ( const [ token, entry ] of selfie.resources ) { - this.resources.set(token, RedirectEntry.fromSelfie(entry)); + this.resources.set(token, RedirectEntry.fromDetails(entry)); } return true; } diff --git a/src/js/scriptlet-filtering.js b/src/js/scriptlet-filtering.js index c9f023b23..3f9e76c18 100644 --- a/src/js/scriptlet-filtering.js +++ b/src/js/scriptlet-filtering.js @@ -24,7 +24,7 @@ /******************************************************************************/ import µb from './background.js'; -import { redirectEngine } from './redirect-engine.js'; +import { redirectEngine as reng } from './redirect-engine.js'; import { sessionFirewall } from './filtering-engines.js'; import { StaticExtFilteringHostnameDB } from './static-ext-filtering-db.js'; import * as sfp from './static-filtering-parser.js'; @@ -119,7 +119,7 @@ const contentscriptCode = (( ) => { // TODO: Probably should move this into StaticFilteringParser // https://github.com/uBlockOrigin/uBlock-issues/issues/1031 // Normalize scriptlet name to its canonical, unaliased name. -const normalizeRawFilter = function(parser) { +const normalizeRawFilter = function(parser, sourceIsTrusted = false) { const root = parser.getBranchFromType(sfp.NODE_TYPE_EXT_PATTERN_SCRIPTLET); const walker = parser.getWalker(root); const args = []; @@ -135,10 +135,14 @@ const normalizeRawFilter = function(parser) { } walker.dispose(); if ( args.length !== 0 ) { - const full = `${args[0]}.js`; - if ( redirectEngine.aliases.has(full) ) { - args[0] = redirectEngine.aliases.get(full).slice(0, -3); + let token = `${args[0]}.js`; + if ( reng.aliases.has(token) ) { + token = reng.aliases.get(token); } + if ( sourceIsTrusted !== true && reng.tokenRequiresTrust(token) ) { + return; + } + args[0] = token.slice(0, -3); } return `+js(${args.join(', ')})`; }; @@ -155,19 +159,19 @@ const lookupScriptlet = function(rawToken, scriptletMap, dependencyMap) { } // TODO: The alias lookup can be removed once scriptlet resources // with obsolete name are converted to their new name. - if ( redirectEngine.aliases.has(token) ) { - token = redirectEngine.aliases.get(token); + if ( reng.aliases.has(token) ) { + token = reng.aliases.get(token); } else { token = `${token}.js`; } - const details = redirectEngine.contentFromName(token, 'text/javascript'); + const details = reng.contentFromName(token, 'text/javascript'); if ( details === undefined ) { return; } const content = patchScriptlet(details.js, args); const dependencies = details.dependencies || []; while ( dependencies.length !== 0 ) { const token = dependencies.shift(); if ( dependencyMap.has(token) ) { continue; } - const details = redirectEngine.contentFromName(token, 'fn/javascript'); + const details = reng.contentFromName(token, 'fn/javascript'); if ( details === undefined ) { continue; } dependencyMap.set(token, details.js); if ( Array.isArray(details.dependencies) === false ) { continue; } @@ -254,7 +258,10 @@ scriptletFilteringEngine.compile = function(parser, writer) { // Only exception filters are allowed to be global. const isException = parser.isException(); - const normalized = normalizeRawFilter(parser); + const normalized = normalizeRawFilter(parser, writer.properties.get('isTrusted')); + + // Can fail if there is a mismatch with trust requirement + if ( normalized === undefined ) { return; } // Tokenless is meaningful only for exception filters. if ( normalized === '+js()' && isException === false ) { return; } @@ -343,7 +350,7 @@ scriptletFilteringEngine.retrieve = function(request) { }; } - if ( scriptletCache.resetTime < redirectEngine.modifyTime ) { + if ( scriptletCache.resetTime < reng.modifyTime ) { scriptletCache.reset(); } diff --git a/src/js/storage.js b/src/js/storage.js index f227bb2d1..07075f76a 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -368,6 +368,14 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { /******************************************************************************/ +µb.isTrustedList = function(assetKey) { + if ( assetKey.startsWith('ublock-') ) { return true; } + if ( assetKey === this.userFiltersPath ) { return true; } + return false; +}; + +/******************************************************************************/ + µb.loadSelectedFilterLists = async function() { const bin = await vAPI.storage.get('selectedFilterLists'); if ( bin instanceof Object && Array.isArray(bin.selectedFilterLists) ) { @@ -560,7 +568,8 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { await this.saveUserFilters(details.content.trim() + '\n' + filters); const compiledFilters = this.compileFilters(filters, { - assetKey: this.userFiltersPath + assetKey: this.userFiltersPath, + isTrusted: true, }); const snfe = staticNetFilteringEngine; const cfe = cosmeticFilteringEngine; @@ -885,10 +894,10 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { if ( µb.inMemoryFilters.length !== 0 ) { if ( µb.inMemoryFiltersCompiled === '' ) { µb.inMemoryFiltersCompiled = - µb.compileFilters( - µb.inMemoryFilters.join('\n'), - { assetKey: 'in-memory'} - ); + µb.compileFilters(µb.inMemoryFilters.join('\n'), { + assetKey: 'in-memory', + isTrusted: true, + }); } if ( µb.inMemoryFiltersCompiled !== '' ) { toLoad.push( @@ -953,8 +962,10 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { return { assetKey, content: '' }; } - const compiledContent = - this.compileFilters(rawDetails.content, { assetKey }); + const compiledContent = this.compileFilters(rawDetails.content, { + assetKey, + isTrusted: this.isTrustedList(assetKey), + }); io.put(compiledPath, compiledContent); return { assetKey, content: compiledContent }; @@ -1020,6 +1031,7 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { // client compilers. if ( details.assetKey ) { writer.properties.set('name', details.assetKey); + writer.properties.set('isTrusted', details.isTrusted === true); } const assetName = details.assetKey ? details.assetKey : '?'; const expertMode = @@ -1539,7 +1551,8 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { io.put( 'compiled/' + details.assetKey, this.compileFilters(details.content, { - assetKey: details.assetKey + assetKey: details.assetKey, + isTrusted: this.isTrustedList(details.assetKey), }) ); }