Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* globals browser, module */
class Injections {
constructor(availableInjections, customFunctions) {
this.INJECTION_PREF = "perform_injections";
this._injectionsEnabled = true;
this._availableInjections = availableInjections;
this._activeInjections = new Set();
// Only used if this.shouldUseScriptingAPI is false and we are falling back
// to use the contentScripts API.
this._activeInjectionHandles = new Map();
this._customFunctions = customFunctions;
this.shouldUseScriptingAPI =
browser.aboutConfigPrefs.getBoolPrefSync("useScriptingAPI");
// Debug log emit only on nightly (similarly to the debug
// helper used in shims.js for similar purpose).
browser.appConstants.getReleaseBranch().then(releaseBranch => {
if (releaseBranch !== "release_or_beta") {
console.debug(
`WebCompat Injections will be injected using ${
this.shouldUseScriptingAPI ? "scripting" : "contentScripts"
} API`
);
}
});
}
bindAboutCompatBroker(broker) {
this._aboutCompatBroker = broker;
}
bootup() {
browser.aboutConfigPrefs.onPrefChange.addListener(() => {
this.checkInjectionPref();
}, this.INJECTION_PREF);
this.checkInjectionPref();
}
checkInjectionPref() {
browser.aboutConfigPrefs.getPref(this.INJECTION_PREF).then(value => {
if (value === undefined) {
browser.aboutConfigPrefs.setPref(this.INJECTION_PREF, true);
} else if (value === false) {
this.unregisterContentScripts();
} else {
this.registerContentScripts();
}
});
}
getAvailableInjections() {
return this._availableInjections;
}
isEnabled() {
return this._injectionsEnabled;
}
async getPromiseRegisteredScriptIds(scriptIds) {
let registeredScriptIds = [];
// Try to avoid re-registering scripts already registered
// (e.g. if the webcompat background page is restarted
// after an extension process crash, after having registered
// the content scripts already once), but do not prevent
// to try registering them again if the getRegisteredContentScripts
// method returns an unexpected rejection.
try {
const registeredScripts =
await browser.scripting.getRegisteredContentScripts({
// By default only look for script ids that belongs to Injections
// (and ignore the ones that may belong to Shims).
ids: scriptIds ?? this._availableInjections.map(inj => inj.id),
});
registeredScriptIds = registeredScripts.map(script => script.id);
} catch (ex) {
console.error(
"Retrieve WebCompat GoFaster registered content scripts failed: ",
ex
);
}
return registeredScriptIds;
}
async registerContentScripts() {
const platformInfo = await browser.runtime.getPlatformInfo();
const platformMatches = [
"all",
platformInfo.os,
platformInfo.os == "android" ? "android" : "desktop",
];
let registeredScriptIds = this.shouldUseScriptingAPI
? await this.getPromiseRegisteredScriptIds()
: [];
for (const injection of this._availableInjections) {
if (platformMatches.includes(injection.platform)) {
injection.availableOnPlatform = true;
try {
if (injection.checkIfNeeded && !injection.checkIfNeeded()) {
console.warn("Skipping un-needed injection for", injection.domain);
} else {
await this.enableInjection(injection, registeredScriptIds);
}
} catch (e) {
console.error("Error enabling injection for", injection.domain, e);
}
}
}
this._injectionsEnabled = true;
this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({
interventionsChanged: this._aboutCompatBroker.filterOverrides(
this._availableInjections
),
});
}
buildContentScriptRegistrations(contentScripts) {
let finalConfig = Object.assign({}, contentScripts);
if (!finalConfig.runAt) {
finalConfig.runAt = "document_start";
}
if (this.shouldUseScriptingAPI) {
// Don't persist the content scripts across browser restarts
// (at least not yet, we would need to apply some more changes
// to adjust webcompat for accounting for the scripts to be
// already registered).
//
// NOTE: scripting API has been introduced in Gecko 102,
// prior to Gecko 105 persistAcrossSessions option was required
// and only accepted false persistAcrossSessions, after Gecko 105
// is optional and defaults to true.
finalConfig.persistAcrossSessions = false;
// Convert js/css from contentScripts.register API method
// format to scripting.registerContentScripts API method
// format.
if (Array.isArray(finalConfig.js)) {
finalConfig.js = finalConfig.js.map(e => e.file);
}
if (Array.isArray(finalConfig.css)) {
finalConfig.css = finalConfig.css.map(e => e.file);
}
}
return finalConfig;
}
async enableInjection(injection, registeredScriptIds) {
if (injection.active) {
return undefined;
}
if (injection.customFunc) {
return this.enableCustomInjection(injection);
}
return this.enableContentScripts(injection, registeredScriptIds);
}
enableCustomInjection(injection) {
if (injection.customFunc in this._customFunctions) {
this._customFunctions[injection.customFunc](injection);
injection.active = true;
} else {
console.error(
`Provided function ${injection.customFunc} wasn't found in functions list`
);
}
}
async enableContentScripts(injection, registeredScriptIds) {
let injectProps;
try {
const { id } = injection;
if (this.shouldUseScriptingAPI) {
// enableContentScripts receives a registeredScriptIds already
// pre-computed once from registerContentScripts to register all
// the injection, whereas it does not expect to receive one when
// it is called from the AboutCompatBroker to re-enable one specific
// injection.
let activeScriptIds = Array.isArray(registeredScriptIds)
? registeredScriptIds
: await this.getPromiseRegisteredScriptIds([id]);
injectProps = this.buildContentScriptRegistrations(
injection.contentScripts
);
injectProps.id = id;
if (!activeScriptIds.includes(id)) {
await browser.scripting.registerContentScripts([injectProps]);
}
this._activeInjections.add(id);
} else {
const handle = await browser.contentScripts.register(
this.buildContentScriptRegistrations(injection.contentScripts)
);
this._activeInjections.add(id);
this._activeInjectionHandles.set(id, handle);
}
injection.active = true;
} catch (ex) {
console.error(
"Registering WebCompat GoFaster content scripts failed: ",
{ injection, injectProps },
ex
);
}
}
unregisterContentScripts() {
for (const injection of this._availableInjections) {
this.disableInjection(injection);
}
this._injectionsEnabled = false;
this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({
interventionsChanged: false,
});
}
async disableInjection(injection) {
if (!injection.active) {
return undefined;
}
if (injection.customFunc) {
return this.disableCustomInjections(injection);
}
return this.disableContentScripts(injection);
}
disableCustomInjections(injection) {
const disableFunc = injection.customFunc + "Disable";
if (disableFunc in this._customFunctions) {
this._customFunctions[disableFunc](injection);
injection.active = false;
} else {
console.error(
`Provided function ${disableFunc} for disabling injection wasn't found in functions list`
);
}
}
async disableContentScripts(injection) {
if (this._activeInjections.has(injection.id)) {
if (this.shouldUseScriptingAPI) {
await browser.scripting.unregisterContentScripts({
ids: [injection.id],
});
} else {
const handle = this._activeInjectionHandles.get(injection.id);
await handle.unregister();
this._activeInjectionHandles.delete(injection.id);
}
this._activeInjections.delete(injection);
}
injection.active = false;
}
}
module.exports = Injections;