1/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2/* vim: set sts=2 sw=2 et tw=80: */
3/* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6"use strict";
7
8var EXPORTED_SYMBOLS = [
9  "Dictionary",
10  "Extension",
11  "ExtensionData",
12  "Langpack",
13  "Management",
14  "ExtensionAddonObserver",
15];
16
17/* exported Extension, ExtensionData */
18
19/*
20 * This file is the main entry point for extensions. When an extension
21 * loads, its bootstrap.js file creates a Extension instance
22 * and calls .startup() on it. It calls .shutdown() when the extension
23 * unloads. Extension manages any extension-specific state in
24 * the chrome process.
25 *
26 * TODO(rpl): we are current restricting the extensions to a single process
27 * (set as the current default value of the "dom.ipc.processCount.extension"
28 * preference), if we switch to use more than one extension process, we have to
29 * be sure that all the browser's frameLoader are associated to the same process,
30 * e.g. by enabling the `maychangeremoteness` attribute, and/or setting
31 * `initialBrowsingContextGroupId` attribute to the correct value.
32 *
33 * At that point we are going to keep track of the existing browsers associated to
34 * a webextension to ensure that they are all running in the same process (and we
35 * are also going to do the same with the browser element provided to the
36 * addon debugging Remote Debugging actor, e.g. because the addon has been
37 * reloaded by the user, we have to  ensure that the new extension pages are going
38 * to run in the same process of the existing addon debugging browser element).
39 */
40
41const { XPCOMUtils } = ChromeUtils.import(
42  "resource://gre/modules/XPCOMUtils.jsm"
43);
44const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
45
46XPCOMUtils.defineLazyModuleGetters(this, {
47  AddonManager: "resource://gre/modules/AddonManager.jsm",
48  AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm",
49  AddonSettings: "resource://gre/modules/addons/AddonSettings.jsm",
50  AMTelemetry: "resource://gre/modules/AddonManager.jsm",
51  AppConstants: "resource://gre/modules/AppConstants.jsm",
52  AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
53  E10SUtils: "resource://gre/modules/E10SUtils.jsm",
54  ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.jsm",
55  ExtensionPreferencesManager:
56    "resource://gre/modules/ExtensionPreferencesManager.jsm",
57  ExtensionProcessScript: "resource://gre/modules/ExtensionProcessScript.jsm",
58  ExtensionStorage: "resource://gre/modules/ExtensionStorage.jsm",
59  ExtensionStorageIDB: "resource://gre/modules/ExtensionStorageIDB.jsm",
60  ExtensionTelemetry: "resource://gre/modules/ExtensionTelemetry.jsm",
61  FileSource: "resource://gre/modules/L10nRegistry.jsm",
62  L10nRegistry: "resource://gre/modules/L10nRegistry.jsm",
63  LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm",
64  Log: "resource://gre/modules/Log.jsm",
65  MessageChannel: "resource://gre/modules/MessageChannel.jsm",
66  NetUtil: "resource://gre/modules/NetUtil.jsm",
67  OS: "resource://gre/modules/osfile.jsm",
68  PluralForm: "resource://gre/modules/PluralForm.jsm",
69  Schemas: "resource://gre/modules/Schemas.jsm",
70  ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.jsm",
71  XPIProvider: "resource://gre/modules/addons/XPIProvider.jsm",
72});
73
74// This is used for manipulating jar entry paths, which always use Unix
75// separators.
76XPCOMUtils.defineLazyGetter(this, "OSPath", () => {
77  let obj = {};
78  ChromeUtils.import("resource://gre/modules/osfile/ospath_unix.jsm", obj);
79  return obj;
80});
81
82XPCOMUtils.defineLazyGetter(this, "resourceProtocol", () =>
83  Services.io
84    .getProtocolHandler("resource")
85    .QueryInterface(Ci.nsIResProtocolHandler)
86);
87
88const { ExtensionCommon } = ChromeUtils.import(
89  "resource://gre/modules/ExtensionCommon.jsm"
90);
91const { ExtensionParent } = ChromeUtils.import(
92  "resource://gre/modules/ExtensionParent.jsm"
93);
94const { ExtensionUtils } = ChromeUtils.import(
95  "resource://gre/modules/ExtensionUtils.jsm"
96);
97
98XPCOMUtils.defineLazyServiceGetters(this, {
99  aomStartup: [
100    "@mozilla.org/addons/addon-manager-startup;1",
101    "amIAddonManagerStartup",
102  ],
103  spellCheck: ["@mozilla.org/spellchecker/engine;1", "mozISpellCheckingEngine"],
104  uuidGen: ["@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"],
105});
106
107XPCOMUtils.defineLazyPreferenceGetter(
108  this,
109  "processCount",
110  "dom.ipc.processCount.extension"
111);
112
113// Temporary pref to be turned on when ready.
114XPCOMUtils.defineLazyPreferenceGetter(
115  this,
116  "userContextIsolation",
117  "extensions.userContextIsolation.enabled",
118  false
119);
120
121XPCOMUtils.defineLazyPreferenceGetter(
122  this,
123  "userContextIsolationDefaultRestricted",
124  "extensions.userContextIsolation.defaults.restricted",
125  "[]"
126);
127
128var {
129  GlobalManager,
130  ParentAPIManager,
131  StartupCache,
132  apiManager: Management,
133} = ExtensionParent;
134
135const { getUniqueId, promiseTimeout } = ExtensionUtils;
136
137const { EventEmitter } = ExtensionCommon;
138
139XPCOMUtils.defineLazyGetter(this, "console", ExtensionCommon.getConsole);
140
141XPCOMUtils.defineLazyGetter(
142  this,
143  "LocaleData",
144  () => ExtensionCommon.LocaleData
145);
146
147XPCOMUtils.defineLazyGetter(this, "LAZY_NO_PROMPT_PERMISSIONS", async () => {
148  // Wait until all extension API schemas have been loaded and parsed.
149  await Management.lazyInit();
150  return new Set(
151    Schemas.getPermissionNames([
152      "PermissionNoPrompt",
153      "OptionalPermissionNoPrompt",
154    ])
155  );
156});
157
158const { sharedData } = Services.ppmm;
159
160const PRIVATE_ALLOWED_PERMISSION = "internal:privateBrowsingAllowed";
161const SVG_CONTEXT_PROPERTIES_PERMISSION =
162  "internal:svgContextPropertiesAllowed";
163
164// The userContextID reserved for the extension storage (its purpose is ensuring that the IndexedDB
165// storage used by the browser.storage.local API is not directly accessible from the extension code,
166// it is defined and reserved as "userContextIdInternal.webextStorageLocal" in ContextualIdentityService.jsm).
167const WEBEXT_STORAGE_USER_CONTEXT_ID = -1 >>> 0;
168
169// The maximum time to wait for extension child shutdown blockers to complete.
170const CHILD_SHUTDOWN_TIMEOUT_MS = 8000;
171
172// Permissions that are only available to privileged extensions.
173const PRIVILEGED_PERMS = new Set([
174  "activityLog",
175  "mozillaAddons",
176  "geckoViewAddons",
177  "telemetry",
178  "urlbar",
179  "nativeMessagingFromContent",
180  "normandyAddonStudy",
181  "networkStatus",
182]);
183
184const INSTALL_AND_UPDATE_STARTUP_REASONS = new Set([
185  "ADDON_INSTALL",
186  "ADDON_UPGRADE",
187  "ADDON_DOWNGRADE",
188]);
189
190// Returns true if the extension is owned by Mozilla (is either privileged,
191// using one of the @mozilla.com/@mozilla.org protected addon id suffixes).
192//
193// This method throws if the extension's startupReason is not one of the expected
194// ones (either ADDON_INSTALL, ADDON_UPGRADE or ADDON_DOWNGRADE).
195//
196// NOTE: This methos is internally referring to "addonData.recommendationState" to
197// identify a Mozilla line extension. That property is part of the addonData only when
198// the extension is installed or updated, and so we enforce the expected
199// startup reason values to prevent it from silently returning different results
200// if called with an unexpected startupReason.
201function isMozillaExtension(extension) {
202  const { addonData, id, isPrivileged, startupReason } = extension;
203
204  if (!INSTALL_AND_UPDATE_STARTUP_REASONS.has(startupReason)) {
205    throw new Error(
206      `isMozillaExtension called with unexpected startupReason: ${startupReason}`
207    );
208  }
209
210  if (isPrivileged) {
211    return true;
212  }
213
214  if (id.endsWith("@mozilla.com") || id.endsWith("@mozilla.org")) {
215    return true;
216  }
217
218  // This check is a subset of what is being checked in AddonWrapper's
219  // recommendationStates (states expire dates for line extensions are
220  // not consideredcimportant in determining that the extension is
221  // provided by mozilla, and so they are omitted here on purpose).
222  const isMozillaLineExtension = addonData.recommendationState?.states?.includes(
223    "line"
224  );
225  const isSigned = addonData.signedState > AddonManager.SIGNEDSTATE_MISSING;
226
227  return isSigned && isMozillaLineExtension;
228}
229
230/**
231 * Classify an individual permission from a webextension manifest
232 * as a host/origin permission, an api permission, or a regular permission.
233 *
234 * @param {string} perm  The permission string to classify
235 * @param {boolean} restrictSchemes
236 * @param {boolean} isPrivileged whether or not the webextension is privileged
237 *
238 * @returns {object}
239 *          An object with exactly one of the following properties:
240 *          "origin" to indicate this is a host/origin permission.
241 *          "api" to indicate this is an api permission
242 *                (as used for webextensions experiments).
243 *          "permission" to indicate this is a regular permission.
244 *          "invalid" to indicate that the given permission cannot be used.
245 */
246function classifyPermission(perm, restrictSchemes, isPrivileged) {
247  let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
248  if (!match) {
249    try {
250      let { pattern } = new MatchPattern(perm, {
251        restrictSchemes,
252        ignorePath: true,
253      });
254      return { origin: pattern };
255    } catch (e) {
256      return { invalid: perm };
257    }
258  } else if (match[1] == "experiments" && match[2]) {
259    return { api: match[2] };
260  } else if (!isPrivileged && PRIVILEGED_PERMS.has(match[1])) {
261    return { invalid: perm };
262  }
263  return { permission: perm };
264}
265
266const LOGGER_ID_BASE = "addons.webextension.";
267const UUID_MAP_PREF = "extensions.webextensions.uuids";
268const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall";
269const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall";
270
271const COMMENT_REGEXP = new RegExp(
272  String.raw`
273    ^
274    (
275      (?:
276        [^"\n] |
277        " (?:[^"\\\n] | \\.)* "
278      )*?
279    )
280
281    //.*
282  `.replace(/\s+/g, ""),
283  "gm"
284);
285
286// All moz-extension URIs use a machine-specific UUID rather than the
287// extension's own ID in the host component. This makes it more
288// difficult for web pages to detect whether a user has a given add-on
289// installed (by trying to load a moz-extension URI referring to a
290// web_accessible_resource from the extension). UUIDMap.get()
291// returns the UUID for a given add-on ID.
292var UUIDMap = {
293  _read() {
294    let pref = Services.prefs.getStringPref(UUID_MAP_PREF, "{}");
295    try {
296      return JSON.parse(pref);
297    } catch (e) {
298      Cu.reportError(`Error parsing ${UUID_MAP_PREF}.`);
299      return {};
300    }
301  },
302
303  _write(map) {
304    Services.prefs.setStringPref(UUID_MAP_PREF, JSON.stringify(map));
305  },
306
307  get(id, create = true) {
308    let map = this._read();
309
310    if (id in map) {
311      return map[id];
312    }
313
314    let uuid = null;
315    if (create) {
316      uuid = uuidGen.generateUUID().number;
317      uuid = uuid.slice(1, -1); // Strip { and } off the UUID.
318
319      map[id] = uuid;
320      this._write(map);
321    }
322    return uuid;
323  },
324
325  remove(id) {
326    let map = this._read();
327    delete map[id];
328    this._write(map);
329  },
330};
331
332/**
333 * Observer AddonManager events and translate them into extension events,
334 * as well as handle any last cleanup after uninstalling an extension.
335 */
336var ExtensionAddonObserver = {
337  initialized: false,
338
339  init() {
340    if (!this.initialized) {
341      AddonManager.addAddonListener(this);
342      this.initialized = true;
343    }
344  },
345
346  // AddonTestUtils will call this as necessary.
347  uninit() {
348    if (this.initialized) {
349      AddonManager.removeAddonListener(this);
350      this.initialized = false;
351    }
352  },
353
354  onEnabling(addon) {
355    if (addon.type !== "extension") {
356      return;
357    }
358    Management._callHandlers([addon.id], "enabling", "onEnabling");
359  },
360
361  onDisabled(addon) {
362    if (addon.type !== "extension") {
363      return;
364    }
365    if (Services.appinfo.inSafeMode) {
366      // Ensure ExtensionPreferencesManager updates its data and
367      // modules can run any disable logic they need to.  We only
368      // handle safeMode here because there is a bunch of additional
369      // logic that happens in Extension.shutdown when running in
370      // normal mode.
371      Management._callHandlers([addon.id], "disable", "onDisable");
372    }
373  },
374
375  onUninstalling(addon) {
376    let extension = GlobalManager.extensionMap.get(addon.id);
377    if (extension) {
378      // Let any other interested listeners respond
379      // (e.g., display the uninstall URL)
380      Management.emit("uninstalling", extension);
381    }
382  },
383
384  onUninstalled(addon) {
385    let uuid = UUIDMap.get(addon.id, false);
386    if (!uuid) {
387      return;
388    }
389
390    let baseURI = Services.io.newURI(`moz-extension://${uuid}/`);
391    let principal = Services.scriptSecurityManager.createContentPrincipal(
392      baseURI,
393      {}
394    );
395
396    // Clear all the registered service workers for the extension
397    // principal.
398    // Any stored data would be cleared below (if the pref
399    // "extensions.webextensions.keepStorageOnUninstall has not been
400    // explicitly set to true, which is usually only done in
401    // tests and by some extensions developers for testing purpose).
402    if (WebExtensionPolicy.backgroundServiceWorkerEnabled) {
403      // TODO: ServiceWorkerCleanUp may go away once Bug 1183245
404      // is fixed, and so this may actually go away, replaced by
405      // marking the registration as disabled or to be removed on
406      // shutdown (where we do know if the extension is shutting
407      // down because is being uninstalled) and then cleared from
408      // the persisted serviceworker registration on the next
409      // startup.
410      AsyncShutdown.profileChangeTeardown.addBlocker(
411        `Clear ServiceWorkers for ${addon.id}`,
412        ServiceWorkerCleanUp.removeFromPrincipal(principal)
413      );
414    }
415
416    if (!Services.prefs.getBoolPref(LEAVE_STORAGE_PREF, false)) {
417      // Clear browser.storage.local backends.
418      AsyncShutdown.profileChangeTeardown.addBlocker(
419        `Clear Extension Storage ${addon.id} (File Backend)`,
420        ExtensionStorage.clear(addon.id, { shouldNotifyListeners: false })
421      );
422
423      // Clear any IndexedDB and Cache API storage created by the extension.
424      // If LSNG is enabled, this also clears localStorage.
425      Services.qms.clearStoragesForPrincipal(principal);
426
427      // Clear any storage.local data stored in the IDBBackend.
428      let storagePrincipal = Services.scriptSecurityManager.createContentPrincipal(
429        baseURI,
430        {
431          userContextId: WEBEXT_STORAGE_USER_CONTEXT_ID,
432        }
433      );
434      Services.qms.clearStoragesForPrincipal(storagePrincipal);
435
436      ExtensionStorageIDB.clearMigratedExtensionPref(addon.id);
437
438      // If LSNG is not enabled, we need to clear localStorage explicitly using
439      // the old API.
440      if (!Services.domStorageManager.nextGenLocalStorageEnabled) {
441        // Clear localStorage created by the extension
442        let storage = Services.domStorageManager.getStorage(
443          null,
444          principal,
445          principal
446        );
447        if (storage) {
448          storage.clear();
449        }
450      }
451
452      // Remove any permissions related to the unlimitedStorage permission
453      // if we are also removing all the data stored by the extension.
454      Services.perms.removeFromPrincipal(
455        principal,
456        "WebExtensions-unlimitedStorage"
457      );
458      Services.perms.removeFromPrincipal(principal, "indexedDB");
459      Services.perms.removeFromPrincipal(principal, "persistent-storage");
460    }
461
462    ExtensionPermissions.removeAll(addon.id);
463
464    if (!Services.prefs.getBoolPref(LEAVE_UUID_PREF, false)) {
465      // Clear the entry in the UUID map
466      UUIDMap.remove(addon.id);
467    }
468  },
469};
470
471ExtensionAddonObserver.init();
472
473const manifestTypes = new Map([
474  ["theme", "manifest.ThemeManifest"],
475  ["langpack", "manifest.WebExtensionLangpackManifest"],
476  ["dictionary", "manifest.WebExtensionDictionaryManifest"],
477  ["extension", "manifest.WebExtensionManifest"],
478]);
479
480/**
481 * Represents the data contained in an extension, contained either
482 * in a directory or a zip file, which may or may not be installed.
483 * This class implements the functionality of the Extension class,
484 * primarily related to manifest parsing and localization, which is
485 * useful prior to extension installation or initialization.
486 *
487 * No functionality of this class is guaranteed to work before
488 * `loadManifest` has been called, and completed.
489 */
490class ExtensionData {
491  constructor(rootURI) {
492    this.rootURI = rootURI;
493    this.resourceURL = rootURI.spec;
494
495    this.manifest = null;
496    this.type = null;
497    this.id = null;
498    this.uuid = null;
499    this.localeData = null;
500    this.fluentL10n = null;
501    this._promiseLocales = null;
502
503    this.apiNames = new Set();
504    this.dependencies = new Set();
505    this.permissions = new Set();
506
507    this.startupData = null;
508
509    this.errors = [];
510    this.warnings = [];
511  }
512
513  get builtinMessages() {
514    return null;
515  }
516
517  get logger() {
518    let id = this.id || "<unknown>";
519    return Log.repository.getLogger(LOGGER_ID_BASE + id);
520  }
521
522  /**
523   * Report an error about the extension's manifest file.
524   * @param {string} message The error message
525   */
526  manifestError(message) {
527    this.packagingError(`Reading manifest: ${message}`);
528  }
529
530  /**
531   * Report a warning about the extension's manifest file.
532   * @param {string} message The warning message
533   */
534  manifestWarning(message) {
535    this.packagingWarning(`Reading manifest: ${message}`);
536  }
537
538  // Report an error about the extension's general packaging.
539  packagingError(message) {
540    this.errors.push(message);
541    this.logError(message);
542  }
543
544  packagingWarning(message) {
545    this.warnings.push(message);
546    this.logWarning(message);
547  }
548
549  logWarning(message) {
550    this._logMessage(message, "warn");
551  }
552
553  logError(message) {
554    this._logMessage(message, "error");
555  }
556
557  _logMessage(message, severity) {
558    this.logger[severity](`Loading extension '${this.id}': ${message}`);
559  }
560
561  ensureNoErrors() {
562    if (this.errors.length) {
563      // startup() repeatedly checks whether there are errors after parsing the
564      // extension/manifest before proceeding with starting up.
565      throw new Error(this.errors.join("\n"));
566    }
567  }
568
569  /**
570   * Returns the moz-extension: URL for the given path within this
571   * extension.
572   *
573   * Must not be called unless either the `id` or `uuid` property has
574   * already been set.
575   *
576   * @param {string} path The path portion of the URL.
577   * @returns {string}
578   */
579  getURL(path = "") {
580    if (!(this.id || this.uuid)) {
581      throw new Error(
582        "getURL may not be called before an `id` or `uuid` has been set"
583      );
584    }
585    if (!this.uuid) {
586      this.uuid = UUIDMap.get(this.id);
587    }
588    return `moz-extension://${this.uuid}/${path}`;
589  }
590
591  async readDirectory(path) {
592    if (this.rootURI instanceof Ci.nsIFileURL) {
593      let uri = Services.io.newURI("./" + path, null, this.rootURI);
594      let fullPath = uri.QueryInterface(Ci.nsIFileURL).file.path;
595
596      let iter = new OS.File.DirectoryIterator(fullPath);
597      let results = [];
598
599      try {
600        await iter.forEach(entry => {
601          results.push(entry);
602        });
603      } catch (e) {
604        // Always return a list, even if the directory does not exist (or is
605        // not a directory) for symmetry with the ZipReader behavior.
606        if (!e.becauseNoSuchFile) {
607          Cu.reportError(e);
608        }
609      }
610      iter.close();
611
612      return results;
613    }
614
615    let uri = this.rootURI.QueryInterface(Ci.nsIJARURI);
616
617    // Append the sub-directory path to the base JAR URI and normalize the
618    // result.
619    let entry = `${uri.JAREntry}/${path}/`
620      .replace(/\/\/+/g, "/")
621      .replace(/^\//, "");
622    uri = Services.io.newURI(`jar:${uri.JARFile.spec}!/${entry}`);
623
624    let results = [];
625    for (let name of aomStartup.enumerateJARSubtree(uri)) {
626      if (!name.startsWith(entry)) {
627        throw new Error("Unexpected ZipReader entry");
628      }
629
630      // The enumerator returns the full path of all entries.
631      // Trim off the leading path, and filter out entries from
632      // subdirectories.
633      name = name.slice(entry.length);
634      if (name && !/\/./.test(name)) {
635        results.push({
636          name: name.replace("/", ""),
637          isDir: name.endsWith("/"),
638        });
639      }
640    }
641
642    return results;
643  }
644
645  readJSON(path) {
646    return new Promise((resolve, reject) => {
647      let uri = this.rootURI.resolve(`./${path}`);
648
649      NetUtil.asyncFetch(
650        { uri, loadUsingSystemPrincipal: true },
651        (inputStream, status) => {
652          if (!Components.isSuccessCode(status)) {
653            // Convert status code to a string
654            let e = Components.Exception("", status);
655            reject(new Error(`Error while loading '${uri}' (${e.name})`));
656            return;
657          }
658          try {
659            let text = NetUtil.readInputStreamToString(
660              inputStream,
661              inputStream.available(),
662              { charset: "utf-8" }
663            );
664
665            text = text.replace(COMMENT_REGEXP, "$1");
666
667            resolve(JSON.parse(text));
668          } catch (e) {
669            reject(e);
670          }
671        }
672      );
673    });
674  }
675
676  canCheckSignature() {
677    // ExtensionData instances can't check the signature because it is not yet
678    // available when XPIProvider does use it to load the extension manifest.
679    //
680    // This method will return true for the ExtensionData subclasses (like
681    // the Extension class) to enable the additional validation that would require
682    // the signature to be available (e.g. to check if the extension is allowed to
683    // use a privileged permission).
684    return this.constructor != ExtensionData;
685  }
686
687  get restrictSchemes() {
688    // mozillaAddons permission is only allowed for privileged addons and
689    // filtered out if the extension isn't privileged.
690    // When the manifest is loaded by an explicit ExtensionData class
691    // instance, the signature data isn't available yet and this helper
692    // would always return false, but it will return true when appropriate
693    // (based on the isPrivileged boolean property) for the Extension class.
694    return !this.hasPermission("mozillaAddons");
695  }
696
697  /**
698   * Given an array of host and permissions, generate a structured permissions object
699   * that contains seperate host origins and permissions arrays.
700   *
701   * @param {Array} permissionsArray
702   * @returns {Object} permissions object
703   */
704  permissionsObject(permissionsArray) {
705    let permissions = new Set();
706    let origins = new Set();
707    let { restrictSchemes, isPrivileged } = this;
708    for (let perm of permissionsArray || []) {
709      let type = classifyPermission(perm, restrictSchemes, isPrivileged);
710      if (type.origin) {
711        origins.add(perm);
712      } else if (type.permission) {
713        permissions.add(perm);
714      }
715    }
716    return {
717      permissions,
718      origins,
719    };
720  }
721
722  /**
723   * Returns an object representing any capabilities that the extension
724   * has access to based on fixed properties in the manifest.  The result
725   * includes the contents of the "permissions" property as well as other
726   * capabilities that are derived from manifest fields that users should
727   * be informed of (e.g., origins where content scripts are injected).
728   */
729  get manifestPermissions() {
730    if (this.type !== "extension") {
731      return null;
732    }
733
734    let { permissions, origins } = this.permissionsObject(
735      this.manifest.permissions
736    );
737
738    if (
739      this.manifest.devtools_page &&
740      !this.manifest.optional_permissions.includes("devtools")
741    ) {
742      permissions.add("devtools");
743    }
744
745    for (let entry of this.manifest.content_scripts || []) {
746      for (let origin of entry.matches) {
747        origins.add(origin);
748      }
749    }
750
751    return {
752      permissions: Array.from(permissions),
753      origins: Array.from(origins),
754    };
755  }
756
757  get manifestOptionalPermissions() {
758    if (this.type !== "extension") {
759      return null;
760    }
761
762    let { permissions, origins } = this.permissionsObject(
763      this.manifest.optional_permissions
764    );
765    return {
766      permissions: Array.from(permissions),
767      origins: Array.from(origins),
768    };
769  }
770
771  /**
772   * Returns an object representing all capabilities this extension has
773   * access to, including fixed ones from the manifest as well as dynamically
774   * granted permissions.
775   */
776  get activePermissions() {
777    if (this.type !== "extension") {
778      return null;
779    }
780
781    let result = {
782      origins: this.allowedOrigins.patterns
783        .map(matcher => matcher.pattern)
784        // moz-extension://id/* is always added to allowedOrigins, but it
785        // is not a valid host permission in the API. So, remove it.
786        .filter(pattern => !pattern.startsWith("moz-extension:")),
787      apis: [...this.apiNames],
788    };
789
790    const EXP_PATTERN = /^experiments\.\w+/;
791    result.permissions = [...this.permissions].filter(
792      p => !result.origins.includes(p) && !EXP_PATTERN.test(p)
793    );
794    return result;
795  }
796
797  // Returns whether the front end should prompt for this permission
798  static async shouldPromptFor(permission) {
799    return !(await LAZY_NO_PROMPT_PERMISSIONS).has(permission);
800  }
801
802  // Compute the difference between two sets of permissions, suitable
803  // for presenting to the user.
804  static comparePermissions(oldPermissions, newPermissions) {
805    let oldMatcher = new MatchPatternSet(oldPermissions.origins, {
806      restrictSchemes: false,
807    });
808    return {
809      // formatPermissionStrings ignores any scheme, so only look at the domain.
810      origins: newPermissions.origins.filter(
811        perm =>
812          !oldMatcher.subsumesDomain(
813            new MatchPattern(perm, { restrictSchemes: false })
814          )
815      ),
816      permissions: newPermissions.permissions.filter(
817        perm => !oldPermissions.permissions.includes(perm)
818      ),
819    };
820  }
821
822  // Return those permissions in oldPermissions that also exist in newPermissions.
823  static intersectPermissions(oldPermissions, newPermissions) {
824    let matcher = new MatchPatternSet(newPermissions.origins, {
825      restrictSchemes: false,
826    });
827
828    return {
829      origins: oldPermissions.origins.filter(perm =>
830        matcher.subsumesDomain(
831          new MatchPattern(perm, { restrictSchemes: false })
832        )
833      ),
834      permissions: oldPermissions.permissions.filter(perm =>
835        newPermissions.permissions.includes(perm)
836      ),
837    };
838  }
839
840  /**
841   * When updating the addon, find and migrate permissions that have moved from required
842   * to optional.  This also handles any updates required for permission removal.
843   *
844   * @param {string} id The id of the addon being updated
845   * @param {Object} oldPermissions
846   * @param {Object} oldOptionalPermissions
847   * @param {Object} newPermissions
848   * @param {Object} newOptionalPermissions
849   */
850  static async migratePermissions(
851    id,
852    oldPermissions,
853    oldOptionalPermissions,
854    newPermissions,
855    newOptionalPermissions
856  ) {
857    let migrated = ExtensionData.intersectPermissions(
858      oldPermissions,
859      newOptionalPermissions
860    );
861    // If a permission is optional in this version and was mandatory in the previous
862    // version, it was already accepted by the user at install time so add it to the
863    // list of granted optional permissions now.
864    await ExtensionPermissions.add(id, migrated);
865
866    // Now we need to update ExtensionPreferencesManager, removing any settings
867    // for old permissions that no longer exist.
868    let permSet = new Set(
869      newPermissions.permissions.concat(newOptionalPermissions.permissions)
870    );
871    let oldPerms = oldPermissions.permissions.concat(
872      oldOptionalPermissions.permissions
873    );
874
875    let removed = oldPerms.filter(x => !permSet.has(x));
876    // Force the removal here to ensure the settings are removed prior
877    // to startup.  This will remove both required or optional permissions,
878    // whereas the call from within ExtensionPermissions would only result
879    // in a removal for optional permissions that were removed.
880    await ExtensionPreferencesManager.removeSettingsForPermissions(id, removed);
881
882    // Remove any optional permissions that have been removed from the manifest.
883    await ExtensionPermissions.remove(id, {
884      permissions: removed,
885      origins: [],
886    });
887  }
888
889  canUseExperiment(manifest) {
890    return this.experimentsAllowed && manifest.experiment_apis;
891  }
892
893  get manifestVersion() {
894    return this.manifest.manifest_version;
895  }
896
897  /**
898   * Load a locale and return a localized manifest.  The extension must
899   * be initialized, and manifest parsed prior to calling.
900   *
901   * @param {string} locale to load, if necessary.
902   * @returns {object} normalized manifest.
903   */
904  async getLocalizedManifest(locale) {
905    if (!this.type || !this.localeData) {
906      throw new Error("The extension has not been initialized.");
907    }
908    // Upon update or reinstall, the Extension.manifest may be read from
909    // StartupCache.manifest, however rawManifest is *not*.  We need the
910    // raw manifest in order to get a localized manifest.
911    if (!this.rawManifest) {
912      this.rawManifest = await this.readJSON("manifest.json");
913    }
914
915    if (!this.localeData.has(locale)) {
916      // Locales are not avialable until some additional
917      // initialization is done.  We could just call initAllLocales,
918      // but that is heavy handed, especially when we likely only
919      // need one out of 20.
920      let locales = await this.promiseLocales();
921      if (locales.get(locale)) {
922        await this.initLocale(locale);
923      }
924      if (!this.localeData.has(locale)) {
925        throw new Error(`The extension does not contain the locale ${locale}`);
926      }
927    }
928    let normalized = await this._getNormalizedManifest(locale);
929    if (normalized.error) {
930      throw new Error(normalized.error);
931    }
932    return normalized.value;
933  }
934
935  async _getNormalizedManifest(locale) {
936    let manifestType = manifestTypes.get(this.type);
937
938    let context = {
939      url: this.baseURI && this.baseURI.spec,
940      principal: this.principal,
941      logError: error => {
942        this.manifestWarning(error);
943      },
944      preprocessors: {},
945      manifestVersion: this.manifestVersion,
946    };
947
948    if (this.fluentL10n || this.localeData) {
949      context.preprocessors.localize = (value, context) =>
950        this.localize(value, locale);
951    }
952
953    return Schemas.normalize(this.rawManifest, manifestType, context);
954  }
955
956  // eslint-disable-next-line complexity
957  async parseManifest() {
958    let [manifest] = await Promise.all([
959      this.readJSON("manifest.json"),
960      Management.lazyInit(),
961    ]);
962
963    this.manifest = manifest;
964    this.rawManifest = manifest;
965
966    if (manifest && manifest.default_locale) {
967      await this.initLocale();
968    }
969
970    // When parsing the manifest from an ExtensionData instance, we don't
971    // have isPrivileged, so ignore fluent localization in that pass.
972    // This means that fluent cannot be used to localize manifest properties
973    // read from the add-on manager (e.g., author, homepage, etc.)
974    if (manifest && manifest.l10n_resources && "isPrivileged" in this) {
975      if (this.isPrivileged) {
976        this.fluentL10n = new Localization(manifest.l10n_resources, true);
977      } else {
978        // Warn but don't make this fatal.
979        Cu.reportError("Ignoring l10n_resources in unprivileged extension");
980      }
981    }
982
983    if (this.manifest.theme) {
984      this.type = "theme";
985    } else if (this.manifest.langpack_id) {
986      this.type = "langpack";
987    } else if (this.manifest.dictionaries) {
988      this.type = "dictionary";
989    } else {
990      this.type = "extension";
991    }
992
993    let normalized = await this._getNormalizedManifest();
994    if (normalized.error) {
995      this.manifestError(normalized.error);
996      return null;
997    }
998
999    manifest = normalized.value;
1000
1001    let id;
1002    try {
1003      if (manifest.applications.gecko.id) {
1004        id = manifest.applications.gecko.id;
1005      }
1006    } catch (e) {
1007      // Errors are handled by the type checks above.
1008    }
1009
1010    if (!this.id) {
1011      this.id = id;
1012    }
1013
1014    let apiNames = new Set();
1015    let dependencies = new Set();
1016    let originPermissions = new Set();
1017    let permissions = new Set();
1018    let webAccessibleResources = [];
1019
1020    let schemaPromises = new Map();
1021
1022    let result = {
1023      apiNames,
1024      dependencies,
1025      id,
1026      manifest,
1027      modules: null,
1028      originPermissions,
1029      permissions,
1030      schemaURLs: null,
1031      type: this.type,
1032      webAccessibleResources,
1033    };
1034
1035    if (this.type === "extension") {
1036      let { isPrivileged } = this;
1037      let restrictSchemes = !(
1038        isPrivileged && manifest.permissions.includes("mozillaAddons")
1039      );
1040
1041      for (let perm of manifest.permissions) {
1042        if (perm === "geckoProfiler" && !isPrivileged) {
1043          const acceptedExtensions = Services.prefs.getStringPref(
1044            "extensions.geckoProfiler.acceptedExtensionIds",
1045            ""
1046          );
1047          if (!acceptedExtensions.split(",").includes(id)) {
1048            this.manifestError(
1049              "Only specific extensions are allowed to access the geckoProfiler."
1050            );
1051            continue;
1052          }
1053        }
1054
1055        let type = classifyPermission(perm, restrictSchemes, isPrivileged);
1056        if (type.origin) {
1057          perm = type.origin;
1058          originPermissions.add(perm);
1059        } else if (type.api) {
1060          apiNames.add(type.api);
1061        } else if (type.invalid) {
1062          if (!this.canCheckSignature() && PRIVILEGED_PERMS.has(perm)) {
1063            // Do not emit the warning if the invalid permission is a privileged one
1064            // and the current instance can't yet check for a valid signature
1065            // (see Bug 1675858 and the inline comment inside the canCheckSignature
1066            // method for more details).
1067            //
1068            // This parseManifest method will be called again on the Extension class
1069            // instance, which will have the signature available and the invalid
1070            // extension permission warnings will be collected and logged if necessary.
1071            continue;
1072          }
1073
1074          this.manifestWarning(`Invalid extension permission: ${perm}`);
1075          continue;
1076        }
1077
1078        // Unfortunately, we treat <all_urls> as an API permission as well.
1079        if (!type.origin || perm === "<all_urls>") {
1080          permissions.add(perm);
1081        }
1082      }
1083
1084      if (this.id) {
1085        // An extension always gets permission to its own url.
1086        let matcher = new MatchPattern(this.getURL(), { ignorePath: true });
1087        originPermissions.add(matcher.pattern);
1088
1089        // Apply optional permissions
1090        let perms = await ExtensionPermissions.get(this.id);
1091        for (let perm of perms.permissions) {
1092          permissions.add(perm);
1093        }
1094        for (let origin of perms.origins) {
1095          originPermissions.add(origin);
1096        }
1097      }
1098
1099      for (let api of apiNames) {
1100        dependencies.add(`${api}@experiments.addons.mozilla.org`);
1101      }
1102
1103      let moduleData = data => ({
1104        url: this.rootURI.resolve(data.script),
1105        events: data.events,
1106        paths: data.paths,
1107        scopes: data.scopes,
1108      });
1109
1110      let computeModuleInit = (scope, modules) => {
1111        let manager = new ExtensionCommon.SchemaAPIManager(scope);
1112        return manager.initModuleJSON([modules]);
1113      };
1114
1115      result.contentScripts = [];
1116      for (let options of manifest.content_scripts || []) {
1117        result.contentScripts.push({
1118          allFrames: options.all_frames,
1119          matchAboutBlank: options.match_about_blank,
1120          frameID: options.frame_id,
1121          runAt: options.run_at,
1122
1123          matches: options.matches,
1124          excludeMatches: options.exclude_matches || [],
1125          includeGlobs: options.include_globs,
1126          excludeGlobs: options.exclude_globs,
1127
1128          jsPaths: options.js || [],
1129          cssPaths: options.css || [],
1130        });
1131      }
1132
1133      if (this.canUseExperiment(manifest)) {
1134        let parentModules = {};
1135        let childModules = {};
1136
1137        for (let [name, data] of Object.entries(manifest.experiment_apis)) {
1138          let schema = this.getURL(data.schema);
1139
1140          if (!schemaPromises.has(schema)) {
1141            schemaPromises.set(
1142              schema,
1143              this.readJSON(data.schema).then(json =>
1144                Schemas.processSchema(json)
1145              )
1146            );
1147          }
1148
1149          if (data.parent) {
1150            parentModules[name] = moduleData(data.parent);
1151          }
1152
1153          if (data.child) {
1154            childModules[name] = moduleData(data.child);
1155          }
1156        }
1157
1158        result.modules = {
1159          child: computeModuleInit("addon_child", childModules),
1160          parent: computeModuleInit("addon_parent", parentModules),
1161        };
1162      }
1163
1164      // Normalize all patterns to contain a single leading /
1165      if (manifest.web_accessible_resources) {
1166        // Normalize into V3 objects
1167        let wac =
1168          this.manifestVersion >= 3
1169            ? manifest.web_accessible_resources
1170            : [{ resources: manifest.web_accessible_resources }];
1171        webAccessibleResources.push(
1172          ...wac.map(obj => {
1173            obj.resources = obj.resources.map(path =>
1174              path.replace(/^\/*/, "/")
1175            );
1176            return obj;
1177          })
1178        );
1179      }
1180    } else if (this.type == "langpack") {
1181      // Langpack startup is performance critical, so we want to compute as much
1182      // as possible here to make startup not trigger async DB reads.
1183      // We'll store the four items below in the startupData.
1184
1185      // 1. Compute the chrome resources to be registered for this langpack.
1186      const platform = AppConstants.platform;
1187      const chromeEntries = [];
1188      for (const [language, entry] of Object.entries(manifest.languages)) {
1189        for (const [alias, path] of Object.entries(
1190          entry.chrome_resources || {}
1191        )) {
1192          if (typeof path === "string") {
1193            chromeEntries.push(["locale", alias, language, path]);
1194          } else if (platform in path) {
1195            // If the path is not a string, it's an object with path per
1196            // platform where the keys are taken from AppConstants.platform
1197            chromeEntries.push(["locale", alias, language, path[platform]]);
1198          }
1199        }
1200      }
1201
1202      // 2. Compute langpack ID.
1203      const productCodeName = AppConstants.MOZ_BUILD_APP.replace("/", "-");
1204
1205      // The result path looks like this:
1206      //   Firefox - `langpack-pl-browser`
1207      //   Fennec - `langpack-pl-mobile-android`
1208      const langpackId = `langpack-${manifest.langpack_id}-${productCodeName}`;
1209
1210      // 3. Compute L10nRegistry sources for this langpack.
1211      const l10nRegistrySources = {};
1212
1213      // Check if there's a root directory `/localization` in the langpack.
1214      // If there is one, add it with the name `toolkit` as a FileSource.
1215      const entries = await this.readDirectory("localization");
1216      if (entries.length) {
1217        l10nRegistrySources.toolkit = "";
1218      }
1219
1220      // Add any additional sources listed in the manifest
1221      if (manifest.sources) {
1222        for (const [sourceName, { base_path }] of Object.entries(
1223          manifest.sources
1224        )) {
1225          l10nRegistrySources[sourceName] = base_path;
1226        }
1227      }
1228
1229      // 4. Save the list of languages handled by this langpack.
1230      const languages = Object.keys(manifest.languages);
1231
1232      this.startupData = {
1233        chromeEntries,
1234        langpackId,
1235        l10nRegistrySources,
1236        languages,
1237      };
1238    } else if (this.type == "dictionary") {
1239      let dictionaries = {};
1240      for (let [lang, path] of Object.entries(manifest.dictionaries)) {
1241        path = path.replace(/^\/+/, "");
1242
1243        let dir = OSPath.dirname(path);
1244        if (dir === ".") {
1245          dir = "";
1246        }
1247        let leafName = OSPath.basename(path);
1248        let affixPath = leafName.slice(0, -3) + "aff";
1249
1250        let entries = Array.from(
1251          await this.readDirectory(dir),
1252          entry => entry.name
1253        );
1254        if (!entries.includes(leafName)) {
1255          this.manifestError(
1256            `Invalid dictionary path specified for '${lang}': ${path}`
1257          );
1258        }
1259        if (!entries.includes(affixPath)) {
1260          this.manifestError(
1261            `Invalid dictionary path specified for '${lang}': Missing affix file: ${path}`
1262          );
1263        }
1264
1265        dictionaries[lang] = path;
1266      }
1267
1268      this.startupData = { dictionaries };
1269    }
1270
1271    if (schemaPromises.size) {
1272      let schemas = new Map();
1273      for (let [url, promise] of schemaPromises) {
1274        schemas.set(url, await promise);
1275      }
1276      result.schemaURLs = schemas;
1277    }
1278
1279    return result;
1280  }
1281
1282  // Reads the extension's |manifest.json| file, and stores its
1283  // parsed contents in |this.manifest|.
1284  async loadManifest() {
1285    let [manifestData] = await Promise.all([
1286      this.parseManifest(),
1287      Management.lazyInit(),
1288    ]);
1289
1290    if (!manifestData) {
1291      return;
1292    }
1293
1294    // Do not override the add-on id that has been already assigned.
1295    if (!this.id) {
1296      this.id = manifestData.id;
1297    }
1298
1299    this.manifest = manifestData.manifest;
1300    this.apiNames = manifestData.apiNames;
1301    this.contentScripts = manifestData.contentScripts;
1302    this.dependencies = manifestData.dependencies;
1303    this.permissions = manifestData.permissions;
1304    this.schemaURLs = manifestData.schemaURLs;
1305    this.type = manifestData.type;
1306
1307    this.modules = manifestData.modules;
1308
1309    this.apiManager = this.getAPIManager();
1310    await this.apiManager.lazyInit();
1311
1312    this.webAccessibleResources = manifestData.webAccessibleResources;
1313    this.allowedOrigins = new MatchPatternSet(manifestData.originPermissions, {
1314      restrictSchemes: this.restrictSchemes,
1315    });
1316
1317    return this.manifest;
1318  }
1319
1320  hasPermission(perm, includeOptional = false) {
1321    // If the permission is a "manifest property" permission, we check if the extension
1322    // does have the required property in its manifest.
1323    let manifest_ = "manifest:";
1324    if (perm.startsWith(manifest_)) {
1325      // Handle nested "manifest property" permission (e.g. as in "manifest:property.nested").
1326      let value = this.manifest;
1327      for (let prop of perm.substr(manifest_.length).split(".")) {
1328        if (!value) {
1329          break;
1330        }
1331        value = value[prop];
1332      }
1333
1334      return value != null;
1335    }
1336
1337    if (this.permissions.has(perm)) {
1338      return true;
1339    }
1340
1341    if (includeOptional && this.manifest.optional_permissions.includes(perm)) {
1342      return true;
1343    }
1344
1345    return false;
1346  }
1347
1348  getAPIManager() {
1349    let apiManagers = [Management];
1350
1351    for (let id of this.dependencies) {
1352      let policy = WebExtensionPolicy.getByID(id);
1353      if (policy) {
1354        if (policy.extension.experimentAPIManager) {
1355          apiManagers.push(policy.extension.experimentAPIManager);
1356        } else if (AppConstants.DEBUG) {
1357          Cu.reportError(`Cannot find experimental API exported from ${id}`);
1358        }
1359      }
1360    }
1361
1362    if (this.modules) {
1363      this.experimentAPIManager = new ExtensionCommon.LazyAPIManager(
1364        "main",
1365        this.modules.parent,
1366        this.schemaURLs
1367      );
1368
1369      apiManagers.push(this.experimentAPIManager);
1370    }
1371
1372    if (apiManagers.length == 1) {
1373      return apiManagers[0];
1374    }
1375
1376    return new ExtensionCommon.MultiAPIManager("main", apiManagers.reverse());
1377  }
1378
1379  localizeMessage(...args) {
1380    return this.localeData.localizeMessage(...args);
1381  }
1382
1383  localize(str, locale) {
1384    // If the extension declares fluent resources in the manifest, try
1385    // first to localize with fluent.  Also use the original webextension
1386    // method (_locales/xx.json) so extensions can migrate bit by bit.
1387    // Note also that fluent keys typically use hyphense, so hyphens are
1388    // allowed in the __MSG_foo__ keys used by fluent, though they are
1389    // not allowed in the keys used for json translations.
1390    if (this.fluentL10n) {
1391      str = str.replace(/__MSG_([-A-Za-z0-9@_]+?)__/g, (matched, message) => {
1392        let translation = this.fluentL10n.formatValueSync(message);
1393        return translation !== undefined ? translation : matched;
1394      });
1395    }
1396    if (this.localeData) {
1397      str = this.localeData.localize(str, locale);
1398    }
1399    return str;
1400  }
1401
1402  // If a "default_locale" is specified in that manifest, returns it
1403  // as a Gecko-compatible locale string. Otherwise, returns null.
1404  get defaultLocale() {
1405    if (this.manifest.default_locale != null) {
1406      return this.normalizeLocaleCode(this.manifest.default_locale);
1407    }
1408
1409    return null;
1410  }
1411
1412  // Returns true if an addon is builtin to Firefox or
1413  // distributed via Normandy into a system location.
1414  get isAppProvided() {
1415    return this.addonData.builtIn || this.addonData.isSystem;
1416  }
1417
1418  // Normalizes a Chrome-compatible locale code to the appropriate
1419  // Gecko-compatible variant. Currently, this means simply
1420  // replacing underscores with hyphens.
1421  normalizeLocaleCode(locale) {
1422    return locale.replace(/_/g, "-");
1423  }
1424
1425  // Reads the locale file for the given Gecko-compatible locale code, and
1426  // stores its parsed contents in |this.localeMessages.get(locale)|.
1427  async readLocaleFile(locale) {
1428    let locales = await this.promiseLocales();
1429    let dir = locales.get(locale) || locale;
1430    let file = `_locales/${dir}/messages.json`;
1431
1432    try {
1433      let messages = await this.readJSON(file);
1434      return this.localeData.addLocale(locale, messages, this);
1435    } catch (e) {
1436      this.packagingError(`Loading locale file ${file}: ${e}`);
1437      return new Map();
1438    }
1439  }
1440
1441  async _promiseLocaleMap() {
1442    let locales = new Map();
1443
1444    let entries = await this.readDirectory("_locales");
1445    for (let file of entries) {
1446      if (file.isDir) {
1447        let locale = this.normalizeLocaleCode(file.name);
1448        locales.set(locale, file.name);
1449      }
1450    }
1451
1452    return locales;
1453  }
1454
1455  _setupLocaleData(locales) {
1456    if (this.localeData) {
1457      return this.localeData.locales;
1458    }
1459
1460    this.localeData = new LocaleData({
1461      defaultLocale: this.defaultLocale,
1462      locales,
1463      builtinMessages: this.builtinMessages,
1464    });
1465
1466    return locales;
1467  }
1468
1469  // Reads the list of locales available in the extension, and returns a
1470  // Promise which resolves to a Map upon completion.
1471  // Each map key is a Gecko-compatible locale code, and each value is the
1472  // "_locales" subdirectory containing that locale:
1473  //
1474  // Map(gecko-locale-code -> locale-directory-name)
1475  promiseLocales() {
1476    if (!this._promiseLocales) {
1477      this._promiseLocales = (async () => {
1478        let locales = this._promiseLocaleMap();
1479        return this._setupLocaleData(locales);
1480      })();
1481    }
1482
1483    return this._promiseLocales;
1484  }
1485
1486  // Reads the locale messages for all locales, and returns a promise which
1487  // resolves to a Map of locale messages upon completion. Each key in the map
1488  // is a Gecko-compatible locale code, and each value is a locale data object
1489  // as returned by |readLocaleFile|.
1490  async initAllLocales() {
1491    let locales = await this.promiseLocales();
1492
1493    await Promise.all(
1494      Array.from(locales.keys(), locale => this.readLocaleFile(locale))
1495    );
1496
1497    let defaultLocale = this.defaultLocale;
1498    if (defaultLocale) {
1499      if (!locales.has(defaultLocale)) {
1500        this.manifestError(
1501          'Value for "default_locale" property must correspond to ' +
1502            'a directory in "_locales/". Not found: ' +
1503            JSON.stringify(`_locales/${this.manifest.default_locale}/`)
1504        );
1505      }
1506    } else if (locales.size) {
1507      this.manifestError(
1508        'The "default_locale" property is required when a ' +
1509          '"_locales/" directory is present.'
1510      );
1511    }
1512
1513    return this.localeData.messages;
1514  }
1515
1516  // Reads the locale file for the given Gecko-compatible locale code, or the
1517  // default locale if no locale code is given, and sets it as the currently
1518  // selected locale on success.
1519  //
1520  // Pre-loads the default locale for fallback message processing, regardless
1521  // of the locale specified.
1522  //
1523  // If no locales are unavailable, resolves to |null|.
1524  async initLocale(locale = this.defaultLocale) {
1525    if (locale == null) {
1526      return null;
1527    }
1528
1529    let promises = [this.readLocaleFile(locale)];
1530
1531    let { defaultLocale } = this;
1532    if (locale != defaultLocale && !this.localeData.has(defaultLocale)) {
1533      promises.push(this.readLocaleFile(defaultLocale));
1534    }
1535
1536    let results = await Promise.all(promises);
1537
1538    this.localeData.selectedLocale = locale;
1539    return results[0];
1540  }
1541
1542  /**
1543   * Classify host permissions
1544   * @param {array<string>} origins
1545   *                        permission origins
1546   * @returns {object}
1547   *              "object.allUrls" contains the permission used to obtain all urls access
1548   *              "object.wildcards" set contains permissions with wildcards
1549   *              "object.sites" set contains explicit host permissions
1550   */
1551  static classifyOriginPermissions(origins = []) {
1552    let allUrls = null,
1553      wildcards = new Set(),
1554      sites = new Set();
1555    for (let permission of origins) {
1556      if (permission == "<all_urls>") {
1557        allUrls = permission;
1558        break;
1559      }
1560
1561      // Privileged extensions may request access to "about:"-URLs, such as
1562      // about:reader.
1563      let match = /^[a-z*]+:\/\/([^/]*)\/|^about:/.exec(permission);
1564      if (!match) {
1565        throw new Error(`Unparseable host permission ${permission}`);
1566      }
1567      // Note: the scheme is ignored in the permission warnings. If this ever
1568      // changes, update the comparePermissions method as needed.
1569      if (!match[1] || match[1] == "*") {
1570        allUrls = permission;
1571      } else if (match[1].startsWith("*.")) {
1572        wildcards.add(match[1].slice(2));
1573      } else {
1574        sites.add(match[1]);
1575      }
1576    }
1577    return { allUrls, wildcards, sites };
1578  }
1579
1580  /**
1581   * Formats all the strings for a permissions dialog/notification.
1582   *
1583   * @param {object} info Information about the permissions being requested.
1584   *
1585   * @param {array<string>} info.permissions.origins
1586   *                        Origin permissions requested.
1587   * @param {array<string>} info.permissions.permissions
1588   *                        Regular (non-origin) permissions requested.
1589   * @param {array<string>} info.optionalPermissions.origins
1590   *                        Optional origin permissions listed in the manifest.
1591   * @param {array<string>} info.optionalPermissions.permissions
1592   *                        Optional (non-origin) permissions listed in the manifest.
1593   * @param {boolean} info.unsigned
1594   *                  True if the prompt is for installing an unsigned addon.
1595   * @param {string} info.type
1596   *                 The type of prompt being shown.  May be one of "update",
1597   *                 "sideload", "optional", or omitted for a regular
1598   *                 install prompt.
1599   * @param {string} info.appName
1600   *                 The localized name of the application, to be substituted
1601   *                 in computed strings as needed.
1602   * @param {nsIStringBundle} bundle
1603   *                          The string bundle to use for l10n.
1604   * @param {object} options
1605   * @param {boolean} options.collapseOrigins
1606   *                  Wether to limit the number of displayed host permissions.
1607   *                  Default is false.
1608   * @param {function} options.getKeyForPermission
1609   *                   An optional callback function that returns the locale key for a given
1610   *                   permission name (set by default to a callback returning the locale
1611   *                   key following the default convention `webextPerms.description.PERMNAME`).
1612   *                   Overriding the default mapping can become necessary, when a permission
1613   *                   description needs to be modified and a non-default locale key has to be
1614   *                   used. There is at least one non-default locale key used in Thunderbird.
1615   *
1616   * @returns {object} An object with properties containing localized strings
1617   *                   for various elements of a permission dialog. The "header"
1618   *                   property on this object is the notification header text
1619   *                   and it has the string "<>" as a placeholder for the
1620   *                   addon name.
1621   *
1622   *                   "object.msgs" is an array of localized strings describing required permissions
1623   *
1624   *                   "object.optionalPermissions" is a map of permission name to localized
1625   *                   strings describing the permission.
1626   *
1627   *                   "object.optionalOrigins" is a map of a host permission to localized strings
1628   *                   describing the host permission, where appropriate.  Currently only
1629   *                   all url style permissions are included.
1630   */
1631  static formatPermissionStrings(
1632    info,
1633    bundle,
1634    {
1635      collapseOrigins = false,
1636      getKeyForPermission = perm => `webextPerms.description.${perm}`,
1637    } = {}
1638  ) {
1639    let result = {
1640      msgs: [],
1641      optionalPermissions: {},
1642      optionalOrigins: {},
1643    };
1644
1645    let perms = info.permissions || { origins: [], permissions: [] };
1646    let optional_permissions = info.optionalPermissions || {
1647      origins: [],
1648      permissions: [],
1649    };
1650
1651    // First classify our host permissions
1652    let { allUrls, wildcards, sites } = ExtensionData.classifyOriginPermissions(
1653      perms.origins
1654    );
1655
1656    // Format the host permissions.  If we have a wildcard for all urls,
1657    // a single string will suffice.  Otherwise, show domain wildcards
1658    // first, then individual host permissions.
1659    if (allUrls) {
1660      result.msgs.push(
1661        bundle.GetStringFromName("webextPerms.hostDescription.allUrls")
1662      );
1663    } else {
1664      // Formats a list of host permissions.  If we have 4 or fewer, display
1665      // them all, otherwise display the first 3 followed by an item that
1666      // says "...plus N others"
1667      let format = (list, itemKey, moreKey) => {
1668        function formatItems(items) {
1669          result.msgs.push(
1670            ...items.map(item => bundle.formatStringFromName(itemKey, [item]))
1671          );
1672        }
1673        if (list.length < 5 || !collapseOrigins) {
1674          formatItems(list);
1675        } else {
1676          formatItems(list.slice(0, 3));
1677
1678          let remaining = list.length - 3;
1679          result.msgs.push(
1680            PluralForm.get(
1681              remaining,
1682              bundle.GetStringFromName(moreKey)
1683            ).replace("#1", remaining)
1684          );
1685        }
1686      };
1687
1688      format(
1689        Array.from(wildcards),
1690        "webextPerms.hostDescription.wildcard",
1691        "webextPerms.hostDescription.tooManyWildcards"
1692      );
1693      format(
1694        Array.from(sites),
1695        "webextPerms.hostDescription.oneSite",
1696        "webextPerms.hostDescription.tooManySites"
1697      );
1698    }
1699
1700    // Next, show the native messaging permission if it is present.
1701    const NATIVE_MSG_PERM = "nativeMessaging";
1702    if (perms.permissions.includes(NATIVE_MSG_PERM)) {
1703      result.msgs.push(
1704        bundle.formatStringFromName(getKeyForPermission(NATIVE_MSG_PERM), [
1705          info.appName,
1706        ])
1707      );
1708    }
1709
1710    // Finally, show remaining permissions, in the same order as AMO.
1711    // The permissions are sorted alphabetically by the permission
1712    // string to match AMO.
1713    let permissionsCopy = perms.permissions.slice(0);
1714    for (let permission of permissionsCopy.sort()) {
1715      // Handled above
1716      if (permission == NATIVE_MSG_PERM) {
1717        continue;
1718      }
1719      try {
1720        result.msgs.push(
1721          bundle.GetStringFromName(getKeyForPermission(permission))
1722        );
1723      } catch (err) {
1724        // We deliberately do not include all permissions in the prompt.
1725        // So if we don't find one then just skip it.
1726      }
1727    }
1728
1729    // Generate a map of permission names to permission strings for optional
1730    // permissions.  The key is necessary to handle toggling those permissions.
1731    for (let permission of optional_permissions.permissions) {
1732      if (permission == NATIVE_MSG_PERM) {
1733        result.optionalPermissions[
1734          permission
1735        ] = bundle.formatStringFromName(getKeyForPermission(permission), [
1736          info.appName,
1737        ]);
1738        continue;
1739      }
1740      try {
1741        result.optionalPermissions[permission] = bundle.GetStringFromName(
1742          getKeyForPermission(permission)
1743        );
1744      } catch (err) {
1745        // We deliberately do not have strings for all permissions.
1746        // So if we don't find one then just skip it.
1747      }
1748    }
1749    allUrls = ExtensionData.classifyOriginPermissions(
1750      optional_permissions.origins
1751    ).allUrls;
1752    if (allUrls) {
1753      result.optionalOrigins[allUrls] = bundle.GetStringFromName(
1754        "webextPerms.hostDescription.allUrls"
1755      );
1756    }
1757
1758    const haveAccessKeys = AppConstants.platform !== "android";
1759
1760    let headerKey;
1761    result.text = "";
1762    result.listIntro = "";
1763    result.acceptText = bundle.GetStringFromName("webextPerms.add.label");
1764    result.cancelText = bundle.GetStringFromName("webextPerms.cancel.label");
1765    if (haveAccessKeys) {
1766      result.acceptKey = bundle.GetStringFromName("webextPerms.add.accessKey");
1767      result.cancelKey = bundle.GetStringFromName(
1768        "webextPerms.cancel.accessKey"
1769      );
1770    }
1771
1772    if (info.type == "sideload") {
1773      headerKey = "webextPerms.sideloadHeader";
1774      let key = !result.msgs.length
1775        ? "webextPerms.sideloadTextNoPerms"
1776        : "webextPerms.sideloadText2";
1777      result.text = bundle.GetStringFromName(key);
1778      result.acceptText = bundle.GetStringFromName(
1779        "webextPerms.sideloadEnable.label"
1780      );
1781      result.cancelText = bundle.GetStringFromName(
1782        "webextPerms.sideloadCancel.label"
1783      );
1784      if (haveAccessKeys) {
1785        result.acceptKey = bundle.GetStringFromName(
1786          "webextPerms.sideloadEnable.accessKey"
1787        );
1788        result.cancelKey = bundle.GetStringFromName(
1789          "webextPerms.sideloadCancel.accessKey"
1790        );
1791      }
1792    } else if (info.type == "update") {
1793      headerKey = "webextPerms.updateText2";
1794      result.text = "";
1795      result.acceptText = bundle.GetStringFromName(
1796        "webextPerms.updateAccept.label"
1797      );
1798      if (haveAccessKeys) {
1799        result.acceptKey = bundle.GetStringFromName(
1800          "webextPerms.updateAccept.accessKey"
1801        );
1802      }
1803    } else if (info.type == "optional") {
1804      headerKey = "webextPerms.optionalPermsHeader";
1805      result.text = "";
1806      result.listIntro = bundle.GetStringFromName(
1807        "webextPerms.optionalPermsListIntro"
1808      );
1809      result.acceptText = bundle.GetStringFromName(
1810        "webextPerms.optionalPermsAllow.label"
1811      );
1812      result.cancelText = bundle.GetStringFromName(
1813        "webextPerms.optionalPermsDeny.label"
1814      );
1815      if (haveAccessKeys) {
1816        result.acceptKey = bundle.GetStringFromName(
1817          "webextPerms.optionalPermsAllow.accessKey"
1818        );
1819        result.cancelKey = bundle.GetStringFromName(
1820          "webextPerms.optionalPermsDeny.accessKey"
1821        );
1822      }
1823    } else {
1824      headerKey = "webextPerms.header";
1825      if (result.msgs.length) {
1826        headerKey = info.unsigned
1827          ? "webextPerms.headerUnsignedWithPerms"
1828          : "webextPerms.headerWithPerms";
1829      } else if (info.unsigned) {
1830        headerKey = "webextPerms.headerUnsigned";
1831      }
1832    }
1833    result.header = bundle.formatStringFromName(headerKey, ["<>"]);
1834    return result;
1835  }
1836}
1837
1838const PROXIED_EVENTS = new Set([
1839  "test-harness-message",
1840  "add-permissions",
1841  "remove-permissions",
1842]);
1843
1844class BootstrapScope {
1845  install(data, reason) {}
1846  uninstall(data, reason) {
1847    AsyncShutdown.profileChangeTeardown.addBlocker(
1848      `Uninstalling add-on: ${data.id}`,
1849      Management.emit("uninstall", { id: data.id }).then(() => {
1850        Management.emit("uninstall-complete", { id: data.id });
1851      })
1852    );
1853  }
1854
1855  fetchState() {
1856    if (this.extension) {
1857      return { state: this.extension.state };
1858    }
1859    return null;
1860  }
1861
1862  async update(data, reason) {
1863    // Retain any previously granted permissions that may have migrated
1864    // into the optional list.
1865    if (data.oldPermissions) {
1866      // New permissions may be null, ensure we have an empty
1867      // permission set in that case.
1868      let emptyPermissions = { permissions: [], origins: [] };
1869      await ExtensionData.migratePermissions(
1870        data.id,
1871        data.oldPermissions,
1872        data.oldOptionalPermissions,
1873        data.userPermissions || emptyPermissions,
1874        data.optionalPermissions || emptyPermissions
1875      );
1876    }
1877
1878    return Management.emit("update", {
1879      id: data.id,
1880      resourceURI: data.resourceURI,
1881    });
1882  }
1883
1884  startup(data, reason) {
1885    // eslint-disable-next-line no-use-before-define
1886    this.extension = new Extension(
1887      data,
1888      this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]
1889    );
1890    return this.extension.startup();
1891  }
1892
1893  async shutdown(data, reason) {
1894    let result = await this.extension.shutdown(
1895      this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]
1896    );
1897    this.extension = null;
1898    return result;
1899  }
1900}
1901
1902XPCOMUtils.defineLazyGetter(
1903  BootstrapScope.prototype,
1904  "BOOTSTRAP_REASON_TO_STRING_MAP",
1905  () => {
1906    const { BOOTSTRAP_REASONS } = AddonManagerPrivate;
1907
1908    return Object.freeze({
1909      [BOOTSTRAP_REASONS.APP_STARTUP]: "APP_STARTUP",
1910      [BOOTSTRAP_REASONS.APP_SHUTDOWN]: "APP_SHUTDOWN",
1911      [BOOTSTRAP_REASONS.ADDON_ENABLE]: "ADDON_ENABLE",
1912      [BOOTSTRAP_REASONS.ADDON_DISABLE]: "ADDON_DISABLE",
1913      [BOOTSTRAP_REASONS.ADDON_INSTALL]: "ADDON_INSTALL",
1914      [BOOTSTRAP_REASONS.ADDON_UNINSTALL]: "ADDON_UNINSTALL",
1915      [BOOTSTRAP_REASONS.ADDON_UPGRADE]: "ADDON_UPGRADE",
1916      [BOOTSTRAP_REASONS.ADDON_DOWNGRADE]: "ADDON_DOWNGRADE",
1917    });
1918  }
1919);
1920
1921class DictionaryBootstrapScope extends BootstrapScope {
1922  install(data, reason) {}
1923  uninstall(data, reason) {}
1924
1925  startup(data, reason) {
1926    // eslint-disable-next-line no-use-before-define
1927    this.dictionary = new Dictionary(data);
1928    return this.dictionary.startup(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]);
1929  }
1930
1931  shutdown(data, reason) {
1932    this.dictionary.shutdown(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]);
1933    this.dictionary = null;
1934  }
1935}
1936
1937class LangpackBootstrapScope extends BootstrapScope {
1938  install(data, reason) {}
1939  uninstall(data, reason) {}
1940  update(data, reason) {}
1941
1942  startup(data, reason) {
1943    // eslint-disable-next-line no-use-before-define
1944    this.langpack = new Langpack(data);
1945    return this.langpack.startup(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]);
1946  }
1947
1948  shutdown(data, reason) {
1949    this.langpack.shutdown(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]);
1950    this.langpack = null;
1951  }
1952}
1953
1954let activeExtensionIDs = new Set();
1955
1956let pendingExtensions = new Map();
1957
1958/**
1959 * This class is the main representation of an active WebExtension
1960 * in the main process.
1961 * @extends ExtensionData
1962 */
1963class Extension extends ExtensionData {
1964  constructor(addonData, startupReason) {
1965    super(addonData.resourceURI);
1966
1967    this.startupStates = new Set();
1968    this.state = "Not started";
1969    this.userContextIsolation = userContextIsolation;
1970
1971    this.sharedDataKeys = new Set();
1972
1973    this.uuid = UUIDMap.get(addonData.id);
1974    this.instanceId = getUniqueId();
1975
1976    this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
1977    Services.ppmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
1978
1979    if (addonData.cleanupFile) {
1980      Services.obs.addObserver(this, "xpcom-shutdown");
1981      this.cleanupFile = addonData.cleanupFile || null;
1982      delete addonData.cleanupFile;
1983    }
1984
1985    if (addonData.TEST_NO_ADDON_MANAGER) {
1986      this.dontSaveStartupData = true;
1987    }
1988
1989    this.addonData = addonData;
1990    this.startupData = addonData.startupData || {};
1991    this.startupReason = startupReason;
1992
1993    if (["ADDON_UPGRADE", "ADDON_DOWNGRADE"].includes(startupReason)) {
1994      StartupCache.clearAddonData(addonData.id);
1995    }
1996
1997    this.remote = !WebExtensionPolicy.isExtensionProcess;
1998    this.remoteType = this.remote ? E10SUtils.EXTENSION_REMOTE_TYPE : null;
1999
2000    if (this.remote && processCount !== 1) {
2001      throw new Error(
2002        "Out-of-process WebExtensions are not supported with multiple child processes"
2003      );
2004    }
2005
2006    // This is filled in the first time an extension child is created.
2007    this.parentMessageManager = null;
2008
2009    this.id = addonData.id;
2010    this.version = addonData.version;
2011    this.baseURL = this.getURL("");
2012    this.baseURI = Services.io.newURI(this.baseURL).QueryInterface(Ci.nsIURL);
2013    this.principal = this.createPrincipal();
2014
2015    this.views = new Set();
2016    this._backgroundPageFrameLoader = null;
2017
2018    this.onStartup = null;
2019
2020    this.hasShutdown = false;
2021    this.onShutdown = new Set();
2022
2023    this.uninstallURL = null;
2024
2025    this.allowedOrigins = null;
2026    this._optionalOrigins = null;
2027    this.webAccessibleResources = null;
2028
2029    this.registeredContentScripts = new Map();
2030
2031    this.emitter = new EventEmitter();
2032
2033    if (this.startupData.lwtData && this.startupReason == "APP_STARTUP") {
2034      LightweightThemeManager.fallbackThemeData = this.startupData.lwtData;
2035    }
2036
2037    /* eslint-disable mozilla/balanced-listeners */
2038    this.on("add-permissions", (ignoreEvent, permissions) => {
2039      for (let perm of permissions.permissions) {
2040        this.permissions.add(perm);
2041      }
2042
2043      if (permissions.origins.length) {
2044        let patterns = this.allowedOrigins.patterns.map(host => host.pattern);
2045
2046        this.allowedOrigins = new MatchPatternSet(
2047          new Set([...patterns, ...permissions.origins]),
2048          {
2049            restrictSchemes: this.restrictSchemes,
2050            ignorePath: true,
2051          }
2052        );
2053      }
2054
2055      this.policy.permissions = Array.from(this.permissions);
2056      this.policy.allowedOrigins = this.allowedOrigins;
2057
2058      this.cachePermissions();
2059      this.updatePermissions();
2060    });
2061
2062    this.on("remove-permissions", (ignoreEvent, permissions) => {
2063      for (let perm of permissions.permissions) {
2064        this.permissions.delete(perm);
2065      }
2066
2067      let origins = permissions.origins.map(
2068        origin => new MatchPattern(origin, { ignorePath: true }).pattern
2069      );
2070
2071      this.allowedOrigins = new MatchPatternSet(
2072        this.allowedOrigins.patterns.filter(
2073          host => !origins.includes(host.pattern)
2074        )
2075      );
2076
2077      this.policy.permissions = Array.from(this.permissions);
2078      this.policy.allowedOrigins = this.allowedOrigins;
2079
2080      this.cachePermissions();
2081      this.updatePermissions();
2082    });
2083    /* eslint-enable mozilla/balanced-listeners */
2084  }
2085
2086  set state(startupState) {
2087    this.startupStates.clear();
2088    this.startupStates.add(startupState);
2089  }
2090
2091  get state() {
2092    return `${Array.from(this.startupStates).join(", ")}`;
2093  }
2094
2095  async addStartupStatePromise(name, fn) {
2096    this.startupStates.add(name);
2097    try {
2098      await fn();
2099    } finally {
2100      this.startupStates.delete(name);
2101    }
2102  }
2103
2104  // Some helpful properties added elsewhere:
2105
2106  static getBootstrapScope() {
2107    return new BootstrapScope();
2108  }
2109
2110  get browsingContextGroupId() {
2111    return this.policy.browsingContextGroupId;
2112  }
2113
2114  get groupFrameLoader() {
2115    let frameLoader = this._backgroundPageFrameLoader;
2116    for (let view of this.views) {
2117      if (view.viewType === "background" && view.xulBrowser) {
2118        return view.xulBrowser.frameLoader;
2119      }
2120      if (!frameLoader && view.xulBrowser) {
2121        frameLoader = view.xulBrowser.frameLoader;
2122      }
2123    }
2124    return frameLoader || ExtensionParent.DebugUtils.getFrameLoader(this.id);
2125  }
2126
2127  on(hook, f) {
2128    return this.emitter.on(hook, f);
2129  }
2130
2131  off(hook, f) {
2132    return this.emitter.off(hook, f);
2133  }
2134
2135  once(hook, f) {
2136    return this.emitter.once(hook, f);
2137  }
2138
2139  emit(event, ...args) {
2140    if (PROXIED_EVENTS.has(event)) {
2141      Services.ppmm.broadcastAsyncMessage(this.MESSAGE_EMIT_EVENT, {
2142        event,
2143        args,
2144      });
2145    }
2146
2147    return this.emitter.emit(event, ...args);
2148  }
2149
2150  receiveMessage({ name, data }) {
2151    if (name === this.MESSAGE_EMIT_EVENT) {
2152      this.emitter.emit(data.event, ...data.args);
2153    }
2154  }
2155
2156  testMessage(...args) {
2157    this.emit("test-harness-message", ...args);
2158  }
2159
2160  createPrincipal(uri = this.baseURI, originAttributes = {}) {
2161    return Services.scriptSecurityManager.createContentPrincipal(
2162      uri,
2163      originAttributes
2164    );
2165  }
2166
2167  // Checks that the given URL is a child of our baseURI.
2168  isExtensionURL(url) {
2169    let uri = Services.io.newURI(url);
2170
2171    let common = this.baseURI.getCommonBaseSpec(uri);
2172    return common == this.baseURL;
2173  }
2174
2175  checkLoadURL(url, options = {}) {
2176    // As an optimization, f the URL starts with the extension's base URL,
2177    // don't do any further checks. It's always allowed to load it.
2178    if (url.startsWith(this.baseURL)) {
2179      return true;
2180    }
2181
2182    return ExtensionCommon.checkLoadURL(url, this.principal, options);
2183  }
2184
2185  async promiseLocales(locale) {
2186    let locales = await StartupCache.locales.get(
2187      [this.id, "@@all_locales"],
2188      () => this._promiseLocaleMap()
2189    );
2190
2191    return this._setupLocaleData(locales);
2192  }
2193
2194  readLocaleFile(locale) {
2195    return StartupCache.locales
2196      .get([this.id, this.version, locale], () => super.readLocaleFile(locale))
2197      .then(result => {
2198        this.localeData.messages.set(locale, result);
2199      });
2200  }
2201
2202  get manifestCacheKey() {
2203    return [this.id, this.version, Services.locale.appLocaleAsBCP47];
2204  }
2205
2206  get isPrivileged() {
2207    return (
2208      this.addonData.signedState === AddonManager.SIGNEDSTATE_PRIVILEGED ||
2209      this.addonData.signedState === AddonManager.SIGNEDSTATE_SYSTEM ||
2210      this.addonData.builtIn ||
2211      (AddonSettings.EXPERIMENTS_ENABLED && this.addonData.temporarilyInstalled)
2212    );
2213  }
2214
2215  get experimentsAllowed() {
2216    return AddonSettings.EXPERIMENTS_ENABLED || this.isPrivileged;
2217  }
2218
2219  saveStartupData() {
2220    if (this.dontSaveStartupData) {
2221      return;
2222    }
2223    XPIProvider.setStartupData(this.id, this.startupData);
2224  }
2225
2226  parseManifest() {
2227    return StartupCache.manifests.get(this.manifestCacheKey, () =>
2228      super.parseManifest()
2229    );
2230  }
2231
2232  async cachePermissions() {
2233    let manifestData = await this.parseManifest();
2234
2235    manifestData.originPermissions = this.allowedOrigins.patterns.map(
2236      pat => pat.pattern
2237    );
2238    manifestData.permissions = this.permissions;
2239    return StartupCache.manifests.set(this.manifestCacheKey, manifestData);
2240  }
2241
2242  async loadManifest() {
2243    let manifest = await super.loadManifest();
2244
2245    this.ensureNoErrors();
2246
2247    return manifest;
2248  }
2249
2250  get extensionPageCSP() {
2251    const { content_security_policy } = this.manifest;
2252    // While only manifest v3 should contain an object,
2253    // we'll remain lenient here.
2254    if (
2255      content_security_policy &&
2256      typeof content_security_policy === "object"
2257    ) {
2258      return content_security_policy.extension_pages;
2259    }
2260    return content_security_policy;
2261  }
2262
2263  get backgroundScripts() {
2264    return this.manifest.background?.scripts;
2265  }
2266
2267  get backgroundWorkerScript() {
2268    return this.manifest.background?.service_worker;
2269  }
2270
2271  get optionalPermissions() {
2272    return this.manifest.optional_permissions;
2273  }
2274
2275  get privateBrowsingAllowed() {
2276    return this.policy.privateBrowsingAllowed;
2277  }
2278
2279  canAccessWindow(window) {
2280    return this.policy.canAccessWindow(window);
2281  }
2282
2283  // TODO bug 1699481: move this logic to WebExtensionPolicy
2284  canAccessContainer(userContextId) {
2285    userContextId = userContextId ?? 0; // firefox-default has userContextId as 0.
2286    let defaultRestrictedContainers = JSON.parse(
2287      userContextIsolationDefaultRestricted
2288    );
2289    let extensionRestrictedContainers = JSON.parse(
2290      Services.prefs.getStringPref(
2291        `extensions.userContextIsolation.${this.id}.restricted`,
2292        "[]"
2293      )
2294    );
2295    if (
2296      extensionRestrictedContainers.includes(userContextId) ||
2297      defaultRestrictedContainers.includes(userContextId)
2298    ) {
2299      return false;
2300    }
2301
2302    return true;
2303  }
2304
2305  // Representation of the extension to send to content
2306  // processes. This should include anything the content process might
2307  // need.
2308  serialize() {
2309    return {
2310      id: this.id,
2311      uuid: this.uuid,
2312      name: this.name,
2313      manifestVersion: this.manifestVersion,
2314      extensionPageCSP: this.extensionPageCSP,
2315      instanceId: this.instanceId,
2316      resourceURL: this.resourceURL,
2317      contentScripts: this.contentScripts,
2318      webAccessibleResources: this.webAccessibleResources,
2319      allowedOrigins: this.allowedOrigins.patterns.map(pat => pat.pattern),
2320      permissions: this.permissions,
2321      optionalPermissions: this.optionalPermissions,
2322      isPrivileged: this.isPrivileged,
2323    };
2324  }
2325
2326  // Extended serialized data which is only needed in the extensions process,
2327  // and is never deserialized in web content processes.
2328  serializeExtended() {
2329    return {
2330      backgroundScripts: this.backgroundScripts,
2331      backgroundWorkerScript: this.backgroundWorkerScript,
2332      childModules: this.modules && this.modules.child,
2333      dependencies: this.dependencies,
2334      schemaURLs: this.schemaURLs,
2335    };
2336  }
2337
2338  broadcast(msg, data) {
2339    return new Promise(resolve => {
2340      let { ppmm } = Services;
2341      let children = new Set();
2342      for (let i = 0; i < ppmm.childCount; i++) {
2343        children.add(ppmm.getChildAt(i));
2344      }
2345
2346      let maybeResolve;
2347      function listener(data) {
2348        children.delete(data.target);
2349        maybeResolve();
2350      }
2351      function observer(subject, topic, data) {
2352        children.delete(subject);
2353        maybeResolve();
2354      }
2355
2356      maybeResolve = () => {
2357        if (children.size === 0) {
2358          ppmm.removeMessageListener(msg + "Complete", listener);
2359          Services.obs.removeObserver(observer, "message-manager-close");
2360          Services.obs.removeObserver(observer, "message-manager-disconnect");
2361          resolve();
2362        }
2363      };
2364      ppmm.addMessageListener(msg + "Complete", listener, true);
2365      Services.obs.addObserver(observer, "message-manager-close");
2366      Services.obs.addObserver(observer, "message-manager-disconnect");
2367
2368      ppmm.broadcastAsyncMessage(msg, data);
2369    });
2370  }
2371
2372  setSharedData(key, value) {
2373    key = `extension/${this.id}/${key}`;
2374    this.sharedDataKeys.add(key);
2375
2376    sharedData.set(key, value);
2377  }
2378
2379  getSharedData(key, value) {
2380    key = `extension/${this.id}/${key}`;
2381    return sharedData.get(key);
2382  }
2383
2384  initSharedData() {
2385    this.setSharedData("", this.serialize());
2386    this.setSharedData("extendedData", this.serializeExtended());
2387    this.setSharedData("locales", this.localeData.serialize());
2388    this.setSharedData("manifest", this.manifest);
2389    this.updateContentScripts();
2390  }
2391
2392  updateContentScripts() {
2393    this.setSharedData("contentScripts", this.registeredContentScripts);
2394  }
2395
2396  runManifest(manifest) {
2397    let promises = [];
2398    let addPromise = (name, fn) => {
2399      promises.push(this.addStartupStatePromise(name, fn));
2400    };
2401
2402    for (let directive in manifest) {
2403      if (manifest[directive] !== null) {
2404        addPromise(`asyncEmitManifestEntry("${directive}")`, () =>
2405          Management.asyncEmitManifestEntry(this, directive)
2406        );
2407      }
2408    }
2409
2410    activeExtensionIDs.add(this.id);
2411    sharedData.set("extensions/activeIDs", activeExtensionIDs);
2412
2413    pendingExtensions.delete(this.id);
2414    sharedData.set("extensions/pending", pendingExtensions);
2415
2416    Services.ppmm.sharedData.flush();
2417    this.broadcast("Extension:Startup", this.id);
2418
2419    return Promise.all(promises);
2420  }
2421
2422  /**
2423   * Call the close() method on the given object when this extension
2424   * is shut down.  This can happen during browser shutdown, or when
2425   * an extension is manually disabled or uninstalled.
2426   *
2427   * @param {object} obj
2428   *        An object on which to call the close() method when this
2429   *        extension is shut down.
2430   */
2431  callOnClose(obj) {
2432    this.onShutdown.add(obj);
2433  }
2434
2435  forgetOnClose(obj) {
2436    this.onShutdown.delete(obj);
2437  }
2438
2439  get builtinMessages() {
2440    return new Map([["@@extension_id", this.uuid]]);
2441  }
2442
2443  // Reads the locale file for the given Gecko-compatible locale code, or if
2444  // no locale is given, the available locale closest to the UI locale.
2445  // Sets the currently selected locale on success.
2446  async initLocale(locale = undefined) {
2447    if (locale === undefined) {
2448      let locales = await this.promiseLocales();
2449
2450      let matches = Services.locale.negotiateLanguages(
2451        Services.locale.appLocalesAsBCP47,
2452        Array.from(locales.keys()),
2453        this.defaultLocale
2454      );
2455
2456      locale = matches[0];
2457    }
2458
2459    return super.initLocale(locale);
2460  }
2461
2462  /**
2463   * Update site permissions as necessary.
2464   *
2465   * @param {string|undefined} reason
2466   *        If provided, this is a BOOTSTRAP_REASON string.  If reason is undefined,
2467   *        addon permissions are being added or removed that may effect the site permissions.
2468   */
2469  updatePermissions(reason) {
2470    const { principal } = this;
2471
2472    const testPermission = perm =>
2473      Services.perms.testPermissionFromPrincipal(principal, perm);
2474
2475    const addUnlimitedStoragePermissions = () => {
2476      // Set the indexedDB permission and a custom "WebExtensions-unlimitedStorage" to
2477      // remember that the permission hasn't been selected manually by the user.
2478      Services.perms.addFromPrincipal(
2479        principal,
2480        "WebExtensions-unlimitedStorage",
2481        Services.perms.ALLOW_ACTION
2482      );
2483      Services.perms.addFromPrincipal(
2484        principal,
2485        "indexedDB",
2486        Services.perms.ALLOW_ACTION
2487      );
2488      Services.perms.addFromPrincipal(
2489        principal,
2490        "persistent-storage",
2491        Services.perms.ALLOW_ACTION
2492      );
2493    };
2494
2495    // Only update storage permissions when the extension changes in
2496    // some way.
2497    if (reason !== "APP_STARTUP" && reason !== "APP_SHUTDOWN") {
2498      if (this.hasPermission("unlimitedStorage")) {
2499        addUnlimitedStoragePermissions();
2500      } else {
2501        // Remove the indexedDB permission if it has been enabled using the
2502        // unlimitedStorage WebExtensions permissions.
2503        Services.perms.removeFromPrincipal(
2504          principal,
2505          "WebExtensions-unlimitedStorage"
2506        );
2507        Services.perms.removeFromPrincipal(principal, "indexedDB");
2508        Services.perms.removeFromPrincipal(principal, "persistent-storage");
2509      }
2510    } else if (
2511      reason === "APP_STARTUP" &&
2512      this.hasPermission("unlimitedStorage") &&
2513      (testPermission("indexedDB") !== Services.perms.ALLOW_ACTION ||
2514        testPermission("persistent-storage") !== Services.perms.ALLOW_ACTION)
2515    ) {
2516      // If the extension does have the unlimitedStorage permission, but the
2517      // expected site permissions are missing during the app startup, then
2518      // add them back (See Bug 1454192).
2519      addUnlimitedStoragePermissions();
2520    }
2521
2522    // Never change geolocation permissions at shutdown, since it uses a
2523    // session-only permission.
2524    if (reason !== "APP_SHUTDOWN") {
2525      if (this.hasPermission("geolocation")) {
2526        if (testPermission("geo") === Services.perms.UNKNOWN_ACTION) {
2527          Services.perms.addFromPrincipal(
2528            principal,
2529            "geo",
2530            Services.perms.ALLOW_ACTION,
2531            Services.perms.EXPIRE_SESSION
2532          );
2533        }
2534      } else if (
2535        reason !== "APP_STARTUP" &&
2536        testPermission("geo") === Services.perms.ALLOW_ACTION
2537      ) {
2538        Services.perms.removeFromPrincipal(principal, "geo");
2539      }
2540    }
2541  }
2542
2543  static async migratePrivateBrowsing(addonData) {
2544    if (addonData.incognito !== "not_allowed") {
2545      ExtensionPermissions.add(addonData.id, {
2546        permissions: [PRIVATE_ALLOWED_PERMISSION],
2547        origins: [],
2548      });
2549      await StartupCache.clearAddonData(addonData.id);
2550
2551      // Record a telemetry event for the extension automatically allowed on private browsing as
2552      // part of the Firefox upgrade.
2553      AMTelemetry.recordActionEvent({
2554        extra: { addonId: addonData.id },
2555        object: "appUpgrade",
2556        action: "privateBrowsingAllowed",
2557        value: "on",
2558      });
2559    }
2560  }
2561
2562  async startup() {
2563    this.state = "Startup";
2564
2565    // readyPromise is resolved with the policy upon success,
2566    // and with null if startup was interrupted.
2567    let resolveReadyPromise;
2568    let readyPromise = new Promise(resolve => {
2569      resolveReadyPromise = resolve;
2570    });
2571
2572    // Create a temporary policy object for the devtools and add-on
2573    // manager callers that depend on it being available early.
2574    this.policy = new WebExtensionPolicy({
2575      id: this.id,
2576      mozExtensionHostname: this.uuid,
2577      baseURL: this.resourceURL,
2578      isPrivileged: this.isPrivileged,
2579      allowedOrigins: new MatchPatternSet([]),
2580      localizeCallback() {},
2581      readyPromise,
2582    });
2583
2584    this.policy.extension = this;
2585    if (!WebExtensionPolicy.getByID(this.id)) {
2586      this.policy.active = true;
2587    }
2588
2589    pendingExtensions.set(this.id, {
2590      mozExtensionHostname: this.uuid,
2591      baseURL: this.resourceURL,
2592      isPrivileged: this.isPrivileged,
2593    });
2594    sharedData.set("extensions/pending", pendingExtensions);
2595
2596    ExtensionTelemetry.extensionStartup.stopwatchStart(this);
2597    try {
2598      this.state = "Startup: Loading manifest";
2599      await this.loadManifest();
2600      this.state = "Startup: Loaded manifest";
2601
2602      if (!this.hasShutdown) {
2603        this.state = "Startup: Init locale";
2604        await this.initLocale();
2605        this.state = "Startup: Initted locale";
2606      }
2607
2608      this.ensureNoErrors();
2609
2610      if (this.hasShutdown) {
2611        // Startup was interrupted and shutdown() has taken care of unloading
2612        // the extension and running cleanup logic.
2613        return;
2614      }
2615
2616      // We automatically add permissions to system/built-in extensions.
2617      // Extensions expliticy stating not_allowed will never get permission.
2618      let isAllowed = this.permissions.has(PRIVATE_ALLOWED_PERMISSION);
2619      if (this.manifest.incognito === "not_allowed") {
2620        // If an extension previously had permission, but upgrades/downgrades to
2621        // a version that specifies "not_allowed" in manifest, remove the
2622        // permission.
2623        if (isAllowed) {
2624          ExtensionPermissions.remove(this.id, {
2625            permissions: [PRIVATE_ALLOWED_PERMISSION],
2626            origins: [],
2627          });
2628          this.permissions.delete(PRIVATE_ALLOWED_PERMISSION);
2629        }
2630      } else if (
2631        !isAllowed &&
2632        this.isPrivileged &&
2633        !this.addonData.temporarilyInstalled
2634      ) {
2635        // Add to EP so it is preserved after ADDON_INSTALL.  We don't wait on the add here
2636        // since we are pushing the value into this.permissions.  EP will eventually save.
2637        ExtensionPermissions.add(this.id, {
2638          permissions: [PRIVATE_ALLOWED_PERMISSION],
2639          origins: [],
2640        });
2641        this.permissions.add(PRIVATE_ALLOWED_PERMISSION);
2642      }
2643
2644      // We only want to update the SVG_CONTEXT_PROPERTIES_PERMISSION during install and
2645      // upgrade/downgrade startups.
2646      if (INSTALL_AND_UPDATE_STARTUP_REASONS.has(this.startupReason)) {
2647        if (isMozillaExtension(this)) {
2648          // Add to EP so it is preserved after ADDON_INSTALL.  We don't wait on the add here
2649          // since we are pushing the value into this.permissions.  EP will eventually save.
2650          ExtensionPermissions.add(this.id, {
2651            permissions: [SVG_CONTEXT_PROPERTIES_PERMISSION],
2652            origins: [],
2653          });
2654          this.permissions.add(SVG_CONTEXT_PROPERTIES_PERMISSION);
2655        } else {
2656          ExtensionPermissions.remove(this.id, {
2657            permissions: [SVG_CONTEXT_PROPERTIES_PERMISSION],
2658            origins: [],
2659          });
2660          this.permissions.delete(SVG_CONTEXT_PROPERTIES_PERMISSION);
2661        }
2662      }
2663
2664      // Ensure devtools permission is set
2665      if (
2666        this.manifest.devtools_page &&
2667        !this.manifest.optional_permissions.includes("devtools")
2668      ) {
2669        ExtensionPermissions.add(this.id, {
2670          permissions: ["devtools"],
2671          origins: [],
2672        });
2673        this.permissions.add("devtools");
2674      }
2675
2676      GlobalManager.init(this);
2677
2678      this.initSharedData();
2679
2680      this.policy.active = false;
2681      this.policy = ExtensionProcessScript.initExtension(this);
2682      this.policy.extension = this;
2683
2684      this.updatePermissions(this.startupReason);
2685
2686      // Select the storage.local backend if it is already known,
2687      // and start the data migration if needed.
2688      if (this.hasPermission("storage")) {
2689        if (!ExtensionStorageIDB.isBackendEnabled) {
2690          this.setSharedData("storageIDBBackend", false);
2691        } else if (ExtensionStorageIDB.isMigratedExtension(this)) {
2692          this.setSharedData("storageIDBBackend", true);
2693          this.setSharedData(
2694            "storageIDBPrincipal",
2695            ExtensionStorageIDB.getStoragePrincipal(this)
2696          );
2697        } else if (
2698          this.startupReason === "ADDON_INSTALL" &&
2699          !Services.prefs.getBoolPref(LEAVE_STORAGE_PREF, false)
2700        ) {
2701          // If the extension has been just installed, set it as migrated,
2702          // because there will not be any data to migrate.
2703          ExtensionStorageIDB.setMigratedExtensionPref(this, true);
2704          this.setSharedData("storageIDBBackend", true);
2705          this.setSharedData(
2706            "storageIDBPrincipal",
2707            ExtensionStorageIDB.getStoragePrincipal(this)
2708          );
2709        }
2710      }
2711
2712      resolveReadyPromise(this.policy);
2713
2714      // The "startup" Management event sent on the extension instance itself
2715      // is emitted just before the Management "startup" event,
2716      // and it is used to run code that needs to be executed before
2717      // any of the "startup" listeners.
2718      this.emit("startup", this);
2719
2720      this.startupStates.clear();
2721      await Promise.all([
2722        this.addStartupStatePromise("Startup: Emit startup", () =>
2723          Management.emit("startup", this)
2724        ),
2725        this.addStartupStatePromise("Startup: Run manifest", () =>
2726          this.runManifest(this.manifest)
2727        ),
2728      ]);
2729      this.state = "Startup: Ran manifest";
2730
2731      Management.emit("ready", this);
2732      this.emit("ready");
2733
2734      this.state = "Startup: Complete";
2735    } catch (e) {
2736      this.state = `Startup: Error: ${e}`;
2737
2738      Cu.reportError(e);
2739
2740      if (this.policy) {
2741        this.policy.active = false;
2742      }
2743
2744      this.cleanupGeneratedFile();
2745
2746      throw e;
2747    } finally {
2748      ExtensionTelemetry.extensionStartup.stopwatchFinish(this);
2749      // Mark readyPromise as resolved in case it has not happened before,
2750      // e.g. due to an early return or an error.
2751      resolveReadyPromise(null);
2752    }
2753  }
2754
2755  cleanupGeneratedFile() {
2756    if (!this.cleanupFile) {
2757      return;
2758    }
2759
2760    let file = this.cleanupFile;
2761    this.cleanupFile = null;
2762
2763    Services.obs.removeObserver(this, "xpcom-shutdown");
2764
2765    return this.broadcast("Extension:FlushJarCache", { path: file.path })
2766      .then(() => {
2767        // We can't delete this file until everyone using it has
2768        // closed it (because Windows is dumb). So we wait for all the
2769        // child processes (including the parent) to flush their JAR
2770        // caches. These caches may keep the file open.
2771        file.remove(false);
2772      })
2773      .catch(Cu.reportError);
2774  }
2775
2776  async shutdown(reason) {
2777    this.state = "Shutdown";
2778
2779    this.hasShutdown = true;
2780
2781    if (!this.policy) {
2782      return;
2783    }
2784
2785    if (
2786      this.hasPermission("storage") &&
2787      ExtensionStorageIDB.selectedBackendPromises.has(this)
2788    ) {
2789      this.state = "Shutdown: Storage";
2790
2791      // Wait the data migration to complete.
2792      try {
2793        await ExtensionStorageIDB.selectedBackendPromises.get(this);
2794      } catch (err) {
2795        Cu.reportError(
2796          `Error while waiting for extension data migration on shutdown: ${this.policy.debugName} - ${err.message}::${err.stack}`
2797        );
2798      }
2799      this.state = "Shutdown: Storage complete";
2800    }
2801
2802    if (this.rootURI instanceof Ci.nsIJARURI) {
2803      this.state = "Shutdown: Flush jar cache";
2804      let file = this.rootURI.JARFile.QueryInterface(Ci.nsIFileURL).file;
2805      Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {
2806        path: file.path,
2807      });
2808      this.state = "Shutdown: Flushed jar cache";
2809    }
2810
2811    const isAppShutdown = reason === "APP_SHUTDOWN";
2812    if (this.cleanupFile || !isAppShutdown) {
2813      StartupCache.clearAddonData(this.id);
2814    }
2815
2816    activeExtensionIDs.delete(this.id);
2817    sharedData.set("extensions/activeIDs", activeExtensionIDs);
2818
2819    for (let key of this.sharedDataKeys) {
2820      sharedData.delete(key);
2821    }
2822
2823    Services.ppmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this);
2824
2825    this.updatePermissions(reason);
2826
2827    if (!this.manifest) {
2828      this.state = "Shutdown: Complete: No manifest";
2829      this.policy.active = false;
2830
2831      return this.cleanupGeneratedFile();
2832    }
2833
2834    GlobalManager.uninit(this);
2835
2836    for (let obj of this.onShutdown) {
2837      obj.close();
2838    }
2839
2840    ParentAPIManager.shutdownExtension(this.id, reason);
2841
2842    Management.emit("shutdown", this);
2843    this.emit("shutdown", isAppShutdown);
2844
2845    const TIMED_OUT = Symbol();
2846
2847    this.state = "Shutdown: Emit shutdown";
2848    let result = await Promise.race([
2849      this.broadcast("Extension:Shutdown", { id: this.id }),
2850      promiseTimeout(CHILD_SHUTDOWN_TIMEOUT_MS).then(() => TIMED_OUT),
2851    ]);
2852    this.state = `Shutdown: Emitted shutdown: ${result === TIMED_OUT}`;
2853    if (result === TIMED_OUT) {
2854      Cu.reportError(
2855        `Timeout while waiting for extension child to shutdown: ${this.policy.debugName}`
2856      );
2857    }
2858
2859    MessageChannel.abortResponses({ extensionId: this.id });
2860
2861    this.policy.active = false;
2862
2863    this.state = `Shutdown: Complete (${this.cleanupFile})`;
2864    return this.cleanupGeneratedFile();
2865  }
2866
2867  observe(subject, topic, data) {
2868    if (topic === "xpcom-shutdown") {
2869      this.cleanupGeneratedFile();
2870    }
2871  }
2872
2873  get name() {
2874    return this.manifest.name;
2875  }
2876
2877  get optionalOrigins() {
2878    if (this._optionalOrigins == null) {
2879      let { restrictSchemes, isPrivileged } = this;
2880      let origins = this.manifest.optional_permissions.filter(
2881        perm => classifyPermission(perm, restrictSchemes, isPrivileged).origin
2882      );
2883      this._optionalOrigins = new MatchPatternSet(origins, {
2884        restrictSchemes,
2885        ignorePath: true,
2886      });
2887    }
2888    return this._optionalOrigins;
2889  }
2890}
2891
2892class Dictionary extends ExtensionData {
2893  constructor(addonData, startupReason) {
2894    super(addonData.resourceURI);
2895    this.id = addonData.id;
2896    this.startupData = addonData.startupData;
2897  }
2898
2899  static getBootstrapScope() {
2900    return new DictionaryBootstrapScope();
2901  }
2902
2903  async startup(reason) {
2904    this.dictionaries = {};
2905    for (let [lang, path] of Object.entries(this.startupData.dictionaries)) {
2906      let uri = Services.io.newURI(
2907        path.slice(0, -4) + ".aff",
2908        null,
2909        this.rootURI
2910      );
2911      this.dictionaries[lang] = uri;
2912
2913      spellCheck.addDictionary(lang, uri);
2914    }
2915
2916    Management.emit("ready", this);
2917  }
2918
2919  async shutdown(reason) {
2920    if (reason !== "APP_SHUTDOWN") {
2921      XPIProvider.unregisterDictionaries(this.dictionaries);
2922    }
2923  }
2924}
2925
2926class Langpack extends ExtensionData {
2927  constructor(addonData, startupReason) {
2928    super(addonData.resourceURI);
2929    this.startupData = addonData.startupData;
2930    this.manifestCacheKey = [addonData.id, addonData.version];
2931  }
2932
2933  static getBootstrapScope() {
2934    return new LangpackBootstrapScope();
2935  }
2936
2937  async promiseLocales(locale) {
2938    let locales = await StartupCache.locales.get(
2939      [this.id, "@@all_locales"],
2940      () => this._promiseLocaleMap()
2941    );
2942
2943    return this._setupLocaleData(locales);
2944  }
2945
2946  parseManifest() {
2947    return StartupCache.manifests.get(this.manifestCacheKey, () =>
2948      super.parseManifest()
2949    );
2950  }
2951
2952  async startup(reason) {
2953    this.chromeRegistryHandle = null;
2954
2955    // If this langpack overlaps with a packaged locale, then bail out of
2956    // starting up this langpack. Registering the same locale multiple times
2957    // wreaks havoc.
2958    if (
2959      this.startupData.languages.some(lang =>
2960        Services.locale.packagedLocales.includes(lang)
2961      )
2962    ) {
2963      Services.obs.notifyObservers(
2964        { wrappedJSObject: { langpack: this } },
2965        "webextension-langpack-startup-aborted"
2966      );
2967      return;
2968    }
2969
2970    if (this.startupData.chromeEntries.length) {
2971      const manifestURI = Services.io.newURI(
2972        "manifest.json",
2973        null,
2974        this.rootURI
2975      );
2976      this.chromeRegistryHandle = aomStartup.registerChrome(
2977        manifestURI,
2978        this.startupData.chromeEntries
2979      );
2980    }
2981
2982    const langpackId = this.startupData.langpackId;
2983    const l10nRegistrySources = this.startupData.l10nRegistrySources;
2984
2985    resourceProtocol.setSubstitution(langpackId, this.rootURI);
2986
2987    const fileSources = Object.entries(l10nRegistrySources).map(entry => {
2988      const [sourceName, basePath] = entry;
2989      return new FileSource(
2990        `${sourceName}-${langpackId}`,
2991        this.startupData.languages,
2992        `resource://${langpackId}/${basePath}localization/{locale}/`
2993      );
2994    });
2995
2996    L10nRegistry.registerSources(fileSources);
2997
2998    Services.obs.notifyObservers(
2999      { wrappedJSObject: { langpack: this } },
3000      "webextension-langpack-startup"
3001    );
3002  }
3003
3004  async shutdown(reason) {
3005    if (reason === "APP_SHUTDOWN") {
3006      // If we're shutting down, let's not bother updating the state of each
3007      // system.
3008      return;
3009    }
3010
3011    const sourcesToRemove = Object.keys(
3012      this.startupData.l10nRegistrySources
3013    ).map(sourceName => `${sourceName}-${this.startupData.langpackId}`);
3014    L10nRegistry.removeSources(sourcesToRemove);
3015
3016    if (this.chromeRegistryHandle) {
3017      this.chromeRegistryHandle.destruct();
3018      this.chromeRegistryHandle = null;
3019    }
3020
3021    resourceProtocol.setSubstitution(this.startupData.langpackId, null);
3022  }
3023}
3024