Bug 1308271 - Import sources of the WebCompat Go Faster add-on V1. r?felipe draft
authorDennis Schubert <dschubert@mozilla.com>
Wed, 25 Jan 2017 20:15:11 +0100
changeset 466366 dbcf1402174899b8cc91778ca10970fd0125dcf1
parent 466286 24d9eb148461bb4789848b9880867c63c783a2ca
child 481685 1c7122b3325808a13e669144d5a5bd902afb649a
push id42882
push userdschubert@mozilla.com
push dateWed, 25 Jan 2017 22:05:38 +0000
reviewersfelipe
bugs1308271
milestone54.0a1
Bug 1308271 - Import sources of the WebCompat Go Faster add-on V1. r?felipe MozReview-Commit-ID: 58iV4MqTeKA
browser/extensions/webcompat/bootstrap.js
browser/extensions/webcompat/content/data/ua_overrides.jsm
browser/extensions/webcompat/content/lib/ua_overrider.jsm
browser/extensions/webcompat/jar.mn
browser/extensions/webcompat/moz.build
browser/extensions/webcompat/test/browser.ini
browser/extensions/webcompat/test/browser/.eslintrc.js
browser/extensions/webcompat/test/browser/browser.ini
browser/extensions/webcompat/test/browser/browser_check_installed.js
browser/extensions/webcompat/test/browser_check_installed.js
browser/extensions/webcompat/test/browser_overrider.js
--- a/browser/extensions/webcompat/bootstrap.js
+++ b/browser/extensions/webcompat/bootstrap.js
@@ -1,9 +1,73 @@
 /* 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";
-function startup() {}
-function shutdown() {}
-function install() {}
-function uninstall() {}
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const PREF_BRANCH = "extensions.webcompat.";
+const PREF_DEFAULTS = {perform_ua_overrides: true};
+
+const UA_ENABLE_PREF_NAME = "extensions.webcompat.perform_ua_overrides";
+
+XPCOMUtils.defineLazyModuleGetter(this, "UAOverrider", "chrome://webcompat/content/lib/ua_overrider.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UAOverrides", "chrome://webcompat/content/data/ua_overrides.jsm");
+
+let overrider;
+
+function UAEnablePrefObserver() {
+  let isEnabled = Services.prefs.getBoolPref(UA_ENABLE_PREF_NAME);
+  if (isEnabled && !overrider) {
+    overrider = new UAOverrider(UAOverrides);
+    overrider.init();
+  } else if (!isEnabled && overrider) {
+    overrider.uninit();
+    overrider = null;
+  }
+}
+
+function setDefaultPrefs() {
+  const branch = Services.prefs.getDefaultBranch(PREF_BRANCH);
+  for (const [key, val] of Object.entries(PREF_DEFAULTS)) {
+    // If someone beat us to setting a default, don't overwrite it.
+    if (branch.getPrefType(key) !== branch.PREF_INVALID) {
+      continue;
+    }
+
+    switch (typeof val) {
+      case "boolean":
+        branch.setBoolPref(key, val);
+        break;
+      case "number":
+        branch.setIntPref(key, val);
+        break;
+      case "string":
+        branch.setCharPref(key, val);
+        break;
+    }
+  }
+}
+
+this.install = function() {};
+this.uninstall = function() {};
+
+this.startup = function({webExtension}) {
+  setDefaultPrefs();
+
+  // Intentionally reset the preference on every browser restart to avoid site
+  // breakage by accidentally toggled preferences or by leaving it off after
+  // debugging a site.
+  Services.prefs.clearUserPref(UA_ENABLE_PREF_NAME);
+  Services.prefs.addObserver(UA_ENABLE_PREF_NAME, UAEnablePrefObserver, false);
+
+  overrider = new UAOverrider(UAOverrides);
+  overrider.init();
+};
+
+this.shutdown = function() {
+  Services.prefs.removeObserver(UA_ENABLE_PREF_NAME, UAEnablePrefObserver);
+
+  if (overrider) {
+    overrider.uninit();
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/content/data/ua_overrides.jsm
@@ -0,0 +1,60 @@
+/* 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/. */
+
+/**
+ * This is an array of objects that specify user agent overrides. Each object
+ * can have three attributes:
+ *
+ *   * `baseDomain`, required: The base domain that further checks and user
+ *     agents override are applied to. This does not include subdomains.
+ *   * `uriMatcher`: Function that gets the requested URI passed in the first
+ *     argument and needs to return boolean whether or not the override should
+ *     be applied. If not provided, the user agent override will be applied
+ *     every time.
+ *   * `uaTransformer`, required: Function that gets the original Firefox user
+ *     agent passed as its first argument and needs to return a string that
+ *     will be used as the the user agent for this URI.
+ *
+ * Examples:
+ *
+ * Gets applied for all requests to mozilla.org and subdomains:
+ *
+ * ```
+ *   {
+ *     baseDomain: "mozilla.org",
+ *     uaTransformer: (originalUA) => `Ohai Mozilla, it's me, ${originalUA}`
+ *   }
+ * ```
+ *
+ * Applies to *.example.com/app/*:
+ *
+ * ```
+ *   {
+ *     baseDomain: "example.com",
+ *     uriMatcher: (uri) => uri.includes("/app/"),
+ *     uaTransformer: (originalUA) => originalUA.replace("Firefox", "Otherfox")
+ *   }
+ * ```
+ */
+
+const UAOverrides = [
+
+  /*
+   * This is a dummy override that applies a Chrome UA to a dummy site that
+   * blocks all browsers but Chrome.
+   *
+   * This was only put in place to allow QA to test this system addon on an
+   * actual site, since we were not able to find a proper override in time.
+   */
+  {
+    baseDomain: "schub.io",
+    uriMatcher: (uri) => uri.includes("webcompat-ua-dummy.schub.io"),
+    uaTransformer: (originalUA) => {
+      let prefix = originalUA.substr(0, originalUA.indexOf(")") + 1);
+      return `${prefix} AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36`;
+    }
+  }
+];
+
+this.EXPORTED_SYMBOLS = ["UAOverrides"]; /* exported UAOverrides */
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/content/lib/ua_overrider.jsm
@@ -0,0 +1,122 @@
+/* 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/. */
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Console.jsm");
+
+const DefaultUA = Cc["@mozilla.org/network/protocol;1?name=http"].getService(Ci.nsIHttpProtocolHandler).userAgent;
+const NS_HTTP_ON_USERAGENT_REQUEST_TOPIC = "http-on-useragent-request";
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "eTLDService", "@mozilla.org/network/effective-tld-service;1", "nsIEffectiveTLDService");
+
+class UAOverrider {
+  constructor(overrides) {
+    this._overrides = {};
+    this._overrideForURICache = new Map();
+
+    this.initOverrides(overrides);
+  }
+
+  initOverrides(overrides) {
+    for (let override of overrides) {
+      if (!this._overrides[override.baseDomain]) {
+        this._overrides[override.baseDomain] = [];
+      }
+
+      if (!override.uriMatcher) {
+        override.uriMatcher = () => true;
+      }
+
+      this._overrides[override.baseDomain].push(override);
+    }
+  }
+
+  init() {
+    Services.obs.addObserver(this, NS_HTTP_ON_USERAGENT_REQUEST_TOPIC, false);
+  }
+
+  uninit() {
+    Services.obs.removeObserver(this, NS_HTTP_ON_USERAGENT_REQUEST_TOPIC);
+  }
+
+  observe(subject, topic) {
+    if (topic !== NS_HTTP_ON_USERAGENT_REQUEST_TOPIC) {
+      return;
+    }
+
+    let channel = subject.QueryInterface(Components.interfaces.nsIHttpChannel);
+    let uaOverride = this.getUAForURI(channel.URI);
+
+    if (uaOverride) {
+      console.log("The user agent has been overridden for compatibility reasons.");
+      channel.setRequestHeader("User-Agent", uaOverride, false);
+    }
+  }
+
+  getUAForURI(uri) {
+    let bareUri = uri.specIgnoringRef;
+    if (this._overrideForURICache.has(bareUri)) {
+      // Although the cache could have an entry to a bareUri, `false` is also
+      // a value that could be cached. A `false` cache entry means that there
+      // is no override for this URI.
+      // We cache these to avoid having to walk through all overrides to see
+      // if a domain matches.
+      return this._overrideForURICache.get(bareUri);
+    }
+
+    let finalUA = this.lookupUAOverride(uri);
+    this._overrideForURICache.set(bareUri, finalUA);
+
+    return finalUA;
+  }
+
+  /**
+   * This function gets called from within the embedded webextension to check
+   * if the current site has been overriden or not. We only check the cached
+   * URI list here, but that's safe in our case since the tabUpdateHandler will
+   * always run after our message observer.
+   */
+  hasUAForURIInCache(uri) {
+    let bareUri = uri.specIgnoringRef;
+    if (this._overrideForURICache.has(bareUri)) {
+      return !!this._overrideForURICache.get(bareUri);
+    }
+
+    return false;
+  }
+
+  /**
+   * This function returns a User Agent based on the URI passed into. All
+   * override rules are defined in data/ua_overrides.jsm and the required format
+   * is explained there.
+   *
+   * Since it is expected and designed to have more than one override per base
+   * domain, we have to loop over this._overrides[baseDomain], which contains
+   * all available overrides.
+   *
+   * If the uriMatcher function returns true, the uaTransformer function gets
+   * called and its result will be used as the Use Agent for the current
+   * request.
+   *
+   * If there are more than one possible overrides, that is if two or more
+   * uriMatchers would return true, the first one gets applied.
+   */
+  lookupUAOverride(uri) {
+    let baseDomain = eTLDService.getBaseDomain(uri);
+    if (this._overrides[baseDomain]) {
+      for (let uaOverride of this._overrides[baseDomain]) {
+        if (uaOverride.uriMatcher(uri.specIgnoringRef)) {
+          return uaOverride.uaTransformer(DefaultUA);
+        }
+      }
+    }
+
+    return false;
+  }
+}
+
+this.EXPORTED_SYMBOLS = ["UAOverrider"]; /* exported UAOverrider */
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/jar.mn
@@ -0,0 +1,3 @@
+[features/webcompat@mozilla.org] chrome.jar:
+% content webcompat %content/
+  content/ (content/*)
--- a/browser/extensions/webcompat/moz.build
+++ b/browser/extensions/webcompat/moz.build
@@ -10,9 +10,10 @@ DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['
 FINAL_TARGET_FILES.features['webcompat@mozilla.org'] += [
   'bootstrap.js'
 ]
 
 FINAL_TARGET_PP_FILES.features['webcompat@mozilla.org'] += [
   'install.rdf.in'
 ]
 
-BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+JAR_MANIFESTS += ['jar.mn']
rename from browser/extensions/webcompat/test/browser/browser.ini
rename to browser/extensions/webcompat/test/browser.ini
--- a/browser/extensions/webcompat/test/browser/browser.ini
+++ b/browser/extensions/webcompat/test/browser.ini
@@ -1,3 +1,4 @@
 [DEFAULT]
 
 [browser_check_installed.js]
+[browser_overrider.js]
deleted file mode 100644
--- a/browser/extensions/webcompat/test/browser/.eslintrc.js
+++ /dev/null
@@ -1,7 +0,0 @@
-"use strict";
-
-module.exports = {
-  "extends": [
-    "../../../../../testing/mochitest/browser.eslintrc.js"
-  ]
-};
rename from browser/extensions/webcompat/test/browser/browser_check_installed.js
rename to browser/extensions/webcompat/test/browser_check_installed.js
--- a/browser/extensions/webcompat/test/browser/browser_check_installed.js
+++ b/browser/extensions/webcompat/test/browser_check_installed.js
@@ -1,13 +1,19 @@
+/* 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/. */
+
+/* global AddonManager */
+
 "use strict";
 
-add_task(function* test_enabled() {
+add_task(function* installed() {
   let addon = yield new Promise(
-    resolve => AddonManager.getAddonByID("webcompat@mozilla.org", resolve)
+    (resolve) => AddonManager.getAddonByID("webcompat@mozilla.org", resolve)
   );
-  isnot(addon, null, "Check addon exists");
-  is(addon.version, "1.0", "Check version");
-  is(addon.name, "Web Compat", "Check name");
-  ok(addon.isCompatible, "Check application compatibility");
-  ok(!addon.appDisabled, "Check not app disabled");
-  ok(addon.isActive, "Check addon is active");
+  isnot(addon, null, "Webcompat addon should exist");
+  is(addon.name, "Web Compat");
+  ok(addon.isCompatible, "Webcompat addon is compatible with Firefox");
+  ok(!addon.appDisabled, "Webcompat addon is not app disabled");
+  ok(addon.isActive, "Webcompat addon is active");
+  is(addon.type, "extension", "Webcompat addon is type extension");
 });
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/test/browser_overrider.js
@@ -0,0 +1,40 @@
+/* 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/. */
+
+/* globals XPCOMUtils, UAOverrider, IOService */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UAOverrider", "chrome://webcompat/content/lib/ua_overrider.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "IOService", "@mozilla.org/network/io-service;1", "nsIIOService");
+
+function getnsIURI(uri) {
+  return IOService.newURI(uri, "utf-8");
+}
+
+add_task(function test() {
+  let overrider = new UAOverrider([
+    {
+      baseDomain: "example.org",
+      uaTransformer: () => "Test UA"
+    }
+  ]);
+
+  let finalUA = overrider.getUAForURI(getnsIURI("http://www.example.org/foobar/"));
+  is(finalUA, "Test UA", "Overrides the UA without a matcher function");
+});
+
+add_task(function test() {
+  let overrider = new UAOverrider([
+    {
+      baseDomain: "example.org",
+      uriMatcher: () => false,
+      uaTransformer: () => "Test UA"
+    }
+  ]);
+
+  let finalUA = overrider.getUAForURI(getnsIURI("http://www.example.org/foobar/"));
+  isnot(finalUA, "Test UA", "Does not override the UA with the matcher returning false");
+});