Bug 1365709 - Consume new webextension based language packs. r=kmag
authorZibi Braniecki <zbraniecki@mozilla.com>
Mon, 28 Aug 2017 10:48:00 -0700
changeset 428295 960c3df4ced4cef42bf844dbc1337b820f60b542
parent 428294 68b71f2b49825ea9f67026c95b5974d13d8804be
child 428296 b5cbf8e3f77628c9bf9bebdadec422151cd9c01f
push id7761
push userjlund@mozilla.com
push dateFri, 15 Sep 2017 00:19:52 +0000
treeherdermozilla-beta@c38455951db4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1365709
milestone57.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 1365709 - Consume new webextension based language packs. r=kmag MozReview-Commit-ID: DeJlq8MWpfs
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/schemas/manifest.json
toolkit/mozapps/extensions/internal/XPIInstall.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -1,15 +1,15 @@
 /* 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";
 
-this.EXPORTED_SYMBOLS = ["Extension", "ExtensionData"];
+this.EXPORTED_SYMBOLS = ["Extension", "ExtensionData", "Langpack"];
 
 /* exported Extension, ExtensionData */
 /* globals Extension ExtensionData */
 
 /*
  * This file is the main entry point for extensions. When an extension
  * loads, its bootstrap.js file creates a Extension instance
  * and calls .startup() on it. It calls .shutdown() when the extension
@@ -35,39 +35,48 @@ const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 Cu.importGlobalProperties(["TextEncoder"]);
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.importGlobalProperties(["fetch"]);
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AddonManager: "resource://gre/modules/AddonManager.jsm",
   AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm",
+  AppConstants: "resource://gre/modules/AppConstants.jsm",
   AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
   ExtensionCommon: "resource://gre/modules/ExtensionCommon.jsm",
   ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.jsm",
   ExtensionStorage: "resource://gre/modules/ExtensionStorage.jsm",
   ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.jsm",
+  FileSource: "resource://gre/modules/L10nRegistry.jsm",
+  L10nRegistry: "resource://gre/modules/L10nRegistry.jsm",
   Log: "resource://gre/modules/Log.jsm",
   MessageChannel: "resource://gre/modules/MessageChannel.jsm",
   NetUtil: "resource://gre/modules/NetUtil.jsm",
   OS: "resource://gre/modules/osfile.jsm",
   Schemas: "resource://gre/modules/Schemas.jsm",
   setTimeout: "resource://gre/modules/Timer.jsm",
   TelemetryStopwatch: "resource://gre/modules/TelemetryStopwatch.jsm",
 });
 
 XPCOMUtils.defineLazyGetter(
   this, "processScript",
   () => Cc["@mozilla.org/webextensions/extension-process-script;1"]
           .getService().wrappedJSObject);
 
+XPCOMUtils.defineLazyGetter(
+  this, "resourceProtocol",
+  () => Services.io.getProtocolHandler("resource")
+          .QueryInterface(Ci.nsIResProtocolHandler));
+
 Cu.import("resource://gre/modules/ExtensionParent.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetters(this, {
   aomStartup: ["@mozilla.org/addons/addon-manager-startup;1", "amIAddonManagerStartup"],
   uuidGen: ["@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"],
 });
 
@@ -290,16 +299,17 @@ UninstallObserver.init();
 // No functionality of this class is guaranteed to work before
 // |loadManifest| has been called, and completed.
 this.ExtensionData = class {
   constructor(rootURI) {
     this.rootURI = rootURI;
     this.resourceURL = rootURI.spec;
 
     this.manifest = null;
+    this.type = null;
     this.id = null;
     this.uuid = null;
     this.localeData = null;
     this._promiseLocales = null;
 
     this.apiNames = new Set();
     this.dependencies = new Set();
     this.permissions = new Set();
@@ -448,16 +458,20 @@ this.ExtensionData = class {
   }
 
   // This method should return a structured representation of any
   // capabilities this extension has access to, as derived from the
   // manifest.  The current implementation just returns the contents
   // of the permissions attribute, if we add things like url_overrides,
   // they should also be added here.
   get userPermissions() {
+    if (this.type !== "extension") {
+      return null;
+    }
+
     let result = {
       origins: this.whiteListedHosts.patterns.map(matcher => matcher.pattern),
       apis: [...this.apiNames],
     };
 
     if (Array.isArray(this.manifest.content_scripts)) {
       for (let entry of this.manifest.content_scripts) {
         result.origins.push(...entry.matches);
@@ -503,32 +517,40 @@ this.ExtensionData = class {
 
       logError: error => {
         this.manifestWarning(error);
       },
 
       preprocessors: {},
     };
 
+    let manifestType = "manifest.WebExtensionManifest";
     if (this.manifest.theme) {
+      this.type = "theme";
+      // XXX create a separate manifest type for themes
       let invalidProps = validateThemeManifest(Object.getOwnPropertyNames(this.manifest));
 
       if (invalidProps.length) {
         let message = `Themes defined in the manifest may only contain static resources. ` +
           `If you would like to use additional properties, please use the "theme" permission instead. ` +
           `(the invalid properties found are: ${invalidProps})`;
         this.manifestError(message);
       }
+    } else if (this.manifest.langpack_id) {
+      this.type = "langpack";
+      manifestType = "manifest.WebExtensionLangpackManifest";
+    } else {
+      this.type = "extension";
     }
 
     if (this.localeData) {
       context.preprocessors.localize = (value, context) => this.localize(value);
     }
 
-    let normalized = Schemas.normalize(this.manifest, "manifest.WebExtensionManifest", context);
+    let normalized = Schemas.normalize(this.manifest, manifestType, context);
     if (normalized.error) {
       this.manifestError(normalized.error);
       return null;
     }
 
     manifest = normalized.value;
 
     let id;
@@ -543,64 +565,69 @@ this.ExtensionData = class {
     if (!this.id) {
       this.id = id;
     }
 
     let apiNames = new Set();
     let dependencies = new Set();
     let originPermissions = new Set();
     let permissions = new Set();
+    let webAccessibleResources = [];
 
-    for (let perm of manifest.permissions) {
-      if (perm === "geckoProfiler") {
-        const acceptedExtensions = Services.prefs.getStringPref("extensions.geckoProfiler.acceptedExtensionIds", "");
-        if (!acceptedExtensions.split(",").includes(id)) {
-          this.manifestError("Only whitelisted extensions are allowed to access the geckoProfiler.");
-          continue;
+    if (this.type === "extension") {
+      for (let perm of manifest.permissions) {
+        if (perm === "geckoProfiler") {
+          const acceptedExtensions = Services.prefs.getStringPref("extensions.geckoProfiler.acceptedExtensionIds", "");
+          if (!acceptedExtensions.split(",").includes(id)) {
+            this.manifestError("Only whitelisted extensions are allowed to access the geckoProfiler.");
+            continue;
+          }
+        }
+
+        let type = classifyPermission(perm);
+        if (type.origin) {
+          let matcher = new MatchPattern(perm, {ignorePath: true});
+
+          perm = matcher.pattern;
+          originPermissions.add(perm);
+        } else if (type.api) {
+          apiNames.add(type.api);
+        }
+
+        permissions.add(perm);
+      }
+
+      if (this.id) {
+        // An extension always gets permission to its own url.
+        let matcher = new MatchPattern(this.getURL(), {ignorePath: true});
+        originPermissions.add(matcher.pattern);
+
+        // Apply optional permissions
+        let perms = await ExtensionPermissions.get(this);
+        for (let perm of perms.permissions) {
+          permissions.add(perm);
+        }
+        for (let origin of perms.origins) {
+          originPermissions.add(origin);
         }
       }
 
-      let type = classifyPermission(perm);
-      if (type.origin) {
-        let matcher = new MatchPattern(perm, {ignorePath: true});
-
-        perm = matcher.pattern;
-        originPermissions.add(perm);
-      } else if (type.api) {
-        apiNames.add(type.api);
+      for (let api of apiNames) {
+        dependencies.add(`${api}@experiments.addons.mozilla.org`);
       }
 
-      permissions.add(perm);
-    }
-
-    if (this.id) {
-      // An extension always gets permission to its own url.
-      let matcher = new MatchPattern(this.getURL(), {ignorePath: true});
-      originPermissions.add(matcher.pattern);
-
-      // Apply optional permissions
-      let perms = await ExtensionPermissions.get(this);
-      for (let perm of perms.permissions) {
-        permissions.add(perm);
-      }
-      for (let origin of perms.origins) {
-        originPermissions.add(origin);
+      // Normalize all patterns to contain a single leading /
+      if (manifest.web_accessible_resources) {
+        webAccessibleResources = manifest.web_accessible_resources
+          .map(path => path.replace(/^\/*/, "/"));
       }
     }
 
