Bug 1452307: Remove support for old-style experiment API extensions. r?aswan draft
authorKris Maglione <maglione.k@gmail.com>
Fri, 06 Apr 2018 18:14:59 -0700
changeset 779046 72337be11a0e5d7ebe6bb01d5ad71cb1da1cda7b
parent 778884 2fce968f46582ccb40145a5c0d6e9f58f34ada41
child 779047 e697d8fd91c40c15df21551514b239de6a9a117f
push id105641
push usermaglione.k@gmail.com
push dateSun, 08 Apr 2018 19:55:52 +0000
reviewersaswan
bugs1452307
milestone61.0a1
Bug 1452307: Remove support for old-style experiment API extensions. r?aswan MozReview-Commit-ID: 5y48pMRQ5XW
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionCommon.jsm
toolkit/components/extensions/ExtensionParent.jsm
toolkit/components/extensions/test/xpcshell/test_ext_experiments.js
toolkit/components/extensions/test/xpcshell/test_ext_schemas_interactive.js
toolkit/mozapps/extensions/internal/APIExtensionBootstrap.js
toolkit/mozapps/extensions/internal/XPIInstall.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/internal/moz.build
toolkit/mozapps/extensions/test/xpcshell/test_legacy.js
toolkit/mozapps/extensions/test/xpcshell/test_temporary.js
toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -1188,17 +1188,16 @@ class Extension extends ExtensionData {
 
     this.onStartup = null;
 
     this.hasShutdown = false;
     this.onShutdown = new Set();
 
     this.uninstallURL = null;
 
-    this.apis = [];
     this.whiteListedHosts = null;
     this._optionalOrigins = null;
     this.webAccessibleResources = null;
 
     this.emitter = new EventEmitter();
 
     /* eslint-disable mozilla/balanced-listeners */
     this.on("add-permissions", (ignoreEvent, permissions) => {
@@ -1387,28 +1386,16 @@ class Extension extends ExtensionData {
 
   async loadManifest() {
     let manifest = await super.loadManifest();
 
     if (this.errors.length) {
       return Promise.reject({errors: this.errors});
     }
 
-    if (this.apiNames.size) {
-      // Load Experiments APIs that this extension depends on.
-      let apis = await Promise.all(
-        Array.from(this.apiNames, api => ExtensionCommon.ExtensionAPIs.load(api)));
-
-      for (let API of apis) {
-        if (API) {
-          this.apis.push(new API(this));
-        }
-      }
-    }
-
     return manifest;
   }
 
   // Representation of the extension to send to content
   // processes. This should include anything the content process might
   // need.
   serialize() {
     return {
@@ -1644,27 +1631,29 @@ class Extension extends ExtensionData {
       this.emit("startup", this);
       Management.emit("startup", this);
 
       await this.runManifest(this.manifest);
 
       Management.emit("ready", this);
       this.emit("ready");
       TelemetryStopwatch.finish("WEBEXT_EXTENSION_STARTUP_MS", this);
-    } catch (e) {
-      dump(`Extension error: ${e.message || e} ${e.filename || e.fileName}:${e.lineNumber} :: ${e.stack || new Error().stack}\n`);
-      Cu.reportError(e);
+    } catch (errors) {
+      for (let e of [].concat(errors)) {
+        dump(`Extension error: ${e.message || e} ${e.filename || e.fileName}:${e.lineNumber} :: ${e.stack || new Error().stack}\n`);
+        Cu.reportError(e);
+      }
 
       if (this.policy) {
         this.policy.active = false;
       }
 
       this.cleanupGeneratedFile();
 
-      throw e;
+      throw errors;
     }
 
     this.startupPromise = null;
   }
 
   cleanupGeneratedFile() {
     if (!this.cleanupFile) {
       return;
@@ -1755,20 +1744,16 @@ class Extension extends ExtensionData {
     }
 
     GlobalManager.uninit(this);
 
     for (let obj of this.onShutdown) {
       obj.close();
     }
 
-    for (let api of this.apis) {
-      api.destroy();
-    }
-
     ParentAPIManager.shutdownExtension(this.id);
 
     Management.emit("shutdown", this);
     this.emit("shutdown");
 
     await this.broadcast("Extension:Shutdown", {id: this.id});
 
     MessageChannel.abortResponses({extensionId: this.id});
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -27,18 +27,16 @@ XPCOMUtils.defineLazyModuleGetters(this,
   Schemas: "resource://gre/modules/Schemas.jsm",
   SchemaRoot: "resource://gre/modules/Schemas.jsm",
 });
 
 XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
                                    "@mozilla.org/content/style-sheet-service;1",
                                    "nsIStyleSheetService");
 
-const global = Cu.getGlobalForObject(this);
-
 ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 
 var {
   DefaultMap,
   DefaultWeakMap,
   EventEmitter,
   ExtensionError,
   defineLazyGetter,
@@ -109,87 +107,16 @@ class ExtensionAPI extends ExtensionUtil
   onManifestEntry(entry) {
   }
 
   getAPI(context) {
     throw new Error("Not Implemented");
   }
 }
 
-var ExtensionAPIs = {
-  apis: new Map(),
-
-  load(apiName) {
-    let api = this.apis.get(apiName);
-    if (!api) {
-      return null;
-    }
-
-    if (api.loadPromise) {
-      return api.loadPromise;
-    }
-
-    let {script, schema} = api;
-
-    let addonId = `${apiName}@experiments.addons.mozilla.org`;
-    api.sandbox = Cu.Sandbox(global, {
-      wantXrays: false,
-      sandboxName: script,
-      addonId,
-      wantGlobalProperties: ["ChromeUtils"],
-      metadata: {addonID: addonId},
-    });
-
-    api.sandbox.ExtensionAPI = ExtensionAPI;
-
-    // Create a console getter which lazily provide a ConsoleAPI instance.
-    XPCOMUtils.defineLazyGetter(api.sandbox, "console", () => {
-      return new ConsoleAPI({prefix: addonId});
-    });
-
-    Services.scriptloader.loadSubScript(script, api.sandbox, "UTF-8");
-
-    api.loadPromise = Schemas.load(schema).then(() => {
-      let API = Cu.evalInSandbox("API", api.sandbox);
-      API.prototype.namespace = apiName;
-      return API;
-    });
-
-    return api.loadPromise;
-  },
-
-  unload(apiName) {
-    let api = this.apis.get(apiName);
-
-    let {schema} = api;
-
-    Schemas.unload(schema);
-    Cu.nukeSandbox(api.sandbox);
-
-    api.sandbox = null;
-    api.loadPromise = null;
-  },
-
-  register(namespace, schema, script) {
-    if (this.apis.has(namespace)) {
-      throw new Error(`API namespace already exists: ${namespace}`);
-    }
-
-    this.apis.set(namespace, {schema, script});
-  },
-
-  unregister(namespace) {
-    if (!this.apis.has(namespace)) {
-      throw new Error(`API namespace does not exist: ${namespace}`);
-    }
-
-    this.apis.delete(namespace);
-  },
-};
-
 /**
  * This class contains the information we have about an individual
  * extension.  It is never instantiated directly, instead subclasses
  * for each type of process extend this class and add members that are
  * relevant for that process.
  * @abstract
  */
 class BaseContext {
@@ -1417,36 +1344,16 @@ class SchemaAPIManager extends EventEmit
     // in the sandbox's context instead of here.
     let scope = Cu.createObjectIn(this.global);
 
     Services.scriptloader.loadSubScript(scriptUrl, scope, "UTF-8");
 
     // Save the scope to avoid it being garbage collected.
     this._scriptScopes.push(scope);
   }
-
-  /**
-   * Mash together all the APIs from `apis` into `obj`.
-   *
-   * @param {BaseContext} context The context for which the API bindings are
-   *     generated.
-   * @param {Array} apis A list of objects, see `registerSchemaAPI`.
-   * @param {object} obj The destination of the API.
-   */
-  static generateAPIs(context, apis, obj) {
-    function hasPermission(perm) {
-      return context.extension.hasPermission(perm, true);
-    }
-    for (let api of apis) {
-      if (Schemas.checkPermissions(api.namespace, {hasPermission})) {
-        api = api.getAPI(context);
-        deepCopy(obj, api);
-      }
-    }
-  }
 }
 
 class LazyAPIManager extends SchemaAPIManager {
   constructor(processType, moduleData, schemaURLs) {
     super(processType);
 
     this.initialized = false;
 
@@ -1893,17 +1800,16 @@ const stylesheetMap = new DefaultMap(url
 });
 
 
 ExtensionCommon = {
   BaseContext,
   CanOfAPIs,
   EventManager,
   ExtensionAPI,
-  ExtensionAPIs,
   LocalAPIImplementation,
   LocaleData,
   NoCloneSpreadArgs,
   SchemaAPIInterface,
   SchemaAPIManager,
   SpreadArgs,
   ignoreEvent,
   stylesheetMap,
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -424,20 +424,16 @@ GlobalManager = {
       browser.messageManager.sendAsyncMessage("Extension:SetFrameData",
                                               data);
     }
   },
 
   getExtension(extensionId) {
     return this.extensionMap.get(extensionId);
   },
-
-  injectInObject(context, isChromeCompat, dest) {
-    SchemaAPIManager.generateAPIs(context, context.extension.apis, dest);
-  },
 };
 
 /**
  * The proxied parent side of a context in ExtensionChild.jsm, for the
  * parent side of a proxied API.
  */
 class ProxyContextParent extends BaseContext {
   constructor(envType, extension, params, xulBrowser, principal) {
@@ -507,17 +503,16 @@ class ProxyContextParent extends BaseCon
     super.unload();
     apiManager.emit("proxy-context-unload", this);
   }
 }
 
 defineLazyGetter(ProxyContextParent.prototype, "apiCan", function() {
   let obj = {};
   let can = new CanOfAPIs(this, this.extension.apiManager, obj);
-  GlobalManager.injectInObject(this, false, obj);
   return can;
 });
 
 defineLazyGetter(ProxyContextParent.prototype, "apiObj", function() {
   return this.apiCan.root;
 });
 
 defineLazyGetter(ProxyContextParent.prototype, "sandbox", function() {
--- a/toolkit/components/extensions/test/xpcshell/test_ext_experiments.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_experiments.js
@@ -7,192 +7,16 @@ ChromeUtils.defineModuleGetter(this, "Ad
 
 AddonTestUtils.init(this);
 
 add_task(async function setup() {
   AddonTestUtils.overrideCertDB();
   await ExtensionTestUtils.startAddonManager();
 });
 
-add_task(async function test_experiments_api() {
-  let apiAddonFile = Extension.generateZipFile({
-    "install.rdf": `<?xml version="1.0" encoding="UTF-8"?>
-      <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-           xmlns:em="http://www.mozilla.org/2004/em-rdf#">
-          <Description about="urn:mozilla:install-manifest"
-              em:id="fooBar@experiments.addons.mozilla.org"
-              em:name="FooBar Experiment"
-              em:type="256"
-              em:version="0.1"
-              em:description="FooBar experiment"
-              em:creator="Mozilla">
-
-              <em:targetApplication>
-                  <Description
-                      em:id="xpcshell@tests.mozilla.org"
-                      em:minVersion="48"
-                      em:maxVersion="*"/>
-              </em:targetApplication>
-          </Description>
-      </RDF>
-    `,
-
-    "api.js": String.raw`
-      Components.utils.import("resource://gre/modules/Services.jsm");
-
-      Services.obs.notifyObservers(null, "webext-api-loaded", "");
-
-      class API extends ExtensionAPI {
-        getAPI(context) {
-          return {
-            fooBar: {
-              hello(text) {
-                console.log('fooBar.hello API called', text);
-                Services.obs.notifyObservers(null, "webext-api-hello", text);
-              }
-            }
-          }
-        }
-      }
-    `,
-
-    "schema.json": [
-      {
-        "namespace": "fooBar",
-        "description": "All full of fooBar.",
-        "permissions": ["experiments.fooBar"],
-        "functions": [
-          {
-            "name": "hello",
-            "type": "function",
-            "description": "Hates you. This is all.",
-            "parameters": [
-              {"type": "string", "name": "text"},
-            ],
-          },
-        ],
-      },
-    ],
-  });
-
-  let addonFile = Extension.generateXPI({
-    manifest: {
-      applications: {gecko: {id: "fooBar@web.extension"}},
-      permissions: ["experiments.fooBar"],
-    },
-
-    background() {
-      // The test code below checks that hello() is called at the right
-      // time with the string "Here I am".  Verify that the api schema is
-      // being correctly interpreted by calling hello() with bad arguments
-      // and only calling hello() with the magic string if the call with
-      // bad arguments throws.
-      try {
-        browser.fooBar.hello("I should not see this", "since two arguments are bad");
-      } catch (err) {
-        browser.fooBar.hello("Here I am");
-      }
-    },
-  });
-
-  let boringAddonFile = Extension.generateXPI({
-    manifest: {
-      applications: {gecko: {id: "boring@web.extension"}},
-    },
-    background() {
-      if (browser.fooBar) {
-        browser.fooBar.hello("Here I should not be");
-      }
-    },
-  });
-
-  registerCleanupFunction(() => {
-    for (let file of [apiAddonFile, addonFile, boringAddonFile]) {
-      Services.obs.notifyObservers(file, "flush-cache-entry");
-      file.remove(false);
-    }
-  });
-
-
-  let resolveHello;
-  let observer = (subject, topic, data) => {
-    if (topic == "webext-api-loaded") {
-      ok(!!resolveHello, "Should not see API loaded until dependent extension loads");
-    } else if (topic == "webext-api-hello") {
-      resolveHello(data);
-    }
-  };
-
-  Services.obs.addObserver(observer, "webext-api-loaded");
-  Services.obs.addObserver(observer, "webext-api-hello");
-  registerCleanupFunction(() => {
-    Services.obs.removeObserver(observer, "webext-api-loaded");
-    Services.obs.removeObserver(observer, "webext-api-hello");
-  });
-
-
-  // Install API add-on.
-  let apiAddon = await AddonManager.installTemporaryAddon(apiAddonFile);
-
-  let {ExtensionAPIs} = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm", {}).ExtensionCommon;
-  ok(ExtensionAPIs.apis.has("fooBar"), "Should have fooBar API.");
-
-
-  // Install boring WebExtension add-on.
-  let boringAddon = await AddonManager.installTemporaryAddon(boringAddonFile);
-  await AddonTestUtils.promiseWebExtensionStartup();
-
-  // Install interesting WebExtension add-on.
-  let promise = new Promise(resolve => {
-    resolveHello = resolve;
-  });
-
-  let addon = await AddonManager.installTemporaryAddon(addonFile);
-  await AddonTestUtils.promiseWebExtensionStartup();
-
-  let hello = await promise;
-  equal(hello, "Here I am", "Should get hello from add-on");
-
-  // Install management test add-on.
-  let managementAddon = ExtensionTestUtils.loadExtension({
-    manifest: {
-      applications: {gecko: {id: "management@web.extension"}},
-      permissions: ["management"],
-    },
-    async background() {
-      // Should find the simple extension.
-      let normalAddon = await browser.management.get("boring@web.extension");
-      browser.test.assertEq(normalAddon.id, "boring@web.extension", "Found boring addon");
-
-      try {
-        // Not allowed to get the API experiment.
-        await browser.management.get("fooBar@experiments.addons.mozilla.org");
-      } catch (e) {
-        browser.test.sendMessage("done");
-      }
-    },
-    useAddonManager: "temporary",
-  });
-
-  await managementAddon.startup();
-  await managementAddon.awaitMessage("done");
-  await managementAddon.unload();
-
-  // Cleanup.
-  apiAddon.uninstall();
-
-  boringAddon.userDisabled = true;
-  await new Promise(executeSoon);
-
-  equal(addon.appDisabled, true, "Add-on should be app-disabled after its dependency is removed.");
-
-  addon.uninstall();
-  boringAddon.uninstall();
-});
-
 let fooExperimentAPIs = {
   foo: {
     schema: "schema.json",
     parent: {
       scopes: ["addon_parent"],
       script: "parent.js",
       paths: [["experiments", "foo", "parent"]],
     },
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_interactive.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_interactive.js
@@ -1,44 +1,79 @@
 "use strict";
 
-ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
-
-const {ExtensionAPI, ExtensionAPIs} = ExtensionCommon;
-
 const {ExtensionManager} = ChromeUtils.import("resource://gre/modules/ExtensionChild.jsm", {});
 
 Cu.importGlobalProperties(["Blob", "URL"]);
 
-let schema = [
-  {
-    namespace: "userinputtest",
-    functions: [
-      {
-        name: "test",
-        type: "function",
-        async: true,
-        requireUserInput: true,
-        parameters: [],
-      },
-    ],
+let experimentAPIs = {
+  userinputtest: {
+    schema: "schema.json",
+    parent: {
+      scopes: ["addon_parent"],
+      script: "parent.js",
+      paths: [["userinputtest"]],
+    },
+    child: {
+      scopes: ["addon_child"],
+      script: "child.js",
+      paths: [["userinputtest", "child"]],
+    },
   },
-];
+};
+
+let experimentFiles = {
+  "schema.json": JSON.stringify([
+    {
+      namespace: "userinputtest",
+      functions: [
+        {
+          name: "test",
+          type: "function",
+          async: true,
+          requireUserInput: true,
+          parameters: [],
+        },
+        {
+          name: "child",
+          type: "function",
+          async: true,
+          requireUserInput: true,
+          parameters: [],
+        },
+      ],
+    },
+  ]),
 
-class API extends ExtensionAPI {
-  getAPI(context) {
-    return {
-      userinputtest: {
-        test() {},
-      },
+  /* globals ExtensionAPI */
+  "parent.js": () => {
+    this.userinputtest = class extends ExtensionAPI {
+      getAPI(context) {
+        return {
+          userinputtest: {
+            test() {},
+          },
+        };
+      }
     };
-  }
-}
+  },
 
-let schemaUrl = `data:,${JSON.stringify(schema)}`;
+  /* globals ExtensionAPI */
+  "child.js": () => {
+    this.userinputtest = class extends ExtensionAPI {
+      getAPI(context) {
+        return {
+          userinputtest: {
+            child() {},
+          },
+        };
+      }
+    };
+  },
+};
 
 // Set the "handlingUserInput" flag for the given extension's background page.
 // Returns an RAIIHelper that should be destruct()ed eventually.
 function setHandlingUserInput(extension) {
   let extensionChild = ExtensionManager.extensions.get(extension.extension.id);
   let bgwin = null;
   for (let view of extensionChild.views) {
     if (view.viewType == "background") {
@@ -50,88 +85,77 @@ function setHandlingUserInput(extension)
   let winutils = bgwin.QueryInterface(Ci.nsIInterfaceRequestor)
                       .getInterface(Ci.nsIDOMWindowUtils);
   return winutils.setHandlingUserInput(true);
 }
 
 // Test that the schema requireUserInput flag works correctly for
 // proxied api implementations.
 add_task(async function test_proxy() {
-  let apiUrl = URL.createObjectURL(new Blob([API.toString()]));
-  ExtensionAPIs.register("userinputtest", schemaUrl, apiUrl);
-
   let extension = ExtensionTestUtils.loadExtension({
     background() {
       browser.test.onMessage.addListener(async () => {
         try {
           await browser.userinputtest.test();
           browser.test.sendMessage("result", null);
         } catch (err) {
           browser.test.sendMessage("result", err.message);
         }
       });
     },
     manifest: {
       permissions: ["experiments.userinputtest"],
+      experiment_apis: experimentAPIs,
     },
+    files: experimentFiles,
   });
 
   await extension.startup();
 
   extension.sendMessage("test");
   let result = await extension.awaitMessage("result");
   ok(/test may only be called from a user input handler/.test(result),
-     "function failed when not called from a user input handler");
-
-  let handle = setHandlingUserInput(extension);
-  extension.sendMessage("test");
-  result = await extension.awaitMessage("result");
-  equal(result, null, "function succeeded when called from a user input handler");
-  handle.destruct();
-
-  await extension.unload();
-  ExtensionAPIs.unregister("userinputtest");
-});
-
-// Test that the schema requireUserInput flag works correctly for
-// non-proxied api implementations.
-add_task(async function test_local() {
-  let apiString = `this.userinputtest = ${API.toString()};`;
-  let apiUrl = URL.createObjectURL(new Blob([apiString]));
-  await Schemas.load(schemaUrl);
-  const {apiManager} = ChromeUtils.import("resource://gre/modules/ExtensionPageChild.jsm", {});
-  apiManager.registerModules({
-    userinputtest: {
-      url: apiUrl,
-      scopes: ["addon_child"],
-      paths: [["userinputtest"]],
-    },
-  });
-
-  let extension = ExtensionTestUtils.loadExtension({
-    background() {
-      browser.test.onMessage.addListener(async () => {
-        try {
-          await browser.userinputtest.test();
-          browser.test.sendMessage("result", null);
-        } catch (err) {
-          browser.test.sendMessage("result", err.message);
-        }
-      });
-    },
-    manifest: {},
-  });
-
-  await extension.startup();
-
-  extension.sendMessage("test");
-  let result = await extension.awaitMessage("result");
-  ok(/test may only be called from a user input handler/.test(result),
-     "function failed when not called from a user input handler");
+     `function failed when not called from a user input handler: ${result}`);
 
   let handle = setHandlingUserInput(extension);
   extension.sendMessage("test");
   result = await extension.awaitMessage("result");
   equal(result, null, "function succeeded when called from a user input handler");
   handle.destruct();
 
   await extension.unload();
 });
+
+// Test that the schema requireUserInput flag works correctly for
+// non-proxied api implementations.
+add_task(async function test_local() {
+  let extension = ExtensionTestUtils.loadExtension({
+    background() {
+      browser.test.onMessage.addListener(async () => {
+        try {
+          await browser.userinputtest.child();
+          browser.test.sendMessage("result", null);
+        } catch (err) {
+          browser.test.sendMessage("result", err.message);
+        }
+      });
+    },
+    manifest: {
+      experiment_apis: experimentAPIs,
+    },
+    files: experimentFiles,
+  });
+
+  await extension.startup();
+
+  extension.sendMessage("test");
+  let result = await extension.awaitMessage("result");
+  ok(/child may only be called from a user input handler/.test(result),
+     `function failed when not called from a user input handler: ${result}`);
+
+  let handle = setHandlingUserInput(extension);
+  extension.sendMessage("test");
+  result = await extension.awaitMessage("result");
+  equal(result, null, "function succeeded when called from a user input handler");
+  handle.destruct();
+
+  await extension.unload();
+});
deleted file mode 100644
--- a/toolkit/mozapps/extensions/internal/APIExtensionBootstrap.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/* 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";
-
-/* exported startup, shutdown, install, uninstall, ExtensionAPIs */
-
-ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
-ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-const {ExtensionAPIs} = ExtensionCommon;
-
-var namespace;
-var resource;
-var resProto;
-
-function install(data, reason) {
-}
-
-function startup(data, reason) {
-  namespace = data.id.replace(/@.*/, "");
-  resource = `extension-${namespace.toLowerCase()}-api`;
-
-  resProto = Services.io.getProtocolHandler("resource")
-                     .QueryInterface(Ci.nsIResProtocolHandler);
-
-  resProto.setSubstitution(resource, data.resourceURI);
-
-  ExtensionAPIs.register(
-    namespace,
-    `resource://${resource}/schema.json`,
-    `resource://${resource}/api.js`);
-}
-
-function shutdown(data, reason) {
-  resProto.setSubstitution(resource, null);
-
-  ExtensionAPIs.unregister(namespace);
-}
-
-function uninstall(data, reason) {
-}
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -135,31 +135,30 @@ const PROP_METADATA      = ["id", "versi
 const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"];
 const PROP_LOCALE_MULTI  = ["developers", "translators", "contributors"];
 const PROP_TARGETAPP     = ["id", "minVersion", "maxVersion"];
 
 // Map new string type identifiers to old style nsIUpdateItem types.
 // Retired values:
 // 32 = multipackage xpi file
 // 8 = locale
+// 256 = apiextension
 const TYPES = {
   extension: 2,
   theme: 4,
   dictionary: 64,
   experiment: 128,
-  apiextension: 256,
 };
 
 const COMPATIBLE_BY_DEFAULT_TYPES = {
   extension: true,
   dictionary: true,
 };
 
 const RESTARTLESS_TYPES = new Set([
-  "apiextension",
   "dictionary",
   "experiment",
   "webextension",
   "webextension-theme",
 ]);
 
 // This is a random number array that can be used as "salt" when generating
 // an automatic ID based on the directory path of an add-on. It will prevent
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -188,38 +188,35 @@ const BOOTSTRAP_REASONS = {
   ADDON_UNINSTALL: 6,
   ADDON_UPGRADE: 7,
   ADDON_DOWNGRADE: 8
 };
 
 // Some add-on types that we track internally are presented as other types
 // externally
 const TYPE_ALIASES = {
-  "apiextension": "extension",
   "webextension": "extension",
   "webextension-theme": "theme",
   "webextension-langpack": "locale",
 };
 
 const CHROME_TYPES = new Set([
   "extension",
   "experiment",
 ]);
 
 const SIGNED_TYPES = new Set([
-  "apiextension",
   "extension",
   "experiment",
   "webextension",
   "webextension-langpack",
   "webextension-theme",
 ]);
 
 const LEGACY_TYPES = new Set([
-  "apiextension",
   "extension",
   "theme",
 ]);
 
 const ALL_EXTERNAL_TYPES = new Set([
   "dictionary",
   "extension",
   "experiment",
@@ -3796,18 +3793,16 @@ var XPIProvider = {
     if (isWebExtension(aType)) {
       activeAddon.bootstrapScope = Extension.getBootstrapScope(aId, aFile);
     } else if (aType === "webextension-langpack") {
       activeAddon.bootstrapScope = Langpack.getBootstrapScope(aId, aFile);
     } else {
       let uri = getURIForResourceInFile(aFile, "bootstrap.js").spec;
       if (aType == "dictionary")
         uri = "resource://gre/modules/addons/SpellCheckDictionaryBootstrap.js";
-      else if (aType == "apiextension")
-        uri = "resource://gre/modules/addons/APIExtensionBootstrap.js";
 
       activeAddon.bootstrapScope =
         new Cu.Sandbox(principal, { sandboxName: uri,
                                     addonId: aId,
                                     wantGlobalProperties: ["ChromeUtils"],
                                     metadata: { addonID: aId, URI: uri } });
 
       try {
@@ -4767,20 +4762,16 @@ AddonWrapper.prototype = {
   get type() {
     return getExternalType(addonFor(this).type);
   },
 
   get isWebExtension() {
     return isWebExtension(addonFor(this).type);
   },
 
-  get isAPIExtension() {
-    return addonFor(this).type == "apiextension";
-  },
-
   get temporarilyInstalled() {
     return addonFor(this)._installLocation == TemporaryInstallLocation;
   },
 
   get aboutURL() {
     return this.isActive ? addonFor(this).aboutURL : null;
   },
 
--- a/toolkit/mozapps/extensions/internal/moz.build
+++ b/toolkit/mozapps/extensions/internal/moz.build
@@ -3,17 +3,16 @@
 # 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/.
 
 EXTRA_JS_MODULES.addons += [
     'AddonRepository.jsm',
     'AddonSettings.jsm',
     'AddonUpdateChecker.jsm',
-    'APIExtensionBootstrap.js',
     'Content.js',
     'GMPProvider.jsm',
     'LightweightThemeImageOptimizer.jsm',
     'ProductAddonChecker.jsm',
     'SpellCheckDictionaryBootstrap.js',
     'UpdateRDFConverter.jsm',
     'XPIInstall.jsm',
     'XPIProvider.jsm',
--- a/toolkit/mozapps/extensions/test/xpcshell/test_legacy.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_legacy.js
@@ -7,22 +7,16 @@ startupManager();
 add_task(async function test_disable() {
   let legacy = [
     {
       id: "bootstrap@tests.mozilla.org",
       name: "Bootstrap add-on",
       version: "1.0",
       bootstrap: true,
     },
-    {
-      id: "apiexperiment@tests.mozilla.org",
-      name: "WebExtension Experiment",
-      version: "1.0",
-      type: 256,
-    },
   ];
 
   let nonLegacy = [
     {
       id: "webextension@tests.mozilla.org",
       manifest: {
         applications: {gecko: {id: "webextension@tests.mozilla.org"}},
       },
--- a/toolkit/mozapps/extensions/test/xpcshell/test_temporary.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_temporary.js
@@ -753,42 +753,8 @@ add_task(async function() {
   await promiseInstallAllFiles([do_get_addon("test_bootstrap1_1")], true);
   const addon = await promiseAddonByID(ID);
 
   notEqual(addon, null);
   equal(addon.temporarilyInstalled, false);
 
   await promiseRestartManager();
 });
-
-// Check that WebExtensions experiments can only be installed temporarily
-// in builds that allow legacy extensions.
-add_task(async function() {
-  AddonTestUtils.usePrivilegedSignatures = false;
-
-  const API_ID = "apiexperiment@tests.mozilla.org";
-  let xpi = createTempXPIFile({
-    id: API_ID,
-    name: "WebExtension Experiment",
-    version: "1.0",
-    type: 256,
-    targetApplications: [{
-      id: "xpcshell@tests.mozilla.org",
-      minVersion: "1",
-      maxVersion: "1"
-    }],
-  });
-
-  let addon = null;
-  try {
-    await AddonManager.installTemporaryAddon(xpi);
-    addon = await promiseAddonByID(API_ID);
-  } catch (err) {
-    // fall through, level addon null
-  }
-
-  if (AppConstants.MOZ_ALLOW_LEGACY_EXTENSIONS) {
-    notEqual(addon, null, "Temporary install of WebExtension experiment succeeded");
-    addon.uninstall();
-  } else {
-    equal(addon, null, "Temporary install of WebExtension experiment was not allowed");
-  }
-});
--- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
@@ -295,39 +295,16 @@ add_task(async function test_experiments
   deepEqual(addon.dependencies, ["meh@experiments.addons.mozilla.org"],
             "Addon should have the expected dependencies");
 
   equal(addon.appDisabled, true, "Add-on should be app disabled due to missing dependencies");
 
   addon.uninstall();
 });
 
-// Test that experiments API extensions install correctly.
-add_task(async function test_experiments_api() {
-  const extensionId = "meh@experiments.addons.mozilla.org";
-
-  let addonFile = createTempXPIFile({
-    id: extensionId,
-    type: 256,
-    version: "0.1",
-    name: "Meh API",
-  });
-
-  await promiseInstallAllFiles([addonFile]);
-
-  let addons = await AddonManager.getAddonsByTypes(["extension"]);
-  let addon = addons.pop();
-  equal(addon.id, extensionId, "Add-on should be installed as an API extension");
-
-  addons = await AddonManager.getAddonsByTypes(["extension"]);
-  equal(addons.pop().id, extensionId, "Add-on type should be aliased to extension");
-
-  addon.uninstall();
-});
-
 add_task(async function developerShouldOverride() {
   let addon = await promiseInstallWebExtension({
     manifest: {
       default_locale: "en",
       developer: {
         name: "__MSG_name__",
         url: "__MSG_url__"
       },