-
Notifications
You must be signed in to change notification settings - Fork 142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(adblocker): inject scriplets with browser.contentScripts
on Firefox
#2074
base: main
Are you sure you want to change the base?
Changes from 3 commits
ac86ec3
a3ec1b4
bcdaf85
9ad938d
9e251aa
b0198ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -86,6 +86,9 @@ export async function reloadMainEngine() { | |
await engines.create(engines.MAIN_ENGINE); | ||
console.info('[adblocker] Main engine reloaded with no filters'); | ||
} | ||
if (__PLATFORM__ === 'firefox') { | ||
contentScripts.unregisterAll(); | ||
} | ||
} | ||
|
||
let updating = false; | ||
|
@@ -123,6 +126,8 @@ async function updateEngines() { | |
} | ||
} | ||
|
||
let EXPERIMENTAL_SCRIPTLETS_INJECTION = false; | ||
|
||
const HOUR_IN_MS = 60 * 60 * 1000; | ||
export const setup = asyncSetup('adblocker', [ | ||
OptionsObserver.addListener( | ||
|
@@ -153,6 +158,12 @@ export const setup = asyncSetup('adblocker', [ | |
OptionsObserver.addListener( | ||
'experimentalFilters', | ||
async (value, lastValue) => { | ||
if (__PLATFORM__ === 'firefox') { | ||
EXPERIMENTAL_SCRIPTLETS_INJECTION = value; | ||
if (!value) { | ||
contentScripts.unregisterAll(); | ||
} | ||
} | ||
engines.setEnv('env_experimental', value); | ||
|
||
// Experimental filters changed to enabled | ||
|
@@ -163,7 +174,51 @@ export const setup = asyncSetup('adblocker', [ | |
), | ||
]); | ||
|
||
function injectScriptlets(filters, tabId, frameId) { | ||
const contentScripts = (() => { | ||
smalluban marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const map = new Map(); | ||
return { | ||
async register(hostname, code) { | ||
this.unregister(hostname); | ||
try { | ||
const contentScript = await browser.contentScripts.register({ | ||
js: [ | ||
{ | ||
code, | ||
}, | ||
], | ||
allFrames: true, | ||
matches: [`https://*.${hostname}/*`, `http://*.${hostname}/*`], | ||
matchAboutBlank: true, | ||
matchOriginAsFallback: true, | ||
runAt: 'document_start', | ||
world: 'MAIN', | ||
}); | ||
map.set(hostname, contentScript); | ||
} catch (e) { | ||
console.warn(e); | ||
this.unregister(hostname); | ||
} | ||
}, | ||
isRegistered(hostname) { | ||
return map.has(hostname); | ||
}, | ||
unregister(hostname) { | ||
const contentScript = map.get(hostname); | ||
if (contentScript) { | ||
contentScript.unregister(); | ||
map.delete(hostname); | ||
} | ||
}, | ||
unregisterAll() { | ||
for (const hostname of map.keys()) { | ||
this.unregister(hostname); | ||
} | ||
}, | ||
}; | ||
})(); | ||
|
||
function injectScriptlets(filters, tabId, frameId, hostname) { | ||
let contentScript = ''; | ||
for (const filter of filters) { | ||
const parsed = filter.parseScript(); | ||
|
||
|
@@ -177,12 +232,19 @@ function injectScriptlets(filters, tabId, frameId) { | |
|
||
const scriptletName = `${parsed.name}${parsed.name.endsWith('.js') ? '' : '.js'}`; | ||
const scriptlet = scriptlets[scriptletName]; | ||
const func = scriptlet.func; | ||
const args = parsed.args.map((arg) => decodeURIComponent(arg)); | ||
|
||
if (!scriptlet) { | ||
console.warn('[adblocker] unknown scriptlet with name:', scriptletName); | ||
continue; | ||
} | ||
|
||
if (__PLATFORM__ === 'firefox' && EXPERIMENTAL_SCRIPTLETS_INJECTION) { | ||
contentScript += `(${func.toString()})(...${JSON.stringify(args)});\n`; | ||
continue; | ||
} | ||
|
||
chrome.scripting.executeScript( | ||
{ | ||
injectImmediately: true, | ||
|
@@ -193,8 +255,8 @@ function injectScriptlets(filters, tabId, frameId) { | |
tabId, | ||
frameIds: [frameId], | ||
}, | ||
func: scriptlet.func, | ||
args: parsed.args.map((arg) => decodeURIComponent(arg)), | ||
func, | ||
args, | ||
}, | ||
() => { | ||
if (chrome.runtime.lastError) { | ||
|
@@ -203,6 +265,16 @@ function injectScriptlets(filters, tabId, frameId) { | |
}, | ||
); | ||
} | ||
|
||
if (__PLATFORM__ === 'firefox' && EXPERIMENTAL_SCRIPTLETS_INJECTION) { | ||
if (filters.length === 0) { | ||
contentScripts.unregister(hostname); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering if this part of the code is necessary or how it should be best handled. Is the code here intended especially for pause (which won't trigger an engine reload)? If so, would be good to leave a comment explaining this. |
||
} else if (!contentScripts.isRegistered(hostname)) { | ||
contentScripts.register(hostname, contentScript); | ||
} else { | ||
// do nothing if already registered | ||
} | ||
} | ||
} | ||
|
||
function injectStyles(styles, tabId, frameId) { | ||
|
@@ -224,6 +296,9 @@ function injectStyles(styles, tabId, frameId) { | |
} | ||
|
||
async function injectCosmetics(details, config) { | ||
const isBootstrap = config.bootstrap; | ||
const scriptletsOnly = Boolean(config.scriptletsOnly); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are in control over the config, and |
||
|
||
try { | ||
setup.pending && (await setup.pending); | ||
} catch (e) { | ||
|
@@ -237,6 +312,8 @@ async function injectCosmetics(details, config) { | |
const domain = parsed.domain || ''; | ||
const hostname = parsed.hostname || ''; | ||
|
||
if (scriptletsOnly && contentScripts.isRegistered(hostname)) return; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This There is an unrelated bug in Firefox requiring the separation of injecting styles and scriptlets (styles must be injected not in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not quite right, this block is just an optimization that prevent a matching if content scripts are already registered. But yes, the condition should be improved |
||
|
||
if (isPaused(options, hostname)) return; | ||
|
||
const tabHostname = tabStats.get(tabId)?.hostname; | ||
|
@@ -245,7 +322,6 @@ async function injectCosmetics(details, config) { | |
} | ||
|
||
const engine = engines.get(engines.MAIN_ENGINE); | ||
const isBootstrap = config.bootstrap; | ||
|
||
{ | ||
const { matches } = engine.matchCosmeticFilters({ | ||
|
@@ -282,8 +358,12 @@ async function injectCosmetics(details, config) { | |
} | ||
} | ||
|
||
if (isBootstrap && scriptFilters.length > 0) { | ||
injectScriptlets(scriptFilters, tabId, frameId); | ||
if (isBootstrap) { | ||
injectScriptlets(scriptFilters, tabId, frameId, hostname); | ||
} | ||
|
||
if (scriptletsOnly) { | ||
return; | ||
} | ||
|
||
const { styles } = engine.injectCosmeticFilters(styleFilters, { | ||
|
@@ -377,6 +457,15 @@ function isTrusted(request, type) { | |
} | ||
|
||
if (__PLATFORM__ === 'firefox') { | ||
chrome.webNavigation.onBeforeNavigate.addListener( | ||
(details) => { | ||
if (EXPERIMENTAL_SCRIPTLETS_INJECTION) { | ||
injectCosmetics(details, { bootstrap: true, scriptletsOnly: true }); | ||
} | ||
}, | ||
{ url: [{ urlPrefix: 'http://' }, { urlPrefix: 'https://' }] }, | ||
); | ||
|
||
function isExtensionRequest(details) { | ||
return ( | ||
(details.tabId === -1 && details.url.startsWith('moz-extension://')) || | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please create another observer to separate logic. You are using the same option, but for another purpose.