tor-browser

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

AboutNewTabComponents.sys.mjs (7642B)


      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 import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
      6 
      7 const CATEGORY_NAME = "browser-newtab-external-component";
      8 
      9 const lazy = {};
     10 
     11 ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
     12  return console.createInstance({
     13    prefix: "AboutNewTabComponents",
     14    maxLogLevel: Services.prefs.getBoolPref(
     15      "browser.newtabpage.activity-stream.externalComponents.log",
     16      false
     17    )
     18      ? "Debug"
     19      : "Warn",
     20  });
     21 });
     22 
     23 /**
     24 * @typedef {object} NewTabComponentConfiguration
     25 * @property {string} type
     26 * @property {string[]} l10nURLs
     27 * @property {string} componentURL
     28 * @property {string} tagName
     29 */
     30 
     31 /**
     32 * The AboutNewTabComponentRegistry is a class that manages a list of
     33 * external components registered to appear on the newtab page.
     34 *
     35 * The registry is an EventEmitter, and will emit the UPDATED_EVENT when the
     36 * registry changes.
     37 *
     38 * The registry is bootstrapped via entries in the nsICategoryManager of name
     39 * CATEGORY_NAME.
     40 */
     41 class AboutNewTabComponentRegistry extends EventEmitter {
     42  static TYPES = Object.freeze({
     43    SEARCH: "SEARCH",
     44  });
     45  static UPDATED_EVENT = "updated";
     46 
     47  /**
     48   * The list of registered external component configurations, keyed on their
     49   * type.
     50   *
     51   * @type {Map<string, NewTabComponentConfiguration>}
     52   */
     53  #registeredComponents = new Map();
     54 
     55  /**
     56   * A mapping of external component registrant instances, keyed on their
     57   * module URI.
     58   *
     59   * @type {Map<string, BaseAboutNewTabComponentRegistrant>}
     60   */
     61  #registrants = new Map();
     62 
     63  constructor() {
     64    super();
     65 
     66    lazy.logConsole.debug("Instantiating AboutNewTabComponentRegistry");
     67    this.#infalliblyLoadConfigurations();
     68 
     69    Services.obs.addObserver(this, "xpcom-category-entry-removed");
     70    Services.obs.addObserver(this, "xpcom-category-entry-added");
     71    Services.obs.addObserver(this, "xpcom-category-cleared");
     72    Services.obs.addObserver(this, "profile-before-change");
     73  }
     74 
     75  observe(subject, topic, data) {
     76    switch (topic) {
     77      case "xpcom-category-entry-removed":
     78      // Intentional fall-through
     79      case "xpcom-category-entry-added":
     80      // Intentional fall-through
     81      case "xpcom-category-cleared": {
     82        // Intentional fall-through
     83        if (data === CATEGORY_NAME) {
     84          this.#infalliblyLoadConfigurations();
     85        }
     86        break;
     87      }
     88      case "profile-before-change": {
     89        this.destroy();
     90        break;
     91      }
     92    }
     93  }
     94 
     95  destroy() {
     96    for (let registrant of this.#registrants.values()) {
     97      registrant.destroy();
     98    }
     99    this.#registrants.clear();
    100    this.#registeredComponents.clear();
    101 
    102    Services.obs.removeObserver(this, "xpcom-category-entry-removed");
    103    Services.obs.removeObserver(this, "xpcom-category-entry-added");
    104    Services.obs.removeObserver(this, "xpcom-category-cleared");
    105    Services.obs.removeObserver(this, "profile-before-change");
    106  }
    107 
    108  /**
    109   * Iterates the CATEGORY_NAME nsICategoryManager category, and attempts to
    110   * load each registrant's configuration, which updates the
    111   * #registeredComponents. Updating the #registeredComponents will cause the
    112   * UPDATED_EVENT event to be emitted from this class.
    113   *
    114   * Invalid configurations are skipped.
    115   *
    116   * This method will log errors but is guaranteed to return, even if one or
    117   * more of the configurations is invalid.
    118   */
    119  #infalliblyLoadConfigurations() {
    120    lazy.logConsole.debug("Loading configurations");
    121    this.#registeredComponents.clear();
    122 
    123    for (let { entry, value } of Services.catMan.enumerateCategory(
    124      CATEGORY_NAME
    125    )) {
    126      try {
    127        lazy.logConsole.debug("Loading ", entry, value);
    128        let registrar = null;
    129        if (this.#registrants.has(entry)) {
    130          lazy.logConsole.debug("Found pre-existing registrant for ", entry);
    131          registrar = this.#registrants.get(entry);
    132        } else {
    133          lazy.logConsole.debug("Constructing registrant for ", entry);
    134          const module = ChromeUtils.importESModule(entry);
    135          const registrarClass = module[value];
    136 
    137          if (
    138            !(
    139              registrarClass.prototype instanceof
    140              BaseAboutNewTabComponentRegistrant
    141            )
    142          ) {
    143            throw new Error(
    144              `Registrant for ${entry} does not subclass BaseAboutNewTabComponentRegistrant`
    145            );
    146          }
    147 
    148          registrar = new registrarClass();
    149          this.#registrants.set(entry, registrar);
    150          registrar.on(AboutNewTabComponentRegistry.UPDATED_EVENT, () => {
    151            this.#infalliblyLoadConfigurations();
    152          });
    153        }
    154 
    155        let configurations = registrar.getComponents();
    156        for (let configuration of configurations) {
    157          if (this.#validateConfiguration(configuration)) {
    158            lazy.logConsole.debug(
    159              `Validated a configuration for type ${configuration.type}`
    160            );
    161            this.#registeredComponents.set(configuration.type, configuration);
    162          } else {
    163            lazy.logConsole.error(
    164              `Failed to validate a configuration:`,
    165              configuration
    166            );
    167          }
    168        }
    169      } catch (e) {
    170        lazy.logConsole.error(
    171          "Failed to load configurations ",
    172          entry,
    173          value,
    174          e.message
    175        );
    176      }
    177    }
    178 
    179    this.emit(AboutNewTabComponentRegistry.UPDATED_EVENT);
    180  }
    181 
    182  /**
    183   * Ensures that the configuration abides by newtab's external component
    184   * rules. Currently, that just means that two components cannot share the
    185   * same type.
    186   *
    187   * @param {NewTabComponentConfiguration} configuration
    188   * @returns {boolean}
    189   */
    190  #validateConfiguration(configuration) {
    191    if (!configuration.type) {
    192      return false;
    193    }
    194 
    195    // Currently, the only validation is to ensure that something isn't already
    196    // registered with the same type. This rule might evolve over time if we
    197    // start allowing multiples of a type.
    198    if (this.#registeredComponents.has(configuration.type)) {
    199      return false;
    200    }
    201 
    202    return true;
    203  }
    204 
    205  /**
    206   * Returns a copy of the configuration registry for external consumption.
    207   *
    208   * @returns {NewTabComponentConfiguration[]}
    209   */
    210  get values() {
    211    return Array.from(this.#registeredComponents.values());
    212  }
    213 }
    214 
    215 /**
    216 * Any registrants that want to register an external component onto newtab must
    217 * subclass this base class in order to provide the configuration for their
    218 * component. They must then add their registrant to the nsICategoryManager
    219 * category `browser-newtab-external-component`, where the entry is the URI
    220 * for the module containing the subclass, and the value is the name of the
    221 * subclass exported by the module.
    222 */
    223 class BaseAboutNewTabComponentRegistrant extends EventEmitter {
    224  /**
    225   * Subclasses can override this method to do any cleanup when the component
    226   * registry starts being shut down.
    227   */
    228  destroy() {}
    229 
    230  /**
    231   * Subclasses should override this method to provide one or more
    232   * NewTabComponentConfiguration's.
    233   *
    234   * @returns {NewTabComponentConfiguration[]}
    235   */
    236  getComponents() {
    237    return [];
    238  }
    239 
    240  /**
    241   * Subclasses can call this method if their component registry ever needs
    242   * updating. This will alert the registry to update itself.
    243   */
    244  updated() {
    245    this.emit(AboutNewTabComponentRegistry.UPDATED_EVENT);
    246  }
    247 }
    248 
    249 export { AboutNewTabComponentRegistry, BaseAboutNewTabComponentRegistrant };