Bug 1587558 - Import WebCompat GoFaster 6.4.0 sources. r=twisniewski
[gecko.git] / browser / extensions / webcompat / lib / ua_overrides.js
blob40f6c5192f2505e5af72823396ab7e34f98741ad
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 /* globals browser, module */
9 class UAOverrides {
10   constructor(availableOverrides) {
11     this.OVERRIDE_PREF = "perform_ua_overrides";
13     this._overridesEnabled = true;
15     this._availableOverrides = availableOverrides;
16     this._activeListeners = new Map();
17   }
19   bindAboutCompatBroker(broker) {
20     this._aboutCompatBroker = broker;
21   }
23   bootup() {
24     browser.aboutConfigPrefs.onPrefChange.addListener(() => {
25       this.checkOverridePref();
26     }, this.OVERRIDE_PREF);
27     this.checkOverridePref();
28   }
30   checkOverridePref() {
31     browser.aboutConfigPrefs.getPref(this.OVERRIDE_PREF).then(value => {
32       if (value === undefined) {
33         browser.aboutConfigPrefs.setPref(this.OVERRIDE_PREF, true);
34       } else if (value === false) {
35         this.unregisterUAOverrides();
36       } else {
37         this.registerUAOverrides();
38       }
39     });
40   }
42   getAvailableOverrides() {
43     return this._availableOverrides;
44   }
46   isEnabled() {
47     return this._overridesEnabled;
48   }
50   enableOverride(override) {
51     if (override.active) {
52       return;
53     }
55     const { blocks, matches, telemetryKey, uaTransformer } = override.config;
56     const listener = details => {
57       // We set the "used" telemetry key if the user would have had the
58       // override applied, regardless of whether it is actually applied.
59       if (!details.frameId && override.shouldSendDetailedTelemetry) {
60         // For now, we only care about Telemetry on Fennec, where telemetry
61         // is sent in Java code (as part of the core ping). That code must
62         // be aware of each key we send, which we send as a SharedPreference.
63         browser.sharedPreferences.setBoolPref(`${telemetryKey}Used`, true);
64       }
66       // Don't actually override the UA for an experiment if the user is not
67       // part of the experiment (unless they force-enabed the override).
68       if (
69         !override.config.experiment ||
70         override.experimentActive ||
71         override.permanentPrefEnabled === true
72       ) {
73         for (const header of details.requestHeaders) {
74           if (header.name.toLowerCase() === "user-agent") {
75             header.value = uaTransformer(header.value);
76           }
77         }
78       }
79       return { requestHeaders: details.requestHeaders };
80     };
82     browser.webRequest.onBeforeSendHeaders.addListener(
83       listener,
84       { urls: matches },
85       ["blocking", "requestHeaders"]
86     );
88     const listeners = { onBeforeSendHeaders: listener };
89     if (blocks) {
90       const blistener = details => {
91         return { cancel: true };
92       };
94       browser.webRequest.onBeforeRequest.addListener(
95         blistener,
96         { urls: blocks },
97         ["blocking"]
98       );
100       listeners.onBeforeRequest = blistener;
101     }
102     this._activeListeners.set(override, listeners);
103     override.active = true;
105     // If telemetry is being collected, note the addon version.
106     if (telemetryKey) {
107       const { version } = browser.runtime.getManifest();
108       browser.sharedPreferences.setCharPref(`${telemetryKey}Version`, version);
109     }
111     // If collecting detailed telemetry on the override, note that it was activated.
112     if (override.shouldSendDetailedTelemetry) {
113       browser.sharedPreferences.setBoolPref(`${telemetryKey}Ready`, true);
114     }
115   }
117   onOverrideConfigChanged(override) {
118     // Check whether the override should be hidden from about:compat.
119     override.hidden = override.config.hidden;
121     // Also hide if the override is in an experiment the user is not part of.
122     if (override.config.experiment && !override.experimentActive) {
123       override.hidden = true;
124     }
126     // Setting the override's permanent pref overrules whether it is hidden.
127     if (override.permanentPrefEnabled !== undefined) {
128       override.hidden = !override.permanentPrefEnabled;
129     }
131     // Also check whether the override should be active.
132     let shouldBeActive = true;
134     // Overrides can be force-deactivated by their permanent preference.
135     if (override.permanentPrefEnabled === false) {
136       shouldBeActive = false;
137     }
139     // Only send detailed telemetry if the user is actively in an experiment or
140     // has opted into an experimental feature.
141     override.shouldSendDetailedTelemetry =
142       override.config.telemetryKey &&
143       (override.experimentActive || override.permanentPrefEnabled);
145     // Overrides gated behind an experiment the user is not part of do not
146     // have to be activated, unless they are gathering telemetry, or the
147     // user has force-enabled them with their permanent pref.
148     if (
149       override.config.experiment &&
150       !override.experimentActive &&
151       !override.config.telemetryKey &&
152       override.permanentPrefEnabled !== true
153     ) {
154       shouldBeActive = false;
155     }
157     if (shouldBeActive) {
158       this.enableOverride(override);
159     } else {
160       this.disableOverride(override);
161     }
163     if (this._overridesEnabled) {
164       this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({
165         overridesChanged: this._aboutCompatBroker.filterOverrides(
166           this._availableOverrides
167         ),
168       });
169     }
170   }
172   async registerUAOverrides() {
173     const platformMatches = ["all"];
174     let platformInfo = await browser.runtime.getPlatformInfo();
175     platformMatches.push(platformInfo.os == "android" ? "android" : "desktop");
177     for (const override of this._availableOverrides) {
178       if (platformMatches.includes(override.platform)) {
179         override.availableOnPlatform = true;
181         // Note whether the user is actively in the override's experiment (if any).
182         override.experimentActive = false;
183         const experiment = override.config.experiment;
184         if (experiment) {
185           // We expect the definition to have either one string for 'experiment'
186           // (just one branch) or an array of strings (multiple branches). So
187           // here we turn the string case into a one-element array for the loop.
188           const branches = Array.isArray(experiment)
189             ? experiment
190             : [experiment];
191           for (const branch of branches) {
192             if (await browser.experiments.isActive(branch)) {
193               override.experimentActive = true;
194               break;
195             }
196           }
197         }
199         // If there is a specific about:config preference governing
200         // this override, monitor its state.
201         const pref = override.config.permanentPref;
202         override.permanentPrefEnabled =
203           pref && (await browser.aboutConfigPrefs.getPref(pref));
204         if (pref) {
205           const checkOverridePref = () => {
206             browser.aboutConfigPrefs.getPref(pref).then(value => {
207               override.permanentPrefEnabled = value;
208               this.onOverrideConfigChanged(override);
209             });
210           };
211           browser.aboutConfigPrefs.onPrefChange.addListener(
212             checkOverridePref,
213             pref
214           );
215         }
217         this.onOverrideConfigChanged(override);
218       }
219     }
221     this._overridesEnabled = true;
222     this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({
223       overridesChanged: this._aboutCompatBroker.filterOverrides(
224         this._availableOverrides
225       ),
226     });
227   }
229   unregisterUAOverrides() {
230     for (const override of this._availableOverrides) {
231       this.disableOverride(override);
232     }
234     this._overridesEnabled = false;
235     this._aboutCompatBroker.portsToAboutCompatTabs.broadcast({
236       overridesChanged: false,
237     });
238   }
240   disableOverride(override) {
241     if (!override.active) {
242       return;
243     }
245     const listeners = this._activeListeners.get(override);
246     for (const [name, listener] of Object.entries(listeners)) {
247       browser.webRequest[name].removeListener(listener);
248     }
249     override.active = false;
250     this._activeListeners.delete(override);
251   }
254 module.exports = UAOverrides;