Bug 857456 Part 1: Get rid of internal/external type distinction in addons manager r=kmag
authorAndrew Swan <aswan@mozilla.com>
Wed, 21 Nov 2018 20:01:26 -0800
changeset 448838 51bb2e2f30d2c1486ebbc75568e09976a374fe6c
parent 448837 efac27a2bec4fa7e81fa3c8a6d023795aa6447dc
child 448839 061b97e02ede2133f50956feba25fd8798745347
push id110255
push useraswan@mozilla.com
push dateThu, 29 Nov 2018 21:53:45 +0000
treeherdermozilla-inbound@0c715a8f0170 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs857456
milestone65.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 857456 Part 1: Get rid of internal/external type distinction in addons manager r=kmag AddonInternal objects have a "type" field which is used inside the addon manager, but AddonWrapper objects map the internal values to different externally visible values. The internal values generally convey whether a particular addon uses webextension packaging (ie manifest.json) or not. This patch cleans that all up by adding a new isWebExtension property and then just using the externally visible values for type consistently.
toolkit/mozapps/extensions/AddonManagerStartup.cpp
toolkit/mozapps/extensions/internal/XPIDatabase.jsm
toolkit/mozapps/extensions/internal/XPIInstall.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
--- a/toolkit/mozapps/extensions/AddonManagerStartup.cpp
+++ b/toolkit/mozapps/extensions/AddonManagerStartup.cpp
@@ -413,17 +413,17 @@ public:
   nsString Type() { return GetString("type", "extension"); }
 
   bool Enabled() { return GetBool("enabled"); }
 
   double LastModifiedTime() { return GetNumber("lastModifiedTime"); }
 
   bool ShouldCheckStartupModifications()
   {
-    return Type().EqualsLiteral("webextension-langpack");
+    return Type().EqualsLiteral("locale");
   }
 
 
   Result<nsCOMPtr<nsIFile>, nsresult> FullPath();
 
   Result<bool, nsresult> UpdateLastModifiedTime();
 
 
