Bug 1308271 - Import sources of the WebCompat Go Faster add-on V1. r=Felipe
authorDennis Schubert <dschubert@mozilla.com>
Wed, 25 Jan 2017 20:15:11 +0100
changeset 331420 a4fe5a905e414fa4e1b7d18becd21ad3c6e31aae
parent 331419 a4a6217867d01e42da89cbc26294a2ca8d1a9941
child 331421 9a2eef2935bf14eaef118cfc3603f073b4e54c80
push id36597
push userryanvm@gmail.com
push dateFri, 27 Jan 2017 21:12:38 +0000
treeherderautoland@a4fe5a905e41 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersFelipe
bugs1308271
milestone54.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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");
+});