tor-browser

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

AboutNewTabResourceMapping.sys.mjs (29446B)


      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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      7 
      8 export const BUILTIN_ADDON_ID = "newtab@mozilla.org";
      9 export const DISABLE_NEWTAB_AS_ADDON_PREF =
     10  "browser.newtabpage.disableNewTabAsAddon";
     11 export const TRAINHOP_NIMBUS_FEATURE_ID = "newtabTrainhopAddon";
     12 export const TRAINHOP_NIMBUS_FIRST_STARTUP_FEATURE_ID =
     13  "newtabTrainhopFirstStartup";
     14 export const TRAINHOP_XPI_BASE_URL_PREF =
     15  "browser.newtabpage.trainhopAddon.xpiBaseURL";
     16 export const TRAINHOP_XPI_VERSION_PREF =
     17  "browser.newtabpage.trainhopAddon.version";
     18 export const TRAINHOP_SCHEDULED_UPDATE_STATE_DELAY_PREF =
     19  "browser.newtabpage.trainhopAddon.scheduledUpdateState.delay";
     20 export const TRAINHOP_SCHEDULED_UPDATE_STATE_TIMEOUT_PREF =
     21  "browser.newtabpage.trainhopAddon.scheduledUpdateState.timeout";
     22 
     23 const FLUENT_SOURCE_NAME = "newtab";
     24 const TOPIC_LOCALES_CHANGED = "intl:app-locales-changed";
     25 const TOPIC_SHUTDOWN = "profile-before-change";
     26 
     27 const lazy = XPCOMUtils.declareLazy({
     28  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
     29  AddonSettings: "resource://gre/modules/addons/AddonSettings.sys.mjs",
     30  AboutHomeStartupCache: "resource:///modules/AboutHomeStartupCache.sys.mjs",
     31  AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
     32  DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
     33  ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
     34  NewTabGleanUtils: "resource://newtab/lib/NewTabGleanUtils.sys.mjs",
     35  NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
     36 
     37  resProto: {
     38    service: "@mozilla.org/network/protocol;1?name=resource",
     39    iid: Ci.nsISubstitutingProtocolHandler,
     40  },
     41  aomStartup: {
     42    service: "@mozilla.org/addons/addon-manager-startup;1",
     43    iid: Ci.amIAddonManagerStartup,
     44  },
     45  aboutRedirector: {
     46    service: "@mozilla.org/network/protocol/about;1?what=newtab",
     47    iid: Ci.nsIAboutModule,
     48  },
     49 
     50  // NOTE: the following timeout and delay prefs are used to customize the
     51  // DeferredTask that calls updateTrainhopAddonState, and they are meant to
     52  // be set for testing, debugging or QA verification.
     53  trainhopAddonScheduledUpdateDelay: {
     54    pref: TRAINHOP_SCHEDULED_UPDATE_STATE_DELAY_PREF,
     55    default: 5000,
     56  },
     57  trainhopAddonScheduledUpdateTimeout: {
     58    pref: TRAINHOP_SCHEDULED_UPDATE_STATE_TIMEOUT_PREF,
     59    default: -1,
     60  },
     61  trainhopAddonXPIBaseURL: {
     62    pref: TRAINHOP_XPI_BASE_URL_PREF,
     63    default: "",
     64  },
     65  trainhopAddonXPIVersion: {
     66    pref: TRAINHOP_XPI_VERSION_PREF,
     67    default: "",
     68  },
     69 });
     70 
     71 /**
     72 * AboutNewTabResourceMapping is responsible for creating the mapping between
     73 * the built-in add-on newtab code, and the chrome://newtab and resource://newtab
     74 * URI prefixes (which are also used by the component mode for newtab, and acts
     75 * as a compatibility layer).
     76 *
     77 * When the built-in add-on newtab is being read in from an XPI, the
     78 * AboutNewTabResourceMapping is also responsible for doing dynamic Fluent
     79 * and Glean ping/metric registration.
     80 */
     81 export var AboutNewTabResourceMapping = {
     82  initialized: false,
     83  log: null,
     84  newTabAsAddonDisabled: false,
     85 
     86  _rootURISpec: null,
     87  _addonIsXPI: null,
     88  _addonVersion: null,
     89  _addonListener: null,
     90  _builtinVersion: null,
     91  _updateAddonStateDeferredTask: null,
     92  _supportedLocales: null,
     93 
     94  /**
     95   * Returns the version string for whichever version of New Tab is currently
     96   * being used.
     97   *
     98   * @type {string}
     99   */
    100  get addonVersion() {
    101    return this._addonVersion;
    102  },
    103 
    104  /**
    105   * This should be called early on in the lifetime of the browser, before any
    106   * attempt to load a resource from resource://newtab or chrome://newtab.
    107   *
    108   * This method is a no-op after the first call.
    109   */
    110  init() {
    111    if (this.initialized) {
    112      return;
    113    }
    114 
    115    this.logger.debug("Initializing:");
    116 
    117    // NOTE: this pref is read only once per session on purpose
    118    // (and it is expected to be used by the resource mapping logic
    119    // on the next application startup if flipped at runtime, e.g. as
    120    // part of an emergency pref flip through Nimbus).
    121    this.newTabAsAddonDisabled = Services.prefs.getBoolPref(
    122      DISABLE_NEWTAB_AS_ADDON_PREF,
    123      false
    124    );
    125    this.inSafeMode = Services.appinfo.inSafeMode;
    126    this.getBuiltinAddonVersion();
    127    this.registerNewTabResources();
    128    this.addAddonListener();
    129 
    130    this.initialized = true;
    131    this.logger.debug("Initialized");
    132  },
    133 
    134  /**
    135   * Adds an add-on listener to detect postponed installations of the newtab add-on
    136   * and invalidate the AboutHomeStartupCache. This method is a no-op when the
    137   * emergency fallback `browser.newtabpage.disableNewTabAsAddon` about:config pref
    138   * is set to true.
    139   */
    140  addAddonListener() {
    141    if (!this._addonListener && !this.newTabAsAddonDisabled) {
    142      // The newtab add-on has a background.js script which defers updating until
    143      // the next restart. We still, however, want to blow away the about:home
    144      // startup cache when we notice this postponed install, to avoid loading
    145      // a cache created with another version of newtab.
    146      const addonInstallListener = {};
    147      addonInstallListener.onInstallPostponed = install => {
    148        if (install.addon.id === BUILTIN_ADDON_ID) {
    149          this.logger.debug(
    150            "Invalidating AboutHomeStartupCache on detected newly installed newtab resources"
    151          );
    152          lazy.AboutHomeStartupCache.clearCacheAndUninit();
    153        }
    154      };
    155      lazy.AddonManager.addInstallListener(addonInstallListener);
    156      this._addonListener = addonInstallListener;
    157    }
    158  },
    159 
    160  /**
    161   * Retrieves the version of the built-in newtab add-on from AddonManager.
    162   * If AddonManager.getBuiltinAddonVersion hits an unexpected exception (e.g.
    163   * if the method is unexpectedly called before AddonManager and XPIProvider
    164   * are being started), it sets the _builtinVersion property to null and logs
    165   * a warning message.
    166   */
    167  getBuiltinAddonVersion() {
    168    try {
    169      this._builtinVersion =
    170        lazy.AddonManager.getBuiltinAddonVersion(BUILTIN_ADDON_ID);
    171    } catch (e) {
    172      this._builtinVersion = null;
    173      this.logger.warn(
    174        "Unexpected failure on retrieving builtin addon version",
    175        e
    176      );
    177    }
    178  },
    179 
    180  /**
    181   * Gets the preferred mapping for newtab resources. This method tries to retrieve
    182   * the rootURI from the WebExtensionPolicy instance of the newtab add-on, or falling
    183   * back to the URI of the newtab resources bundled in the Desktop omni jar if not found.
    184   * The newtab resources bundled in the Desktop omni jar are instead always preferred
    185   * while running in safe mode or if the emergency fallback about:config pref
    186   * (`browser.newtabpage.disableNewTabAsAddon`) is set to true.
    187   *
    188   * @returns {{version: ?string, rootURI: nsIURI}}
    189   *   Returns the preferred newtab root URI for resource://newtab and chrome://newtab,
    190   *   along with add-on version if using the newtab add-on root URI, or null
    191   *   when the newtab add-on root URI was not selected as the preferred one.
    192   */
    193  getPreferredMapping() {
    194    const { inSafeMode, newTabAsAddonDisabled } = this;
    195    const policy = WebExtensionPolicy.getByID(BUILTIN_ADDON_ID);
    196    // Retrieve the mapping url (but fallback to the known url for the
    197    // newtab resources bundled in the Desktop omni jar if that fails).
    198    let { version, rootURI } = policy?.extension ?? {};
    199    let isXPI = rootURI?.spec.endsWith(".xpi!/");
    200 
    201    // If we failed to retrieve the builtin add-on version, avoid mapping
    202    // XPI resources as an additional safety measure, because later it
    203    // wouldn't be possible to check if the builtin version is more recent
    204    // than the train-hop add-on version that may be already installed.
    205    if (isXPI && this._builtinVersion === null) {
    206      rootURI = null;
    207      isXPI = false;
    208    }
    209 
    210    // Do not use XPI resources to prepare to uninstall the train-hop add-on xpi
    211    // later in the current application session from updateTrainhopAddonState, if:
    212    //
    213    // - the train-hop add-on version set in the pref is empty (the client has been
    214    //   unenrolled in the previous browsing session and so we fallback to the
    215    //   resources bundled in the Desktop omni jar)
    216    // - the builtin add-on version is equal or greater than the train-hop add-on
    217    //   version (and so the application has been updated and the old train-hop
    218    //   add-on is obsolete and can be uninstalled)
    219    // - the train-hop add-on xpi is not system-signed (as specifically required for
    220    //   newtab xpi being installed in the `extensions` profile subdirectory by
    221    //   the custom install logic provided by the _installTrainhopAddon method).
    222    const shouldUninstallXPI = isXPI
    223      ? lazy.trainhopAddonXPIVersion === "" ||
    224        Services.vc.compare(this._builtinVersion, version) >= 0 ||
    225        (lazy.AddonSettings.REQUIRE_SIGNING && !policy.isPrivileged)
    226      : false;
    227 
    228    if (!rootURI || inSafeMode || newTabAsAddonDisabled || shouldUninstallXPI) {
    229      const builtinAddonsURI = lazy.resProto.getSubstitution("builtin-addons");
    230      rootURI = Services.io.newURI("newtab/", null, builtinAddonsURI);
    231      version = null;
    232      isXPI = false;
    233    }
    234    return { isXPI, version, rootURI };
    235  },
    236 
    237  /**
    238   * Registers the resource://newtab and chrome://newtab resources, and also
    239   * kicks off dynamic Fluent and Glean registration if the add-on is installed
    240   * via an XPI.
    241   */
    242  registerNewTabResources() {
    243    const RES_PATH = "newtab";
    244    try {
    245      const { isXPI, version, rootURI } = this.getPreferredMapping();
    246      this._rootURISpec = rootURI.spec;
    247      this._addonVersion = version;
    248      this._addonIsXPI = isXPI;
    249      this.logger.log(
    250        this.newTabAsAddonDisabled || !version
    251          ? `Mapping newtab resources from ${rootURI.spec}`
    252          : `Mapping newtab resources from ${isXPI ? "XPI" : "built-in add-on"} version ${version} ` +
    253              `on application version ${AppConstants.MOZ_APP_VERSION_DISPLAY}`
    254      );
    255      lazy.resProto.setSubstitutionWithFlags(
    256        RES_PATH,
    257        rootURI,
    258        Ci.nsISubstitutingProtocolHandler.ALLOW_CONTENT_ACCESS
    259      );
    260      const manifestURI = Services.io.newURI("manifest.json", null, rootURI);
    261      this._chromeHandle = lazy.aomStartup.registerChrome(manifestURI, [
    262        ["content", "newtab", "data/content", "contentaccessible=yes"],
    263      ]);
    264 
    265      if (isXPI) {
    266        // We must be a train-hopped XPI running in this app. This means we
    267        // may have Fluent files or Glean pings/metrics to register dynamically.
    268        this.registerFluentSources(rootURI);
    269        this.registerMetricsFromJson();
    270      }
    271      lazy.aboutRedirector.wrappedJSObject.notifyBuiltInAddonInitialized();
    272      Glean.newtab.addonReadySuccess.set(true);
    273      Glean.newtab.addonXpiUsed.set(isXPI);
    274      this.logger.debug("Newtab resource mapping completed successfully");
    275    } catch (e) {
    276      this.logger.error("Failed to complete resource mapping: ", e);
    277      Glean.newtab.addonReadySuccess.set(false);
    278      throw e;
    279    }
    280  },
    281 
    282  /**
    283   * Registers Fluent strings contained within the XPI.
    284   *
    285   * @param {nsIURI} rootURI
    286   *   The rootURI for the newtab add-on.
    287   * @returns {Promise<undefined>}
    288   *   Resolves once the Fluent strings have been registered, or even if a
    289   *   failure to register them has occurred (which will log the error).
    290   */
    291  async registerFluentSources(rootURI) {
    292    try {
    293      // Read in the list of locales included with the XPI. This will prevent
    294      // us from accidentally registering a L10nFileSource that wasn't included.
    295      this._supportedLocales = new Set(
    296        await fetch(rootURI.resolve("/locales/supported-locales.json")).then(
    297          r => r.json()
    298        )
    299      );
    300 
    301      // Set up observers so that if the user changes the list of available
    302      // locales, we'll re-register.
    303      Services.obs.addObserver(this, TOPIC_LOCALES_CHANGED);
    304      Services.obs.addObserver(this, TOPIC_SHUTDOWN);
    305      // Now actually do the registration.
    306      this._updateFluentSourcesRegistration();
    307    } catch (e) {
    308      // TODO: consider if we should collect this in telemetry.
    309      this.logger.error(
    310        `Error on registering fluent files from ${rootURI.spec}:`,
    311        e
    312      );
    313    }
    314  },
    315 
    316  /**
    317   * Sets up the L10nFileSource for the newtab Fluent files included in the
    318   * XPI that are in the available locales for the app. If a pre-existing
    319   * registration exists, it will be updated.
    320   */
    321  _updateFluentSourcesRegistration() {
    322    let availableLocales = new Set(Services.locale.availableLocales);
    323    let availableSupportedLocales =
    324      this._supportedLocales.intersection(availableLocales);
    325 
    326    const newtabFileSource = new L10nFileSource(
    327      FLUENT_SOURCE_NAME,
    328      "app",
    329      [...availableSupportedLocales],
    330      `resource://newtab/locales/{locale}/`
    331    );
    332 
    333    let registry = L10nRegistry.getInstance();
    334    if (registry.hasSource(FLUENT_SOURCE_NAME)) {
    335      registry.updateSources([newtabFileSource]);
    336      this.logger.debug(
    337        "Newtab strings updated for ",
    338        Array.from(availableSupportedLocales)
    339      );
    340    } else {
    341      registry.registerSources([newtabFileSource]);
    342      this.logger.debug(
    343        "Newtab strings registered for ",
    344        Array.from(availableSupportedLocales)
    345      );
    346    }
    347  },
    348 
    349  observe(_subject, topic, _data) {
    350    switch (topic) {
    351      case TOPIC_LOCALES_CHANGED: {
    352        this._updateFluentSourcesRegistration();
    353        break;
    354      }
    355      case TOPIC_SHUTDOWN: {
    356        Services.obs.removeObserver(this, TOPIC_LOCALES_CHANGED);
    357        Services.obs.removeObserver(this, TOPIC_SHUTDOWN);
    358        break;
    359      }
    360    }
    361  },
    362 
    363  /**
    364   * Registers any dynamic Glean metrics that have been included with the XPI
    365   * version of the add-on.
    366   */
    367  registerMetricsFromJson() {
    368    // The metrics we need to process were placed in webext-glue/metrics/runtime-metrics-<version>.json
    369    // That file will be generated by build script getting implemented with Bug 1960111
    370    const version = AppConstants.MOZ_APP_VERSION.match(/\d+/)[0];
    371    const metricsPath = `resource://newtab/webext-glue/metrics/runtime-metrics-${version}.json`;
    372    this.logger.debug(`Registering FOG Glean metrics from ${metricsPath}`);
    373    lazy.NewTabGleanUtils.registerMetricsAndPings(metricsPath);
    374  },
    375 
    376  scheduleUpdateTrainhopAddonState() {
    377    if (!this._updateAddonStateDeferredTask) {
    378      this.logger.debug("creating _updateAddonStateDeferredTask");
    379      const delayMs = lazy.trainhopAddonScheduledUpdateDelay;
    380      const idleTimeoutMs = lazy.trainhopAddonScheduledUpdateTimeout;
    381      this._updateAddonStateDeferredTask = new lazy.DeferredTask(
    382        async () => {
    383          const isPastShutdownConfirmed =
    384            Services.startup.isInOrBeyondShutdownPhase(
    385              Ci.nsIAppStartup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
    386            );
    387          if (isPastShutdownConfirmed) {
    388            this.logger.debug(
    389              "updateAddonStateDeferredTask cancelled after appShutdownConfirmed barrier"
    390            );
    391            return;
    392          }
    393          this.logger.debug("_updateAddonStateDeferredTask running");
    394          await this.updateTrainhopAddonState().catch(e => {
    395            this.logger.warn("updateAddonStateDeferredTask failure", e);
    396          });
    397          this.logger.debug("_updateAddonStateDeferredTask completed");
    398        },
    399        delayMs,
    400        idleTimeoutMs
    401      );
    402      lazy.AsyncShutdown.appShutdownConfirmed.addBlocker(
    403        `${TRAINHOP_NIMBUS_FEATURE_ID} scheduleUpdateTrainhopAddonState shutting down`,
    404        () => this._updateAddonStateDeferredTask.finalize()
    405      );
    406      lazy.NimbusFeatures[TRAINHOP_NIMBUS_FEATURE_ID].onUpdate(() =>
    407        this._updateAddonStateDeferredTask.arm()
    408      );
    409    }
    410    this.logger.debug("re-arming _updateAddonStateDeferredTask");
    411    this._updateAddonStateDeferredTask.arm();
    412  },
    413 
    414  /**
    415   * Updates the state of the train-hop add-on based on the Nimbus feature variables.
    416   *
    417   * @returns {Promise<void>}
    418   *   Resolves once the train-hop add-on has been staged to be installed or uninstalled (e.g.
    419   *   when the client has been unenrolled from the Nimbus feature), or after it has been
    420   *   determined that no action was needed (e.g. while running in safemode, or if the same
    421   *   or an higher add-on version than the train-hop add-on version is already in use,
    422   *   installed or pending to be installed). Rejects on failures or unexpected cancellations
    423   *   during installation or uninstallation process.
    424   */
    425  async updateTrainhopAddonState(forceRestartlessInstall = false) {
    426    if (this.inSafeMode) {
    427      this.logger.debug(
    428        "train-hop add-on update state disabled while running in SafeMode"
    429      );
    430      return;
    431    }
    432 
    433    const nimbusFeature = lazy.NimbusFeatures[TRAINHOP_NIMBUS_FEATURE_ID];
    434    await nimbusFeature.ready();
    435    const { addon_version, xpi_download_path } = nimbusFeature.getAllVariables({
    436      defaultValues: { addon_version: null, xpi_download_path: null },
    437    });
    438 
    439    this.logger.debug("Force restartless install: ", forceRestartlessInstall);
    440    this.logger.debug("Received addon version:", addon_version);
    441    this.logger.debug("Received XPI download path:", xpi_download_path);
    442 
    443    let addon = await lazy.AddonManager.getAddonByID(BUILTIN_ADDON_ID);
    444 
    445    // Uninstall train-hop add-on xpi if its resources are not currently
    446    // being used and the client has been unenrolled from the newtabTrainhopAddon
    447    // Nimbus feature.
    448    if (!this._addonIsXPI && addon) {
    449      let changed = false;
    450      if (addon_version === null && xpi_download_path === null) {
    451        changed ||= await this.uninstallAddon({
    452          uninstallReason:
    453            "uninstalling train-hop add-on version on Nimbus feature unenrolled",
    454        });
    455        return;
    456      }
    457 
    458      if (
    459        this._builtinVersion &&
    460        Services.vc.compare(this._builtinVersion, addon.version) >= 0
    461      ) {
    462        changed ||= await this.uninstallAddon({
    463          uninstallReason:
    464            "uninstalling train-hop add-on version on builtin add-on with equal or higher version",
    465        });
    466      }
    467 
    468      if (
    469        lazy.AddonSettings.REQUIRE_SIGNING &&
    470        addon.signedState !== lazy.AddonManager.SIGNEDSTATE_SYSTEM
    471      ) {
    472        changed ||= await this.uninstallAddon({
    473          uninstallReason: `uninstall train-hop add-on xpi on unexpected signedState ${addon.signedState} (expected ${lazy.AddonManager.SIGNEDSTATE_SYSTEM})`,
    474        });
    475      }
    476 
    477      // Retrieve the new add-on wrapper if the xpi version has been uninstalled.
    478      if (changed) {
    479        addon = await lazy.AddonManager.getAddonByID(BUILTIN_ADDON_ID);
    480 
    481        this.logger.debug(
    482          "Invalidating AboutHomeStartupCache after train-hop uninstall"
    483        );
    484        lazy.AboutHomeStartupCache.clearCacheAndUninit();
    485      }
    486    }
    487 
    488    // Record Nimbus feature newtabTrainhopAddon exposure event if NewTab
    489    // is currently using the resources from the train-hop add-on version.
    490    if (this._addonIsXPI && this._addonVersion === addon_version) {
    491      this.logger.debug(
    492        `train-hop add-on version ${addon_version} already in use`
    493      );
    494      // Record exposure event for the train hop feature if the train-hop
    495      // add-on version is already in use.
    496      nimbusFeature.recordExposureEvent({ once: true });
    497      return;
    498    }
    499 
    500    // Verify if the train-hop add-on version is already installed.
    501    if (addon?.version === addon_version) {
    502      this.logger.warn(
    503        `train-hop add-on version ${addon_version} already installed but not in use`
    504      );
    505      return;
    506    }
    507 
    508    if (!lazy.trainhopAddonXPIBaseURL) {
    509      this.logger.debug(
    510        "train-hop add-on download disabled on empty download base URL"
    511      );
    512      return;
    513    }
    514 
    515    if (addon_version == null && xpi_download_path == null) {
    516      this.logger.debug("train-hop cancelled: client not enrolled");
    517      return;
    518    }
    519 
    520    if (addon_version == null) {
    521      this.logger.warn("train-hop failure: missing mandatory addon_version");
    522      return;
    523    }
    524 
    525    if (xpi_download_path == null) {
    526      this.logger.warn(
    527        "train-hop failure: missing mandatory xpi_download_path"
    528      );
    529      return;
    530    }
    531 
    532    const xpiDownloadURL = `${lazy.trainhopAddonXPIBaseURL}${xpi_download_path}`;
    533    await this._installTrainhopAddon({
    534      trainhopAddonVersion: addon_version,
    535      xpiDownloadURL,
    536      forceRestartlessInstall,
    537    });
    538  },
    539 
    540  /**
    541   * Downloads and installs the newtab train-hop add-on version based on Nimbus feature configuration,
    542   * or record the Nimbus feature exposure event if the newtab train-hop add-on version is already in use.
    543   *
    544   * @param {object} params
    545   * @param {string} params.trainhopAddonVersion - The version of the train-hop add-on to install.
    546   * @param {string} params.xpiDownloadURL - The URL from which to download the XPI file.
    547   * @param {boolean} params.forceRestartlessInstall
    548   *   After the XPI is downloaded, attempt to complete a restartless install. Note that if
    549   *   AboutNewTabResourceMapping.init has been called by the time the XPI has finished
    550   *   downloading, this directive is ignored, and we fallback to installing on the next
    551   *   restart.
    552   *
    553   * @returns {Promise<void>}
    554   *   Resolves when the train-hop add-on installation is completed or not needed, or rejects
    555   *   on failures or unexpected cancellations hit during the installation process.
    556   */
    557  async _installTrainhopAddon({
    558    trainhopAddonVersion,
    559    xpiDownloadURL,
    560    forceRestartlessInstall,
    561  }) {
    562    if (
    563      this._builtinVersion &&
    564      Services.vc.compare(this._builtinVersion, trainhopAddonVersion) >= 0
    565    ) {
    566      this.logger.warn(
    567        `cancel xpi download on train-hop add-on version ${trainhopAddonVersion} on equal or higher builtin version ${this._builtinVersion}`
    568      );
    569      return;
    570    }
    571 
    572    let addon = await lazy.AddonManager.getAddonByID(BUILTIN_ADDON_ID);
    573    if (
    574      addon?.version &&
    575      Services.vc.compare(addon.version, trainhopAddonVersion) >= 0
    576    ) {
    577      this.logger.warn(
    578        `cancel xpi download on train-hop add-on version ${trainhopAddonVersion} on equal or higher version ${addon.version} already installed`
    579      );
    580      return;
    581    }
    582 
    583    // Verify if there is already a pending install for the same or higher add-on version
    584    // (in case of multiple pending installations for the same add-on id, the last one wins).
    585    let pendingInstall = (await lazy.AddonManager.getAllInstalls())
    586      .filter(
    587        install =>
    588          install.addon?.id === BUILTIN_ADDON_ID &&
    589          install.state === lazy.AddonManager.STATE_POSTPONED
    590      )
    591      .pop();
    592    if (
    593      pendingInstall &&
    594      Services.vc.compare(pendingInstall.addon.version, trainhopAddonVersion) >=
    595        0
    596    ) {
    597      this.logger.debug(
    598        `cancel xpi download on train-hop add-on version ${trainhopAddonVersion} on equal or higher versions ${pendingInstall.addon.version} install already in progress`
    599      );
    600      return;
    601    }
    602    this.logger.log(
    603      `downloading train-hop add-on version ${trainhopAddonVersion} from ${xpiDownloadURL}`
    604    );
    605    try {
    606      let newInstall = await lazy.AddonManager.getInstallForURL(
    607        xpiDownloadURL,
    608        {
    609          telemetryInfo: { source: "nimbus-newtabTrainhopAddon" },
    610        }
    611      );
    612      const deferred = Promise.withResolvers();
    613      newInstall.addListener({
    614        onDownloadEnded: () => {
    615          if (
    616            newInstall.addon.id !== BUILTIN_ADDON_ID ||
    617            newInstall.addon.version !== trainhopAddonVersion
    618          ) {
    619            deferred.reject(
    620              new Error(
    621                `train-hop add-on install cancelled on mismatching add-on version` +
    622                  `(actual ${newInstall.addon.version}, expected ${trainhopAddonVersion})`
    623              )
    624            );
    625            newInstall.cancel();
    626          }
    627 
    628          if (
    629            lazy.AddonSettings.REQUIRE_SIGNING &&
    630            newInstall.addon.signedState !==
    631              lazy.AddonManager.SIGNEDSTATE_SYSTEM
    632          ) {
    633            deferred.reject(
    634              new Error(
    635                `trainhop add-on install cancelled on invalid signed state` +
    636                  `(actual ${newInstall.addon.signedState}, expected ${lazy.AddonManager.SIGNEDSTATE_SYSTEM})`
    637              )
    638            );
    639            newInstall.cancel();
    640          }
    641 
    642          this.logger.debug("Train-hop download ended");
    643        },
    644        onInstallPostponed: () => {
    645          this.logger.debug("Train-hop install postponed, as expected");
    646          if (forceRestartlessInstall && !this.initialized) {
    647            this.logger.debug("Forcing restartless install of train-hop");
    648            newInstall.continuePostponedInstall();
    649          } else {
    650            this.logger.debug("Not forcing restartless install");
    651            if (forceRestartlessInstall) {
    652              this.logger.debug(
    653                "We must have initialized before the XPI finished downloading."
    654              );
    655            }
    656            deferred.resolve();
    657          }
    658        },
    659        onInstallEnded: () => {
    660          this.logger.debug("Train-hop restartless install ended");
    661          if (forceRestartlessInstall) {
    662            this.logger.debug("Resolving train-hop install promise");
    663            deferred.resolve();
    664          }
    665        },
    666        onDownloadCancelled: () => {
    667          deferred.reject(
    668            new Error(
    669              `Unexpected download cancelled while downloading xpi from ${xpiDownloadURL}`
    670            )
    671          );
    672        },
    673        onDownloadFailed: () => {
    674          deferred.reject(
    675            new Error(`Failed to download xpi from ${xpiDownloadURL}`)
    676          );
    677        },
    678        onInstallCancelled: () => {
    679          deferred.reject(
    680            new Error(
    681              `Unexpected install cancelled while installing xpi from ${xpiDownloadURL}`
    682            )
    683          );
    684        },
    685        onInstallFailed: () => {
    686          deferred.reject(
    687            new Error(`Failed to install xpi from ${xpiDownloadURL}`)
    688          );
    689        },
    690      });
    691      newInstall.install();
    692      await deferred.promise;
    693 
    694      if (forceRestartlessInstall) {
    695        this.logger.debug(
    696          `train-hop add-on ${trainhopAddonVersion} downloaded and we will attempt a restartless install`
    697        );
    698      } else {
    699        this.logger.debug(
    700          `train-hop add-on ${trainhopAddonVersion} downloaded and pending install on next startup`
    701        );
    702      }
    703    } catch (e) {
    704      this.logger.error(`train-hop add-on install failure: ${e}`);
    705    }
    706  },
    707 
    708  /**
    709   * Uninstalls the newtab add-on, if it exists and has the PERM_CAN_UNINSTALL permission,
    710   * optionally logs a reason for the add-on being uninstalled.
    711   *
    712   * @param {object} params
    713   * @param {string} [params.uninstallReason]
    714   *   Reason for uninstalling the add-on to log along with uninstalling
    715   *   the add-on.
    716   *
    717   * @returns {Promise<boolean>}
    718   *   Resolves once the add-on is uninstalled, if it was found and had the
    719   *   PERM_CAN_UNINSTALL permission, with a boolean set to true if the
    720   *   add-on was found and uninstalled.
    721   */
    722  async uninstallAddon({ uninstallReason } = {}) {
    723    let addon = await lazy.AddonManager.getAddonByID(BUILTIN_ADDON_ID);
    724    if (addon && addon.permissions & lazy.AddonManager.PERM_CAN_UNINSTALL) {
    725      if (uninstallReason) {
    726        this.logger.info(uninstallReason);
    727      }
    728      await addon.uninstall();
    729      return true;
    730    }
    731    return false;
    732  },
    733 
    734  /**
    735   * This is registered to be called on first startup for new profiles on
    736   * Windows. It is expected to be called very early on in the lifetime of
    737   * new profiles, such that the AboutNewTabResourceMapping.init routine has
    738   * not yet had a chance to run.
    739   *
    740   * @returns {Promise<void>}
    741   */
    742  async firstStartupNewProfile() {
    743    if (this.initialized) {
    744      this.logger.error(
    745        "firstStartupNewProfile is being run after AboutNewTabResourceMapping initializes, so we're too late."
    746      );
    747      return;
    748    }
    749    this.logger.debug(
    750      "First startup with a new profile. Checking for any train-hops to perform restartless install."
    751    );
    752    await lazy.ExperimentAPI.ready();
    753 
    754    const nimbusFeature =
    755      lazy.NimbusFeatures[TRAINHOP_NIMBUS_FIRST_STARTUP_FEATURE_ID];
    756    await nimbusFeature.ready();
    757    const { enabled } = nimbusFeature.getAllVariables({
    758      defaultValues: { enabled: true },
    759    });
    760    if (!enabled) {
    761      // We've been configured to bypass the FirstStartup install for
    762      // train-hops, so exit now.
    763      this.logger.debug(
    764        "Not forcing install of any newtab XPIs, as we're currently configured not to."
    765      );
    766      return;
    767    }
    768 
    769    await lazy.AddonManager.readyPromise;
    770    await this.updateTrainhopAddonState(true /* forceRestartlessInstall */);
    771    this.logger.debug("First startup - new profile done");
    772  },
    773 };
    774 
    775 AboutNewTabResourceMapping.logger = console.createInstance({
    776  prefix: "AboutNewTabResourceMapping",
    777  maxLogLevel: Services.prefs.getBoolPref(
    778    "browser.newtabpage.resource-mapping.log",
    779    false
    780  )
    781    ? "Debug"
    782    : "Warn",
    783 });