--- a/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
@@ -42,24 +42,22 @@ XPCOMUtils.defineLazyModuleGetters(this,
 
 const {nsIBlocklistService} = Ci;
 
 // These are injected from XPIProvider.jsm
 /* globals
  *         BOOTSTRAP_REASONS,
  *         DB_SCHEMA,
  *         XPIStates,
- *         isWebExtension,
  */
 
 for (let sym of [
   "BOOTSTRAP_REASONS",
   "DB_SCHEMA",
   "XPIStates",
-  "isWebExtension",
 ]) {
   XPCOMUtils.defineLazyGetter(this, sym, () => XPIInternal[sym]);
 }
 
 ChromeUtils.import("resource://gre/modules/Log.jsm");
 const LOGGER_ID = "addons.xpi-utils";
 
 const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
@@ -95,22 +93,21 @@ const PENDING_INSTALL_METADATA =
     ["syncGUID", "targetApplications", "userDisabled", "softDisabled",
      "existingAddonID", "sourceURI", "releaseNotesURI", "installDate",
      "updateDate", "applyBackgroundUpdates", "compatibilityOverrides",
      "installTelemetryInfo"];
 
 const COMPATIBLE_BY_DEFAULT_TYPES = {
   extension: true,
   dictionary: true,
-  "webextension-dictionary": true,
 };
 
 // Properties to save in JSON file
 const PROP_JSON_FIELDS = ["id", "syncGUID", "version", "type",
-                          "updateURL", "optionsURL",
+                          "isWebExtension", "updateURL", "optionsURL",
                           "optionsType", "optionsBrowserStyle", "aboutURL",
                           "defaultLocale", "visible", "active", "userDisabled",
                           "appDisabled", "pendingUninstall", "installDate",
                           "updateDate", "applyBackgroundUpdates", "path",
                           "skinnable", "sourceURI", "releaseNotesURI",
                           "softDisabled", "foreignInstall",
                           "strictCompatibility", "locales", "targetApplications",
                           "targetPlatforms", "signedState",
@@ -118,30 +115,20 @@ const PROP_JSON_FIELDS = ["id", "syncGUI
                           "userPermissions", "icons", "iconURL",
                           "blocklistState", "blocklistURL", "startupData",
                           "previewImage", "hidden", "installTelemetryInfo"];
 
 const LEGACY_TYPES = new Set([
   "extension",
 ]);
 
-// Some add-on types that we track internally are presented as other types
-// externally
-const TYPE_ALIASES = {
-  "webextension": "extension",
-  "webextension-dictionary": "dictionary",
-  "webextension-langpack": "locale",
-  "webextension-theme": "theme",
-};
-
 const SIGNED_TYPES = new Set([
   "extension",
-  "webextension",
-  "webextension-langpack",
-  "webextension-theme",
+  "locale",
+  "theme",
 ]);
 
 // Time to wait before async save of XPI JSON database, in milliseconds
 const ASYNC_SAVE_DELAY_MS = 20;
 
 /**
  * Schedules an idle task, and returns a promise which resolves to an
  * IdleDeadline when an idle slice is available. The caller should
@@ -193,56 +180,16 @@ async function idleForEach(array, func, 
 async function getRepositoryAddon(aAddon) {
   if (aAddon) {
     aAddon._repositoryAddon = await AddonRepository.getCachedAddonByID(aAddon.id);
   }
   return aAddon;
 }
 
 /**
- * Helper function that determines whether an addon of a certain type is a
- * theme.
- *
- * @param {string} type
- *        The add-on type to check.
- * @returns {boolean}
- */
-function isTheme(type) {
-  return type == "theme" || TYPE_ALIASES[type] == "theme";
-}
-
-/**
- * Converts a list of API types to a list of API types and any aliases for those
- * types.
- *
- * @param {Array<string>?} aTypes
- *        An array of types or null for all types
- * @returns {Set<string>?}
- *        An set of types or null for all types
- */
-function getAllAliasesForTypes(aTypes) {
-  if (!aTypes)
-    return null;
-
-  let types = new Set(aTypes);
-  for (let [alias, type] of Object.entries(TYPE_ALIASES)) {
-    // Add any alias for the internal type
-    if (types.has(type)) {
-      types.add(alias);
-    } else {
-      // If this internal type was explicitly requested and its external
-      // type wasn't, ignore it.
-      types.delete(alias);
-    }
-  }
-
-  return types;
-}
-
-/**
  * Copies properties from one object to another. If no target object is passed
  * a new object will be created and returned.
  *
  * @param {object} aObject
  *        An object to copy from
  * @param {string[]} aProperties
  *        An array of properties to be copied
  * @param {object?} [aTarget]
@@ -492,17 +439,17 @@ class AddonInternal {
       version = aPlatformVersion;
 
     // Only extensions and dictionaries can be compatible by default; themes
     // and language packs always use strict compatibility checking.
     // Dictionaries are compatible by default unless requested by the dictinary.
     if (this.type in COMPATIBLE_BY_DEFAULT_TYPES &&
         !this.strictCompatibility &&
         (!AddonManager.strictCompatibility ||
-         this.type == "webextension-dictionary")) {
+         this.type == "dictionary")) {
 
       // The repository can specify compatibility overrides.
       // Note: For now, only blacklisting is supported by overrides.
       let overrides = AddonRepository.getCompatibilityOverridesSync(this.id);
       if (overrides) {
         let override = AddonRepository.findMatchingCompatOverride(this.version,
                                                                   overrides);
         if (override) {
@@ -724,24 +671,16 @@ AddonWrapper = class {
       if (addon.location.isTemporary) {
         return {source: "temporary-addon"};
       }
     }
 
     return addon.installTelemetryInfo;
   }
 
-  get type() {
-    return XPIDatabase.getExternalType(addonFor(this).type);
-  }
-
-  get isWebExtension() {
-    return isWebExtension(addonFor(this).type);
-  }
-
   get temporarilyInstalled() {
     return addonFor(this).location.isTemporary;
   }
 
   get aboutURL() {
     return this.isActive ? addonFor(this).aboutURL : null;
   }
 
@@ -982,18 +921,18 @@ AddonWrapper = class {
 
   set softDisabled(val) {
     let addon = addonFor(this);
     if (val == addon.softDisabled)
       return val;
 
     if (addon.inDatabase) {
       // When softDisabling a theme just enable the active theme
-      if (isTheme(addon.type) && val && !addon.userDisabled) {
-        if (isWebExtension(addon.type))
+      if (addon.type === "theme" && val && !addon.userDisabled) {
+        if (addon.isWebExtension)
           XPIDatabase.updateAddonDisabledState(addon, undefined, val);
       } else {
         XPIDatabase.updateAddonDisabledState(addon, undefined, val);
       }
     } else if (!addon.userDisabled) {
       // Only set softDisabled if not already disabled
       addon.softDisabled = val;
     }
@@ -1106,17 +1045,18 @@ function chooseValue(aAddon, aObj, aProp
 
 function defineAddonWrapperProperty(name, getter) {
   Object.defineProperty(AddonWrapper.prototype, name, {
     get: getter,
     enumerable: true,
   });
 }
 
-["id", "syncGUID", "version", "isCompatible", "isPlatformCompatible",
+["id", "syncGUID", "version", "type", "isWebExtension",
+ "isCompatible", "isPlatformCompatible",
  "providesUpdatesSecurely", "blocklistState", "appDisabled",
  "softDisabled", "skinnable", "foreignInstall",
  "strictCompatibility", "updateURL", "dependencies",
  "signedState", "isCorrectlySigned"].forEach(function(aProp) {
    defineAddonWrapperProperty(aProp, function() {
      let addon = addonFor(this);
      return (aProp in addon) ? addon[aProp] : undefined;
    });
@@ -1617,51 +1557,36 @@ this.XPIDatabase = {
    *
    * @param {string} aId
    *        The ID of the newly enabled add-on
    * @param {string} aType
    *        The type of the newly enabled add-on
    */
   async addonChanged(aId, aType) {
     // We only care about themes in this provider
-    if (!isTheme(aType))
+    if (aType !== "theme")
       return;
 
-    let addons = this.getAddonsByType("webextension-theme");
+    let addons = this.getAddonsByType("theme");
     for (let theme of addons) {
       if (theme.visible && theme.id != aId)
         await this.updateAddonDisabledState(theme, true, undefined, true);
     }
 
     if (!aId && (!LightweightThemeManager.currentTheme ||
                  LightweightThemeManager.currentTheme !== DEFAULT_THEME_ID)) {
       let theme = LightweightThemeManager.getUsedTheme(DEFAULT_THEME_ID);
       // This can only ever be null in tests.
       // This can all go away once lightweight themes are gone.
       if (theme) {
         LightweightThemeManager.currentTheme = theme;
       }
     }
   },
 
-  /**
-   * Converts an internal add-on type to the type presented through the API.
-   *
-   * @param {string} aType
-   *        The internal add-on type
-   * @returns {string}
-   *        An external add-on type
-   */
-  getExternalType(aType) {
-    if (aType in TYPE_ALIASES)
-      return TYPE_ALIASES[aType];
-    return aType;
-  },
-
-  isTheme,
   SIGNED_TYPES,
 
   /**
    * Asynchronously list all addons that match the filter function
    *
    * @param {function(AddonInternal) : boolean} aFilter
    *        Function that takes an addon instance and returns
    *        true if that addon should be included in the selected array
@@ -1825,47 +1750,48 @@ this.XPIDatabase = {
   /**
    * Called to get Addons of a particular type.
    *
    * @param {Array<string>?} aTypes
    *        An array of types to fetch. Can be null to get all types.
    * @returns {Addon[]}
    */
   async getAddonsByTypes(aTypes) {
-    let addons = await this.getVisibleAddons(getAllAliasesForTypes(aTypes));
+    let addons = await this.getVisibleAddons(aTypes ? new Set(aTypes) : null);
     return addons.map(a => a.wrapper);
   },
 
   /**
    * Returns true if signing is required for the given add-on type.
    *
    * @param {string} aType
    *        The add-on type to check.
    * @returns {boolean}
    */
   mustSign(aType) {
     if (!SIGNED_TYPES.has(aType))
       return false;
 
-    if (aType == "webextension-langpack") {
+    if (aType == "locale") {
       return AddonSettings.LANGPACKS_REQUIRE_SIGNING;
     }
 
     return AddonSettings.REQUIRE_SIGNING;
   },
 
   /**
    * Determine if this addon should be disabled due to being legacy
    *
    * @param {Addon} addon The addon to check
    *
    * @returns {boolean} Whether the addon should be disabled for being legacy
    */
   isDisabledLegacy(addon) {
     return (!AddonSettings.ALLOW_LEGACY_EXTENSIONS &&
+            !addon.isWebExtension &&
             LEGACY_TYPES.has(addon.type) &&
 
             // Legacy add-ons are allowed in the system location.
             !addon.location.isSystem &&
 
             // Legacy extensions may be installed temporarily in
             // non-release builds.
             !(AppConstants.MOZ_ALLOW_LEGACY_EXTENSIONS &&
@@ -2238,17 +2164,17 @@ this.XPIDatabase = {
         AddonManagerPrivate.callAddonListeners("onDisabled", wrapper);
       } else {
         await bootstrap.startup(BOOTSTRAP_REASONS.ADDON_ENABLE);
         AddonManagerPrivate.callAddonListeners("onEnabled", wrapper);
       }
     }
 
     // Notify any other providers that a new theme has been enabled
-    if (isTheme(aAddon.type)) {
+    if (aAddon.type === "theme") {
       if (!isDisabled) {
         AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type);
         this.updateXPIStates(aAddon);
       } else if (isDisabled && !aBecauseSelecting) {
         AddonManagerPrivate.notifyAddonChanged(null, "theme");
       }
     }
 
@@ -2890,17 +2816,17 @@ this.XPIDatabaseReconcile = {
       if (isActive != wasActive) {
         let change = isActive ? AddonManager.STARTUP_CHANGE_ENABLED
                               : AddonManager.STARTUP_CHANGE_DISABLED;
         AddonManagerPrivate.addStartupChange(change, id);
       }
     } else if (xpiState && xpiState.wasRestored) {
       isActive = xpiState.enabled;
 
-      if (currentAddon.type == "webextension-theme")
+      if (currentAddon.isWebExtension && currentAddon.type == "theme")
         currentAddon.userDisabled = !isActive;
 
       // If the add-on wasn't active and it isn't already disabled in some way
       // then it was probably either softDisabled or userDisabled
       if (!isActive && !currentAddon.disabled) {
         // If the add-on is softblocked then assume it is softDisabled
         if (currentAddon.blocklistState == Services.blocklist.STATE_SOFTBLOCKED)
           currentAddon.softDisabled = true;
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -82,38 +82,33 @@ const PREF_PENDING_OPERATIONS         = 
 const PREF_SYSTEM_ADDON_UPDATE_URL    = "extensions.systemAddon.update.url";
 const PREF_XPI_ENABLED                = "xpinstall.enabled";
 const PREF_XPI_DIRECT_WHITELISTED     = "xpinstall.whitelist.directRequest";
 const PREF_XPI_FILE_WHITELISTED       = "xpinstall.whitelist.fileRequest";
 const PREF_XPI_WHITELIST_REQUIRED     = "xpinstall.whitelist.required";
 
 const TOOLKIT_ID                      = "toolkit@mozilla.org";
 
-/* globals BOOTSTRAP_REASONS, KEY_APP_SYSTEM_ADDONS, KEY_APP_SYSTEM_DEFAULTS, PREF_BRANCH_INSTALLED_ADDON, PREF_SYSTEM_ADDON_SET, TEMPORARY_ADDON_SUFFIX, XPI_PERMISSION, XPIStates, isWebExtension, iterDirectory */
+/* globals BOOTSTRAP_REASONS, KEY_APP_SYSTEM_ADDONS, KEY_APP_SYSTEM_DEFAULTS, PREF_BRANCH_INSTALLED_ADDON, PREF_SYSTEM_ADDON_SET, TEMPORARY_ADDON_SUFFIX, XPI_PERMISSION, XPIStates, iterDirectory */
 const XPI_INTERNAL_SYMBOLS = [
   "BOOTSTRAP_REASONS",
   "KEY_APP_SYSTEM_ADDONS",
   "KEY_APP_SYSTEM_DEFAULTS",
   "PREF_BRANCH_INSTALLED_ADDON",
   "PREF_SYSTEM_ADDON_SET",
   "TEMPORARY_ADDON_SUFFIX",
   "XPI_PERMISSION",
   "XPIStates",
-  "isWebExtension",
   "iterDirectory",
 ];
 
 for (let name of XPI_INTERNAL_SYMBOLS) {
   XPCOMUtils.defineLazyGetter(this, name, () => XPIInternal[name]);
 }
 
-function isTheme(type) {
-  return XPIDatabase.isTheme(type);
-}
-
 /**
  * Returns a nsIFile instance for the given path, relative to the given
  * base file, if provided.
  *
  * @param {string} path
  *        The (possibly relative) path of the file.
  * @param {nsIFile} [base]
  *        An optional file to use as a base path if `path` is relative.
@@ -178,17 +173,16 @@ const PROP_LOCALE_MULTI  = ["developers"
 const TYPES = {
   extension: 2,
   dictionary: 64,
 };
 
 const COMPATIBLE_BY_DEFAULT_TYPES = {
   extension: true,
   dictionary: true,
-  "webextension-dictionary": true,
 };
 
 // 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
 // someone from creating an ID for a permanent add-on that could be replaced
 // by a temporary add-on (because that would be confusing, I guess).
 const TEMP_INSTALL_ID_GEN_SESSION =
   new Uint8Array(Float64Array.of(Math.random()).buffer);
@@ -449,30 +443,30 @@ async function loadManifestFromWebManife
   // A * is illegal in strict_min_version
   if (bss.strict_min_version && bss.strict_min_version.split(".").some(part => part == "*")) {
     throw new Error("The use of '*' in strict_min_version is invalid");
   }
 
   let addon = new AddonInternal();
   addon.id = bss.id;
   addon.version = manifest.version;
-  addon.type = extension.type === "extension" ?
-               "webextension" : `webextension-${extension.type}`;
+  addon.type = extension.type === "langpack" ? "locale" : extension.type;
+  addon.isWebExtension = true;
   addon.strictCompatibility = true;
   addon.internalName = null;
   addon.updateURL = bss.update_url;
   addon.optionsBrowserStyle = true;
   addon.optionsURL = null;
   addon.optionsType = null;
   addon.aboutURL = null;
   addon.dependencies = Object.freeze(Array.from(extension.dependencies));
   addon.startupData = extension.startupData;
   addon.hidden = manifest.hidden;
 
-  if (isTheme(addon.type) && await aPackage.hasResource("preview.png")) {
+  if (addon.type === "theme" && await aPackage.hasResource("preview.png")) {
     addon.previewImage = "preview.png";
   }
 
   if (manifest.options_ui) {
     // Store just the relative path here, the AddonWrapper getURL
     // wrapper maps this to a full URL.
     addon.optionsURL = manifest.options_ui.page;
     if (manifest.options_ui.open_in_tab)
@@ -620,16 +614,17 @@ async function loadManifestFromRDF(aUri,
     addon.type = null;
     for (let name in TYPES) {
       if (TYPES[name] == type) {
         addon.type = name;
         break;
       }
     }
   }
+  addon.isWebExtension = false;
 
   if (!(addon.type in TYPES))
     throw new Error("Install manifest specifies unknown type: " + addon.type);
 
   if (!addon.id)
     throw new Error("No ID in install manifest");
   if (!gIDTest.test(addon.id))
     throw new Error("Illegal add-on ID " + addon.id);
@@ -649,17 +644,17 @@ async function loadManifestFromRDF(aUri,
         addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE_BROWSER &&
         addon.optionsType != AddonManager.OPTIONS_TYPE_TAB) {
       throw new Error("Install manifest specifies unknown optionsType: " + addon.optionsType);
     }
   } else {
     // Convert legacy dictionaries into a format the WebExtension
     // dictionary loader can process.
     if (addon.type === "dictionary") {
-      addon.type = "webextension-dictionary";
+      addon.isWebExtension = true;
       let dictionaries = {};
       await aPackage.iterFiles(({path}) => {
         let match = /^dictionaries\/([^\/]+)\.dic$/.exec(path);
         if (match) {
           let lang = match[1].replace(/_/g, "-");
           dictionaries[lang] = match[0];
         }
       });
@@ -1556,17 +1551,17 @@ class AddonInstall {
 
       if (this.existingAddon) {
         // Check various conditions related to upgrades
         if (this.addon.id != this.existingAddon.id) {
           return Promise.reject([AddonManager.ERROR_INCORRECT_ID,
                                  `Refusing to upgrade addon ${this.existingAddon.id} to different ID ${this.addon.id}`]);
         }
 
-        if (isWebExtension(this.existingAddon.type) && !isWebExtension(this.addon.type)) {
+        if (this.existingAddon.isWebExtension && !this.addon.isWebExtension) {
           return Promise.reject([AddonManager.ERROR_UNEXPECTED_ADDON_TYPE,
                                  "WebExtensions may not be updated to other extension types"]);
         }
       }
 
       if (XPIDatabase.mustSign(this.addon.type)) {
         if (this.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
           // This add-on isn't properly signed by a signature that chains to the
@@ -1785,17 +1780,17 @@ class AddonInstall {
 
         logger.debug(`Install of ${this.sourceURI.spec} completed.`);
         this.state = AddonManager.STATE_INSTALLED;
         this._callInstallListeners("onInstallEnded", this.addon.wrapper);
 
         XPIDatabase.recordAddonTelemetry(this.addon);
 
         // Notify providers that a new theme has been enabled.
-        if (isTheme(this.addon.type) && this.addon.active)
+        if (this.addon.type === "theme" && this.addon.active)
           AddonManagerPrivate.notifyAddonChanged(this.addon.id, this.addon.type);
       };
 
       this._startupPromise = (async () => {
         if (this.existingAddon) {
           await XPIInternal.BootstrapScope.get(this.existingAddon).update(
             this.addon, !this.addon.disabled, install);
 
@@ -2494,17 +2489,17 @@ function AddonInstallWrapper(aInstall) {
 }
 
 AddonInstallWrapper.prototype = {
   get __AddonInstallInternal__() {
     return AppConstants.DEBUG ? installFor(this) : undefined;
   },
 
   get type() {
-    return XPIDatabase.getExternalType(installFor(this).type);
+    return installFor(this).type;
   },
 
   get iconURL() {
     return installFor(this).icons[32];
   },
 
   get existingAddon() {
     let install = installFor(this);
@@ -2648,17 +2643,17 @@ UpdateChecker.prototype = {
    *        The list of update details for the add-on
    */
   async onUpdateCheckComplete(aUpdates) {
     XPIInstall.done(this.addon._updateCheck);
     this.addon._updateCheck = null;
     let AUC = AddonUpdateChecker;
     let ignoreMaxVersion = false;
     // Ignore strict compatibility for dictionaries by default.
-    let ignoreStrictCompat = (this.addon.type == "webextension-dictionary");
+    let ignoreStrictCompat = (this.addon.type == "dictionary");
     if (!AddonManager.checkCompatibility) {
       ignoreMaxVersion = true;
       ignoreStrictCompat = true;
     } else if (this.addon.type in COMPATIBLE_BY_DEFAULT_TYPES &&
                !AddonManager.strictCompatibility &&
                !this.addon.strictCompatibility) {
       ignoreMaxVersion = true;
     }
@@ -3852,17 +3847,17 @@ var XPIInstall = {
    * @param {Array<string>?} aTypes
    *        An array of types or null to get all types
    * @returns {AddonInstall[]}
    */
   getInstallsByTypes(aTypes) {
     let results = [...this.installs];
     if (aTypes) {
       results = results.filter(install => {
-        return aTypes.includes(XPIDatabase.getExternalType(install.type));
+        return aTypes.includes(install.type);
       });
     }
 
     return results.map(install => install.wrapper);
   },
 
   /**
    * Temporarily installs add-on from a local XPI file or directory.
@@ -3947,17 +3942,17 @@ var XPIInstall = {
 
     AddonManagerPrivate.callInstallListeners("onExternalInstall",
                                              null, addon.wrapper,
                                              oldAddon ? oldAddon.wrapper : null,
                                              false);
     AddonManagerPrivate.callAddonListeners("onInstalled", addon.wrapper);
 
     // Notify providers that a new theme has been enabled.
-    if (isTheme(addon.type))
+    if (addon.type === "theme")
       AddonManagerPrivate.notifyAddonChanged(addon.id, addon.type, false);
 
     return addon.wrapper;
   },
 
   /**
    * Uninstalls an add-on, immediately if possible or marks it as pending
    * uninstall if not.
@@ -4064,17 +4059,17 @@ var XPIInstall = {
       }
     } else if (aAddon.active) {
       XPIStates.disableAddon(aAddon.id);
       bootstrap.shutdown(BOOTSTRAP_REASONS.ADDON_UNINSTALL);
       XPIDatabase.updateAddonActive(aAddon, false);
     }
 
     // Notify any other providers that a new theme has been enabled
-    if (isTheme(aAddon.type) && aAddon.active)
+    if (aAddon.type === "theme" && aAddon.active)
       AddonManagerPrivate.notifyAddonChanged(null, aAddon.type);
   },
 
   /**
    * Cancels the pending uninstall of an add-on.
    *
    * @param {DBAddonInternal} aAddon
    *        The DBAddonInternal to cancel uninstall for
@@ -4105,15 +4100,15 @@ var XPIInstall = {
     AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
 
     if (!aAddon.disabled) {
       XPIInternal.BootstrapScope.get(aAddon).startup(BOOTSTRAP_REASONS.ADDON_INSTALL);
       XPIDatabase.updateAddonActive(aAddon, true);
     }
 
     // Notify any other providers that this theme is now enabled again.
-    if (isTheme(aAddon.type) && aAddon.active)
+    if (aAddon.type === "theme" && aAddon.active)
       AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, false);
   },
 
   DirectoryInstaller,
   SystemAddonInstaller,
 };
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -218,28 +218,16 @@ function getFile(path, base = null) {
 
   // If the path isn't absolute, we must have a base path.
   let file = base.clone();
   file.appendRelativePath(path);
   return file;
 }
 
 /**
- * Helper function that determines whether an addon of a certain type is a
- * WebExtension.
- *
- * @param {string} type
- *        The add-on type to check.
- * @returns {boolean}
- */
-function isWebExtension(type) {
-  return type == "webextension" || type == "webextension-theme";
-}
-
-/**
  * Returns true if the given file, based on its name, should be treated
  * as an XPI. If the file does not have an appropriate extension, it is
  * assumed to be an unpacked add-on.
  *
  * @param {string} filename
  *        The filename to check.
  * @param {boolean} [strict = false]
  *        If true, this file is in a location maintained by the browser, and
@@ -370,16 +358,17 @@ function* iterDirectory(aDir) {
  * The on-disk state of an individual XPI, created from an Object
  * as stored in the addonStartup.json file.
  */
 const JSON_FIELDS = Object.freeze([
   "changed",
   "dependencies",
   "enabled",
   "file",
+  "isWebExtension",
   "lastModifiedTime",
   "path",
   "runInSafeMode",
   "signedState",
   "startupData",
   "telemetryKey",
   "type",
   "version",
@@ -453,16 +442,17 @@ class XPIState {
    * data, to be saved to addonStartup.json.
    *
    * @returns {Object}
    */
   toJSON() {
     let json = {
       dependencies: this.dependencies,
       enabled: this.enabled,
+      isWebExtension: this.isWebExtension,
       lastModifiedTime: this.lastModifiedTime,
       path: this.relativePath,
       runInSafeMode: this.runInSafeMode,
       signedState: this.signedState,
       telemetryKey: this.telemetryKey,
       version: this.version,
     };
     if (this.type != "extension") {
@@ -526,16 +516,17 @@ class XPIState {
     // did a full recursive scan in that case, so we don't need to do it again.
     // We don't use aDBAddon.active here because it's not updated until after restart.
     let mustGetMod = (aDBAddon.visible && !aDBAddon.disabled && !this.enabled);
 
     this.enabled = aDBAddon.visible && !aDBAddon.disabled;
 
     this.version = aDBAddon.version;
     this.type = aDBAddon.type;
+    this.isWebExtension = aDBAddon.isWebExtension;
     if (aDBAddon.startupData) {
       this.startupData = aDBAddon.startupData;
     }
 
     this.telemetryKey = this.getTelemetryKey();
 
     this.dependencies = aDBAddon.dependencies;
     this.runInSafeMode = canRunInSafeMode(aDBAddon);
@@ -1516,17 +1507,18 @@ class BootstrapScope {
    * @returns {any}
    *        The return value of the bootstrap method.
    */
   async callBootstrapMethod(aMethod, aReason, aExtraParams = {}) {
     let {addon, runInSafeMode} = this;
     if (Services.appinfo.inSafeMode && !runInSafeMode)
       return null;
 
-    if (addon.type == "extension" && aMethod == "startup") {
+    if (!addon.isWebExtension && addon.type == "extension" &&
+        aMethod == "startup") {
       logger.debug(`Registering manifest for ${this.file.path}`);
       Components.manager.addBootstrappedManifestLocation(this.file);
     }
 
     try {
       if (!this.scope) {
         this.loadBootstrapScope(aReason);
       }
@@ -1627,22 +1619,34 @@ class BootstrapScope {
     // But not at app startup, since we'll already have added all of our
     // annotations before starting any loads.
     if (aReason !== BOOTSTRAP_REASONS.APP_STARTUP) {
       XPIProvider.addAddonsToCrashReporter();
     }
 
     logger.debug(`Loading bootstrap scope from ${this.file.path}`);
 
-    if (isWebExtension(this.addon.type)) {
-      this.scope = Extension.getBootstrapScope(this.addon.id, this.file);
-    } else if (this.addon.type === "webextension-langpack") {
-      this.scope = Langpack.getBootstrapScope(this.addon.id, this.file);
-    } else if (this.addon.type === "webextension-dictionary") {
-      this.scope = Dictionary.getBootstrapScope(this.addon.id, this.file);
+    if (this.addon.isWebExtension) {
+      switch (this.addon.type) {
+        case "extension":
+        case "theme":
+          this.scope = Extension.getBootstrapScope(this.addon.id, this.file);
+          break;
+
+        case "locale":
+          this.scope = Langpack.getBootstrapScope(this.addon.id, this.file);
+          break;
+
+        case "dictionary":
+          this.scope = Dictionary.getBootstrapScope(this.addon.id, this.file);
+          break;
+
+        default:
+          throw new Error(`Unknown webextension type ${this.addon.type}`);
+      }
     } else {
       let uri = getURIForResourceInFile(this.file, "bootstrap.js").spec;
 
       let principal = Services.scriptSecurityManager.getSystemPrincipal();
       this.scope =
         new Cu.Sandbox(principal, { sandboxName: uri,
                                     addonId: this.addon.id,
                                     wantGlobalProperties: ["ChromeUtils"],
@@ -1828,17 +1832,17 @@ class BootstrapScope {
    * @returns {Promise}
    *        Resolves when all required bootstrap callbacks have
    *        completed.
    */
   async update(newAddon, startup = false, updateCallback) {
     let reason = XPIInstall.newVersionReason(this.addon.version, newAddon.version);
     let extraArgs = {oldVersion: this.addon.version, newVersion: newAddon.version};
 
-    let callUpdate = isWebExtension(this.addon.type) && isWebExtension(newAddon.type);
+    let callUpdate = this.addon.isWebExtension && newAddon.isWebExtension;
 
     await this._uninstall(reason, callUpdate, extraArgs);
 
     if (updateCallback) {
       await updateCallback();
     }
 
     this.addon = newAddon;
@@ -2663,17 +2667,17 @@ var XPIProvider = {
       let {scope, isSystem} = addon.location;
       result.push({
         id: addon.id,
         version: addon.version,
         type: addon.type,
         updateDate: addon.lastModifiedTime,
         scope,
         isSystem,
-        isWebExtension: isWebExtension(addon),
+        isWebExtension: addon.isWebExtension,
       });
     }
 
     return {addons: result, fullData: false};
   },
 
   onDebugConnectionChange({what, connection}) {
     if (what != "opened")
@@ -2740,17 +2744,16 @@ var XPIInternal = {
   SystemAddonLocation,
   TEMPORARY_ADDON_SUFFIX,
   TemporaryInstallLocation,
   XPIStates,
   XPI_PERMISSION,
   awaitPromise,
   canRunInSafeMode,
   getURIForResourceInFile,
-  isWebExtension,
   isXPI,
   iterDirectory,
 };
 
 var addonTypes = [
   new AddonManagerPrivate.AddonType("extension", URI_EXTENSION_STRINGS,
                                     "type.extension.name",
                                     AddonManager.VIEW_TYPE_LIST, 4000,