Skip to content

Commit

Permalink
Reduce race conditions in scriptlet injection on Firefox
Browse files Browse the repository at this point in the history
This is done by taking advantage through Firefox-specific
contentScripts.register() API:

https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/contentScripts
  • Loading branch information
gorhill committed Oct 2, 2023
1 parent f580cb4 commit 4cac9d1
Showing 1 changed file with 66 additions and 16 deletions.
82 changes: 66 additions & 16 deletions src/js/scriptlet-filtering.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
Home: https://github.com/gorhill/uBlock
*/

/* globals browser */

'use strict';

/******************************************************************************/
Expand Down Expand Up @@ -61,6 +63,55 @@ const scriptletFilteringEngine = {
},
};

const contentScriptRegisterer = new (class {
constructor() {
this.hostnameToDetails = new Map();
}
register(hostname, code) {
if ( browser.contentScripts === undefined ) { return false; }
const details = this.hostnameToDetails.get(hostname);
if ( details !== undefined ) {
if ( code === details.code ) {
return details.handle instanceof Promise === false;
}
details.handle.unregister();
this.hostnameToDetails.delete(hostname);
}
const promise = browser.contentScripts.register({
js: [ { code } ],
allFrames: true,
matches: [ `*://*.${hostname}/*` ],
matchAboutBlank: true,
runAt: 'document_start',
}).then(handle => {
this.hostnameToDetails.set(hostname, { handle, code });
});
this.hostnameToDetails.set(hostname, { handle: promise, code });
return false;
}
unregister(hostname) {
if ( this.hostnameToDetails.size === 0 ) { return; }
const details = this.hostnameToDetails.get(hostname);
if ( details === undefined ) { return; }
this.hostnameToDetails.delete(hostname);
this.unregisterHandle(details.handle);
}
reset() {
if ( this.hostnameToDetails.size === 0 ) { return; }
for ( const details of this.hostnameToDetails.values() ) {
this.unregisterHandle(details.handle);
}
this.hostnameToDetails.clear();
}
unregisterHandle(handle) {
if ( handle instanceof Promise ) {
handle.then(handle => { handle.unregister(); });
} else {
handle.unregister();
}
}
})();

// Purpose of `contentscriptCode` below is too programmatically inject
// content script code which only purpose is to inject scriptlets. This
// essentially does the same as what uBO's declarative content script does,
Expand Down Expand Up @@ -141,7 +192,6 @@ const isolatedWorldInjector = (( ) => {
return {
parts,
jsonSlot: parts.indexOf('json-slot'),
scriptletSlot: parts.indexOf('scriptlet-slot'),
assemble: function(hostname, scriptlets) {
this.parts[this.jsonSlot] = JSON.stringify({ hostname });
const code = this.parts.join('');
Expand Down Expand Up @@ -239,6 +289,7 @@ scriptletFilteringEngine.logFilters = function(tabId, url, filters) {
scriptletFilteringEngine.reset = function() {
scriptletDB.clear();
duplicates.clear();
contentScriptRegisterer.reset();
scriptletCache.reset();
acceptedCount = 0;
discardedCount = 0;
Expand Down Expand Up @@ -441,25 +492,24 @@ scriptletFilteringEngine.injectNow = function(details) {
request.domain = domainFromHostname(request.hostname);
request.entity = entityFromDomain(request.domain);
const scriptletDetails = this.retrieve(request);
if ( scriptletDetails === undefined ) { return; }
if ( scriptletDetails === undefined ) {
contentScriptRegisterer.unregister(request.hostname);
return;
}
const contentScript = [];
if ( µb.hiddenSettings.debugScriptletInjector ) {
contentScript.push('debugger');
}
const { mainWorld = '', isolatedWorld = '', filters } = scriptletDetails;
if ( mainWorld !== '' ) {
let code = mainWorldInjector.assemble(request.hostname, mainWorld, filters);
if ( µb.hiddenSettings.debugScriptletInjector ) {
code = 'debugger;\n' + code;
}
vAPI.tabs.executeScript(details.tabId, {
code,
frameId: details.frameId,
matchAboutBlank: true,
runAt: 'document_start',
});
contentScript.push(mainWorldInjector.assemble(request.hostname, mainWorld, filters));
}
if ( isolatedWorld !== '' ) {
let code = isolatedWorldInjector.assemble(request.hostname, isolatedWorld);
if ( µb.hiddenSettings.debugScriptletInjector ) {
code = 'debugger;\n' + code;
}
contentScript.push(isolatedWorldInjector.assemble(request.hostname, isolatedWorld));
}
const code = contentScript.join('\n\n');
const isAlreadyInjected = contentScriptRegisterer.register(request.hostname, code);
if ( isAlreadyInjected !== true ) {
vAPI.tabs.executeScript(details.tabId, {
code,
frameId: details.frameId,
Expand Down

0 comments on commit 4cac9d1

Please sign in to comment.