summaryrefslogtreecommitdiffstats
path: root/browser/extensions/webcompat/lib/injections.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/extensions/webcompat/lib/injections.js')
-rw-r--r--browser/extensions/webcompat/lib/injections.js272
1 files changed, 272 insertions, 0 deletions
diff --git a/browser/extensions/webcompat/lib/injections.js b/browser/extensions/webcompat/lib/injections.js
new file mode 100644
index 0000000000..92fdc5fbb3
--- /dev/null
+++ b/browser/extensions/webcompat/lib/injections.js
@@ -0,0 +1,272 @@
+/* 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;
+ await this.enableInjection(injection, registeredScriptIds);
+ }
+ }
+
+ 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;