Bug 1637329 - add support for shimming resources blocking by tracking protection to webcompat built-in addon; r=dimi,robwu,webcompat-reviewers,denschub,miketaylr
☠☠ backed out by 015515bcba1f ☠ ☠
authorThomas Wisniewski <twisniewski@mozilla.com>
Sun, 19 Jul 2020 14:31:44 +0000
changeset 541176 fc5242f4332df3d6abe1ea79c23b94f05b13c2e2
parent 541175 dfe9da3d53e5b2b4230c03baecc99d6dddf72d29
child 541178 c94420590362aa17ead6000146b97548fc47051f
push id122091
push usertwisniewski@mozilla.com
push dateSun, 19 Jul 2020 18:38:22 +0000
treeherderautoland@fc5242f4332d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdimi, robwu, webcompat-reviewers, denschub, miketaylr
bugs1637329
milestone80.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 1637329 - add support for shimming resources blocking by tracking protection to webcompat built-in addon; r=dimi,robwu,webcompat-reviewers,denschub,miketaylr Differential Revision: https://phabricator.services.mozilla.com/D77724
browser/extensions/webcompat/data/shims.js
browser/extensions/webcompat/experiment-apis/aboutConfigPrefs.js
browser/extensions/webcompat/experiment-apis/aboutConfigPrefs.json
browser/extensions/webcompat/experiment-apis/appConstants.js
browser/extensions/webcompat/experiment-apis/appConstants.json
browser/extensions/webcompat/experiment-apis/matchPatterns.js
browser/extensions/webcompat/experiment-apis/matchPatterns.json
browser/extensions/webcompat/experiment-apis/trackingProtection.js
browser/extensions/webcompat/experiment-apis/trackingProtection.json
browser/extensions/webcompat/lib/shim_messaging_helper.js
browser/extensions/webcompat/lib/shims.js
browser/extensions/webcompat/manifest.json
browser/extensions/webcompat/moz.build
browser/extensions/webcompat/run.js
browser/extensions/webcompat/shims/adsafeprotected-ima.js
browser/extensions/webcompat/shims/bmauth.js
browser/extensions/webcompat/shims/eluminate.js
browser/extensions/webcompat/shims/empty-script.js
browser/extensions/webcompat/shims/facebook-sdk.js
browser/extensions/webcompat/shims/google-analytics-ecommerce-plugin.js
browser/extensions/webcompat/shims/google-analytics-legacy.js
browser/extensions/webcompat/shims/google-analytics-tag-manager.js
browser/extensions/webcompat/shims/google-analytics.js
browser/extensions/webcompat/shims/google-publisher-tags.js
browser/extensions/webcompat/shims/live-test-shim.js
browser/extensions/webcompat/shims/mochitest-shim-1.js
browser/extensions/webcompat/shims/mochitest-shim-2.js
browser/extensions/webcompat/shims/mochitest-shim-3.js
browser/extensions/webcompat/shims/rambler-authenticator.js
browser/extensions/webcompat/shims/rich-relevance.js
browser/extensions/webcompat/tests/browser/browser.ini
browser/extensions/webcompat/tests/browser/browser_shims.js
browser/extensions/webcompat/tests/browser/head.js
browser/extensions/webcompat/tests/browser/iframe_test.html
browser/extensions/webcompat/tests/browser/shims_test.html
browser/extensions/webcompat/tests/browser/shims_test.js
browser/extensions/webcompat/tests/browser/shims_test_2.html
browser/extensions/webcompat/tests/browser/shims_test_2.js
browser/extensions/webcompat/tests/browser/shims_test_3.html
browser/extensions/webcompat/tests/browser/shims_test_3.js
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/data/shims.js
@@ -0,0 +1,253 @@
+/* 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 module, require */
+
+const AVAILABLE_SHIMS = [
+  {
+    id: "LiveTestShim",
+    platform: "all",
+    name: "Live test shim",
+    bug: "livetest",
+    file: "live-test-shim.js",
+    matches: ["*://webcompat-addon-testbed.herokuapp.com/shims_test.js"],
+    needsShimHelpers: ["getOptions", "optIn"],
+  },
+  {
+    id: "MochitestShim",
+    platform: "all",
+    name: "Test shim for Mochitests",
+    bug: "mochitest",
+    file: "mochitest-shim-1.js",
+    matches: [
+      "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test.js",
+    ],
+    needsShimHelpers: ["getOptions", "optIn"],
+    options: {
+      simpleOption: true,
+      complexOption: { a: 1, b: "test" },
+      branchValue: { value: true, branches: [] },
+      platformValue: { value: true, platform: "neverUsed" },
+    },
+    unblocksOnOptIn: ["*://trackertest.org/*"],
+  },
+  {
+    disabled: true,
+    id: "MochitestShim2",
+    platform: "all",
+    name: "Test shim for Mochitests (disabled by default)",
+    bug: "mochitest",
+    file: "mochitest-shim-2.js",
+    matches: [
+      "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_2.js",
+    ],
+    needsShimHelpers: ["getOptions", "optIn"],
+    options: {
+      simpleOption: true,
+      complexOption: { a: 1, b: "test" },
+      branchValue: { value: true, branches: [] },
+      platformValue: { value: true, platform: "neverUsed" },
+    },
+    unblocksOnOptIn: ["*://trackertest.org/*"],
+  },
+  {
+    id: "MochitestShim3",
+    platform: "all",
+    name: "Test shim for Mochitests (host)",
+    bug: "mochitest",
+    file: "mochitest-shim-3.js",
+    notHosts: ["example.com"],
+    matches: [
+      "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js",
+    ],
+  },
+  {
+    id: "MochitestShim4",
+    platform: "all",
+    name: "Test shim for Mochitests (notHost)",
+    bug: "mochitest",
+    file: "mochitest-shim-3.js",
+    hosts: ["example.net"],
+    matches: [
+      "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js",
+    ],
+  },
+  {
+    id: "MochitestShim5",
+    platform: "all",
+    name: "Test shim for Mochitests (branch)",
+    bug: "mochitest",
+    file: "mochitest-shim-3.js",
+    branches: ["never matches"],
+    matches: [
+      "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js",
+    ],
+  },
+  {
+    id: "MochitestShim6",
+    platform: "never matches",
+    name: "Test shim for Mochitests (platform)",
+    bug: "mochitest",
+    file: "mochitest-shim-3.js",
+    matches: [
+      "*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js",
+    ],
+  },
+  {
+    id: "AdSafeProtectedGoogleIMAAdapter",
+    platform: "all",
+    branches: ["nightly"],
+    name: "Ad Safe Protected Google IMA Adapter",
+    bug: "1508639",
+    file: "adsafeprotected-ima.js",
+    matches: ["*://static.adsafeprotected.com/vans-adapter-google-ima.js"],
+    needsShimHelpers: ["optIn"],
+    onlyIfBlockedByETP: true,
+    unblocksOnOptIn: ["*://pubads.g.doubleclick.net/gampad/ads"],
+  },
+  {
+    id: "AdsByGoogle",
+    platform: "all",
+    branches: ["nightly"],
+    name: "Ads by Google",
+    bug: "1629644",
+    file: "empty-script.js",
+    matches: ["*://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"],
+    onlyIfBlockedByETP: true,
+  },
+  {
+    id: "BmAuth",
+    platform: "all",
+    branches: ["nightly"],
+    name: "BmAuth by 9c9media",
+    bug: "1486337",
+    file: "bmauth.js",
+    matches: ["*://auth.9c9media.ca/auth/main.js"],
+    onlyIfBlockedByETP: true,
+  },
+  {
+    id: "Eluminate",
+    platform: "all",
+    branches: ["nightly"],
+    name: "Eluminate",
+    bug: "1503211",
+    file: "eluminate.js",
+    matches: ["*://libs.coremetrics.com/eluminate.js"],
+    onlyIfBlockedByETP: true,
+  },
+  {
+    id: "FacebookSDK",
+    platform: "all",
+    branches: ["nightly"],
+    name: "Facebook SDK",
+    bug: "1226498",
+    file: "facebook-sdk.js",
+    matches: [
+      "*://connect.facebook.net/*/sdk.js*",
+      "*://connect.facebook.net/*/all.js*",
+    ],
+    needsShimHelpers: ["optIn"],
+    onlyIfBlockedByETP: true,
+    unblocksOnOptIn: [
+      "*://*.xx.fbcdn.net/*", // covers:
+      // "*://scontent-.*-\d.xx.fbcdn.net/*",
+      // "*://static.xx.fbcdn.net/rsrc.php/*",
+
+      "*://www.facebook.com/plugins/comments.php*",
+      "*://www.facebook.com/plugins/comments/async/*",
+      "*://www.facebook.com/plugins/feedback.php*",
+      "*://www.facebook.com/plugins/like_box.php*",
+    ],
+  },
+  {
+    id: "GoogleAnalytics",
+    platform: "all",
+    branches: ["nightly"],
+    name: "Google Analytics",
+    bug: "1493602",
+    file: "google-analytics.js",
+    matches: ["*://www.google-analytics.com/analytics.js"],
+    onlyIfBlockedByETP: true,
+  },
+  {
+    id: "GoogleAnalyticsECommercePlugin",
+    platform: "all",
+    branches: ["nightly"],
+    name: "Google Analytics E-Commerce Plugin",
+    bug: "1620533",
+    file: "google-analytics-ecommerce-plugin.js",
+    matches: ["*://www.google-analytics.com/plugins/ua/ec.js"],
+    onlyIfBlockedByETP: true,
+  },
+  {
+    id: "GoogleAnalyticsTagManager",
+    platform: "all",
+    branches: ["nightly"],
+    name: "Google Analytics Tag Manager",
+    bug: "1478593",
+    file: "google-analytics-tag-manager.js",
+    matches: ["*://www.google-analytics.com/gtm/js"],
+    onlyIfBlockedByETP: true,
+  },
+  {
+    id: "GoogleAnalyticsLegacy",
+    platform: "all",
+    branches: ["nightly"],
+    name: "Legacy Google Analytics",
+    bug: "1487072",
+    file: "google-analytics-legacy.js",
+    matches: ["*://ssl.google-analytics.com/ga.js"],
+    onlyIfBlockedByETP: true,
+  },
+  {
+    id: "GooglePublisherTags",
+    platform: "all",
+    branches: ["nightly"],
+    name: "Google Publisher Tags",
+    bug: "1600538",
+    file: "google-publisher-tags.js",
+    matches: [
+      "*://www.googletagservices.com/tag/js/gpt.js",
+      "*://securepubads.g.doubleclick.net/tag/js/gpt.js",
+      "*://securepubads.g.doubleclick.net/gpt/pubads_impl_*.js",
+    ],
+    onlyIfBlockedByETP: true,
+    unblocksOnOptIn: ["*://pubads.g.doubleclick.net/ssai/event/*/streams"],
+  },
+  {
+    id: "IMA3",
+    platform: "all",
+    branches: ["nightly"],
+    name: "IMA3",
+    bug: "1487373",
+    file: "empty-script.js",
+    onlyIfBlockedByETP: true,
+    matches: ["*://s0.2mdn.net/instream/html5/ima3.js"],
+  },
+  {
+    id: "Rambler",
+    platform: "all",
+    branches: ["nightly"],
+    name: "Rambler Authenticator",
+    bug: "1606428",
+    file: "rambler-authenticator.js",
+    matches: ["*://id.rambler.ru/rambler-id-helper/auth_events.js"],
+    needsShimHelpers: ["optIn"],
+    onlyIfBlockedByETP: true,
+  },
+  {
+    id: "RichRelevance",
+    platform: "all",
+    branches: ["nightly"],
+    name: "Rich Relevance",
+    bug: "1449347",
+    file: "rich-relevance.js",
+    matches: ["*://media.richrelevance.com/rrserver/js/1.2/p13n.js"],
+    onlyIfBlockedByETP: true,
+  },
+];
+
+module.exports = AVAILABLE_SHIMS;
--- a/browser/extensions/webcompat/experiment-apis/aboutConfigPrefs.js
+++ b/browser/extensions/webcompat/experiment-apis/aboutConfigPrefs.js
@@ -27,16 +27,23 @@ this.aboutConfigPrefs = class extends Ex
               fire.async(name).catch(() => {}); // ignore Message Manager disconnects
             };
             Services.prefs.addObserver(prefName, callback);
             return () => {
               Services.prefs.removeObserver(prefName, callback);
             };
           },
         }).api(),
