common/src/ExtensionSupport.jsm
author Magnus Melin <mkmelin+mozilla@iki.fi>
Sun, 26 Apr 2020 12:42:45 +0300
changeset 38935 91432c993ab3a1e95fb031e336053d3d89155005
parent 38152 3f3fc2c0d80474dff5953969f765e16937dde150
permissions -rw-r--r--
Bug 1609760 - Stop assigning properties to the global `this` in common/ JSMs (port bug 1608278). r=pmorris cp ../.gitignore .rgignore && rg -l -g '*.jsm' '' common | jscodeshift --stdin --transform ~/Code/jsm-rewrites/no-this-property-assign.js (Script from https://github.com/bgrins/jsm-rewrites/blob/d2bbd6c459294b65955442e45b5a7f5dba11e639/no-this-property-assign.js)

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
 * Helper functions for use by extensions that should ease them plug
 * into the application.
 */

const EXPORTED_SYMBOLS = ["ExtensionSupport"];

const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");

var extensionHooks = new Map();
var openWindowList;

var ExtensionSupport = {
  /**
   * Register listening for windows getting opened that will run the specified callback function
   * when a matching window is loaded.
   *
   * @param aID {String}  Some identification of the caller, usually the extension ID.
   * @param aExtensionHook {Object}  The object describing the hook the caller wants to register.
   *        Members of the object can be (all optional, but one callback must be supplied):
   *        chromeURLs {Array}         An array of strings of document URLs on which
   *                                   the given callback should run. If not specified,
   *                                   run on all windows.
   *        onLoadWindow {function}    The callback function to run when window loads
   *                                   the matching document.
   *        onUnloadWindow {function}  The callback function to run when window
   *                                   unloads the matching document.
   *        Both callbacks receive the matching window object as argument.
   *
   * @return {boolean}  True if the passed arguments were valid and the caller could be registered.
   *                    False otherwise.
   */
  registerWindowListener(aID, aExtensionHook) {
    if (!aID) {
      Cu.reportError("No extension ID provided for the window listener");
      return false;
    }

    if (extensionHooks.has(aID)) {
      Cu.reportError(
        "Window listener for extension + '" + aID + "' already registered"
      );
      return false;
    }

    if (
      !("onLoadWindow" in aExtensionHook) &&
      !("onUnloadWindow" in aExtensionHook)
    ) {
      Cu.reportError(
        "The extension + '" + aID + "' does not provide any callbacks"
      );
      return false;
    }

    extensionHooks.set(aID, aExtensionHook);

    // Add our global listener if there isn't one already
    // (only when we have first caller).
    if (extensionHooks.size == 1) {
      Services.wm.addListener(this._windowListener);
    }

    if (openWindowList) {
      // We already have a list of open windows, notify the caller about them.
      openWindowList.forEach(domWindow =>
        ExtensionSupport._checkAndRunMatchingExtensions(domWindow, "load", aID)
      );
    } else {
      openWindowList = new Set();
      // Get the list of windows already open.
      let windows = Services.wm.getEnumerator(null);
      while (windows.hasMoreElements()) {
        let domWindow = windows.getNext();
        if (domWindow.document.location.href === "about:blank") {
          ExtensionSupport._waitForLoad(domWindow, aID);
        } else {
          ExtensionSupport._addToListAndNotify(domWindow, aID);
        }
      }
    }

    return true;
  },

  /**
   * Unregister listening for windows for the given caller.
   *
   * @param aID {String}  Some identification of the caller, usually the extension ID.
   *
   * @return {boolean}  True if the passed arguments were valid and the caller could be unregistered.
   *                    False otherwise.
   */
  unregisterWindowListener(aID) {
    if (!aID) {
      Cu.reportError("No extension ID provided for the window listener");
      return false;
    }

    let windowListener = extensionHooks.get(aID);
    if (!windowListener) {
      Cu.reportError(
        "Couldn't remove window listener for extension + '" + aID + "'"
      );
      return false;
    }

    extensionHooks.delete(aID);
    // Remove our global listener if there are no callers registered anymore.
    if (extensionHooks.size == 0) {
      Services.wm.removeListener(this._windowListener);
      openWindowList.clear();
      openWindowList = undefined;
    }

    return true;
  },

  get openWindows() {
    return openWindowList.values();
  },

  _windowListener: {
    // nsIWindowMediatorListener functions
    onOpenWindow(appWindow) {
      // A new window has opened.
      let domWindow = appWindow.docShell.domWindow;

      // Here we pass no caller ID, so all registered callers get notified.
      ExtensionSupport._waitForLoad(domWindow);
    },

    onCloseWindow(appWindow) {
      // One of the windows has closed.
      let domWindow = appWindow.docShell.domWindow;
      openWindowList.delete(domWindow);
    },
  },

  /**
   * Set up listeners to run the callbacks on the given window.
   *
   * @param aWindow {nsIDOMWindow}  The window to set up.
   * @param aID {String} Optional.  ID of the new caller that has registered right now.
   */
  _waitForLoad(aWindow, aID) {
    // Wait for the load event of the window. At that point
    // aWindow.document.location.href will not be "about:blank" any more.
    aWindow.addEventListener(
      "load",
      function() {
        ExtensionSupport._addToListAndNotify(aWindow, aID);
      },
      { once: true }
    );
  },

  /**
   * Once the window is fully loaded with the href referring to the XUL document,
   * add it to our list, attach the "unload" listener to it and notify interested
   * callers.
   *
   * @param aWindow {nsIDOMWindow}  The window to process.
   * @param aID {String} Optional.  ID of the new caller that has registered right now.
   */
  _addToListAndNotify(aWindow, aID) {
    openWindowList.add(aWindow);
    aWindow.addEventListener(
      "unload",
      function() {
        ExtensionSupport._checkAndRunMatchingExtensions(aWindow, "unload");
      },
      { once: true }
    );
    ExtensionSupport._checkAndRunMatchingExtensions(aWindow, "load", aID);
  },

  /**
   * Check if the caller matches the given window and run its callback function.
   *
   * @param aWindow {nsIDOMWindow}  The window to run the callbacks on.
   * @param aEventType {String}     Which callback to run if caller matches (load/unload).
   * @param aID {String}            Optional ID of the caller whose callback is to be run.
   *                                If not given, all registered callers are notified.
   */
  _checkAndRunMatchingExtensions(aWindow, aEventType, aID) {
    if (aID) {
      checkAndRunExtensionCode(extensionHooks.get(aID));
    } else {
      for (let extensionHook of extensionHooks.values()) {
        checkAndRunExtensionCode(extensionHook);
      }
    }

    /**
     * Check if the single given caller matches the given window
     * and run its callback function.
     *
     * @param aExtensionHook {Object}  The object describing the hook the caller
     *                                 has registered.
     */
    function checkAndRunExtensionCode(aExtensionHook) {
      let windowChromeURL = aWindow.document.location.href;
      // Check if extension applies to this document URL.
      if (
        "chromeURLs" in aExtensionHook &&
        !aExtensionHook.chromeURLs.some(url => url == windowChromeURL)
      ) {
        return;
      }

      // Run the relevant callback.
      switch (aEventType) {
        case "load":
          if ("onLoadWindow" in aExtensionHook) {
            aExtensionHook.onLoadWindow(aWindow);
          }
          break;
        case "unload":
          if ("onUnloadWindow" in aExtensionHook) {
            aExtensionHook.onUnloadWindow(aWindow);
          }
          break;
      }
    }
  },

  get registeredWindowListenerCount() {
    return extensionHooks.size;
  },
};