-    for (let api of apiNames) {
-      dependencies.add(`${api}@experiments.addons.mozilla.org`);
-    }
-
-    // Normalize all patterns to contain a single leading /
-    let webAccessibleResources = (manifest.web_accessible_resources || [])
-        .map(path => path.replace(/^\/*/, "/"));
-
     return {apiNames, dependencies, originPermissions, id, manifest, permissions,
-            webAccessibleResources};
+            webAccessibleResources, type: this.type};
   }
 
   // Reads the extension's |manifest.json| file, and stores its
   // parsed contents in |this.manifest|.
   async loadManifest() {
     let [manifestData] = await Promise.all([
       this.parseManifest(),
       Management.lazyInit(),
@@ -614,16 +641,17 @@ this.ExtensionData = class {
     if (!this.id) {
       this.id = manifestData.id;
     }
 
     this.manifest = manifestData.manifest;
     this.apiNames = manifestData.apiNames;
     this.dependencies = manifestData.dependencies;
     this.permissions = manifestData.permissions;
+    this.type = manifestData.type;
 
     this.webAccessibleResources = manifestData.webAccessibleResources.map(res => new MatchGlob(res));
     this.whiteListedHosts = new MatchPatternSet(manifestData.originPermissions);
 
     return this.manifest;
   }
 
   localizeMessage(...args) {
@@ -793,16 +821,31 @@ XPCOMUtils.defineLazyGetter(BootstrapSco
     [BOOTSTRAP_REASONS.ADDON_DISABLE]: "ADDON_DISABLE",
     [BOOTSTRAP_REASONS.ADDON_INSTALL]: "ADDON_INSTALL",
     [BOOTSTRAP_REASONS.ADDON_UNINSTALL]: "ADDON_UNINSTALL",
     [BOOTSTRAP_REASONS.ADDON_UPGRADE]: "ADDON_UPGRADE",
     [BOOTSTRAP_REASONS.ADDON_DOWNGRADE]: "ADDON_DOWNGRADE",
   });
 });
 
+class LangpackBootstrapScope {
+  install(data, reason) {}
+  uninstall(data, reason) {}
+
+  startup(data, reason) {
+    this.langpack = new Langpack(data);
+    return this.langpack.startup();
+  }
+
+  shutdown(data, reason) {
+    this.langpack.shutdown();
+    this.langpack = null;
+  }
+}
+
 // We create one instance of this class per extension. |addonData|
 // comes directly from bootstrap.js when initializing.
 this.Extension = class extends ExtensionData {
   constructor(addonData, startupReason) {
     super(addonData.resourceURI);
 
     this.uuid = UUIDMap.get(addonData.id);
     this.instanceId = getUniqueId();
@@ -1402,8 +1445,132 @@ this.Extension = class extends Extension
   get optionalOrigins() {
     if (this._optionalOrigins == null) {
       let origins = this.manifest.optional_permissions.filter(perm => classifyPermission(perm).origin);
       this._optionalOrigins = new MatchPatternSet(origins, {ignorePath: true});
     }
     return this._optionalOrigins;
   }
 };
+
+this.Langpack = class extends ExtensionData {
+  constructor(addonData, startupReason) {
+    super(addonData.resourceURI);
+  }
+
+  static getBootstrapScope(id, file) {
+    return new LangpackBootstrapScope();
+  }
+
+  async promiseLocales(locale) {
+    let locales = await StartupCache.locales
+      .get([this.id, "@@all_locales"], () => this._promiseLocaleMap());
+
+    return this._setupLocaleData(locales);
+  }
+
+  readLocaleFile(locale) {
+    return StartupCache.locales.get([this.id, this.version, locale],
+                                    () => super.readLocaleFile(locale))
+      .then(result => {
+        this.localeData.messages.set(locale, result);
+      });
+  }
+
+  get manifestCacheKey() {
+    return [this.id, this.version, Services.locale.getAppLocaleAsLangTag()];
+  }
+
+  async _parseManifest() {
+    let data = await super.parseManifest();
+
+    const productCodeName = AppConstants.MOZ_BUILD_APP.replace("/", "-");
+
+    // The result path looks like this:
+    //   Firefox - `langpack-pl-browser`
+    //   Fennec - `langpack-pl-mobile-android`
+    data.langpackId =
+      `langpack-${data.manifest.langpack_id}-${productCodeName}`;
+
+    const l10nRegistrySources = {};
+
+    // Check if there's a root directory `/localization` in the langpack.
+    // If there is one, add it with the name `toolkit` as a FileSource.
+    const entries = await this.readDirectory("./localization");
+    if (entries.length > 0) {
+      l10nRegistrySources["toolkit"] = "";
+    }
+
+    // Add any additional sources listed in the manifest
+    if (data.manifest.sources) {
+      for (const [sourceName, {base_path}] of Object.entries(data.manifest.sources)) {
+        l10nRegistrySources[sourceName] = base_path;
+      }
+    }
+
+    data.l10nRegistrySources = l10nRegistrySources;
+    data.chromeResources = this.getChromeResources(data.manifest);
+
+    return data;
+  }
+
+  parseManifest() {
+    return StartupCache.manifests.get(this.manifestCacheKey,
+                                      () => this._parseManifest());
+  }
+
+  async startup(reason) {
+    const data = await this.parseManifest();
+    this.langpackId = data.langpackId;
+    this.l10nRegistrySources = data.l10nRegistrySources;
+
+    const languages = Object.keys(data.manifest.languages);
+    const manifestURI = Services.io.newURI("manifest.json", null, this.rootURI);
+
+    this.chromeRegistryHandle = null;
+    if (data.chromeResources.length > 0) {
+      this.chromeRegistryHandle =
+        aomStartup.registerChrome(manifestURI, data.chromeResources);
+    }
+
+    resourceProtocol.setSubstitution(this.langpackId, this.rootURI);
+
+    for (const [sourceName, basePath] of Object.entries(this.l10nRegistrySources)) {
+      L10nRegistry.registerSource(new FileSource(
+        `${sourceName}-${this.langpackId}`,
+        languages,
+        `resource://${this.langpackId}/${basePath}localization/{locale}/`
+      ));
+    }
+  }
+
+  async shutdown(reason) {
+    for (const sourceName of Object.keys(this.l10nRegistrySources)) {
+      L10nRegistry.removeSource(`${sourceName}-${this.langpackId}`);
+    }
+    if (this.chromeRegistryHandle) {
+      this.chromeRegistryHandle.destruct();
+      this.chromeRegistryHandle = null;
+    }
+
+    resourceProtocol.setSubstitution(this.langpackId, null);
+  }
+
+  getChromeResources(manifest) {
+    const chromeEntries = [];
+    for (const [language, entry] of Object.entries(manifest.languages)) {
+      for (const [alias, path] of Object.entries(entry.chrome_resources || {})) {
+        if (typeof path === "string") {
+          chromeEntries.push(["locale", alias, language, path]);
+        } else {
+          // If the path is not a string, it's an object with path per platform
+          // where the keys are taken from AppConstants.platform
+          const platform = AppConstants.platform;
+          if (platform in path) {
+            chromeEntries.push(["locale", alias, language, path[platform]]);
+          }
+        }
+
+      }
+    }
+    return chromeEntries;
+  }
+}
--- a/toolkit/components/extensions/schemas/manifest.json
+++ b/toolkit/components/extensions/schemas/manifest.json
@@ -207,16 +207,148 @@
             }
           }
 
         },
 
         "additionalProperties": { "$ref": "UnrecognizedProperty" }
       },
       {
+        "id": "WebExtensionLangpackManifest",
+        "type": "object",
+        "description": "Represents a WebExtension language pack manifest.json file",
+        "properties": {
+          "manifest_version": {
+            "type": "integer",
+            "minimum": 2,
+            "maximum": 2
+          },
+
+          "applications": {
+            "type": "object",
+            "optional": true,
+            "properties": {
+              "gecko": {
+                "$ref": "FirefoxSpecificProperties",
+                "optional": true
+              }
+            }
+          },
+
+          "browser_specific_settings": {
+            "type": "object",
+            "optional": true,
+            "properties": {
+              "gecko": {
+                "$ref": "FirefoxSpecificProperties",
+                "optional": true
+              }
+            }
+          },
+
+          "name": {
+            "type": "string",
+            "optional": false,
+            "preprocess": "localize"
+          },
+
+          "short_name": {
+            "type": "string",
+            "optional": true,
+            "preprocess": "localize"
+          },
+
+          "description": {
+            "type": "string",
+            "optional": true,
+            "preprocess": "localize"
+          },
+
+          "author": {
+            "type": "string",
+            "optional": true,
+            "preprocess": "localize",
+            "onError": "warn"
+          },
+
+          "version": {
+            "type": "string",
+            "optional": false
+          },
+
+          "homepage_url": {
+            "type": "string",
+            "format": "url",
+            "optional": true,
+            "preprocess": "localize"
+          },
+
+          "langpack_id": {
+            "type": "string",
+            "pattern": "^[a-zA-Z][a-zA-Z-]+$"
+          },
+
+          "languages": {
+            "type": "object",
+            "patternProperties": {
+              "^[a-z]{2}[a-zA-Z-]*$": {
+                "type": "object",
+                "properties": {
+                  "chrome_resources": {
+                    "type": "object",
+                    "patternProperties": {
+                      "^[a-zA-Z-.]+$": {
+                        "choices": [
+                          {
+                            "$ref": "ExtensionURL"
+                          },
+                          {
+                            "type": "object",
+                            "patternProperties": {
+                              "^[a-z]+$": {
+                                "$ref": "ExtensionURL"
+                              }
+                            }
+                          }
+                        ]
+                      }
+                    }
+                  },
+                  "version": {
+                    "type": "string"
+                  }
+                }
+              }
+            }
+          },
+          "sources": {
+            "type": "object",
+            "optional": true,
+            "patternProperties": {
+              "^[a-z]+$": {
+                "type": "object",
+                "properties": {
+                  "base_path": {
+                    "$ref": "ExtensionURL"
+                  },
+                  "paths": {
+                    "type": "array",
+                    "items": {
+                      "type": "string",
+                      "format": "strictRelativeUrl"
+                    },
+                    "optional": true
+                  }
+                }
+              }
+            }
+          }
+        }
+      },
+      {
         "id": "ThemeIcons",
         "type": "object",
         "properties": {
           "light": {
             "$ref": "ExtensionURL",
             "description": "A light icon to use for dark themes"
           },
           "dark": {
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -329,33 +329,32 @@ async function loadManifestFromWebManife
   // all locales.
   let locales = (extension.errors.length == 0) ?
                 await extension.initAllLocales() : null;
 
   if (extension.errors.length > 0) {
     throw new Error("Extension is invalid");
   }
 
-  let theme = Boolean(manifest.theme);
-
   let bss = (manifest.browser_specific_settings && manifest.browser_specific_settings.gecko)
       || (manifest.applications && manifest.applications.gecko) || {};
   if (manifest.browser_specific_settings && manifest.applications) {
     logger.warn("Ignoring applications property in manifest");
   }
 
   // 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 = "webextension" + (theme ? "-theme" : "");
+  addon.type = extension.type === "extension" ?
+               "webextension" : `webextension-${extension.type}`;
   addon.unpack = false;
   addon.strictCompatibility = true;
   addon.bootstrap = true;
   addon.hasBinaryComponents = false;
   addon.multiprocessCompatible = true;
   addon.internalName = null;
   addon.updateURL = bss.update_url;
   addon.updateKey = null;
@@ -430,17 +429,17 @@ async function loadManifestFromWebManife
   addon.targetApplications = [{
     id: TOOLKIT_ID,
     minVersion: bss.strict_min_version,
     maxVersion: bss.strict_max_version,
   }];
 
   addon.targetPlatforms = [];
   // Themes are disabled by default, except when they're installed from a web page.
-  addon.userDisabled = theme;
+  addon.userDisabled = (extension.type === "theme");
   addon.softDisabled = addon.blocklistState == nsIBlocklistService.STATE_SOFTBLOCKED;
 
   return addon;
 }
 
 /**
  * Reads an AddonInternal object from an RDF stream.
  *
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -18,16 +18,17 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/AddonManager.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AddonRepository: "resource://gre/modules/addons/AddonRepository.jsm",
   AddonSettings: "resource://gre/modules/addons/AddonSettings.jsm",
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   ChromeManifestParser: "resource://gre/modules/ChromeManifestParser.jsm",
   Extension: "resource://gre/modules/Extension.jsm",
+  Langpack: "resource://gre/modules/Extension.jsm",
   LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm",
   FileUtils: "resource://gre/modules/FileUtils.jsm",
   ZipUtils: "resource://gre/modules/ZipUtils.jsm",
   NetUtil: "resource://gre/modules/NetUtil.jsm",
   PermissionsUtils: "resource://gre/modules/PermissionsUtils.jsm",
   OS: "resource://gre/modules/osfile.jsm",
   ConsoleAPI: "resource://gre/modules/Console.jsm",
   ProductAddonChecker: "resource://gre/modules/addons/ProductAddonChecker.jsm",
@@ -202,16 +203,17 @@ const BOOTSTRAP_REASONS = {
 };
 
 // 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",
   "locale",
   "experiment",
 ]);
 
@@ -4208,16 +4210,18 @@ this.XPIProvider = {
                                     addonId: aId,
                                     metadata: { addonID: aId } });
       logger.error("Attempted to load bootstrap scope from missing directory " + aFile.path);
       return;
     }
 
     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 =