+        async getBranch(branchName) {
+          const branch = `${extensionPrefNameBase}${branchName}.`;
+          return Services.prefs.getChildList(branch).map(pref => {
+            const name = pref.replace(branch, "");
+            return { name, value: Services.prefs.getBoolPref(pref) };
+          });
+        },
         async getPref(name) {
           try {
             return Services.prefs.getBoolPref(
               `${extensionPrefNameBase}${name}`
             );
           } catch (_) {
             return undefined;
           }
--- a/browser/extensions/webcompat/experiment-apis/aboutConfigPrefs.json
+++ b/browser/extensions/webcompat/experiment-apis/aboutConfigPrefs.json
@@ -19,16 +19,29 @@
             "type": "string",
             "description": "The preference to monitor"
           }
         ]
       }
     ],
     "functions": [
       {
+        "name": "getBranch",
+        "type": "function",
+        "description": "Get all child prefs for a branch",
+        "parameters": [
+          {
+            "name": "branchName",
+            "type": "string",
+            "description": "The branch name"
+          }
+        ],
+        "async": true
+      },
+      {
         "name": "getPref",
         "type": "function",
         "description": "Get a preference's value",
         "parameters": [
           {
             "name": "name",
             "type": "string",
             "description": "The preference name"
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/experiment-apis/appConstants.js
@@ -0,0 +1,32 @@
+/* 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";
+
+/* global ExtensionAPI, XPCOMUtils */
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  AppConstants: "resource://gre/modules/AppConstants.jsm",
+});
+
+this.appConstants = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      appConstants: {
+        getReleaseBranch: () => {
+          if (AppConstants.NIGHTLY_BUILD) {
+            return "nightly";
+          } else if (AppConstants.MOZ_DEV_EDITION) {
+            return "dev_edition";
+          } else if (AppConstants.EARLY_BETA_OR_EARLIER) {
+            return "early_beta_or_earlier";
+          } else if (AppConstants.BETA_OR_RELEASE) {
+            return "beta_or_release";
+          }
+          return "unknown";
+        },
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/experiment-apis/appConstants.json
@@ -0,0 +1,15 @@
+[
+  {
+    "namespace": "appConstants",
+    "description": "experimental API to expose some app constants",
+    "functions": [
+      {
+        "name": "getReleaseBranch",
+        "type": "function",
+        "description": "",
+        "async": true,
+        "parameters": []
+      }
+    ]
+  }
+]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/experiment-apis/matchPatterns.js
@@ -0,0 +1,30 @@
+/* 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";
+
+/* global ExtensionAPI */
+
+this.matchPatterns = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      matchPatterns: {
+        getMatcher(patterns) {
+          const set = new MatchPatternSet(patterns);
+          return Cu.cloneInto(
+            {
+              matches: url => {
+                return set.matches(url);
+              },
+            },
+            context.cloneScope,
+            {
+              cloneFunctions: true,
+            }
+          );
+        },
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/experiment-apis/matchPatterns.json
@@ -0,0 +1,29 @@
+[
+  {
+    "namespace": "matchPatterns",
+    "description": "experimental API extension to expose MatchPattern functionality",
+    "functions": [
+      {
+        "name": "getMatcher",
+        "type": "function",
+        "description": "get a MatchPatternSet",
+        "parameters": [
+          {
+            "name": "patterns",
+            "description": "Array of string URL patterns to whitelist",
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          }
+        ],
+        "returns": {
+          "type": "object",
+          "properties": {
+            "matches": { "type": "function" }
+          }
+        }
+      }
+    ]
+  }
+]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/experiment-apis/trackingProtection.js
@@ -0,0 +1,163 @@
+/* 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";
+
+/* global ExtensionAPI, ExtensionCommon, ExtensionParent, Services, XPCOMUtils */
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  Services: "resource://gre/modules/Services.jsm",
+});
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["URL", "ChannelWrapper"]);
+
+class Manager {
+  constructor() {
+    this._allowLists = new Map();
+  }
+
+  _ensureStarted() {
+    if (this._classifierObserver) {
+      return;
+    }
+
+    this._unblockedChannelIds = new Set();
+    this._channelClassifier = Cc[
+      "@mozilla.org/url-classifier/channel-classifier-service;1"
+    ].getService(Ci.nsIChannelClassifierService);
+    this._classifierObserver = {};
+    this._classifierObserver.observe = (subject, topic, data) => {
+      switch (topic) {
+        case "http-on-stop-request": {
+          const { channelId } = subject.QueryInterface(Ci.nsIIdentChannel);
+          this._unblockedChannelIds.delete(channelId);
+          break;
+        }
+        case "urlclassifier-before-block-channel": {
+          const channel = subject.QueryInterface(
+            Ci.nsIUrlClassifierBlockedChannel
+          );
+          const { channelId, topLevelUrl, url } = channel;
+          const topHost = new URL(topLevelUrl).hostname;
+          for (const allowList of this._allowLists.values()) {
+            for (const entry of allowList.values()) {
+              const { matcher, hosts, notHosts } = entry;
+              if (matcher.matches(url)) {
+                if (
+                  !notHosts?.has(topHost) &&
+                  (hosts === true || hosts.has(topHost))
+                ) {
+                  this._unblockedChannelIds.add(channelId);
+                  channel.unblock();
+                  return;
+                }
+              }
+            }
+          }
+          break;
+        }
+      }
+    };
+    Services.obs.addObserver(this._classifierObserver, "http-on-stop-request");
+    this._channelClassifier.addListener(this._classifierObserver);
+  }
+
+  stop() {
+    if (!this._classifierObserver) {
+      return;
+    }
+
+    Services.obs.removeObserver(
+      this._classifierObserver,
+      "http-on-stop-request"
+    );
+    this._channelClassifier.removeListener(this._classifierObserver);
+    delete this._channelClassifier;
+    delete this._classifierObserver;
+  }
+
+  wasChannelIdUnblocked(channelId) {
+    return this._unblockedChannelIds.has(channelId);
+  }
+
+  allow(allowListId, patterns, { hosts, notHosts }) {
+    this._ensureStarted();
+
+    if (!this._allowLists.has(allowListId)) {
+      this._allowLists.set(allowListId, new Map());
+    }
+    const allowList = this._allowLists.get(allowListId);
+    for (const pattern of patterns) {
+      if (!allowList.has(pattern)) {
+        allowList.set(pattern, {
+          matcher: new MatchPattern(pattern),
+        });
+      }
+      const allowListPattern = allowList.get(pattern);
+      if (!hosts) {
+        allowListPattern.hosts = true;
+      } else {
+        if (!allowListPattern.hosts) {
+          allowListPattern.hosts = new Set();
+        }
+        for (const host of hosts) {
+          allowListPattern.hosts.add(host);
+        }
+      }
+      if (notHosts) {
+        if (!allowListPattern.notHosts) {
+          allowListPattern.notHosts = new Set();
+        }
+        for (const notHost of notHosts) {
+          allowListPattern.notHosts.add(notHost);
+        }
+      }
+    }
+  }
+
+  revoke(allowListId) {
+    this._allowLists.delete(allowListId);
+  }
+}
+var manager = new Manager();
+
+function getChannelId(context, requestId) {
+  const wrapper = ChannelWrapper.getRegisteredChannel(
+    requestId,
+    context.extension.policy,
+    context.xulBrowser.frameLoader.remoteTab
+  );
+  return wrapper?.channel?.QueryInterface(Ci.nsIIdentChannel)?.channelId;
+}
+
+this.trackingProtection = class extends ExtensionAPI {
+  onShutdown(isAppShutdown) {
+    if (manager) {
+      manager.stop();
+    }
+  }
+
+  getAPI(context) {
+    return {
+      trackingProtection: {
+        async allow(allowListId, patterns, options) {
+          manager.allow(allowListId, patterns, options);
+        },
+        async revoke(allowListId) {
+          manager.revoke(allowListId);
+        },
+        async wasRequestUnblocked(requestId) {
+          if (!manager) {
+            return false;
+          }
+          const channelId = getChannelId(context, requestId);
+          if (!channelId) {
+            return false;
+          }
+          return manager.wasChannelIdUnblocked(channelId);
+        },
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/experiment-apis/trackingProtection.json
@@ -0,0 +1,75 @@
+[
+  {
+    "namespace": "trackingProtection",
+    "description": "experimental API allow requests through ETP",
+    "functions": [
+      {
+        "name": "allow",
+        "type": "function",
+        "description": "Add specific requests to a given allow-list",
+        "parameters": [
+          {
+            "name": "allowlistId",
+            "type": "string"
+          },
+          {
+            "name": "patterns",
+            "description": "Array of match patterns",
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "options",
+            "type": "object",
+            "optional": true,
+            "properties": {
+              "hosts": {
+                "description": "Hosts to limit this bypass to (optional)",
+                "type": "array",
+                "items": {
+                  "type": "string"
+                },
+                "optional": true
+              },
+              "notHosts": {
+                "description": "Hosts to not allow this bypass for (optional)",
+                "type": "array",
+                "items": {
+                  "type": "string"
+                },
+                "optional": true
+              }
+            }
+          }
+        ],
+        "async": true
+      },
+      {
+        "name": "revoke",
+        "type": "function",
+        "description": "Revokes the given allow-list",
+        "parameters": [
+          {
+            "name": "allowListId",
+            "type": "string"
+          }
+        ],
+        "async": true
+      },
+      {
+        "name": "wasRequestUnblocked",
+        "type": "function",
+        "description": "Whether the given requestId was unblocked by any allowList",
+        "parameters": [
+          {
+            "name": "requestId",
+            "type": "string"
+          }
+        ],
+        "async": true
+      }
+    ]
+  }
+]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/lib/shim_messaging_helper.js
@@ -0,0 +1,65 @@
+/* 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 */
+
+if (!window.Shims) {
+  window.Shims = new Map();
+}
+
+if (!window.ShimsHelperReady) {
+  window.ShimsHelperReady = true;
+
+  browser.runtime.onMessage.addListener(details => {
+    const { shimId, warning } = details;
+    if (!shimId) {
+      return;
+    }
+    window.Shims.set(shimId, details);
+    if (warning) {
+      console.warn(warning);
+    }
+  });
+
+  async function handleMessage(port, shimId, messageId, message) {
+    let response;
+    const shim = window.Shims.get(shimId);
+    if (shim) {
+      const { needsShimHelpers, origin } = shim;
+      if (origin === location.origin) {
+        if (needsShimHelpers?.includes(message)) {
+          const msg = { shimId, message };
+          try {
+            response = await browser.runtime.sendMessage(msg);
+          } catch (_) {}
+        }
+      }
+    }
+    port.postMessage({ messageId, response });
+  }
+
+  window.addEventListener(
+    "ShimConnects",
+    e => {
+      e.stopPropagation();
+      e.preventDefault();
+      const { port, pendingMessages, shimId } = e.detail;
+      const shim = window.Shims.get(shimId);
+      if (!shim) {
+        return;
+      }
+      port.onmessage = ({ data }) => {
+        handleMessage(port, shimId, data.messageId, data.message);
+      };
+      for (const [messageId, message] of pendingMessages) {
+        handleMessage(port, shimId, messageId, message);
+      }
+    },
+    true
+  );
+
+  window.dispatchEvent(new CustomEvent("ShimHelperReady"));
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/lib/shims.js
@@ -0,0 +1,410 @@
+/* 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 */
+
+const releaseBranchPromise = browser.appConstants.getReleaseBranch();
+
+const platformPromise = browser.runtime.getPlatformInfo().then(info => {
+  return info.os === "android" ? "android" : "desktop";
+});
+
+let debug = async function() {
+  if ((await releaseBranchPromise) !== "beta_or_release") {
+    console.debug.apply(this, arguments);
+  }
+};
+let error = async function() {
+  if ((await releaseBranchPromise) !== "beta_or_release") {
+    console.error.apply(this, arguments);
+  }
+};
+let warn = async function() {
+  if ((await releaseBranchPromise) !== "beta_or_release") {
+    console.warn.apply(this, arguments);
+  }
+};
+
+class Shim {
+  constructor(opts) {
+    const { matches, unblocksOnOptIn } = opts;
+
+    this.branches = opts.branches;
+    this.bug = opts.bug;
+    this.file = opts.file;
+    this.hosts = opts.hosts;
+    this.id = opts.id;
+    this.matches = matches;
+    this.name = opts.name;
+    this.notHosts = opts.notHosts;
+    this.onlyIfBlockedByETP = opts.onlyIfBlockedByETP;
+    this._options = opts.options || {};
+    this.needsShimHelpers = opts.needsShimHelpers;
+    this.platform = opts.platform || "all";
+    this.unblocksOnOptIn = unblocksOnOptIn;
+
+    this._hostOptIns = new Set();
+
+    this._disabledByConfig = opts.disabled;
+    this._disabledGlobally = false;
+    this._disabledByPlatform = false;
+    this._disabledByReleaseBranch = false;
+
+    const pref = `disabled_shims.${this.id}`;
+
+    browser.aboutConfigPrefs.onPrefChange.addListener(async () => {
+      const value = await browser.aboutConfigPrefs.getPref(pref);
+      this._disabledPrefValue = value;
+      this._onEnabledStateChanged();
+    }, pref);
+
+    this.ready = Promise.all([
+      browser.aboutConfigPrefs.getPref(pref).then(value => {
+        this._disabledPrefValue = value;
+      }),
+      platformPromise.then(platform => {
+        this._disabledByPlatform =
+          this.platform !== "all" && this.platform !== platform;
+        return platform;
+      }),
+      releaseBranchPromise.then(branch => {
+        this._disabledByReleaseBranch =
+          this.branches && !this.branches.includes(branch);
+        return branch;
+      }),
+    ]).then(([_, platform, branch]) => {
+      this._preprocessOptions(platform, branch);
+      this._onEnabledStateChanged();
+    });
+  }
+
+  _preprocessOptions(platform, branch) {
+    // options may be any value, but can optionally be gated for specified
+    // platform/branches, if in the format `{value, branches, platform}`
+    this.options = {};
+    for (const [k, v] of Object.entries(this._options)) {
+      if (v?.value) {
+        if (
+          (!v.platform || v.platform === platform) &&
+          (!v.branches || v.branches.includes(branch))
+        ) {
+          this.options[k] = v.value;
+        }
+      } else {
+        this.options[k] = v;
+      }
+    }
+  }
+
+  get enabled() {
+    if (this._disabledGlobally) {
+      return false;
+    }
+
+    if (this._disabledPrefValue !== undefined) {
+      return !this._disabledPrefValue;
+    }
+
+    return (
+      !this._disabledByConfig &&
+      !this._disabledByPlatform &&
+      !this._disabledByReleaseBranch
+    );
+  }
+
+  enable() {
+    this._disabledGlobally = false;
+    this._onEnabledStateChanged();
+  }
+
+  disable() {
+    this._disabledGlobally = true;
+    this._onEnabledStateChanged();
+  }
+
+  _onEnabledStateChanged() {
+    if (!this.enabled) {
+      return this._revokeRequestsInETP();
+    }
+    return this._allowRequestsInETP();
+  }
+
+  _allowRequestsInETP() {
+    return browser.trackingProtection.allow(this.id, this.matches, {
+      hosts: this.hosts,
+      notHosts: this.notHosts,
+    });
+  }
+
+  _revokeRequestsInETP() {
+    return browser.trackingProtection.revoke(this.id);
+  }
+
+  meantForHost(host) {
+    const { hosts, notHosts } = this;
+    if (hosts || notHosts) {
+      if (
+        (notHosts && notHosts.includes(host)) ||
+        (hosts && !hosts.includes(host))
+      ) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  isTriggeredByURL(url) {
+    if (!this.matches) {
+      return false;
+    }
+
+    if (!this._matcher) {
+      this._matcher = browser.matchPatterns.getMatcher(this.matches);
+    }
+
+    return this._matcher.matches(url);
+  }
+
+  async onUserOptIn(host) {
+    const { unblocksOnOptIn } = this;
+    if (unblocksOnOptIn) {
+      await browser.trackingProtection.allow(this.id, unblocksOnOptIn, {
+        hosts: [host],
+      });
+    }
+
+    this._hostOptIns.add(host);
+  }
+
+  hasUserOptedInAlready(host) {
+    return this._hostOptIns.has(host);
+  }
+}
+
+class Shims {
+  constructor(availableShims) {
+    if (!browser.trackingProtection) {
+      console.error("Required experimental add-on APIs for shims unavailable");
+      return;
+    }
+
+    this._registerShims(availableShims);
+
+    browser.runtime.onMessage.addListener(this._onMessageFromShim.bind(this));
+
+    this.ENABLED_PREF = "enable_shims";
+    browser.aboutConfigPrefs.onPrefChange.addListener(() => {
+      this._checkEnabledPref();
+    }, this.ENABLED_PREF);
+    this._haveCheckedEnabledPref = this._checkEnabledPref();
+  }
+
+  _registerShims(shims) {
+    if (this.shims) {
+      throw new Error("_registerShims has already been called");
+    }
+
+    this.shims = new Map();
+    for (const shimOpts of shims) {
+      const { id } = shimOpts;
+      if (!this.shims.has(id)) {
+        this.shims.set(shimOpts.id, new Shim(shimOpts));
+      }
+    }
+
+    const allShimPatterns = new Set();
+    for (const { matches } of this.shims.values()) {
+      for (const matchPattern of matches) {
+        allShimPatterns.add(matchPattern);
+      }
+    }
+
+    if (!allShimPatterns.size) {
+      debug("Skipping shims; none enabled");
+      return;
+    }
+
+    const urls = [...allShimPatterns];
+    debug("Shimming these match patterns", urls);
+
+    browser.webRequest.onBeforeRequest.addListener(
+      this._ensureShimForRequestOnTab.bind(this),
+      { urls, types: ["script"] },
+      ["blocking"]
+    );
+  }
+
+  async _checkEnabledPref() {
+    await browser.aboutConfigPrefs.getPref(this.ENABLED_PREF).then(value => {
+      if (value === undefined) {
+        browser.aboutConfigPrefs.setPref(this.ENABLED_PREF, true);
+      } else if (value === false) {
+        this.enabled = false;
+      } else {
+        this.enabled = true;
+      }
+    });
+  }
+
+  get enabled() {
+    return this._enabled;
+  }
+
+  set enabled(enabled) {
+    if (enabled === this._enabled) {
+      return;
+    }
+
+    this._enabled = enabled;
+
+    for (const shim of this.shims.values()) {
+      if (enabled) {
+        shim.enable();
+      } else {
+        shim.disable();
+      }
+    }
+  }
+
+  async _onMessageFromShim(payload, sender, sendResponse) {
+    const { tab } = sender;
+    const { id, url } = tab;
+    if (sender.id !== browser.runtime.id || id === -1) {
+      throw new Error("not allowed");
+    }
+
+    // Important! It is entirely possible for sites to spoof
+    // these messages, due to shims allowing web pages to
+    // communicate with the extension.
+
+    const { shimId, message } = payload;
+
+    const shim = this.shims.get(shimId);
+    if (!shim?.needsShimHelpers?.includes(message)) {
+      throw new Error("not allowed");
+    }
+
+    if (message === "getOptions") {
+      return shim.options;
+    } else if (message === "optIn") {
+      try {
+        await shim.onUserOptIn(new URL(url).hostname);
+        warn("** User opted in on tab ", id, "for", shimId);
+      } catch (err) {
+        console.error(err);
+        throw new Error("error");
+      }
+    }
+
+    return undefined;
+  }
+
+  async _ensureShimForRequestOnTab(details) {
+    await this._haveCheckedEnabledPref;
+
+    if (!this.enabled) {
+      return undefined;
+    }
+
+    // We only ever reach this point if a request is for a URL which ought to
+    // be shimmed. We never get here if a request is blocked, and we only
+    // unblock requests if at least one shim matches it.
+
+    const { frameId, originUrl, requestId, tabId, url } = details;
+
+    // Ignore requests unrelated to tabs
+    if (tabId < 0) {
+      return undefined;
+    }
+
+    // We need to base our checks not on the frame's host, but the tab's.
+    const topHost = new URL((await browser.tabs.get(tabId)).url).hostname;
+    const unblocked = await browser.trackingProtection.wasRequestUnblocked(
+      requestId
+    );
+
+    let shimToApply;
+    for (const shim of this.shims.values()) {
+      await shim.ready;
+
+      if (!shim.enabled) {
+        continue;
+      }
+
+      // Do not apply the shim if it is only meant to apply when strict mode ETP
+      // (content blocking) was going to block the request.
+      if (!unblocked && shim.onlyIfBlockedByETP) {
+        continue;
+      }
+
+      if (!shim.meantForHost(topHost)) {
+        continue;
+      }
+
+      // If the user has already opted in for this shim, all requests it covers
+      // should be allowed; no need for a shim anymore.
+      if (shim.hasUserOptedInAlready(topHost)) {
+        return undefined;
+      }
+
+      // If this URL isn't meant for this shim, don't apply it.
+      if (!shim.isTriggeredByURL(url)) {
+        continue;
+      }
+
+      shimToApply = shim;
+      break;
+    }
+
+    if (shimToApply) {
+      // Note that sites may request the same shim twice, but because the requests
+      // may differ enough for some to fail (CSP/CORS/etc), we always re-run the
+      // shim JS just in case. Shims should gracefully handle this as well.
+      const { bug, file, id, name, needsShimHelpers } = shimToApply;
+      warn("Shimming", name, "on tabId", tabId, "frameId", frameId);
+
+      const warning = `${name} is being shimmed by Firefox. See https://bugzilla.mozilla.org/show_bug.cgi?id=${bug} for details.`;
+
+      try {
+        if (needsShimHelpers?.length) {
+          await browser.tabs.executeScript(tabId, {
+            file: "/lib/shim_messaging_helper.js",
+            frameId,
+            runAt: "document_start",
+          });
+          const origin = new URL(originUrl).origin;
+          await browser.tabs.sendMessage(
+            tabId,
+            { origin, shimId: id, needsShimHelpers, warning },
+            { frameId }
+          );
+        } else {
+          await browser.tabs.executeScript(tabId, {
+            code: `console.warn(${JSON.stringify(warning)})`,
+            frameId,
+            runAt: "document_start",
+          });
+        }
+      } catch (_) {}
+
+      // If any shims matched the script to replace it, then let the original
+      // request complete without ever hitting the network, with a blank script.
+      return { redirectUrl: browser.runtime.getURL(`shims/${file}`) };
+    }
+
+    // Sanity check: if no shims are over-riding a given URL and it was meant to
+    // be blocked by ETP, then block it.
+    if (unblocked) {
+      error("unexpected:", url, "was not shimmed, and had to be re-blocked");
+      return { cancel: true };
+    }
+
+    debug("allowing", url);
+    return undefined;
+  }
+}
+
+module.exports = Shims;
--- a/browser/extensions/webcompat/manifest.json
+++ b/browser/extensions/webcompat/manifest.json
@@ -1,13 +1,13 @@
 {
   "manifest_version": 2,
   "name": "Web Compat",
   "description": "Urgent post-release fixes for web compatibility.",
-  "version": "13.0.0",
+  "version": "13.1.0",
 
   "applications": {
     "gecko": {
       "id": "webcompat@mozilla.org",
       "strict_min_version": "59.0b5"
     }
   },
 
@@ -15,32 +15,48 @@
     "aboutConfigPrefs": {
       "schema": "experiment-apis/aboutConfigPrefs.json",
       "parent": {
         "scopes": ["addon_parent"],
         "script": "experiment-apis/aboutConfigPrefs.js",
         "paths": [["aboutConfigPrefs"]]
       }
     },
+    "appConstants": {
+      "schema": "experiment-apis/appConstants.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "experiment-apis/appConstants.js",
+        "paths": [["appConstants"]]
+      }
+    },
     "aboutPage": {
       "schema": "about-compat/aboutPage.json",
       "parent": {
         "scopes": ["addon_parent"],
         "script": "about-compat/aboutPage.js",
         "events": ["startup"]
       }
     },
     "experiments": {
       "schema": "experiment-apis/experiments.json",
       "parent": {
         "scopes": ["addon_parent"],
         "script": "experiment-apis/experiments.js",
         "paths": [["experiments"]]
       }
     },
+    "matchPatterns": {
+      "schema": "experiment-apis/matchPatterns.json",
+      "child": {
+        "scopes": ["addon_child"],
+        "script": "experiment-apis/matchPatterns.js",
+        "paths": [["matchPatterns"]]
+      }
+    },
     "pictureInPictureChild": {
       "schema": "experiment-apis/pictureInPicture.json",
       "child": {
         "scopes": ["addon_child"],
         "script": "experiment-apis/pictureInPicture.js",
         "paths": [["pictureInPictureChild"]]
       }
     },
@@ -62,31 +78,65 @@
     },
     "systemManufacturer": {
       "schema": "experiment-apis/systemManufacturer.json",
       "child": {
         "scopes": ["addon_child"],
         "script": "experiment-apis/systemManufacturer.js",
         "paths": [["systemManufacturer"]]
       }
+    },
+    "trackingProtection": {
+      "schema": "experiment-apis/trackingProtection.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "experiment-apis/trackingProtection.js",
+        "paths": [["trackingProtection"]]
+      }
     }
   },
 
   "content_security_policy": "script-src 'self' 'sha256-MmZkN2QaIHhfRWPZ8TVRjijTn5Ci1iEabtTEWrt9CCo='; default-src 'self'; base-uri moz-extension://*; object-src 'none'",
 
-  "permissions": ["webRequest", "webRequestBlocking", "<all_urls>"],
+  "permissions": [
+    "tabs",
+    "webNavigation",
+    "webRequest",
+    "webRequestBlocking",
+    "<all_urls>"
+  ],
 
   "background": {
     "scripts": [
       "lib/module_shim.js",
       "lib/intervention_helpers.js",
       "data/injections.js",
       "data/picture_in_picture_overrides.js",
+      "data/shims.js",
       "data/ua_overrides.js",
       "lib/about_compat_broker.js",
       "lib/custom_functions.js",
       "lib/injections.js",
       "lib/picture_in_picture_overrides.js",
+      "lib/shims.js",
       "lib/ua_overrides.js",
       "run.js"
     ]
-  }
+  },
+
+  "web_accessible_resources": [
+    "shims/adsafeprotected-ima.js",
+    "shims/bmauth.js",
+    "shims/eluminate.js",
+    "shims/empty-script.js",
+    "shims/facebook-sdk.js",
+    "shims/google-analytics-ecommerce-plugin.js",
+    "shims/google-analytics-legacy.js",
+    "shims/google-analytics.js",
+    "shims/google-publisher-tags.js",
+    "shims/live-test-shim.js",
+    "shims/mochitest-shim-1.js",
+    "shims/mochitest-shim-2.js",
+    "shims/mochitest-shim-3.js",
+    "shims/rambler-authenticator.js",
+    "shims/rich-relevance.js"
+  ]
 }
--- a/browser/extensions/webcompat/moz.build
+++ b/browser/extensions/webcompat/moz.build
@@ -20,30 +20,37 @@ FINAL_TARGET_FILES.features['webcompat@m
   'about-compat/aboutPage.js',
   'about-compat/aboutPage.json',
   'about-compat/aboutPageProcessScript.js',
 ]
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org']['data'] += [
   'data/injections.js',
   'data/picture_in_picture_overrides.js',
+  'data/shims.js',
   'data/ua_overrides.js',
 ]
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org']['experiment-apis'] += [
   'experiment-apis/aboutConfigPrefs.js',
   'experiment-apis/aboutConfigPrefs.json',
+  'experiment-apis/appConstants.js',
+  'experiment-apis/appConstants.json',
   'experiment-apis/experiments.js',
   'experiment-apis/experiments.json',
+  'experiment-apis/matchPatterns.js',
+  'experiment-apis/matchPatterns.json',
   'experiment-apis/pictureInPicture.js',
   'experiment-apis/pictureInPicture.json',
   'experiment-apis/sharedPreferences.js',
   'experiment-apis/sharedPreferences.json',
   'experiment-apis/systemManufacturer.js',
   'experiment-apis/systemManufacturer.json',
+  'experiment-apis/trackingProtection.js',
+  'experiment-apis/trackingProtection.json',
 ]
 
 FINAL_TARGET_FILES.features['webcompat@mozilla.org']['injections']['css'] += [
   'injections/css/bug0000000-testbed-css-injection.css',
   'injections/css/bug1561371-mail.google.com-allow-horizontal-scrolling.css',
   'injections/css/bug1567610-dns.google.com-moz-fit-content.css',
   'injections/css/bug1568908-console.cloud.google.com-scrollbar-fix.css',
   'injections/css/bug1570119-teamcoco.com-scrollbar-width.css',
@@ -68,24 +75,46 @@ FINAL_TARGET_FILES.features['webcompat@m
   'injections/js/bug1570856-medium.com-menu-isTier1.js',
   'injections/js/bug1579159-m.tailieu.vn-pdfjs-worker-disable.js',
   'injections/js/bug1605611-maps.google.com-directions-time.js',
   'injections/js/bug1610358-pcloud.com-appVersion-change.js',
   'injections/js/bug1623375-salesforce-communities-hide-unsupported.js',
   'injections/js/bug1641998-embedded-twitter-videos-etp-indexeddb.js',
 ]
 
+FINAL_TARGET_FILES.features['webcompat@mozilla.org']['shims'] += [
+  'shims/adsafeprotected-ima.js',
+  'shims/bmauth.js',
+  'shims/eluminate.js',
+  'shims/empty-script.js',
+  'shims/facebook-sdk.js',
+  'shims/google-analytics-ecommerce-plugin.js',
+  'shims/google-analytics-legacy.js',
+  'shims/google-analytics.js',
+  'shims/google-publisher-tags.js',
+  'shims/live-test-shim.js',
+  'shims/mochitest-shim-1.js',
+  'shims/mochitest-shim-2.js',
+  'shims/mochitest-shim-3.js',
+  'shims/rambler-authenticator.js',
+  'shims/rich-relevance.js',
+]
+
 FINAL_TARGET_FILES.features['webcompat@mozilla.org']['lib'] += [
   'lib/about_compat_broker.js',
   'lib/custom_functions.js',
   'lib/injections.js',
   'lib/intervention_helpers.js',
   'lib/module_shim.js',
   'lib/picture_in_picture_overrides.js',
+  'lib/shim_messaging_helper.js',
+  'lib/shims.js',
   'lib/ua_overrides.js',
 ]
 
 XPCOM_MANIFESTS += [
     'components.conf',
 ]
 
+BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
+
 with Files('**'):
   BUG_COMPONENT = ('Web Compatibility', 'Tooling & Investigations')
--- a/browser/extensions/webcompat/run.js
+++ b/browser/extensions/webcompat/run.js
@@ -1,21 +1,22 @@
 /* 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 AVAILABLE_INJECTIONS, AVAILABLE_UA_OVERRIDES, AboutCompatBroker,
-           Injections, UAOverrides, CUSTOM_FUNCTIONS, AVAILABLE_PIP_OVERRIDES,
-           PictureInPictureOverrides */
+/* globals AboutCompatBroker, AVAILABLE_INJECTIONS, AVAILABLE_SHIMS,
+           AVAILABLE_PIP_OVERRIDES, AVAILABLE_UA_OVERRIDES, CUSTOM_FUNCTIONS,
+           Injections, PictureInPictureOverrides, Shims, UAOverrides */
 
 const injections = new Injections(AVAILABLE_INJECTIONS, CUSTOM_FUNCTIONS);
 const uaOverrides = new UAOverrides(AVAILABLE_UA_OVERRIDES);
 const pipOverrides = new PictureInPictureOverrides(AVAILABLE_PIP_OVERRIDES);
+const shims = new Shims(AVAILABLE_SHIMS);
 
 const aboutCompatBroker = new AboutCompatBroker({
   injections,
   uaOverrides,
 });
 
 aboutCompatBroker.bootup();
 injections.bootup();
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/shims/adsafeprotected-ima.js
@@ -0,0 +1,69 @@
+/* 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";
+
+/**
+ * Bug 1508639 - Shim Ad Safe Protected's Google IMA adapter
+ */
+
+if (!window.googleImaVansAdapter) {
+  const shimId = "AdSafeProtectedGoogleIMAAdapter";
+
+  const sendMessageToAddon = (function() {
+    const pendingMessages = new Map();
+    const channel = new MessageChannel();
+    channel.port1.onerror = console.error;
+    channel.port1.onmessage = event => {
+      const { messageId, response } = event.data;
+      const resolve = pendingMessages.get(messageId);
+      if (resolve) {
+        pendingMessages.delete(messageId);
+        resolve(response);
+      }
+    };
+    function reconnect() {
+      const detail = {
+        pendingMessages: [...pendingMessages.values()],
+        port: channel.port2,
+        shimId,
+      };
+      window.dispatchEvent(new CustomEvent("ShimConnects", { detail }));
+    }
+    window.addEventListener("ShimHelperReady", reconnect);
+    reconnect();
+    return function(message) {
+      const messageId =
+        Math.random()
+          .toString(36)
+          .substring(2) + Date.now().toString(36);
+      return new Promise(resolve => {
+        const payload = {
+          message,
+          messageId,
+          shimId,
+        };
+        pendingMessages.set(messageId, resolve);
+        channel.port1.postMessage(payload);
+      });
+    };
+  })();
+
+  window.googleImaVansAdapter = {
+    init: () => {},
+    dispose: () => {},
+  };
+
+  // Treat it as an opt-in when the user clicks on a video
+  // TODO: Improve this! It races to tell the bg script to unblock the ad from
+  // https://pubads.g.doubleclick.net/gampad/ads before the page loads them.
+  async function click(e) {
+    if (e.isTrusted && e.target.closest("#video-player")) {
+      document.documentElement.removeEventListener("click", click, true);
+      await sendMessageToAddon("optIn");
+      // TODO: reload ima3.js?
+    }
+  }
+  document.documentElement.addEventListener("click", click, true);
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/shims/bmauth.js
@@ -0,0 +1,21 @@
+/* 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";
+
+if (!window.BmAuth) {
+  window.BmAuth = {
+    init: () => new Promise(() => {}),
+    handleSignIn: () => {
+      // TODO: handle this properly!
+    },
+    isAuthenticated: () => Promise.resolve(false),
+    addListener: () => {},
+    api: {
+      event: {
+        addListener: () => {},
+      },
+    },
+  };
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/shims/eluminate.js
@@ -0,0 +1,68 @@
+/* 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";
+
+if (!window.CM_DDX) {
+  window.CM_DDX = {
+    domReadyFired: false,
+    headScripts: true,
+    dispatcherLoadRequested: false,
+    firstPassFunctionBinding: false,
+    BAD_PAGE_ID_ELAPSED_TIMEOUT: 5000,
+    version: -1,
+    standalone: false,
+    test: {
+      syndicate: true,
+      testCounter: "",
+      doTest: false,
+      newWin: false,
+      process: () => {},
+    },
+    partner: {},
+    invokeFunctionWhenAvailable: a => {
+      a();
+    },
+    gup: d => "",
+    privacy: {
+      isDoNotTrackEnabled: () => false,
+      setDoNotTrack: () => {},
+      getDoNotTrack: () => false,
+    },
+    setSubCookie: () => {},
+  };
+
+  const noopfn = () => {};
+  const w = window;
+  w.cmAddShared = noopfn;
+  w.cmCalcSKUString = noopfn;
+  w.cmCreateManualImpressionTag = noopfn;
+  w.cmCreateManualLinkClickTag = noopfn;
+  w.cmCreateManualPageviewTag = noopfn;
+  w.cmCreateOrderTag = noopfn;
+  w.cmCreatePageviewTag = noopfn;
+  w.cmRetrieveUserID = noopfn;
+  w.cmSetClientID = noopfn;
+  w.cmSetCurrencyCode = noopfn;
+  w.cmSetFirstPartyIDs = noopfn;
+  w.cmSetSubCookie = noopfn;
+  w.cmSetupCookieMigration = noopfn;
+  w.cmSetupNormalization = noopfn;
+  w.cmSetupOther = noopfn;
+  w.cmStartTagSet = noopfn;
+
+  function cmExecuteTagQueue() {
+    var b = window.cmTagQueue;
+    if (b) {
+      if (!Array.isArray(b)) {
+        return undefined;
+      }
+      for (var a = 0; a < b.length; ++a) {
+        window[b[a][0]].apply(window, b[a].slice(1));
+      }
+    }
+    return true;
+  }
+  cmExecuteTagQueue();
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/shims/empty-script.js
@@ -0,0 +1,5 @@
+/* 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 script is intentionally empty */
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/shims/facebook-sdk.js
@@ -0,0 +1,198 @@
+/* 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";
+
+/**
+ * Bug 1226498 - Shim Facebook SDK
+ *
+ * The Facebook SDK is commonly used by sites to allow users to authenticate
+ * for logins, but it is blocked by strict tracking protection. It is possible
+ * to shim the SDK and allow users to still opt into logging in via Facebook.
+ * It is also possible to replace any Facebook widgets or comments with
+ * placeholders that the user may click to opt into loading the content.
+ */
+
+if (!window.FB) {
+  const originalUrl = document.currentScript.src;
+  const pendingParses = [];
+
+  function getGUID() {
+    return (
+      Math.random()
+        .toString(36)
+        .substring(2) + Date.now().toString(36)
+    );
+  }
+
+  const shimId = "FacebookSDK";
+
+  const sendMessageToAddon = (function() {
+    const pendingMessages = new Map();
+    const channel = new MessageChannel();
+    channel.port1.onerror = console.error;
+    channel.port1.onmessage = event => {
+      const { messageId, response } = event.data;
+      const resolve = pendingMessages.get(messageId);
+      if (resolve) {
+        pendingMessages.delete(messageId);
+        resolve(response);
+      }
+    };
+    function reconnect() {
+      const detail = {
+        pendingMessages: [...pendingMessages.values()],
+        port: channel.port2,
+        shimId,
+      };
+      window.dispatchEvent(new CustomEvent("ShimConnects", { detail }));
+    }
+    window.addEventListener("ShimHelperReady", reconnect);
+    reconnect();
+    return function(message) {
+      const messageId = getGUID();
+      return new Promise(resolve => {
+        const payload = {
+          message,
+          messageId,
+          shimId,
+        };
+        pendingMessages.set(messageId, resolve);
+        channel.port1.postMessage(payload);
+      });
+    };
+  })();
+
+  let ready = false;
+  let initInfo;
+  const needPopup =
+    !/app_runner/.test(window.name) && !/iframe_canvas/.test(window.name);
+  const popupName = getGUID();
+
+  if (needPopup) {
+    const oldWindowOpen = window.open;
+    window.open = function(href, name, params) {
+      try {
+        const url = new URL(href);
+        if (
+          url.protocol === "https:" &&
+          (url.hostname === "m.facebook.com" ||
+            url.hostname === "www.facebook.com") &&
+          url.pathname.endsWith("/oauth")
+        ) {
+          name = popupName;
+        }
+      } catch (_) {}
+      return oldWindowOpen.call(window, href, name, params);
+    };
+  }
+
+  async function allowFacebookSDK(callback) {
+    await sendMessageToAddon("optIn");
+
+    window.FB = undefined;
+    const oldInit = window.fbAsyncInit;
+    window.fbAsyncInit = () => {
+      ready = true;
+      if (typeof initInfo !== "undefined") {
+        window.FB.init(initInfo);
+      } else if (oldInit) {
+        oldInit();
+      }
+      if (callback) {
+        callback();
+      }
+    };
+
+    const s = document.createElement("script");
+    s.src = originalUrl;
+    await new Promise((resolve, reject) => {
+      s.onerror = reject;
+      s.onload = function() {
+        for (const args of pendingParses) {
+          window.FB.XFBML.parse.apply(window.FB.XFBML, args);
+        }
+        resolve();
+      };
+      document.head.appendChild(s);
+    });
+  }
+
+  function buildPopupParams() {
+    const { outerWidth, outerHeight, screenX, screenY } = window;
+    const { width, height } = window.screen;
+    const w = Math.min(width, 400);
+    const h = Math.min(height, 400);
+    const ua = navigator.userAgent;
+    const isMobile = ua.includes("Mobile") || ua.includes("Tablet");
+    const left = screenX + (screenX < 0 ? width : 0) + (outerWidth - w) / 2;
+    const top = screenY + (screenY < 0 ? height : 0) + (outerHeight - h) / 2.5;
+    let params = `left=${left},top=${top},width=${w},height=${h},scrollbars=1,toolbar=0,location=1`;
+    if (!isMobile) {
+      params = `${params},width=${w},height=${h}`;
+    }
+    return params;
+  }
+
+  async function doLogin(a, b) {
+    window.FB.login(a, b);
+  }
+
+  function proxy(name, fn) {
+    return function() {
+      if (ready) {
+        return window.FB[name].apply(this, arguments);
+      }
+      return fn.apply(this, arguments);
+    };
+  }
+
+  window.FB = {
+    api: proxy("api", () => {}),
+    AppEvents: {
+      EventNames: {},
+      logPageView: () => {},
+    },
+    Event: {
+      subscribe: () => {},
+    },
+    getAccessToken: proxy("getAccessToken", () => null),
+    getAuthResponse: proxy("getAuthResponse", () => {
+      return { status: "" };
+    }),
+    getLoginStatus: proxy("getLoginStatus", cb => {
+      cb({ status: "" });
+    }),
+    getUserID: proxy("getUserID", () => {}),
+    init: _initInfo => {
+      if (ready) {
+        doLogin(_initInfo);
+      } else {
+        initInfo = _initInfo; // in case the site is not using fbAsyncInit
+      }
+    },
+    login: (a, b) => {
+      // We have to load Facebook's script, and then wait for it to call
+      // window.open. By that time, the popup blocker will likely trigger.
+      // So we open a popup now with about:blank, and then make sure FB
+      // will re-use that same popup later.
+      if (needPopup) {
+        window.open("about:blank", popupName, buildPopupParams());
+      }
+      allowFacebookSDK(() => {
+        doLogin(a, b);
+      });
+    },
+    logout: proxy("logout", cb => cb()),
+    XFBML: {
+      parse: e => {
+        pendingParses.push([e]);
+      },
+    },
+  };
+
+  if (window.fbAsyncInit) {
+    window.fbAsyncInit();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/shims/google-analytics-ecommerce-plugin.js
@@ -0,0 +1,13 @@
+/* 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";
+
+if (!window.gaplugins) {
+  window.gaplugins = {};
+}
+
+if (!window.gaplugins.EC) {
+  window.gaplugins.EC = () => {};
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/shims/google-analytics-legacy.js
@@ -0,0 +1,133 @@
+/* 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/. */
+
+// based on https://github.com/gorhill/uBlock/blob/caa8e7d35ba61214a9d13e7d324b2bd2aa73237f/src/web_accessible_resources/google-analytics_ga.js
+
+"use strict";
+
+if (!window._gaq) {
+  function noopfn() {}
+
+  const gaq = {
+    Na: noopfn,
+    O: noopfn,
+    Sa: noopfn,
+    Ta: noopfn,
+    Va: noopfn,
+    _createAsyncTracker: noopfn,
+    _getAsyncTracker: noopfn,
+    _getPlugin: noopfn,
+    push: a => {
+      if (typeof a === "function") {
+        a();
+        return;
+      }
+      if (!Array.isArray(a)) {
+        return;
+      }
+      if (a[0] === "_link" && typeof a[1] === "string") {
+        window.location.assign(a[1]);
+      }
+      if (
+        a[0] === "_set" &&
+        a[1] === "hitCallback" &&
+        typeof a[2] === "function"
+      ) {
+        a[2]();
+      }
+    },
+  };
+
+  const tracker = {
+    _addIgnoredOrganic: noopfn,
+    _addIgnoredRef: noopfn,
+    _addItem: noopfn,
+    _addOrganic: noopfn,
+    _addTrans: noopfn,
+    _clearIgnoredOrganic: noopfn,
+    _clearIgnoredRef: noopfn,
+    _clearOrganic: noopfn,
+    _cookiePathCopy: noopfn,
+    _deleteCustomVar: noopfn,
+    _getName: noopfn,
+    _setAccount: noopfn,
+    _getAccount: noopfn,
+    _getClientInfo: noopfn,
+    _getDetectFlash: noopfn,
+    _getDetectTitle: noopfn,
+    _getLinkerUrl: a => a,
+    _getLocalGifPath: noopfn,
+    _getServiceMode: noopfn,
+    _getVersion: noopfn,
+    _getVisitorCustomVar: noopfn,
+    _initData: noopfn,
+    _link: noopfn,
+    _linkByPost: noopfn,
+    _setAllowAnchor: noopfn,
+    _setAllowHash: noopfn,
+    _setAllowLinker: noopfn,
+    _setCampContentKey: noopfn,
+    _setCampMediumKey: noopfn,
+    _setCampNameKey: noopfn,
+    _setCampNOKey: noopfn,
+    _setCampSourceKey: noopfn,
+    _setCampTermKey: noopfn,
+    _setCampaignCookieTimeout: noopfn,
+    _setCampaignTrack: noopfn,
+    _setClientInfo: noopfn,
+    _setCookiePath: noopfn,
+    _setCookiePersistence: noopfn,
+    _setCookieTimeout: noopfn,
+    _setCustomVar: noopfn,
+    _setDetectFlash: noopfn,
+    _setDetectTitle: noopfn,
+    _setDomainName: noopfn,
+    _setLocalGifPath: noopfn,
+    _setLocalRemoteServerMode: noopfn,
+    _setLocalServerMode: noopfn,
+    _setReferrerOverride: noopfn,
+    _setRemoteServerMode: noopfn,
+    _setSampleRate: noopfn,
+    _setSessionTimeout: noopfn,
+    _setSiteSpeedSampleRate: noopfn,
+    _setSessionCookieTimeout: noopfn,
+    _setVar: noopfn,
+    _setVisitorCookieTimeout: noopfn,
+    _trackEvent: noopfn,
+    _trackPageLoadTime: noopfn,
+    _trackPageview: noopfn,
+    _trackSocial: noopfn,
+    _trackTiming: noopfn,
+    _trackTrans: noopfn,
+    _visitCode: noopfn,
+  };
+
+  const gat = {
+    _anonymizeIP: noopfn,
+    _createTracker: noopfn,
+    _forceSSL: noopfn,
+    _getPlugin: noopfn,
+    _getTracker: () => tracker,
+    _getTrackerByName: () => tracker,
+    _getTrackers: noopfn,
+    aa: noopfn,
+    ab: noopfn,
+    hb: noopfn,
+    la: noopfn,
+    oa: noopfn,
+    pa: noopfn,
+    u: noopfn,
+  };
+
+  window._gat = gat;
+
+  const aa = window._gaq || [];
+  if (Array.isArray(aa)) {
+    while (aa[0]) {
+      gaq.push(aa.shift());
+    }
+  }
+
+  window._gaq = gaq.qf = gaq;
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/shims/google-analytics-tag-manager.js
@@ -0,0 +1,24 @@
+/* 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/. */
+
+// based on https://github.com/gorhill/uBlock/blob/caa8e7d35ba61214a9d13e7d324b2bd2aa73237f/src/web_accessible_resources/googletagmanager_gtm.js
+
+"use strict";
+
+if (!window.ga) {
+  window.ga = () => {};
+
+  try {
+    window.dataLayer.hide.end();
+  } catch (_) {}
+
+  const dl = window.dataLayer;
+  if (typeof dl.push === "function") {
+    dl.push = o => {
+      if (o instanceof Object && typeof o.eventCallback === "function") {
+        setTimeout(o.eventCallback, 1);
+      }
+    };
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/shims/google-analytics.js
@@ -0,0 +1,52 @@
+/* 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/. */
+
+// based on https://github.com/gorhill/uBlock/blob/8a1a8b103f56e4fcef1264e02dfd718a29bda006/src/web_accessible_resources/google-analytics_analytics.js
+
+"use strict";
+
+if (!window[window.GoogleAnalyticsObject || "ga"]) {
+  function ga() {
+    const len = arguments.length;
+    if (!len) {
+      return;
+    }
+    const args = Array.from(arguments);
+    let fn;
+    let a = args[len - 1];
+    if (a instanceof Object && a.hitCallback instanceof Function) {
+      fn = a.hitCallback;
+    } else {
+      const pos = args.indexOf("hitCallback");
+      if (pos !== -1 && args[pos + 1] instanceof Function) {
+        fn = args[pos + 1];
+      }
+    }
+    if (!(fn instanceof Function)) {
+      return;
+    }
+    try {
+      fn();
+    } catch (_) {}
+  }
+  ga.create = () => {};
+  ga.getByName = () => null;
+  ga.getAll = () => [];
+  ga.remove = () => {};
+  ga.loaded = true;
+
+  const gaName = window.GoogleAnalyticsObject || "ga";
+  const gaQueue = window[gaName];
+  window[gaName] = ga;
+
+  try {
+    window.dataLayer.hide.end();
+  } catch (_) {}
+
+  if (gaQueue instanceof Function && Array.isArray(gaQueue.q)) {
+    for (const entry of gaQueue.q) {
+      ga(...entry);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/shims/google-publisher-tags.js
@@ -0,0 +1,163 @@
+/* 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";
+
+/**
+ * Bug 1600538 - Shim Google Publisher Tags
+ */
+
+"use strict";
+
+if (!window.googletag?.apiReady) {
+  const noopfn = function() {};
+  const noopthisfn = function() {
+    return this;
+  };
+  const noopnullfn = function() {
+    return null;
+  };
+  const nooparrayfn = function() {
+    return [];
+  };
+  const noopstrfn = function() {
+    return "";
+  };
+
+  function newPassbackSlot() {
+    return {
+      display: noopfn,
+      get: noopnullfn,
+      set: noopthisfn,
+      setClickUrl: noopthisfn,
+      setTagForChildDirectedTreatment: noopthisfn,
+      setTargeting: noopthisfn,
+      updateTargetingFromMap: noopthisfn,
+    };
+  }
+
+  function display(id) {
+    const parent = document.getElementById(id);
+    if (parent) {
+      parent.appendChild(document.createElement("div"));
+    }
+  }
+
+  const companionAdsService = {
+    addEventListener: noopthisfn,
+    enableSyncLoading: noopfn,
+    setRefreshUnfilledSlots: noopfn,
+  };
+
+  const contentService = {
+    addEventListener: noopthisfn,
+    setContent: noopfn,
+  };
+
+  const pubadsService = {
+    addEventListener: noopthisfn,
+    clear: noopfn,
+    clearCategoryExclusions: noopthisfn,
+    clearTagForChildDirectedTreatment: noopthisfn,
+    clearTargeting: noopthisfn,
+    collapseEmptyDivs: noopfn,
+    defineOutOfPagePassback: () => newPassbackSlot(),
+    definePassback: () => newPassbackSlot(),
+    disableInitialLoad: noopfn,
+    display,
+    enableAsyncRendering: noopfn,
+    enableSingleRequest: noopfn,
+    enableSyncRendering: noopfn,
+    enableVideoAds: noopfn,
+    get: noopnullfn,
+    getAttributeKeys: nooparrayfn,
+    getTargeting: noopfn,
+    getTargetingKeys: nooparrayfn,
+    getSlots: nooparrayfn,
+    refresh: noopfn,
+    set: noopthisfn,
+    setCategoryExclusion: noopthisfn,
+    setCentering: noopfn,
+    setCookieOptions: noopthisfn,
+    setForceSafeFrame: noopthisfn,
+    setLocation: noopthisfn,
+    setPublisherProvidedId: noopthisfn,
+    setRequestNonPersonalizedAds: noopthisfn,
+    setSafeFrameConfig: noopthisfn,
+    setTagForChildDirectedTreatment: noopthisfn,
+    setTargeting: noopthisfn,
+    setVideoContent: noopthisfn,
+    updateCorrelator: noopfn,
+  };
+
+  function newSizeMappingBuilder() {
+    return {
+      addSize: noopthisfn,
+      build: noopnullfn,
+    };
+  }
+
+  function newSlot() {
+    return {
+      addService: noopthisfn,
+      clearCategoryExclusions: noopthisfn,
+      clearTargeting: noopthisfn,
+      defineSizeMapping: noopthisfn,
+      get: noopnullfn,
+      getAdUnitPath: nooparrayfn,
+      getAttributeKeys: nooparrayfn,
+      getCategoryExclusions: nooparrayfn,
+      getDomId: noopstrfn,
+      getSlotElementId: noopstrfn,
+      getSlotId: noopthisfn,
+      getTargeting: nooparrayfn,
+      getTargetingKeys: nooparrayfn,
+      set: noopthisfn,
+      setCategoryExclusion: noopthisfn,
+      setClickUrl: noopthisfn,
+      setCollapseEmptyDiv: noopthisfn,
+      setTargeting: noopthisfn,
+    };
+  }
+
+  let gt = window.googletag;
+  if (!gt) {
+    gt = window.googletag = {};
+  }
+
+  for (const [key, value] of Object.entries({
+    apiReady: true,
+    companionAds: () => companionAdsService,
+    content: () => contentService,
+    defineOutOfPageSlot: () => newSlot(),
+    defineSlot: () => newSlot(),
+    destroySlots: noopfn,
+    disablePublisherConsole: noopfn,
+    display,
+    enableServices: noopfn,
+    getVersion: noopstrfn,
+    pubads: () => pubadsService,
+    pubabsReady: true,
+    setAdIframeTitle: noopfn,
+    sizeMapping: () => newSizeMappingBuilder(),
+  })) {
+    gt[key] = value;
+  }
+
+  function runCmd(fn) {
+    try {
+      fn();
+    } catch (_) {}
+    return 1;
+  }
+
+  const cmds = gt.cmd;
+  const newCmd = [];
+  newCmd.push = runCmd;
+  gt.cmd = newCmd;
+
+  for (const cmd of cmds) {
+    runCmd(cmd);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/shims/live-test-shim.js
@@ -0,0 +1,84 @@
+/* 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 */
+
+if (!window.LiveTestShimPromise) {
+  const originalUrl = document.currentScript.src;
+
+  const shimId = "LiveTestShim";
+
+  const sendMessageToAddon = (function() {
+    const pendingMessages = new Map();
+    const channel = new MessageChannel();
+    channel.port1.onerror = console.error;
+    channel.port1.onmessage = event => {
+      const { messageId, response } = event.data;
+      const resolve = pendingMessages.get(messageId);
+      if (resolve) {
+        pendingMessages.delete(messageId);
+        resolve(response);
+      }
+    };
+    function reconnect() {
+      const detail = {
+        pendingMessages: [...pendingMessages.values()],
+        port: channel.port2,
+        shimId,
+      };
+      window.dispatchEvent(new CustomEvent("ShimConnects", { detail }));
+    }
+    window.addEventListener("ShimHelperReady", reconnect);
+    reconnect();
+    return function(message) {
+      const messageId =
+        Math.random()
+          .toString(36)
+          .substring(2) + Date.now().toString(36);
+      return new Promise(resolve => {
+        const payload = {
+          message,
+          messageId,
+          shimId,
+        };
+        pendingMessages.set(messageId, resolve);
+        channel.port1.postMessage(payload);
+      });
+    };
+  })();
+
+  async function go(options) {
+    try {
+      const o = document.getElementById("shims");
+      const cl = o.classList;
+      cl.remove("red");
+      cl.add("green");
+      o.innerText = JSON.stringify(options || "");
+    } catch (_) {}
+
+    if (window !== top) {
+      return;
+    }
+
+    await sendMessageToAddon("optIn");
+
+    const s = document.createElement("script");
+    s.src = originalUrl;
+    document.head.appendChild(s);
+  }
+
+  window[`${shimId}Promise`] = sendMessageToAddon("getOptions").then(
+    options => {
+      if (document.readyState !== "loading") {
+        go(options);
+      } else {
+        window.addEventListener("DOMContentLoaded", () => {
+          go(options);
+        });
+      }
+    }
+  );
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/shims/mochitest-shim-1.js
@@ -0,0 +1,89 @@
+/* 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 */
+
+if (!window.MochitestShimPromise) {
+  const originalUrl = document.currentScript.src;
+
+  const shimId = "MochitestShim";
+
+  const sendMessageToAddon = (function() {
+    const pendingMessages = new Map();
+    const channel = new MessageChannel();
+    channel.port1.onerror = console.error;
+    channel.port1.onmessage = event => {
+      const { messageId, response } = event.data;
+      const resolve = pendingMessages.get(messageId);
+      if (resolve) {
+        pendingMessages.delete(messageId);
+        resolve(response);
+      }
+    };
+    function reconnect() {
+      const detail = {
+        pendingMessages: [...pendingMessages.values()],
+        port: channel.port2,
+        shimId,
+      };
+      window.dispatchEvent(new CustomEvent("ShimConnects", { detail }));
+    }
+    window.addEventListener("ShimHelperReady", reconnect);
+    reconnect();
+    return function(message) {
+      const messageId =
+        Math.random()
+          .toString(36)
+          .substring(2) + Date.now().toString(36);
+      return new Promise(resolve => {
+        const payload = {
+          message,
+          messageId,
+          shimId,
+        };
+        pendingMessages.set(messageId, resolve);
+        channel.port1.postMessage(payload);
+      });
+    };
+  })();
+
+  async function go(options) {
+    try {
+      const o = document.getElementById("shims");
+      const cl = o.classList;
+      cl.remove("red");
+      cl.add("green");
+      o.innerText = JSON.stringify(options || "");
+    } catch (_) {}
+
+    window.shimPromiseResolve("shimmed");
+
+    if (window !== top) {
+      window.optInPromiseResolve(false);
+      return;
+    }
+
+    await sendMessageToAddon("optIn");
+
+    window.doingOptIn = true;
+    const s = document.createElement("script");
+    s.src = originalUrl;
+    s.onerror = () => window.optInPromiseResolve("error");
+    document.head.appendChild(s);
+  }
+
+  window[`${shimId}Promise`] = new Promise(resolve => {
+    sendMessageToAddon("getOptions").then(options => {
+      if (document.readyState !== "loading") {
+        resolve(go(options));
+      } else {
+        window.addEventListener("DOMContentLoaded", () => {
+          resolve(go(options));
+        });
+      }
+    });
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/shims/mochitest-shim-2.js
@@ -0,0 +1,87 @@
+/* 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 */
+
+if (!window.testPromise) {
+  const originalUrl = document.currentScript.src;
+
+  const shimId = "MochitestShim2";
+
+  const sendMessageToAddon = (function() {
+    const pendingMessages = new Map();
+    const channel = new MessageChannel();
+    channel.port1.onerror = console.error;
+    channel.port1.onmessage = event => {
+      const { messageId, response } = event.data;
+      const resolve = pendingMessages.get(messageId);
+      if (resolve) {
+        pendingMessages.delete(messageId);
+        resolve(response);
+      }
+    };
+    function reconnect() {
+      const detail = {
+        pendingMessages: [...pendingMessages.values()],
+        port: channel.port2,
+        shimId,
+      };
+      window.dispatchEvent(new CustomEvent("ShimConnects", { detail }));
+    }
+    window.addEventListener("ShimHelperReady", reconnect);
+    reconnect();
+    return function(message) {
+      const messageId =
+        Math.random()
+          .toString(36)
+          .substring(2) + Date.now().toString(36);
+      return new Promise(resolve => {
+        const payload = {
+          message,
+          messageId,
+          shimId,
+        };
+        pendingMessages.set(messageId, resolve);
+        channel.port1.postMessage(payload);
+      });
+    };
+  })();
+
+  async function go(options) {
+    try {
+      const o = document.getElementById("shims");
+      const cl = o.classList;
+      cl.remove("red");
+      cl.add("green");
+      o.innerText = JSON.stringify(options || "");
+    } catch (_) {}
+
+    window.shimPromiseResolve("shimmed");
+
+    if (window !== top) {
+      window.optInPromiseResolve(false);
+      return;
+    }
+
+    await sendMessageToAddon("optIn");
+
+    window.doingOptIn = true;
+    const s = document.createElement("script");
+    s.src = originalUrl;
+    s.onerror = () => window.optInPromiseResolve("error");
+    document.head.appendChild(s);
+  }
+
+  sendMessageToAddon("getOptions").then(options => {
+    if (document.readyState !== "loading") {
+      go(options);
+    } else {
+      window.addEventListener("DOMContentLoaded", () => {
+        go(options);
+      });
+    }
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/shims/mochitest-shim-3.js
@@ -0,0 +1,7 @@
+/* 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";
+
+window.shimPromiseResolve("shimmed");
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/shims/rambler-authenticator.js
@@ -0,0 +1,86 @@
+/* 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";
+
+if (!window.ramblerIdHelper) {
+  const originalScript = document.currentScript.src;
+
+  const sendMessageToAddon = (function() {
+    const shimId = "Rambler";
+    const pendingMessages = new Map();
+    const channel = new MessageChannel();
+    channel.port1.onerror = console.error;
+    channel.port1.onmessage = event => {
+      const { messageId, response } = event.data;
+      const resolve = pendingMessages.get(messageId);
+      if (resolve) {
+        pendingMessages.delete(messageId);
+        resolve(response);
+      }
+    };
+    function reconnect() {
+      const detail = {
+        pendingMessages: [...pendingMessages.values()],
+        port: channel.port2,
+        shimId,
+      };
+      window.dispatchEvent(new CustomEvent("ShimConnects", { detail }));
+    }
+    window.addEventListener("ShimHelperReady", reconnect);
+    reconnect();
+    return function(message) {
+      const messageId =
+        Math.random()
+          .toString(36)
+          .substring(2) + Date.now().toString(36);
+      return new Promise(resolve => {
+        const payload = {
+          message,
+          messageId,
+          shimId,
+        };
+        pendingMessages.set(messageId, resolve);
+        channel.port1.postMessage(payload);
+      });
+    };
+  })();
+
+  const ramblerIdHelper = {
+    getProfileInfo: (successCallback, errorCallback) => {
+      successCallback({});
+    },
+    openAuth: () => {
+      sendMessageToAddon("optIn").then(function() {
+        const openAuthArgs = arguments;
+        window.ramblerIdHelper = undefined;
+        const s = document.createElement("script");
+        s.src = originalScript;
+        document.head.appendChild(s);
+        s.addEventListener("load", () => {
+          const helper = window.ramblerIdHelper;
+          for (const { fn, args } of callLog) {
+            helper[fn].apply(helper, args);
+          }
+          helper.openAuth.apply(helper, openAuthArgs);
+        });
+      });
+    },
+  };
+
+  const callLog = [];
+  function addLoggedCall(fn) {
+    ramblerIdHelper[fn] = () => {
+      callLog.push({ fn, args: arguments });
+    };
+  }
+
+  addLoggedCall("registerOnFrameCloseCallback");
+  addLoggedCall("registerOnFrameRedirect");
+  addLoggedCall("registerOnPossibleLoginCallback");
+  addLoggedCall("registerOnPossibleLogoutCallback");
+  addLoggedCall("registerOnPossibleOauthLoginCallback");
+
+  window.ramblerIdHelper = ramblerIdHelper;
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/shims/rich-relevance.js
@@ -0,0 +1,30 @@
+/* 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";
+
+/**
+ * Bug 1449347 - Rich Relevance
+ */
+
+"use strict";
+
+if (!window.r3_common) {
+  const noopfn = () => {};
+
+  window.rr_flush_onload = noopfn;
+  window.r3 = noopfn;
+  window.r3_home = noopfn;
+  window.RR = noopfn;
+  window.r3_common = function() {};
+  window.r3_common.prototype = {
+    addContext: noopfn,
+    addPlacementType: noopfn,
+    setUserId: noopfn,
+    setSessionId: noopfn,
+    setClickthruServer: noopfn,
+    setBaseUrl: noopfn,
+    setApiKey: noopfn,
+  };
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/tests/browser/browser.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+support-files =
+  head.js
+  shims_test.js
+  shims_test_2.js
+  shims_test_3.js
+  iframe_test.html
+  shims_test.html
+  shims_test_2.html
+  shims_test_3.html
+
+[browser_shims.js]
+skip-if = verify
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/tests/browser/browser_shims.js
@@ -0,0 +1,73 @@
+"use strict";
+
+registerCleanupFunction(() => {
+  UrlClassifierTestUtils.cleanupTestTrackers();
+  Services.prefs.clearUserPref(TRACKING_PREF);
+});
+
+add_task(async function setup() {
+  await UrlClassifierTestUtils.addTestTrackers();
+});
+
+add_task(async function test_shim_disabled_by_own_pref() {
+  // Test that a shim will not apply if disabled in about:config
+
+  Services.prefs.setBoolPref(DISABLE_SHIM1_PREF, true);
+  Services.prefs.setBoolPref(TRACKING_PREF, true);
+
+  await testShimDoesNotRun();
+
+  Services.prefs.clearUserPref(DISABLE_SHIM1_PREF);
+  Services.prefs.clearUserPref(TRACKING_PREF);
+});
+
+add_task(async function test_shim_disabled_by_global_pref() {
+  // Test that a shim will not apply if disabled in about:config
+
+  Services.prefs.setBoolPref(GLOBAL_PREF, false);
+  Services.prefs.setBoolPref(DISABLE_SHIM1_PREF, false);
+  Services.prefs.setBoolPref(TRACKING_PREF, true);
+
+  await testShimDoesNotRun();
+
+  Services.prefs.clearUserPref(GLOBAL_PREF);
+  Services.prefs.clearUserPref(DISABLE_SHIM1_PREF);
+  Services.prefs.clearUserPref(TRACKING_PREF);
+});
+
+add_task(async function test_shim_disabled_hosts_notHosts() {
+  Services.prefs.setBoolPref(TRACKING_PREF, true);
+
+  await testShimDoesNotRun(false, SHIMMABLE_TEST_PAGE_3);
+
+  Services.prefs.clearUserPref(TRACKING_PREF);
+});
+
+add_task(async function test_shim_disabled_overridden_by_pref() {
+  Services.prefs.setBoolPref(TRACKING_PREF, true);
+
+  await testShimDoesNotRun(false, SHIMMABLE_TEST_PAGE_2);
+
+  Services.prefs.setBoolPref(DISABLE_SHIM2_PREF, false);
+
+  await testShimRuns(SHIMMABLE_TEST_PAGE_2);
+
+  Services.prefs.clearUserPref(TRACKING_PREF);
+  Services.prefs.clearUserPref(DISABLE_SHIM2_PREF);
+});
+
+add_task(async function test_shim() {
+  // Test that a shim which only runs in strict mode works, and that it
+  // is permitted to opt into showing normally-blocked tracking content.
+
+  Services.prefs.setBoolPref(TRACKING_PREF, true);
+
+  await testShimRuns(SHIMMABLE_TEST_PAGE);
+
+  // test that if the user opts in on one domain, they will still have to opt
+  // in on another domain which embeds an iframe to the first one.
+
+  await testShimRuns(EMBEDDING_TEST_PAGE, 0, false, false);
+
+  Services.prefs.clearUserPref(TRACKING_PREF);
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/tests/browser/head.js
@@ -0,0 +1,139 @@
+"use strict";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+  "chrome://mochitests/content",
+  "http://example.com"
+);
+
+const THIRD_PARTY_ROOT = getRootDirectory(gTestPath).replace(
+  "chrome://mochitests/content",
+  "http://example.net"
+);
+
+const SHIMMABLE_TEST_PAGE = `${TEST_ROOT}shims_test.html`;
+const SHIMMABLE_TEST_PAGE_2 = `${TEST_ROOT}shims_test_2.html`;
+const SHIMMABLE_TEST_PAGE_3 = `${TEST_ROOT}shims_test_3.html`;
+const EMBEDDING_TEST_PAGE = `${THIRD_PARTY_ROOT}iframe_test.html`;
+
+const BLOCKED_TRACKER_URL =
+  "//trackertest.org/tests/toolkit/components/url-classifier/tests/mochitest/evil.js";
+
+const DISABLE_SHIM1_PREF = "extensions.webcompat.disabled_shims.MochitestShim";
+const DISABLE_SHIM2_PREF = "extensions.webcompat.disabled_shims.MochitestShim2";
+const DISABLE_SHIM3_PREF = "extensions.webcompat.disabled_shims.MochitestShim3";
+const DISABLE_SHIM4_PREF = "extensions.webcompat.disabled_shims.MochitestShim4";
+const GLOBAL_PREF = "extensions.webcompat.enable_shims";
+const TRACKING_PREF = "privacy.trackingprotection.enabled";
+
+const { UrlClassifierTestUtils } = ChromeUtils.import(
+  "resource://testing-common/UrlClassifierTestUtils.jsm"
+);
+
+async function testShimRuns(
+  testPage,
+  frame,
+  trackersAllowed = true,
+  expectOptIn = true
+) {
+  const tab = await BrowserTestUtils.openNewForegroundTab({
+    gBrowser,
+    opening: testPage,
+    waitForLoad: true,
+  });
+
+  const TrackingProtection = tab.ownerGlobal.TrackingProtection;
+  ok(TrackingProtection, "TP is attached to the tab");
+  ok(TrackingProtection.enabled, "TP is enabled");
+
+  await SpecialPowers.spawn(
+    tab.linkedBrowser,
+    [[trackersAllowed, BLOCKED_TRACKER_URL, expectOptIn], frame],
+    async (args, _frame) => {
+      const window = _frame === undefined ? content : content.frames[_frame];
+
+      await SpecialPowers.spawn(
+        window,
+        args,
+        async (_trackersAllowed, trackerUrl, _expectOptIn) => {
+          const shimResult = await content.wrappedJSObject.shimPromise;
+          is("shimmed", shimResult, "Shim activated");
+
+          const optInResult = await content.wrappedJSObject.optInPromise;
+          is(_expectOptIn, optInResult, "Shim allowed opt in if appropriate");
+
+          const o = content.document.getElementById("shims");
+          const cl = o.classList;
+          const opts = JSON.parse(o.innerText);
+          is(
+            undefined,
+            opts.branchValue,
+            "Shim script did not receive option for other branch"
+          );
+          is(
+            undefined,
+            opts.platformValue,
+            "Shim script did not receive option for other platform"
+          );
+          is(
+            true,
+            opts.simpleOption,
+            "Shim script received simple option correctly"
+          );
+          ok(opts.complexOption, "Shim script received complex option");
+          is(
+            1,
+            opts.complexOption.a,
+            "Shim script received complex options correctly #1"
+          );
+          is(
+            "test",
+            opts.complexOption.b,
+            "Shim script received complex options correctly #2"
+          );
+          ok(cl.contains("green"), "Shim affected page correctly");
+        }
+      );
+    }
+  );
+
+  await BrowserTestUtils.removeTab(tab);
+}
+
+async function testShimDoesNotRun(
+  trackersAllowed = false,
+  testPage = SHIMMABLE_TEST_PAGE
+) {
+  const tab = await BrowserTestUtils.openNewForegroundTab({
+    gBrowser,
+    opening: testPage,
+    waitForLoad: true,
+  });
+
+  await SpecialPowers.spawn(
+    tab.linkedBrowser,
+    [trackersAllowed, BLOCKED_TRACKER_URL],
+    async (_trackersAllowed, trackerUrl) => {
+      const shimResult = await content.wrappedJSObject.shimPromise;
+      is("did not shim", shimResult, "Shim did not activate");
+
+      ok(
+        !content.document.getElementById("shims").classList.contains("green"),
+        "Shim script did not run"
+      );
+
+      is(
+        _trackersAllowed ? "ALLOWED" : "BLOCKED",
+        await new Promise(resolve => {
+          const s = content.document.createElement("script");
+          s.src = trackerUrl;
+          s.onload = () => resolve("ALLOWED");
+          s.onerror = () => resolve("BLOCKED");
+          content.document.head.appendChild(s);
+        }),
+        "Normally-blocked resources blocked if appropriate"
+      );
+    }
+  );
+
+  await BrowserTestUtils.removeTab(tab);
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/tests/browser/iframe_test.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf8">
+    <script>
+      window.shimPromise = new Promise(resolve => {
+        window.shimPromiseResolve = resolve;
+      });
+      window.optInPromise = new Promise(resolve => {
+        window.optInPromiseResolve = resolve;
+      });
+    </script>
+  </head>
+  <body>
+    <iframe src="http://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test.html"></iframe>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/tests/browser/shims_test.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf8">
+    <script>
+      window.shimPromise = new Promise(resolve => {
+        window.shimPromiseResolve = resolve;
+      });
+      window.optInPromise = new Promise(resolve => {
+        window.optInPromiseResolve = resolve;
+      });
+    </script>
+    <script onerror="window.shimPromiseResolve('error')" src="shims_test.js"></script>
+  </head>
+  <body>
+    <div id="shims"></div>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/tests/browser/shims_test.js
@@ -0,0 +1,11 @@
+/* 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";
+
+if (window.doingOptIn) {
+  window.optInPromiseResolve(true);
+} else {
+  window.shimPromiseResolve("did not shim");
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/tests/browser/shims_test_2.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf8">
+    <script>
+      window.shimPromise = new Promise(resolve => {
+        window.shimPromiseResolve = resolve;
+      });
+      window.optInPromise = new Promise(resolve => {
+        window.optInPromiseResolve = resolve;
+      });
+    </script>
+    <script onerror="window.shimPromiseResolve('error')" src="shims_test_2.js"></script>
+  </head>
+  <body>
+    <div id="shims"></div>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/tests/browser/shims_test_2.js
@@ -0,0 +1,11 @@
+/* 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";
+
+if (window.doingOptIn) {
+  window.optInPromiseResolve(true);
+} else {
+  window.shimPromiseResolve("did not shim");
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/tests/browser/shims_test_3.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf8">
+    <script>
+      window.shimPromise = new Promise(resolve => {
+        window.shimPromiseResolve = resolve;
+      });
+      window.optInPromise = new Promise(resolve => {
+        window.optInPromiseResolve = resolve;
+      });
+    </script>
+    <script onerror="window.shimPromiseResolve('error')" src="shims_test_3.js"></script>
+  </head>
+  <body>
+    <div id="shims"></div>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/tests/browser/shims_test_3.js
@@ -0,0 +1,7 @@
+/* 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";
+
+window.shimPromiseResolve("did not shim");