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