tor-browser

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

webbrowser.js (24367B)


      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
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 var {
      8  DevToolsServer,
      9 } = require("resource://devtools/server/devtools-server.js");
     10 var {
     11  ActorRegistry,
     12 } = require("resource://devtools/server/actors/utils/actor-registry.js");
     13 var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
     14 
     15 loader.lazyRequireGetter(
     16  this,
     17  "RootActor",
     18  "resource://devtools/server/actors/root.js",
     19  true
     20 );
     21 loader.lazyRequireGetter(
     22  this,
     23  "TabDescriptorActor",
     24  "resource://devtools/server/actors/descriptors/tab.js",
     25  true
     26 );
     27 loader.lazyRequireGetter(
     28  this,
     29  "WebExtensionDescriptorActor",
     30  "resource://devtools/server/actors/descriptors/webextension.js",
     31  true
     32 );
     33 loader.lazyRequireGetter(
     34  this,
     35  "WorkerDescriptorActorList",
     36  "resource://devtools/server/actors/worker/worker-descriptor-actor-list.js",
     37  true
     38 );
     39 loader.lazyRequireGetter(
     40  this,
     41  "ServiceWorkerRegistrationActorList",
     42  "resource://devtools/server/actors/worker/service-worker-registration-list.js",
     43  true
     44 );
     45 loader.lazyRequireGetter(
     46  this,
     47  "ProcessActorList",
     48  "resource://devtools/server/actors/process.js",
     49  true
     50 );
     51 const lazy = {};
     52 loader.lazyGetter(lazy, "AddonManager", () => {
     53  return ChromeUtils.importESModule(
     54    "resource://gre/modules/AddonManager.sys.mjs",
     55    { global: "shared" }
     56  ).AddonManager;
     57 });
     58 
     59 /**
     60 * Browser-specific actors.
     61 */
     62 
     63 /**
     64 * Retrieve the window type of the top-level window |window|.
     65 */
     66 function appShellDOMWindowType(window) {
     67  /* This is what nsIWindowMediator's enumerator checks. */
     68  return window.document.documentElement.getAttribute("windowtype");
     69 }
     70 
     71 /**
     72 * Send Debugger:Shutdown events to all "navigator:browser" windows.
     73 */
     74 function sendShutdownEvent() {
     75  for (const win of Services.wm.getEnumerator(
     76    DevToolsServer.chromeWindowType
     77  )) {
     78    const evt = win.document.createEvent("Event");
     79    evt.initEvent("Debugger:Shutdown", true, false);
     80    win.document.documentElement.dispatchEvent(evt);
     81  }
     82 }
     83 
     84 exports.sendShutdownEvent = sendShutdownEvent;
     85 
     86 /**
     87 * Construct a root actor appropriate for use in a server running in a
     88 * browser. The returned root actor:
     89 * - respects the factories registered with ActorRegistry.addGlobalActor,
     90 * - uses a BrowserTabList to supply target actors for tabs,
     91 * - sends all navigator:browser window documents a Debugger:Shutdown event
     92 *   when it exits.
     93 *
     94 * * @param connection DevToolsServerConnection
     95 *          The conection to the client.
     96 */
     97 exports.createRootActor = function createRootActor(connection) {
     98  return new RootActor(connection, {
     99    tabList: new BrowserTabList(connection),
    100    addonList: new BrowserAddonList(connection),
    101    workerList: new WorkerDescriptorActorList(connection, {}),
    102    serviceWorkerRegistrationList: new ServiceWorkerRegistrationActorList(
    103      connection
    104    ),
    105    processList: new ProcessActorList(),
    106    globalActorFactories: ActorRegistry.globalActorFactories,
    107    onShutdown: sendShutdownEvent,
    108  });
    109 };
    110 
    111 /**
    112 * A live list of TabDescriptorActors representing the current browser tabs,
    113 * to be provided to the root actor to answer 'listTabs' requests.
    114 *
    115 * This object also takes care of listening for TabClose events and
    116 * onCloseWindow notifications, and exiting the target actors concerned.
    117 *
    118 * (See the documentation for RootActor for the definition of the "live
    119 * list" interface.)
    120 *
    121 * @param connection DevToolsServerConnection
    122 *     The connection in which this list's target actors may participate.
    123 *
    124 * Some notes:
    125 *
    126 * This constructor is specific to the desktop browser environment; it
    127 * maintains the tab list by tracking XUL windows and their XUL documents'
    128 * "tabbrowser", "tab", and "browser" elements. What's entailed in maintaining
    129 * an accurate list of open tabs in this context?
    130 *
    131 * - Opening and closing XUL windows:
    132 *
    133 * An nsIWindowMediatorListener is notified when new XUL windows (i.e., desktop
    134 * windows) are opened and closed. It is not notified of individual content
    135 * browser tabs coming and going within such a XUL window. That seems
    136 * reasonable enough; it's concerned with XUL windows, not tab elements in the
    137 * window's XUL document.
    138 *
    139 * However, even if we attach TabOpen and TabClose event listeners to each XUL
    140 * window as soon as it is created:
    141 *
    142 * - we do not receive a TabOpen event for the initial empty tab of a new XUL
    143 *   window; and
    144 *
    145 * - we do not receive TabClose events for the tabs of a XUL window that has
    146 *   been closed.
    147 *
    148 * This means that TabOpen and TabClose events alone are not sufficient to
    149 * maintain an accurate list of live tabs and mark target actors as closed
    150 * promptly. Our nsIWindowMediatorListener onCloseWindow handler must find and
    151 * exit all actors for tabs that were in the closing window.
    152 *
    153 * Since this is a bit hairy, we don't make each individual attached target
    154 * actor responsible for noticing when it has been closed; we watch for that,
    155 * and promise to call each actor's 'exit' method when it's closed, regardless
    156 * of how we learn the news.
    157 *
    158 * - nsIWindowMediator locks
    159 *
    160 * nsIWindowMediator holds a lock protecting its list of top-level windows
    161 * while it calls nsIWindowMediatorListener methods. nsIWindowMediator's
    162 * GetEnumerator method also tries to acquire that lock. Thus, enumerating
    163 * windows from within a listener method deadlocks (bug 873589). Rah. One
    164 * can sometimes work around this by leaving the enumeration for a later
    165 * tick.
    166 *
    167 * - Dragging tabs between windows:
    168 *
    169 * When a tab is dragged from one desktop window to another, we receive a
    170 * TabOpen event for the new tab, and a TabClose event for the old tab; tab XUL
    171 * elements do not really move from one document to the other (although their
    172 * linked browser's content window objects do).
    173 *
    174 * However, while we could thus assume that each tab stays with the XUL window
    175 * it belonged to when it was created, I'm not sure this is behavior one should
    176 * rely upon. When a XUL window is closed, we take the less efficient, more
    177 * conservative approach of simply searching the entire table for actors that
    178 * belong to the closing XUL window, rather than trying to somehow track which
    179 * XUL window each tab belongs to.
    180 */
    181 function BrowserTabList(connection) {
    182  this._connection = connection;
    183 
    184  /*
    185   * The XUL document of a tabbed browser window has "tab" elements, whose
    186   * 'linkedBrowser' JavaScript properties are "browser" elements; those
    187   * browsers' 'contentWindow' properties are wrappers on the tabs' content
    188   * window objects.
    189   *
    190   * This map's keys are "browser" XUL elements; it maps each browser element
    191   * to the target actor we've created for its content window, if we've created
    192   * one. This map serves several roles:
    193   *
    194   * - During iteration, we use it to find actors we've created previously.
    195   *
    196   * - On a TabClose event, we use it to find the tab's target actor and exit it.
    197   *
    198   * - When the onCloseWindow handler is called, we iterate over it to find all
    199   *   tabs belonging to the closing XUL window, and exit them.
    200   *
    201   * - When it's empty, and the onListChanged hook is null, we know we can
    202   *   stop listening for events and notifications.
    203   *
    204   * We listen for TabClose events and onCloseWindow notifications in order to
    205   * send onListChanged notifications, but also to tell actors when their
    206   * referent has gone away and remove entries for dead browsers from this map.
    207   * If that code is working properly, neither this map nor the actors in it
    208   * should ever hold dead tabs alive.
    209   */
    210  this._actorByBrowser = new Map();
    211 
    212  /* The current onListChanged handler, or null. */
    213  this._onListChanged = null;
    214 
    215  /*
    216   * True if we've been iterated over since we last called our onListChanged
    217   * hook.
    218   */
    219  this._mustNotify = false;
    220 
    221  /* True if we're testing, and should throw if consistency checks fail. */
    222  this._testing = false;
    223 
    224  this._onPageTitleChangedEvent = this._onPageTitleChangedEvent.bind(this);
    225 }
    226 
    227 BrowserTabList.prototype.constructor = BrowserTabList;
    228 
    229 BrowserTabList.prototype.destroy = function () {
    230  this._actorByBrowser.clear();
    231  this.onListChanged = null;
    232 };
    233 
    234 /**
    235 * Get the selected browser for the given navigator:browser window.
    236 *
    237 * @private
    238 * @param window nsIChromeWindow
    239 *        The navigator:browser window for which you want the selected browser.
    240 * @return Element|null
    241 *         The currently selected xul:browser element, if any. Note that the
    242 *         browser window might not be loaded yet - the function will return
    243 *         |null| in such cases.
    244 */
    245 BrowserTabList.prototype._getSelectedBrowser = function (window) {
    246  return window.gBrowser ? window.gBrowser.selectedBrowser : null;
    247 };
    248 
    249 /**
    250 * Produces an iterable (in this case a generator) to enumerate all available
    251 * browser tabs.
    252 */
    253 BrowserTabList.prototype._getBrowsers = function* () {
    254  // Iterate over all navigator:browser XUL windows.
    255  for (const win of Services.wm.getEnumerator(
    256    DevToolsServer.chromeWindowType
    257  )) {
    258    // For each tab in this XUL window, ensure that we have an actor for
    259    // it, reusing existing actors where possible.
    260    for (const browser of this._getChildren(win)) {
    261      yield browser;
    262    }
    263  }
    264 };
    265 
    266 BrowserTabList.prototype._getChildren = function (window) {
    267  if (!window.gBrowser) {
    268    return [];
    269  }
    270  const { gBrowser } = window;
    271  if (!gBrowser.browsers) {
    272    return [];
    273  }
    274  return gBrowser.browsers.filter(browser => {
    275    // Filter tabs that are closing. listTabs calls made right after TabClose
    276    // events still list tabs in process of being closed.
    277    const tab = gBrowser.getTabForBrowser(browser);
    278    return !tab.closing;
    279  });
    280 };
    281 
    282 BrowserTabList.prototype.getList = async function () {
    283  // As a sanity check, make sure all the actors presently in our map get
    284  // picked up when we iterate over all windows' tabs.
    285  const initialMapSize = this._actorByBrowser.size;
    286  this._foundCount = 0;
    287 
    288  const actors = [];
    289 
    290  for (const browser of this._getBrowsers()) {
    291    try {
    292      const actor = await this._getActorForBrowser(browser);
    293      actors.push(actor);
    294    } catch (e) {
    295      if (e.error === "tabDestroyed") {
    296        // Ignore the error if a tab was destroyed while retrieving the tab list.
    297        continue;
    298      }
    299 
    300      // Forward unexpected errors.
    301      throw e;
    302    }
    303  }
    304 
    305  if (this._testing && initialMapSize !== this._foundCount) {
    306    throw new Error("_actorByBrowser map contained actors for dead tabs");
    307  }
    308 
    309  this._mustNotify = true;
    310  this._checkListening();
    311 
    312  return actors;
    313 };
    314 
    315 BrowserTabList.prototype._getActorForBrowser = async function (browser) {
    316  // Do we have an existing actor for this browser? If not, create one.
    317  let actor = this._actorByBrowser.get(browser);
    318  if (actor) {
    319    this._foundCount++;
    320    return actor;
    321  }
    322 
    323  actor = new TabDescriptorActor(this._connection, browser);
    324  this._actorByBrowser.set(browser, actor);
    325  this._checkListening();
    326  return actor;
    327 };
    328 
    329 /**
    330 * Return the tab descriptor :
    331 * - for the tab matching a browserId if one is passed
    332 * - OR the currently selected tab if no browserId is passed.
    333 *
    334 * @param {number} browserId: use to match any tab
    335 */
    336 BrowserTabList.prototype.getTab = function ({ browserId }) {
    337  if (typeof browserId == "number") {
    338    const browsingContext = BrowsingContext.getCurrentTopByBrowserId(browserId);
    339    if (!browsingContext) {
    340      return Promise.reject({
    341        error: "noTab",
    342        message: `Unable to find tab with browserId '${browserId}' (no browsing-context)`,
    343      });
    344    }
    345    const browser = browsingContext.embedderElement;
    346    if (!browser) {
    347      return Promise.reject({
    348        error: "noTab",
    349        message: `Unable to find tab with browserId '${browserId}' (no embedder element)`,
    350      });
    351    }
    352    return this._getActorForBrowser(browser);
    353  }
    354 
    355  const topAppWindow = Services.wm.getMostRecentWindow(
    356    DevToolsServer.chromeWindowType
    357  );
    358  if (topAppWindow) {
    359    const selectedBrowser = this._getSelectedBrowser(topAppWindow);
    360    return this._getActorForBrowser(selectedBrowser);
    361  }
    362  return Promise.reject({
    363    error: "noTab",
    364    message: "Unable to find any selected browser",
    365  });
    366 };
    367 
    368 Object.defineProperty(BrowserTabList.prototype, "onListChanged", {
    369  enumerable: true,
    370  configurable: true,
    371  get() {
    372    return this._onListChanged;
    373  },
    374  set(v) {
    375    if (v !== null && typeof v !== "function") {
    376      throw new Error(
    377        "onListChanged property may only be set to 'null' or a function"
    378      );
    379    }
    380    this._onListChanged = v;
    381    this._checkListening();
    382  },
    383 });
    384 
    385 /**
    386 * The set of tabs has changed somehow. Call our onListChanged handler, if
    387 * one is set, and if we haven't already called it since the last iteration.
    388 */
    389 BrowserTabList.prototype._notifyListChanged = function () {
    390  if (!this._onListChanged) {
    391    return;
    392  }
    393  if (this._mustNotify) {
    394    this._onListChanged();
    395    this._mustNotify = false;
    396  }
    397 };
    398 
    399 /**
    400 * Exit |actor|, belonging to |browser|, and notify the onListChanged
    401 * handle if needed.
    402 */
    403 BrowserTabList.prototype._handleActorClose = function (actor, browser) {
    404  if (this._testing) {
    405    if (this._actorByBrowser.get(browser) !== actor) {
    406      throw new Error(
    407        "TabDescriptorActor not stored in map under given browser"
    408      );
    409    }
    410    if (actor.browser !== browser) {
    411      throw new Error("actor's browser and map key don't match");
    412    }
    413  }
    414 
    415  this._actorByBrowser.delete(browser);
    416  actor.destroy();
    417 
    418  this._notifyListChanged();
    419  this._checkListening();
    420 };
    421 
    422 /**
    423 * Make sure we are listening or not listening for activity elsewhere in
    424 * the browser, as appropriate. Other than setting up newly created XUL
    425 * windows, all listener / observer management should happen here.
    426 */
    427 BrowserTabList.prototype._checkListening = function () {
    428  /*
    429   * If we have an onListChanged handler that we haven't sent an announcement
    430   * to since the last iteration, we need to watch for tab creation as well as
    431   * change of the currently selected tab and tab title changes of tabs in
    432   * parent process via TabAttrModified (tabs oop uses DOMTitleChanges).
    433   *
    434   * Oddly, we don't need to watch for 'close' events here. If our actor list
    435   * is empty, then either it was empty the last time we iterated, and no
    436   * close events are possible, or it was not empty the last time we
    437   * iterated, but all the actors have since been closed, and we must have
    438   * sent a notification already when they closed.
    439   */
    440  this._listenForEventsIf(
    441    this._onListChanged && this._mustNotify,
    442    "_listeningForTabOpen",
    443    ["TabOpen", "TabSelect", "TabAttrModified"]
    444  );
    445 
    446  /* If we have live actors, we need to be ready to mark them dead. */
    447  this._listenForEventsIf(
    448    this._actorByBrowser.size > 0,
    449    "_listeningForTabClose",
    450    ["TabClose"]
    451  );
    452 
    453  /*
    454   * We must listen to the window mediator in either case, since that's the
    455   * only way to find out about tabs that come and go when top-level windows
    456   * are opened and closed.
    457   */
    458  this._listenToMediatorIf(
    459    (this._onListChanged && this._mustNotify) || this._actorByBrowser.size > 0
    460  );
    461 
    462  /*
    463   * We also listen for title changed events on the browser.
    464   */
    465  this._listenForEventsIf(
    466    this._onListChanged && this._mustNotify,
    467    "_listeningForTitleChange",
    468    ["pagetitlechanged"],
    469    this._onPageTitleChangedEvent
    470  );
    471 };
    472 
    473 /**
    474 * Add or remove event listeners for all XUL windows.
    475 *
    476 * @param shouldListen boolean
    477 *    True if we should add event handlers; false if we should remove them.
    478 * @param guard string
    479 *    The name of a guard property of 'this', indicating whether we're
    480 *    already listening for those events.
    481 * @param eventNames array of strings
    482 *    An array of event names.
    483 */
    484 BrowserTabList.prototype._listenForEventsIf = function (
    485  shouldListen,
    486  guard,
    487  eventNames,
    488  listener = this
    489 ) {
    490  if (!shouldListen !== !this[guard]) {
    491    const op = shouldListen ? "addEventListener" : "removeEventListener";
    492    for (const win of Services.wm.getEnumerator(
    493      DevToolsServer.chromeWindowType
    494    )) {
    495      for (const name of eventNames) {
    496        win[op](name, listener, false);
    497      }
    498    }
    499    this[guard] = shouldListen;
    500  }
    501 };
    502 
    503 /*
    504 * Event listener for pagetitlechanged event.
    505 */
    506 BrowserTabList.prototype._onPageTitleChangedEvent = function (event) {
    507  switch (event.type) {
    508    case "pagetitlechanged": {
    509      const browser = event.target;
    510      this._onDOMTitleChanged(browser);
    511      break;
    512    }
    513  }
    514 };
    515 
    516 /**
    517 * Handle "DOMTitleChanged" event.
    518 */
    519 BrowserTabList.prototype._onDOMTitleChanged = DevToolsUtils.makeInfallible(
    520  function (browser) {
    521    const actor = this._actorByBrowser.get(browser);
    522    if (actor) {
    523      this._notifyListChanged();
    524      this._checkListening();
    525    }
    526  }
    527 );
    528 
    529 /**
    530 * Implement nsIDOMEventListener.
    531 */
    532 BrowserTabList.prototype.handleEvent = DevToolsUtils.makeInfallible(function (
    533  event
    534 ) {
    535  // If event target has `linkedBrowser`, the event target can be assumed <tab> element.
    536  // Else, event target is assumed <browser> element, use the target as it is.
    537  const browser = event.target.linkedBrowser || event.target;
    538  switch (event.type) {
    539    case "TabOpen":
    540    case "TabSelect": {
    541      /* Don't create a new actor; iterate will take care of that. Just notify. */
    542      this._notifyListChanged();
    543      this._checkListening();
    544      break;
    545    }
    546    case "TabClose": {
    547      const actor = this._actorByBrowser.get(browser);
    548      if (actor) {
    549        this._handleActorClose(actor, browser);
    550      }
    551      break;
    552    }
    553    case "TabAttrModified": {
    554      // Remote <browser> title changes are handled via DOMTitleChange message
    555      // TabAttrModified is only here for browsers in parent process which
    556      // don't send this message.
    557      if (browser.isRemoteBrowser) {
    558        break;
    559      }
    560      const actor = this._actorByBrowser.get(browser);
    561      if (actor) {
    562        // TabAttrModified is fired in various cases, here only care about title
    563        // changes
    564        if (event.detail.changed.includes("label")) {
    565          this._notifyListChanged();
    566          this._checkListening();
    567        }
    568      }
    569      break;
    570    }
    571  }
    572 }, "BrowserTabList.prototype.handleEvent");
    573 
    574 /*
    575 * If |shouldListen| is true, ensure we've registered a listener with the
    576 * window mediator. Otherwise, ensure we haven't registered a listener.
    577 */
    578 BrowserTabList.prototype._listenToMediatorIf = function (shouldListen) {
    579  if (!shouldListen !== !this._listeningToMediator) {
    580    const op = shouldListen ? "addListener" : "removeListener";
    581    Services.wm[op](this);
    582    this._listeningToMediator = shouldListen;
    583  }
    584 };
    585 
    586 /**
    587 * nsIWindowMediatorListener implementation.
    588 *
    589 * See _onTabClosed for explanation of why we needn't actually tweak any
    590 * actors or tables here.
    591 *
    592 * An nsIWindowMediatorListener's methods get passed all sorts of windows; we
    593 * only care about the tab containers. Those have 'gBrowser' members.
    594 */
    595 BrowserTabList.prototype.onOpenWindow = DevToolsUtils.makeInfallible(function (
    596  window
    597 ) {
    598  const handleLoad = DevToolsUtils.makeInfallible(() => {
    599    /* We don't want any further load events from this window. */
    600    window.removeEventListener("load", handleLoad);
    601 
    602    if (appShellDOMWindowType(window) !== DevToolsServer.chromeWindowType) {
    603      return;
    604    }
    605 
    606    // Listen for future tab activity.
    607    if (this._listeningForTabOpen) {
    608      window.addEventListener("TabOpen", this);
    609      window.addEventListener("TabSelect", this);
    610      window.addEventListener("TabAttrModified", this);
    611    }
    612    if (this._listeningForTabClose) {
    613      window.addEventListener("TabClose", this);
    614    }
    615    if (this._listeningForTitleChange) {
    616      window.messageManager.addMessageListener("DOMTitleChanged", this);
    617    }
    618 
    619    // As explained above, we will not receive a TabOpen event for this
    620    // document's initial tab, so we must notify our client of the new tab
    621    // this will have.
    622    this._notifyListChanged();
    623  });
    624 
    625  /*
    626   * You can hardly do anything at all with a XUL window at this point; it
    627   * doesn't even have its document yet. Wait until its document has
    628   * loaded, and then see what we've got. This also avoids
    629   * nsIWindowMediator enumeration from within listeners (bug 873589).
    630   */
    631  window = window
    632    .QueryInterface(Ci.nsIInterfaceRequestor)
    633    .getInterface(Ci.nsIDOMWindow);
    634 
    635  window.addEventListener("load", handleLoad);
    636 }, "BrowserTabList.prototype.onOpenWindow");
    637 
    638 BrowserTabList.prototype.onCloseWindow = DevToolsUtils.makeInfallible(function (
    639  window
    640 ) {
    641  if (window instanceof Ci.nsIAppWindow) {
    642    window = window.docShell.domWindow;
    643  }
    644 
    645  if (appShellDOMWindowType(window) !== DevToolsServer.chromeWindowType) {
    646    return;
    647  }
    648 
    649  /*
    650   * nsIWindowMediator deadlocks if you call its GetEnumerator method from
    651   * a nsIWindowMediatorListener's onCloseWindow hook (bug 873589), so
    652   * handle the close in a different tick.
    653   */
    654  Services.tm.dispatchToMainThread(
    655    DevToolsUtils.makeInfallible(() => {
    656      /*
    657       * Scan the entire map for actors representing tabs that were in this
    658       * top-level window, and exit them.
    659       */
    660      for (const [browser, actor] of this._actorByBrowser) {
    661        /* The browser document of a closed window has no default view. */
    662        if (!browser.ownerGlobal) {
    663          this._handleActorClose(actor, browser);
    664        }
    665      }
    666    }, "BrowserTabList.prototype.onCloseWindow's delayed body")
    667  );
    668 }, "BrowserTabList.prototype.onCloseWindow");
    669 
    670 exports.BrowserTabList = BrowserTabList;
    671 
    672 function BrowserAddonList(connection) {
    673  this._connection = connection;
    674  this._actorByAddonId = new Map();
    675  this._onListChanged = null;
    676 }
    677 
    678 BrowserAddonList.prototype.getList = async function () {
    679  const addons = await lazy.AddonManager.getAllAddons();
    680  for (const addon of addons) {
    681    let actor = this._actorByAddonId.get(addon.id);
    682    if (!actor) {
    683      actor = new WebExtensionDescriptorActor(this._connection, addon);
    684      this._actorByAddonId.set(addon.id, actor);
    685    }
    686  }
    687 
    688  return Array.from(this._actorByAddonId, ([_, actor]) => actor);
    689 };
    690 
    691 Object.defineProperty(BrowserAddonList.prototype, "onListChanged", {
    692  enumerable: true,
    693  configurable: true,
    694  get() {
    695    return this._onListChanged;
    696  },
    697  set(v) {
    698    if (v !== null && typeof v != "function") {
    699      throw new Error(
    700        "onListChanged property may only be set to 'null' or a function"
    701      );
    702    }
    703    this._onListChanged = v;
    704    this._adjustListener();
    705  },
    706 });
    707 
    708 /**
    709 * AddonManager listener must implement onDisabled.
    710 */
    711 BrowserAddonList.prototype.onDisabled = function () {
    712  this._onAddonManagerUpdated();
    713 };
    714 
    715 /**
    716 * AddonManager listener must implement onEnabled.
    717 */
    718 BrowserAddonList.prototype.onEnabled = function () {
    719  this._onAddonManagerUpdated();
    720 };
    721 
    722 /**
    723 * AddonManager listener must implement onInstalled.
    724 */
    725 BrowserAddonList.prototype.onInstalled = function () {
    726  this._onAddonManagerUpdated();
    727 };
    728 
    729 /**
    730 * AddonManager listener must implement onOperationCancelled.
    731 */
    732 BrowserAddonList.prototype.onOperationCancelled = function () {
    733  this._onAddonManagerUpdated();
    734 };
    735 
    736 /**
    737 * AddonManager listener must implement onUninstalling.
    738 */
    739 BrowserAddonList.prototype.onUninstalling = function () {
    740  this._onAddonManagerUpdated();
    741 };
    742 
    743 /**
    744 * AddonManager listener must implement onUninstalled.
    745 */
    746 BrowserAddonList.prototype.onUninstalled = function (addon) {
    747  this._actorByAddonId.delete(addon.id);
    748  this._onAddonManagerUpdated();
    749 };
    750 
    751 BrowserAddonList.prototype._onAddonManagerUpdated = function () {
    752  this._notifyListChanged();
    753  this._adjustListener();
    754 };
    755 
    756 BrowserAddonList.prototype._notifyListChanged = function () {
    757  if (this._onListChanged) {
    758    this._onListChanged();
    759  }
    760 };
    761 
    762 BrowserAddonList.prototype._adjustListener = function () {
    763  if (this._onListChanged) {
    764    // As long as the callback exists, we need to listen for changes
    765    // so we can notify about add-on changes.
    766    lazy.AddonManager.addAddonListener(this);
    767  } else if (this._actorByAddonId.size === 0) {
    768    // When the callback does not exist, we only need to keep listening
    769    // if the actor cache will need adjusting when add-ons change.
    770    lazy.AddonManager.removeAddonListener(this);
    771  }
    772 };
    773 
    774 exports.BrowserAddonList = BrowserAddonList;