tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

GeckoViewWebExtension.sys.mjs (43151B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs";
      6 import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
      7 
      8 const PRIVATE_BROWSING_PERM_NAME = "internal:privateBrowsingAllowed";
      9 const PRIVATE_BROWSING_PERMS = {
     10  permissions: [PRIVATE_BROWSING_PERM_NAME],
     11  origins: [],
     12  data_collection: [],
     13 };
     14 
     15 const TECHNICAL_AND_INTERACTION_DATA_PERM_NAME = "technicalAndInteraction";
     16 const TECHNICAL_AND_INTERACTION_DATA_PERMS = {
     17  permissions: [],
     18  origins: [],
     19  data_collection: [TECHNICAL_AND_INTERACTION_DATA_PERM_NAME],
     20 };
     21 
     22 const lazy = {};
     23 
     24 ChromeUtils.defineESModuleGetters(lazy, {
     25  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
     26  AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
     27  AddonSettings: "resource://gre/modules/addons/AddonSettings.sys.mjs",
     28  EventDispatcher: "resource://gre/modules/Messaging.sys.mjs",
     29  Extension: "resource://gre/modules/Extension.sys.mjs",
     30  ExtensionData: "resource://gre/modules/Extension.sys.mjs",
     31  ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.sys.mjs",
     32  ExtensionProcessCrashObserver: "resource://gre/modules/Extension.sys.mjs",
     33  GeckoViewTabBridge: "resource://gre/modules/GeckoViewTab.sys.mjs",
     34  Management: "resource://gre/modules/Extension.sys.mjs",
     35  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     36 });
     37 
     38 const { debug, warn } = GeckoViewUtils.initLogging("Console");
     39 
     40 export var DownloadTracker = new (class extends EventEmitter {
     41  constructor() {
     42    super();
     43 
     44    // maps numeric IDs to DownloadItem objects
     45    this._downloads = new Map();
     46  }
     47 
     48  onEvent(event, data, callback) {
     49    switch (event) {
     50      case "GeckoView:WebExtension:DownloadChanged": {
     51        const downloadItem = this.getDownloadItemById(data.downloadItemId);
     52 
     53        if (!downloadItem) {
     54          callback.onError("Error: Trying to update unknown download");
     55          return;
     56        }
     57 
     58        const delta = downloadItem.update(data);
     59        if (delta) {
     60          this.emit("download-changed", {
     61            delta,
     62            downloadItem,
     63          });
     64        }
     65      }
     66    }
     67  }
     68 
     69  addDownloadItem(item) {
     70    this._downloads.set(item.id, item);
     71  }
     72 
     73  /**
     74   * Finds and returns a DownloadItem with a certain numeric ID
     75   *
     76   * @param {number} id
     77   * @returns {DownloadItem} download item
     78   */
     79  getDownloadItemById(id) {
     80    return this._downloads.get(id);
     81  }
     82 })();
     83 
     84 /** Provides common logic between page and browser actions */
     85 export class ExtensionActionHelper {
     86  constructor({
     87    tabTracker,
     88    windowTracker,
     89    tabContext,
     90    properties,
     91    extension,
     92  }) {
     93    this.tabTracker = tabTracker;
     94    this.windowTracker = windowTracker;
     95    this.tabContext = tabContext;
     96    this.properties = properties;
     97    this.extension = extension;
     98  }
     99 
    100  getTab(aTabId) {
    101    if (aTabId !== null) {
    102      return this.tabTracker.getTab(aTabId);
    103    }
    104    return null;
    105  }
    106 
    107  getWindow(aWindowId) {
    108    if (aWindowId !== null) {
    109      return this.windowTracker.getWindow(aWindowId);
    110    }
    111    return null;
    112  }
    113 
    114  extractProperties(aAction) {
    115    const merged = {};
    116    for (const p of this.properties) {
    117      merged[p] = aAction[p];
    118    }
    119    return merged;
    120  }
    121 
    122  eventDispatcherFor(aTabId) {
    123    if (!aTabId) {
    124      return lazy.EventDispatcher.instance;
    125    }
    126 
    127    const windowId = lazy.GeckoViewTabBridge.tabIdToWindowId(aTabId);
    128    const window = this.windowTracker.getWindow(windowId);
    129    return window.WindowEventDispatcher;
    130  }
    131 
    132  sendRequest(aTabId, aData) {
    133    return this.eventDispatcherFor(aTabId).sendRequest({
    134      ...aData,
    135      aTabId,
    136      extensionId: this.extension.id,
    137    });
    138  }
    139 }
    140 
    141 class EmbedderPort {
    142  constructor(portId, messenger) {
    143    this.id = portId;
    144    this.messenger = messenger;
    145    this.dispatcher = lazy.EventDispatcher.byName(`port:${portId}`);
    146    this.dispatcher.registerListener(this, [
    147      "GeckoView:WebExtension:PortMessageFromApp",
    148      "GeckoView:WebExtension:PortDisconnect",
    149    ]);
    150  }
    151  close() {
    152    this.dispatcher.unregisterListener(this, [
    153      "GeckoView:WebExtension:PortMessageFromApp",
    154      "GeckoView:WebExtension:PortDisconnect",
    155    ]);
    156  }
    157  onPortDisconnect() {
    158    this.dispatcher.sendRequest({
    159      type: "GeckoView:WebExtension:Disconnect",
    160      sender: this.sender,
    161    });
    162    this.close();
    163  }
    164  onPortMessage(holder) {
    165    this.dispatcher.sendRequest({
    166      type: "GeckoView:WebExtension:PortMessage",
    167      data: holder.deserialize({}),
    168    });
    169  }
    170  onEvent(aEvent, aData) {
    171    debug`onEvent ${aEvent} ${aData}`;
    172 
    173    switch (aEvent) {
    174      case "GeckoView:WebExtension:PortMessageFromApp": {
    175        const holder = new StructuredCloneHolder(
    176          "GeckoView:WebExtension:PortMessageFromApp",
    177          null,
    178          aData.message
    179        );
    180        this.messenger.sendPortMessage(this.id, holder);
    181        break;
    182      }
    183 
    184      case "GeckoView:WebExtension:PortDisconnect": {
    185        this.messenger.sendPortDisconnect(this.id);
    186        this.close();
    187        break;
    188      }
    189    }
    190  }
    191 }
    192 
    193 export class GeckoViewConnection {
    194  constructor(sender, target, nativeApp, allowContentMessaging) {
    195    this.sender = sender;
    196    this.target = target;
    197    this.nativeApp = nativeApp;
    198    this.allowContentMessaging = allowContentMessaging;
    199 
    200    if (!allowContentMessaging && sender.envType !== "addon_child") {
    201      throw new Error(`Unexpected messaging sender: ${JSON.stringify(sender)}`);
    202    }
    203  }
    204 
    205  get dispatcher() {
    206    if (this.sender.envType === "addon_child") {
    207      // If this is a WebExtension Page we will have a GeckoSession associated
    208      // to it and thus a dispatcher.
    209      const dispatcher = GeckoViewUtils.getDispatcherForWindow(
    210        this.target.ownerGlobal
    211      );
    212      if (dispatcher) {
    213        return dispatcher;
    214      }
    215 
    216      // No dispatcher means this message is coming from a background script,
    217      // use the global event handler
    218      return lazy.EventDispatcher.instance;
    219    } else if (
    220      this.sender.envType === "content_child" &&
    221      this.allowContentMessaging
    222    ) {
    223      // If this message came from a content script, send the message to
    224      // the corresponding tab messenger so that GeckoSession can pick it
    225      // up.
    226      return GeckoViewUtils.getDispatcherForWindow(this.target.ownerGlobal);
    227    }
    228 
    229    throw new Error(`Uknown sender envType: ${this.sender.envType}`);
    230  }
    231 
    232  _sendMessage({ type, portId, data }) {
    233    const message = {
    234      type,
    235      sender: this.sender,
    236      data,
    237      portId,
    238      extensionId: this.sender.id,
    239      nativeApp: this.nativeApp,
    240    };
    241 
    242    return this.dispatcher.sendRequestForResult(message);
    243  }
    244 
    245  sendMessage(data) {
    246    return this._sendMessage({
    247      type: "GeckoView:WebExtension:Message",
    248      data: data.deserialize({}),
    249    });
    250  }
    251 
    252  onConnect(portId, messenger) {
    253    const port = new EmbedderPort(portId, messenger);
    254 
    255    this._sendMessage({
    256      type: "GeckoView:WebExtension:Connect",
    257      data: {},
    258      portId: port.id,
    259    });
    260 
    261    return port;
    262  }
    263 }
    264 
    265 async function filterPromptPermissions(aPermissions) {
    266  if (!aPermissions) {
    267    return [];
    268  }
    269  const promptPermissions = [];
    270  for (const permission of aPermissions) {
    271    if (!(await lazy.Extension.shouldPromptFor(permission))) {
    272      continue;
    273    }
    274    promptPermissions.push(permission);
    275  }
    276  return promptPermissions;
    277 }
    278 
    279 // Keep in sync with WebExtension.java
    280 const FLAG_NONE = 0;
    281 const FLAG_ALLOW_CONTENT_MESSAGING = 1 << 0;
    282 
    283 function exportFlags(aPolicy) {
    284  let flags = FLAG_NONE;
    285  if (!aPolicy) {
    286    return flags;
    287  }
    288  const { extension } = aPolicy;
    289  if (extension.hasPermission("nativeMessagingFromContent")) {
    290    flags |= FLAG_ALLOW_CONTENT_MESSAGING;
    291  }
    292  return flags;
    293 }
    294 
    295 function normalizePermissions(perms) {
    296  if (perms?.permissions) {
    297    perms = { ...perms };
    298    perms.permissions = perms.permissions.filter(
    299      perm => !perm.startsWith("internal:")
    300    );
    301  }
    302  return perms;
    303 }
    304 
    305 async function exportExtension(aAddon, aSourceURI) {
    306  // First, let's make sure the policy is ready if present
    307  let policy = WebExtensionPolicy.getByID(aAddon.id);
    308  if (policy?.readyPromise) {
    309    policy = await policy.readyPromise;
    310  }
    311  const {
    312    amoListingURL,
    313    averageRating,
    314    blocklistState,
    315    creator,
    316    description,
    317    embedderDisabled,
    318    fullDescription,
    319    homepageURL,
    320    icons,
    321    id,
    322    incognito,
    323    isActive,
    324    isBuiltin,
    325    isCorrectlySigned,
    326    isRecommended,
    327    name,
    328    optionsType,
    329    optionsURL,
    330    reviewCount,
    331    reviewURL,
    332    signedState,
    333    sourceURI,
    334    temporarilyInstalled,
    335    userDisabled,
    336    version,
    337  } = aAddon;
    338  let creatorName = null;
    339  let creatorURL = null;
    340  if (creator) {
    341    const { name, url } = creator;
    342    creatorName = name;
    343    creatorURL = url;
    344  }
    345  const openOptionsPageInTab =
    346    optionsType === lazy.AddonManager.OPTIONS_TYPE_TAB;
    347  const disabledFlags = [];
    348  if (userDisabled) {
    349    disabledFlags.push("userDisabled");
    350  }
    351  if (blocklistState === Ci.nsIBlocklistService.STATE_BLOCKED) {
    352    disabledFlags.push("blocklistDisabled");
    353  } else if (blocklistState === Ci.nsIBlocklistService.STATE_SOFTBLOCKED) {
    354    disabledFlags.push("softBlocklistDisabled");
    355  }
    356  if (embedderDisabled) {
    357    disabledFlags.push("appDisabled");
    358  }
    359  // Add-ons without an `isCorrectlySigned` property are correctly signed as
    360  // they aren't the correct type for signing.
    361  if (lazy.AddonSettings.REQUIRE_SIGNING && isCorrectlySigned === false) {
    362    disabledFlags.push("signatureDisabled");
    363  }
    364  if (lazy.AddonManager.checkCompatibility && !aAddon.isCompatible) {
    365    disabledFlags.push("appVersionDisabled");
    366  }
    367  const baseURL = policy ? policy.getURL() : "";
    368  let privateBrowsingAllowed;
    369  if (policy) {
    370    privateBrowsingAllowed = policy.privateBrowsingAllowed;
    371  } else {
    372    const { permissions } = await lazy.ExtensionPermissions.get(aAddon.id);
    373    privateBrowsingAllowed =
    374      permissions.includes(PRIVATE_BROWSING_PERM_NAME) ||
    375      lazy.PrivateBrowsingUtils.permanentPrivateBrowsing;
    376  }
    377 
    378  let updateDate;
    379  try {
    380    updateDate = aAddon.updateDate?.toISOString();
    381  } catch {
    382    // `installDate` is used as a fallback for `updateDate` but only when the
    383    // add-on is installed. Before that, `installDate` might be undefined,
    384    // which would cause `updateDate` (and `installDate`) to be an "invalid
    385    // date".
    386    updateDate = null;
    387  }
    388 
    389  const requiredPermissions = aAddon.userPermissions?.permissions ?? [];
    390  const requiredOrigins = aAddon.userPermissions?.origins ?? [];
    391  const requiredDataCollectionPermissions =
    392    aAddon.userPermissions?.data_collection ?? [];
    393  const optionalPermissions = aAddon.optionalPermissions?.permissions ?? [];
    394  const optionalOrigins = aAddon.optionalOriginsNormalized;
    395  const optionalDataCollectionPermissions =
    396    aAddon.optionalPermissions?.data_collection ?? [];
    397  const grantedPermissions = normalizePermissions(
    398    await lazy.ExtensionPermissions.get(id)
    399  );
    400  const grantedOptionalPermissions = grantedPermissions?.permissions ?? [];
    401  const grantedOptionalOrigins = grantedPermissions?.origins ?? [];
    402  const grantedOptionalDataCollectionPermissions =
    403    grantedPermissions?.data_collection ?? [];
    404 
    405  return {
    406    webExtensionId: id,
    407    locationURI: aSourceURI != null ? aSourceURI.spec : "",
    408    isBuiltIn: isBuiltin,
    409    webExtensionFlags: exportFlags(policy),
    410    metaData: {
    411      amoListingURL,
    412      averageRating,
    413      baseURL,
    414      blocklistState,
    415      creatorName,
    416      creatorURL,
    417      description,
    418      disabledFlags,
    419      downloadUrl: sourceURI?.displaySpec,
    420      enabled: isActive,
    421      fullDescription,
    422      homepageURL,
    423      icons,
    424      incognito,
    425      isRecommended,
    426      name,
    427      openOptionsPageInTab,
    428      optionsPageURL: optionsURL,
    429      privateBrowsingAllowed,
    430      reviewCount,
    431      reviewURL,
    432      signedState,
    433      temporary: temporarilyInstalled,
    434      updateDate,
    435      version,
    436      requiredPermissions,
    437      requiredOrigins,
    438      requiredDataCollectionPermissions,
    439      optionalPermissions,
    440      optionalOrigins,
    441      optionalDataCollectionPermissions,
    442      grantedOptionalPermissions,
    443      grantedOptionalOrigins,
    444      grantedOptionalDataCollectionPermissions,
    445    },
    446  };
    447 }
    448 
    449 class ExtensionInstallListener {
    450  constructor(aResolve, aInstall, aInstallId) {
    451    this.install = aInstall;
    452    this.installId = aInstallId;
    453    this.resolve = result => {
    454      aResolve(result);
    455      lazy.EventDispatcher.instance.unregisterListener(this, [
    456        "GeckoView:WebExtension:CancelInstall",
    457      ]);
    458    };
    459    lazy.EventDispatcher.instance.registerListener(this, [
    460      "GeckoView:WebExtension:CancelInstall",
    461    ]);
    462  }
    463 
    464  async onEvent(aEvent, aData, aCallback) {
    465    debug`onEvent ${aEvent} ${aData}`;
    466 
    467    switch (aEvent) {
    468      case "GeckoView:WebExtension:CancelInstall": {
    469        const { installId } = aData;
    470        if (this.installId !== installId) {
    471          return;
    472        }
    473        this.cancelling = true;
    474        let cancelled = false;
    475        try {
    476          this.install.cancel();
    477          cancelled = true;
    478        } catch (ex) {
    479          // install may have already failed or been cancelled
    480          debug`Unable to cancel the install installId ${installId}, Error: ${ex}`;
    481          // When we attempt to cancel an install but the cancellation fails for
    482          // some reasons (e.g., because it is too late), we need to revert this
    483          // boolean property to allow another cancellation to be possible.
    484          // Otherwise, events like `onDownloadCancelled` won't resolve and that
    485          // will cause problems in the embedder.
    486          this.cancelling = false;
    487        }
    488        aCallback.onSuccess({ cancelled });
    489        break;
    490      }
    491    }
    492  }
    493 
    494  onDownloadCancelled(aInstall) {
    495    debug`onDownloadCancelled state=${aInstall.state}`;
    496    // Do not resolve we were told to CancelInstall,
    497    // to prevent racing with that handler.
    498    if (!this.cancelling) {
    499      const { error: installError, state } = aInstall;
    500      this.resolve({ installError, state });
    501    }
    502  }
    503 
    504  onDownloadFailed(aInstall) {
    505    debug`onDownloadFailed state=${aInstall.state}`;
    506    const { error: installError, state } = aInstall;
    507    this.resolve({ installError, state });
    508  }
    509 
    510  onDownloadEnded() {
    511    // Nothing to do
    512  }
    513 
    514  onInstallCancelled(aInstall, aCancelledByUser) {
    515    debug`onInstallCancelled state=${aInstall.state} cancelledByUser=${aCancelledByUser}`;
    516    // Do not resolve we were told to CancelInstall,
    517    // to prevent racing with that handler.
    518    if (!this.cancelling) {
    519      const { error: installError, state } = aInstall;
    520      // An install can be cancelled by the user OR something else, e.g. when
    521      // the blocklist prevents the install of a blocked add-on.
    522      this.resolve({ installError, state, cancelledByUser: aCancelledByUser });
    523    }
    524  }
    525 
    526  onInstallFailed(aInstall) {
    527    debug`onInstallFailed state=${aInstall.state}`;
    528    const { error: installError, state } = aInstall;
    529    this.resolve({ installError, state });
    530  }
    531 
    532  onInstallPostponed(aInstall) {
    533    debug`onInstallPostponed state=${aInstall.state}`;
    534    const { error: installError, state } = aInstall;
    535    this.resolve({ installError, state });
    536  }
    537 
    538  async onInstallEnded(aInstall, aAddon) {
    539    debug`onInstallEnded addonId=${aAddon.id}`;
    540    if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) {
    541      await GeckoViewWebExtension.setPrivateBrowsingAllowed(aAddon.id, true);
    542    }
    543    const extension = await exportExtension(aAddon, aInstall.sourceURI);
    544    this.resolve({ extension });
    545  }
    546 }
    547 
    548 class ExtensionPromptObserver {
    549  constructor() {
    550    Services.obs.addObserver(this, "webextension-permission-prompt");
    551    Services.obs.addObserver(this, "webextension-optional-permission-prompt");
    552    Services.obs.addObserver(this, "webextension-update-permission-prompt");
    553  }
    554 
    555  async permissionPromptRequest(aInstall, aAddon, aInfo) {
    556    const { sourceURI } = aInstall;
    557    const { permissions } = aInfo;
    558 
    559    const hasTechnicalAndInteractionDataPerm =
    560      permissions.data_collection.includes(
    561        TECHNICAL_AND_INTERACTION_DATA_PERM_NAME
    562      );
    563 
    564    const extension = await exportExtension(aAddon, sourceURI);
    565    const response = await lazy.EventDispatcher.instance.sendRequestForResult({
    566      type: "GeckoView:WebExtension:InstallPrompt",
    567      extension,
    568      permissions: await filterPromptPermissions(permissions.permissions),
    569      origins: permissions.origins,
    570      dataCollectionPermissions: permissions.data_collection,
    571    });
    572 
    573    if (response.allow) {
    574      if (response.privateBrowsingAllowed) {
    575        await lazy.ExtensionPermissions.add(aAddon.id, PRIVATE_BROWSING_PERMS);
    576      } else {
    577        await lazy.ExtensionPermissions.remove(
    578          aAddon.id,
    579          PRIVATE_BROWSING_PERMS
    580        );
    581      }
    582 
    583      if (hasTechnicalAndInteractionDataPerm) {
    584        if (response.isTechnicalAndInteractionDataGranted) {
    585          await lazy.ExtensionPermissions.add(
    586            aAddon.id,
    587            TECHNICAL_AND_INTERACTION_DATA_PERMS
    588          );
    589        } else {
    590          await lazy.ExtensionPermissions.remove(
    591            aAddon.id,
    592            TECHNICAL_AND_INTERACTION_DATA_PERMS
    593          );
    594        }
    595      }
    596 
    597      aInfo.resolve();
    598    } else {
    599      aInfo.reject();
    600    }
    601  }
    602 
    603  async optionalPermissionPrompt(aExtensionId, aPermissions, resolve) {
    604    const response = await lazy.EventDispatcher.instance.sendRequestForResult({
    605      type: "GeckoView:WebExtension:OptionalPrompt",
    606      extensionId: aExtensionId,
    607      permissions: aPermissions,
    608    });
    609    resolve(response.allow);
    610  }
    611 
    612  async updatePermissionPrompt({ addon, permissions, resolve, reject }) {
    613    const response = await lazy.EventDispatcher.instance.sendRequestForResult({
    614      type: "GeckoView:WebExtension:UpdatePrompt",
    615      extension: await exportExtension(addon, /* aSourceURI */ null),
    616      newPermissions: await filterPromptPermissions(permissions.permissions),
    617      newOrigins: permissions.origins,
    618      newDataCollectionPermissions: permissions.data_collection,
    619    });
    620 
    621    if (response.allow) {
    622      resolve();
    623    } else {
    624      reject();
    625    }
    626  }
    627 
    628  observe(aSubject, aTopic) {
    629    debug`observe ${aTopic}`;
    630 
    631    switch (aTopic) {
    632      case "webextension-permission-prompt": {
    633        const { info } = aSubject.wrappedJSObject;
    634        const { addon, install } = info;
    635        this.permissionPromptRequest(install, addon, info);
    636        break;
    637      }
    638      case "webextension-optional-permission-prompt": {
    639        const { id, permissions, resolve } = aSubject.wrappedJSObject;
    640        this.optionalPermissionPrompt(id, permissions, resolve);
    641        break;
    642      }
    643      case "webextension-update-permission-prompt": {
    644        this.updatePermissionPrompt(aSubject.wrappedJSObject);
    645        break;
    646      }
    647    }
    648  }
    649 }
    650 
    651 class AddonInstallObserver {
    652  constructor() {
    653    Services.obs.addObserver(this, "addon-install-failed");
    654  }
    655 
    656  async onInstallationFailed(aAddon, aAddonName, aError) {
    657    // aAddon could be null if we have a network error where we can't download the xpi file.
    658    // aAddon could also be a valid object without an ID when the xpi file is corrupt.
    659    let extension = null;
    660    if (aAddon?.id) {
    661      extension = await exportExtension(aAddon, /* aSourceURI */ null);
    662    }
    663 
    664    lazy.EventDispatcher.instance.sendRequest({
    665      type: "GeckoView:WebExtension:OnInstallationFailed",
    666      extension,
    667      addonId: aAddon?.id,
    668      addonName: aAddonName,
    669      addonVersion: aAddon?.version,
    670      error: aError,
    671    });
    672  }
    673 
    674  observe(aSubject, aTopic) {
    675    debug`observe ${aTopic}`;
    676    switch (aTopic) {
    677      case "addon-install-failed": {
    678        aSubject.wrappedJSObject.installs.forEach(install => {
    679          const { addon, error, name } = install;
    680          // For some errors, we have a valid `addon` but not the `name` set on
    681          // the `install` object yet so we check both here.
    682          const addonName = name || addon?.name;
    683 
    684          this.onInstallationFailed(addon, addonName, error);
    685        });
    686        break;
    687      }
    688    }
    689  }
    690 }
    691 
    692 new ExtensionPromptObserver();
    693 new AddonInstallObserver();
    694 
    695 class AddonManagerListener {
    696  constructor() {
    697    lazy.AddonManager.addAddonListener(this);
    698    // Some extension properties are not going to be available right away after the extension
    699    // have been installed (e.g. in particular metaData.optionsPageURL), the GeckoView event
    700    // dispatched from onExtensionReady listener will be providing updated extension metadata to
    701    // the GeckoView side when it is actually going to be available.
    702    this.onExtensionReady = this.onExtensionReady.bind(this);
    703    lazy.Management.on("ready", this.onExtensionReady);
    704    lazy.Management.on("change-permissions", this.onOptionalPermissionsChanged);
    705  }
    706 
    707  async onOptionalPermissionsChanged(type, { extensionId }) {
    708    // In xpcshell tests there wil be test extensions that trigger this event while the
    709    // AddonManager has not been started at all, on the contrary on a regular browser
    710    // instance the AddonManager is expected to be already fully started for an extension
    711    // for the extension to be able to reach the "ready" state, and so we just silently
    712    // early exit here if the AddonManager is not ready.
    713    if (!lazy.AddonManager.isReady) {
    714      return;
    715    }
    716 
    717    const addon = await lazy.AddonManager.getAddonByID(extensionId);
    718    if (!addon) {
    719      return;
    720    }
    721    const extension = await exportExtension(addon, /* aSourceURI */ null);
    722    lazy.EventDispatcher.instance.sendRequest({
    723      type: "GeckoView:WebExtension:OnOptionalPermissionsChanged",
    724      extension,
    725    });
    726  }
    727 
    728  async onExtensionReady(name, extInstance) {
    729    // In xpcshell tests there wil be test extensions that trigger this event while the
    730    // AddonManager has not been started at all, on the contrary on a regular browser
    731    // instance the AddonManager is expected to be already fully started for an extension
    732    // for the extension to be able to reach the "ready" state, and so we just silently
    733    // early exit here if the AddonManager is not ready.
    734    if (!lazy.AddonManager.isReady) {
    735      return;
    736    }
    737 
    738    debug`onExtensionReady ${extInstance.id}`;
    739 
    740    const addonWrapper = await lazy.AddonManager.getAddonByID(extInstance.id);
    741    if (!addonWrapper) {
    742      return;
    743    }
    744 
    745    const extension = await exportExtension(
    746      addonWrapper,
    747      /* aSourceURI */ null
    748    );
    749    lazy.EventDispatcher.instance.sendRequest({
    750      type: "GeckoView:WebExtension:OnReady",
    751      extension,
    752    });
    753  }
    754 
    755  async onDisabling(aAddon) {
    756    debug`onDisabling ${aAddon.id}`;
    757 
    758    const extension = await exportExtension(aAddon, /* aSourceURI */ null);
    759    lazy.EventDispatcher.instance.sendRequest({
    760      type: "GeckoView:WebExtension:OnDisabling",
    761      extension,
    762    });
    763  }
    764 
    765  async onDisabled(aAddon) {
    766    debug`onDisabled ${aAddon.id}`;
    767 
    768    const extension = await exportExtension(aAddon, /* aSourceURI */ null);
    769    lazy.EventDispatcher.instance.sendRequest({
    770      type: "GeckoView:WebExtension:OnDisabled",
    771      extension,
    772    });
    773  }
    774 
    775  async onEnabling(aAddon) {
    776    debug`onEnabling ${aAddon.id}`;
    777 
    778    const extension = await exportExtension(aAddon, /* aSourceURI */ null);
    779    lazy.EventDispatcher.instance.sendRequest({
    780      type: "GeckoView:WebExtension:OnEnabling",
    781      extension,
    782    });
    783  }
    784 
    785  async onEnabled(aAddon) {
    786    debug`onEnabled ${aAddon.id}`;
    787 
    788    const extension = await exportExtension(aAddon, /* aSourceURI */ null);
    789    lazy.EventDispatcher.instance.sendRequest({
    790      type: "GeckoView:WebExtension:OnEnabled",
    791      extension,
    792    });
    793  }
    794 
    795  async onUninstalling(aAddon) {
    796    debug`onUninstalling ${aAddon.id}`;
    797 
    798    const extension = await exportExtension(aAddon, /* aSourceURI */ null);
    799    lazy.EventDispatcher.instance.sendRequest({
    800      type: "GeckoView:WebExtension:OnUninstalling",
    801      extension,
    802    });
    803  }
    804 
    805  async onUninstalled(aAddon) {
    806    debug`onUninstalled ${aAddon.id}`;
    807 
    808    const extension = await exportExtension(aAddon, /* aSourceURI */ null);
    809    lazy.EventDispatcher.instance.sendRequest({
    810      type: "GeckoView:WebExtension:OnUninstalled",
    811      extension,
    812    });
    813  }
    814 
    815  async onInstalling(aAddon) {
    816    debug`onInstalling ${aAddon.id}`;
    817 
    818    const extension = await exportExtension(aAddon, /* aSourceURI */ null);
    819    lazy.EventDispatcher.instance.sendRequest({
    820      type: "GeckoView:WebExtension:OnInstalling",
    821      extension,
    822    });
    823  }
    824 
    825  async onInstalled(aAddon) {
    826    debug`onInstalled ${aAddon.id}`;
    827 
    828    const extension = await exportExtension(aAddon, /* aSourceURI */ null);
    829    lazy.EventDispatcher.instance.sendRequest({
    830      type: "GeckoView:WebExtension:OnInstalled",
    831      extension,
    832    });
    833  }
    834 }
    835 
    836 new AddonManagerListener();
    837 
    838 class ExtensionProcessListener {
    839  constructor() {
    840    this.onExtensionProcessCrash = this.onExtensionProcessCrash.bind(this);
    841    lazy.Management.on("extension-process-crash", this.onExtensionProcessCrash);
    842 
    843    lazy.EventDispatcher.instance.registerListener(this, [
    844      "GeckoView:WebExtension:EnableProcessSpawning",
    845      "GeckoView:WebExtension:DisableProcessSpawning",
    846    ]);
    847  }
    848 
    849  async onEvent(aEvent, aData) {
    850    debug`onEvent ${aEvent} ${aData}`;
    851 
    852    switch (aEvent) {
    853      case "GeckoView:WebExtension:EnableProcessSpawning": {
    854        debug`Extension process crash -> re-enable process spawning`;
    855        lazy.ExtensionProcessCrashObserver.enableProcessSpawning();
    856        break;
    857      }
    858    }
    859  }
    860 
    861  async onExtensionProcessCrash(name, { childID, processSpawningDisabled }) {
    862    debug`Extension process crash -> childID=${childID} processSpawningDisabled=${processSpawningDisabled}`;
    863 
    864    // When an extension process has crashed too many times, Gecko will set
    865    // `processSpawningDisabled` and no longer allow the extension process
    866    // spawning. We only want to send a request to the embedder when we are
    867    // disabling the process spawning.  If process spawning is still enabled
    868    // then we short circuit and don't notify the embedder.
    869    if (!processSpawningDisabled) {
    870      return;
    871    }
    872 
    873    lazy.EventDispatcher.instance.sendRequest({
    874      type: "GeckoView:WebExtension:OnDisabledProcessSpawning",
    875    });
    876  }
    877 }
    878 
    879 new ExtensionProcessListener();
    880 
    881 class MobileWindowTracker extends EventEmitter {
    882  constructor() {
    883    super();
    884    this._topWindow = null;
    885    this._topNonPBWindow = null;
    886  }
    887 
    888  get topWindow() {
    889    if (this._topWindow) {
    890      return this._topWindow.get();
    891    }
    892    return null;
    893  }
    894 
    895  get topNonPBWindow() {
    896    if (this._topNonPBWindow) {
    897      return this._topNonPBWindow.get();
    898    }
    899    return null;
    900  }
    901 
    902  setTabActive(aWindow, aActive) {
    903    const { browser, tab: nativeTab, docShell } = aWindow;
    904    nativeTab.active = aActive;
    905 
    906    if (aActive) {
    907      this._topWindow = Cu.getWeakReference(aWindow);
    908      const isPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(browser);
    909      if (!isPrivate) {
    910        this._topNonPBWindow = this._topWindow;
    911      }
    912      this.emit("tab-activated", {
    913        windowId: docShell.outerWindowID,
    914        tabId: nativeTab.id,
    915        isPrivate,
    916        nativeTab,
    917      });
    918    }
    919  }
    920 }
    921 
    922 export var mobileWindowTracker = new MobileWindowTracker();
    923 
    924 export var GeckoViewWebExtension = {
    925  observe(aSubject, aTopic) {
    926    debug`observe ${aTopic}`;
    927 
    928    switch (aTopic) {
    929      case "testing-installed-addon":
    930      case "testing-uninstalled-addon": {
    931        // We pretend devtools installed/uninstalled this addon so we don't
    932        // have to add an API just for internal testing.
    933        // TODO: assert this is under a test
    934        lazy.EventDispatcher.instance.sendRequest({
    935          type: "GeckoView:WebExtension:DebuggerListUpdated",
    936        });
    937        break;
    938      }
    939 
    940      case "devtools-installed-addon": {
    941        lazy.EventDispatcher.instance.sendRequest({
    942          type: "GeckoView:WebExtension:DebuggerListUpdated",
    943        });
    944        break;
    945      }
    946    }
    947  },
    948 
    949  async extensionById(aId) {
    950    const addon = await lazy.AddonManager.getAddonByID(aId);
    951    if (!addon) {
    952      debug`Could not find extension with id=${aId}`;
    953      return null;
    954    }
    955    return addon;
    956  },
    957 
    958  async ensureBuiltIn(aUri, aId) {
    959    await lazy.AddonManager.readyPromise;
    960    // Although the add-on is privileged in practice due to it being installed
    961    // as a built-in extension, we pass isPrivileged=false since the exact flag
    962    // doesn't matter as we are only using ExtensionData to read the version.
    963    const extensionData = new lazy.ExtensionData(aUri, false);
    964    const [extensionVersion, extension] = await Promise.all([
    965      extensionData.getExtensionVersionWithoutValidation(),
    966      this.extensionById(aId),
    967    ]);
    968 
    969    if (!extension || extensionVersion != extension.version) {
    970      return this.installBuiltIn(aUri);
    971    }
    972 
    973    const exported = await exportExtension(extension, aUri);
    974    return { extension: exported };
    975  },
    976 
    977  async installBuiltIn(aUri) {
    978    await lazy.AddonManager.readyPromise;
    979    const addon = await lazy.AddonManager.installBuiltinAddon(aUri.spec);
    980    const exported = await exportExtension(addon, aUri);
    981    return { extension: exported };
    982  },
    983 
    984  async installWebExtension(aInstallId, aUri, installMethod) {
    985    const install = await lazy.AddonManager.getInstallForURL(aUri.spec, {
    986      telemetryInfo: {
    987        source: "geckoview-app",
    988        method: installMethod || undefined,
    989      },
    990    });
    991    const promise = new Promise(resolve => {
    992      install.addListener(
    993        new ExtensionInstallListener(resolve, install, aInstallId)
    994      );
    995    });
    996 
    997    lazy.AddonManager.installAddonFromAOM(null, aUri, install);
    998 
    999    return promise;
   1000  },
   1001 
   1002  async setPrivateBrowsingAllowed(aId, aAllowed) {
   1003    if (aAllowed) {
   1004      await lazy.ExtensionPermissions.add(aId, PRIVATE_BROWSING_PERMS);
   1005    } else {
   1006      await lazy.ExtensionPermissions.remove(aId, PRIVATE_BROWSING_PERMS);
   1007    }
   1008 
   1009    // Reload the extension if it is already enabled.  This ensures any change
   1010    // on the private browsing permission is properly handled.
   1011    const addon = await this.extensionById(aId);
   1012    if (addon.isActive) {
   1013      await addon.reload();
   1014    }
   1015 
   1016    return exportExtension(addon, /* aSourceURI */ null);
   1017  },
   1018 
   1019  async uninstallWebExtension(aId) {
   1020    const extension = await this.extensionById(aId);
   1021    if (!extension) {
   1022      throw new Error(`Could not find an extension with id='${aId}'.`);
   1023    }
   1024 
   1025    return extension.uninstall();
   1026  },
   1027 
   1028  async browserActionClick(aId) {
   1029    const policy = WebExtensionPolicy.getByID(aId);
   1030    if (!policy) {
   1031      return undefined;
   1032    }
   1033 
   1034    const browserAction = this.browserActions.get(policy.extension);
   1035    if (!browserAction) {
   1036      return undefined;
   1037    }
   1038 
   1039    return browserAction.triggerClickOrPopup();
   1040  },
   1041 
   1042  async pageActionClick(aId) {
   1043    const policy = WebExtensionPolicy.getByID(aId);
   1044    if (!policy) {
   1045      return undefined;
   1046    }
   1047 
   1048    const pageAction = this.pageActions.get(policy.extension);
   1049    if (!pageAction) {
   1050      return undefined;
   1051    }
   1052 
   1053    return pageAction.triggerClickOrPopup();
   1054  },
   1055 
   1056  async actionDelegateAttached(aId) {
   1057    const policy = WebExtensionPolicy.getByID(aId);
   1058    if (!policy) {
   1059      debug`Could not find extension with id=${aId}`;
   1060      return;
   1061    }
   1062 
   1063    const { extension } = policy;
   1064 
   1065    const browserAction = this.browserActions.get(extension);
   1066    if (browserAction) {
   1067      // Send information about this action to the delegate
   1068      browserAction.updateOnChange(null);
   1069    }
   1070 
   1071    const pageAction = this.pageActions.get(extension);
   1072    if (pageAction) {
   1073      pageAction.updateOnChange(null);
   1074    }
   1075  },
   1076 
   1077  async enableWebExtension(aId, aSource) {
   1078    const extension = await this.extensionById(aId);
   1079    if (aSource === "user") {
   1080      await extension.enable();
   1081    } else if (aSource === "app") {
   1082      await extension.setEmbedderDisabled(false);
   1083    }
   1084    return exportExtension(extension, /* aSourceURI */ null);
   1085  },
   1086 
   1087  async disableWebExtension(aId, aSource) {
   1088    const extension = await this.extensionById(aId);
   1089    if (aSource === "user") {
   1090      await extension.disable();
   1091    } else if (aSource === "app") {
   1092      await extension.setEmbedderDisabled(true);
   1093    }
   1094    return exportExtension(extension, /* aSourceURI */ null);
   1095  },
   1096 
   1097  /**
   1098   * @return A promise resolved with either an AddonInstall object if an update
   1099   * is available or null if no update is found.
   1100   */
   1101  checkForUpdate(aAddon) {
   1102    return new Promise(resolve => {
   1103      const listener = {
   1104        onUpdateAvailable(_aAddon, install) {
   1105          install.promptHandler = aInfo =>
   1106            lazy.AddonManager.updatePromptHandler(aInfo);
   1107          resolve(install);
   1108        },
   1109        onNoUpdateAvailable() {
   1110          resolve(null);
   1111        },
   1112      };
   1113      aAddon.findUpdates(
   1114        listener,
   1115        lazy.AddonManager.UPDATE_WHEN_PERIODIC_UPDATE
   1116      );
   1117    });
   1118  },
   1119 
   1120  async updateWebExtension(aId) {
   1121    // Refresh the cached metadata when necessary. This allows us to always
   1122    // export relatively recent metadata to the embedder.
   1123    if (lazy.AddonRepository.isMetadataStale()) {
   1124      // We use a promise to avoid more than one call to `backgroundUpdateCheck()`
   1125      // when `updateWebExtension()` is called for multiple add-ons in parallel.
   1126      if (!this._promiseAddonRepositoryUpdate) {
   1127        this._promiseAddonRepositoryUpdate =
   1128          lazy.AddonRepository.backgroundUpdateCheck().finally(() => {
   1129            this._promiseAddonRepositoryUpdate = null;
   1130          });
   1131      }
   1132      await this._promiseAddonRepositoryUpdate;
   1133    }
   1134 
   1135    // Early-return when extension updates are disabled.
   1136    if (!lazy.AddonManager.updateEnabled) {
   1137      return null;
   1138    }
   1139 
   1140    const extension = await this.extensionById(aId);
   1141 
   1142    const install = await this.checkForUpdate(extension);
   1143    if (!install) {
   1144      return null;
   1145    }
   1146    const promise = new Promise(resolve => {
   1147      install.addListener(new ExtensionInstallListener(resolve));
   1148    });
   1149    install.install();
   1150    return promise;
   1151  },
   1152 
   1153  validateBuiltInLocation(aLocationUri, aCallback) {
   1154    let uri;
   1155    try {
   1156      uri = Services.io.newURI(aLocationUri);
   1157    } catch (ex) {
   1158      aCallback.onError(`Could not parse uri: ${aLocationUri}. Error: ${ex}`);
   1159      return null;
   1160    }
   1161 
   1162    if (uri.scheme !== "resource" || uri.host !== "android") {
   1163      aCallback.onError(`Only resource://android/... URIs are allowed.`);
   1164      return null;
   1165    }
   1166 
   1167    if (uri.fileName !== "") {
   1168      aCallback.onError(
   1169        `This URI does not point to a folder. Note: folders URIs must end with a "/".`
   1170      );
   1171      return null;
   1172    }
   1173 
   1174    return uri;
   1175  },
   1176 
   1177  /* eslint-disable complexity */
   1178  async onEvent(aEvent, aData, aCallback) {
   1179    debug`onEvent ${aEvent} ${aData}`;
   1180 
   1181    switch (aEvent) {
   1182      case "GeckoView:BrowserAction:Click": {
   1183        const popupUrl = await this.browserActionClick(aData.extensionId);
   1184        aCallback.onSuccess(popupUrl);
   1185        break;
   1186      }
   1187      case "GeckoView:PageAction:Click": {
   1188        const popupUrl = await this.pageActionClick(aData.extensionId);
   1189        aCallback.onSuccess(popupUrl);
   1190        break;
   1191      }
   1192      case "GeckoView:WebExtension:MenuClick": {
   1193        aCallback.onError(`Not implemented`);
   1194        break;
   1195      }
   1196      case "GeckoView:WebExtension:MenuShow": {
   1197        aCallback.onError(`Not implemented`);
   1198        break;
   1199      }
   1200      case "GeckoView:WebExtension:MenuHide": {
   1201        aCallback.onError(`Not implemented`);
   1202        break;
   1203      }
   1204 
   1205      case "GeckoView:ActionDelegate:Attached": {
   1206        this.actionDelegateAttached(aData.extensionId);
   1207        break;
   1208      }
   1209 
   1210      case "GeckoView:WebExtension:Get": {
   1211        const extension = await this.extensionById(aData.extensionId);
   1212        if (!extension) {
   1213          aCallback.onError(
   1214            `Could not find extension with id: ${aData.extensionId}`
   1215          );
   1216          return;
   1217        }
   1218 
   1219        aCallback.onSuccess({
   1220          extension: await exportExtension(extension, /* aSourceURI */ null),
   1221        });
   1222        break;
   1223      }
   1224 
   1225      case "GeckoView:WebExtension:SetPBAllowed": {
   1226        const { extensionId, allowed } = aData;
   1227        try {
   1228          const extension = await this.setPrivateBrowsingAllowed(
   1229            extensionId,
   1230            allowed
   1231          );
   1232          aCallback.onSuccess({ extension });
   1233        } catch (ex) {
   1234          aCallback.onError(`Unexpected error: ${ex}`);
   1235        }
   1236        break;
   1237      }
   1238 
   1239      case "GeckoView:WebExtension:AddOptionalPermissions": {
   1240        const {
   1241          extensionId,
   1242          permissions,
   1243          origins,
   1244          dataCollectionPermissions: data_collection,
   1245        } = aData;
   1246        try {
   1247          const addon = await this.extensionById(extensionId);
   1248          const normalized = lazy.ExtensionPermissions.normalizeOptional(
   1249            { permissions, origins, data_collection },
   1250            addon.optionalPermissions
   1251          );
   1252          const policy = WebExtensionPolicy.getByID(addon.id);
   1253          await lazy.ExtensionPermissions.add(
   1254            extensionId,
   1255            normalized,
   1256            policy?.extension
   1257          );
   1258          const extension = await exportExtension(addon, /* aSourceURI */ null);
   1259          aCallback.onSuccess({ extension });
   1260        } catch (ex) {
   1261          aCallback.onError(`Unexpected error: ${ex}`);
   1262        }
   1263        break;
   1264      }
   1265 
   1266      case "GeckoView:WebExtension:RemoveOptionalPermissions": {
   1267        const {
   1268          extensionId,
   1269          permissions,
   1270          origins,
   1271          dataCollectionPermissions: data_collection,
   1272        } = aData;
   1273        try {
   1274          const addon = await this.extensionById(extensionId);
   1275          const normalized = lazy.ExtensionPermissions.normalizeOptional(
   1276            { permissions, origins, data_collection },
   1277            addon.optionalPermissions
   1278          );
   1279          const policy = WebExtensionPolicy.getByID(addon.id);
   1280          await lazy.ExtensionPermissions.remove(
   1281            addon.id,
   1282            normalized,
   1283            policy?.extension
   1284          );
   1285          const extension = await exportExtension(addon, /* aSourceURI */ null);
   1286          aCallback.onSuccess({ extension });
   1287        } catch (ex) {
   1288          aCallback.onError(`Unexpected error: ${ex}`);
   1289        }
   1290        break;
   1291      }
   1292 
   1293      case "GeckoView:WebExtension:Install": {
   1294        const { locationUri, installId, installMethod } = aData;
   1295        let uri;
   1296        try {
   1297          uri = Services.io.newURI(locationUri);
   1298        } catch (ex) {
   1299          aCallback.onError(`Could not parse uri: ${locationUri}`);
   1300          return;
   1301        }
   1302 
   1303        try {
   1304          const result = await this.installWebExtension(
   1305            installId,
   1306            uri,
   1307            installMethod
   1308          );
   1309          if (result.extension) {
   1310            aCallback.onSuccess(result);
   1311          } else {
   1312            aCallback.onError(result);
   1313          }
   1314        } catch (ex) {
   1315          debug`Install exception error ${ex}`;
   1316          aCallback.onError(`Unexpected error: ${ex}`);
   1317        }
   1318 
   1319        break;
   1320      }
   1321 
   1322      case "GeckoView:WebExtension:EnsureBuiltIn": {
   1323        const { locationUri, webExtensionId } = aData;
   1324        const uri = this.validateBuiltInLocation(locationUri, aCallback);
   1325        if (!uri) {
   1326          return;
   1327        }
   1328 
   1329        try {
   1330          const result = await this.ensureBuiltIn(uri, webExtensionId);
   1331          if (result.extension) {
   1332            aCallback.onSuccess(result);
   1333          } else {
   1334            aCallback.onError(result);
   1335          }
   1336        } catch (ex) {
   1337          debug`Install exception error ${ex}`;
   1338          aCallback.onError(`Unexpected error: ${ex}`);
   1339        }
   1340 
   1341        break;
   1342      }
   1343 
   1344      case "GeckoView:WebExtension:InstallBuiltIn": {
   1345        const uri = this.validateBuiltInLocation(aData.locationUri, aCallback);
   1346        if (!uri) {
   1347          return;
   1348        }
   1349 
   1350        try {
   1351          const result = await this.installBuiltIn(uri);
   1352          if (result.extension) {
   1353            aCallback.onSuccess(result);
   1354          } else {
   1355            aCallback.onError(result);
   1356          }
   1357        } catch (ex) {
   1358          debug`Install exception error ${ex}`;
   1359          aCallback.onError(`Unexpected error: ${ex}`);
   1360        }
   1361 
   1362        break;
   1363      }
   1364 
   1365      case "GeckoView:WebExtension:Uninstall": {
   1366        try {
   1367          await this.uninstallWebExtension(aData.webExtensionId);
   1368          aCallback.onSuccess();
   1369        } catch (ex) {
   1370          debug`Failed uninstall ${ex}`;
   1371          aCallback.onError(
   1372            `This extension cannot be uninstalled. Error: ${ex}.`
   1373          );
   1374        }
   1375        break;
   1376      }
   1377 
   1378      case "GeckoView:WebExtension:Enable": {
   1379        try {
   1380          const { source, webExtensionId } = aData;
   1381          if (source !== "user" && source !== "app") {
   1382            throw new Error("Illegal source parameter");
   1383          }
   1384          const extension = await this.enableWebExtension(
   1385            webExtensionId,
   1386            source
   1387          );
   1388          aCallback.onSuccess({ extension });
   1389        } catch (ex) {
   1390          debug`Failed enable ${ex}`;
   1391          aCallback.onError(`Unexpected error: ${ex}`);
   1392        }
   1393        break;
   1394      }
   1395 
   1396      case "GeckoView:WebExtension:Disable": {
   1397        try {
   1398          const { source, webExtensionId } = aData;
   1399          if (source !== "user" && source !== "app") {
   1400            throw new Error("Illegal source parameter");
   1401          }
   1402          const extension = await this.disableWebExtension(
   1403            webExtensionId,
   1404            source
   1405          );
   1406          aCallback.onSuccess({ extension });
   1407        } catch (ex) {
   1408          debug`Failed disable ${ex}`;
   1409          aCallback.onError(`Unexpected error: ${ex}`);
   1410        }
   1411        break;
   1412      }
   1413 
   1414      case "GeckoView:WebExtension:List": {
   1415        try {
   1416          await lazy.AddonManager.readyPromise;
   1417          const addons = await lazy.AddonManager.getAddonsByTypes([
   1418            "extension",
   1419          ]);
   1420          const extensions = await Promise.all(
   1421            addons.map(addon => exportExtension(addon, /* aSourceURI */ null))
   1422          );
   1423 
   1424          aCallback.onSuccess({ extensions });
   1425        } catch (ex) {
   1426          debug`Failed list ${ex}`;
   1427          aCallback.onError(`Unexpected error: ${ex}`);
   1428        }
   1429        break;
   1430      }
   1431 
   1432      case "GeckoView:WebExtension:Update": {
   1433        try {
   1434          const { webExtensionId } = aData;
   1435          const result = await this.updateWebExtension(webExtensionId);
   1436          if (result === null || result.extension) {
   1437            aCallback.onSuccess(result);
   1438          } else {
   1439            aCallback.onError(result);
   1440          }
   1441        } catch (ex) {
   1442          debug`Failed update ${ex}`;
   1443          aCallback.onError(`Unexpected error: ${ex}`);
   1444        }
   1445        break;
   1446      }
   1447    }
   1448  },
   1449 };
   1450 
   1451 // WeakMap[Extension -> BrowserAction]
   1452 GeckoViewWebExtension.browserActions = new WeakMap();
   1453 // WeakMap[Extension -> PageAction]
   1454 GeckoViewWebExtension.pageActions = new WeakMap();