tor-browser

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

MigrationUtils.sys.mjs (37588B)


      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 const lazy = {};
      9 
     10 ChromeUtils.defineESModuleGetters(lazy, {
     11  AMBrowserExtensionsImport: "resource://gre/modules/AddonManager.sys.mjs",
     12  LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
     13  PlacesUIUtils: "moz-src:///browser/components/places/PlacesUIUtils.sys.mjs",
     14  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     15  Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
     16  setTimeout: "resource://gre/modules/Timer.sys.mjs",
     17  MigrationWizardConstants:
     18    "chrome://browser/content/migration/migration-wizard-constants.mjs",
     19 });
     20 
     21 ChromeUtils.defineLazyGetter(
     22  lazy,
     23  "gCanGetPermissionsOnPlatformPromise",
     24  () => {
     25    let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
     26    return fp.isModeSupported(Ci.nsIFilePicker.modeGetFolder);
     27  }
     28 );
     29 
     30 var gMigrators = null;
     31 var gFileMigrators = null;
     32 var gProfileStartup = null;
     33 var gL10n = null;
     34 
     35 let gForceExitSpinResolve = false;
     36 let gKeepUndoData = false;
     37 let gUndoData = null;
     38 
     39 function getL10n() {
     40  if (!gL10n) {
     41    gL10n = new Localization(["browser/migrationWizard.ftl"]);
     42  }
     43  return gL10n;
     44 }
     45 
     46 const MIGRATOR_MODULES = Object.freeze({
     47  EdgeProfileMigrator: {
     48    moduleURI: "resource:///modules/EdgeProfileMigrator.sys.mjs",
     49    platforms: ["win"],
     50  },
     51  FirefoxProfileMigrator: {
     52    moduleURI: "resource:///modules/FirefoxProfileMigrator.sys.mjs",
     53    platforms: ["linux", "macosx", "win"],
     54  },
     55  IEProfileMigrator: {
     56    moduleURI: "resource:///modules/IEProfileMigrator.sys.mjs",
     57    platforms: ["win"],
     58  },
     59  SafariProfileMigrator: {
     60    moduleURI: "resource:///modules/SafariProfileMigrator.sys.mjs",
     61    platforms: ["macosx"],
     62  },
     63 
     64  // The following migrators are all variants of the ChromeProfileMigrator
     65 
     66  BraveProfileMigrator: {
     67    moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
     68    platforms: ["linux", "macosx", "win"],
     69  },
     70  CanaryProfileMigrator: {
     71    moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
     72    platforms: ["macosx", "win"],
     73  },
     74  ChromeProfileMigrator: {
     75    moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
     76    platforms: ["linux", "macosx", "win"],
     77  },
     78  ChromeBetaMigrator: {
     79    moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
     80    platforms: ["linux", "win"],
     81  },
     82  ChromeDevMigrator: {
     83    moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
     84    platforms: ["linux"],
     85  },
     86  ChromiumProfileMigrator: {
     87    moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
     88    platforms: ["linux", "macosx", "win"],
     89  },
     90  Chromium360seMigrator: {
     91    moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
     92    platforms: ["win"],
     93  },
     94  ChromiumEdgeMigrator: {
     95    moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
     96    platforms: ["macosx", "win"],
     97  },
     98  ChromiumEdgeBetaMigrator: {
     99    moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
    100    platforms: ["macosx", "win"],
    101  },
    102  OperaProfileMigrator: {
    103    moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
    104    platforms: ["linux", "macosx", "win"],
    105  },
    106  VivaldiProfileMigrator: {
    107    moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
    108    platforms: ["linux", "macosx", "win"],
    109  },
    110  OperaGXProfileMigrator: {
    111    moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
    112    platforms: ["macosx", "win"],
    113  },
    114 
    115  InternalTestingProfileMigrator: {
    116    moduleURI: "resource:///modules/InternalTestingProfileMigrator.sys.mjs",
    117    platforms: ["linux", "macosx", "win"],
    118  },
    119 });
    120 
    121 const FILE_MIGRATOR_MODULES = Object.freeze({
    122  PasswordFileMigrator: {
    123    moduleURI: "resource:///modules/FileMigrators.sys.mjs",
    124  },
    125  BookmarksFileMigrator: {
    126    moduleURI: "resource:///modules/FileMigrators.sys.mjs",
    127  },
    128 });
    129 
    130 /**
    131 * The singleton MigrationUtils service. This service is the primary mechanism
    132 * by which migrations from other browsers to this browser occur. The singleton
    133 * instance of this class is exported from this module as `MigrationUtils`.
    134 */
    135 class MigrationUtils {
    136  constructor() {
    137    XPCOMUtils.defineLazyPreferenceGetter(
    138      this,
    139      "HISTORY_MAX_AGE_IN_DAYS",
    140      "browser.migrate.history.maxAgeInDays",
    141      180
    142    );
    143 
    144    ChromeUtils.registerWindowActor("MigrationWizard", {
    145      parent: {
    146        esModuleURI: "resource:///actors/MigrationWizardParent.sys.mjs",
    147      },
    148 
    149      child: {
    150        esModuleURI: "resource:///actors/MigrationWizardChild.sys.mjs",
    151        events: {
    152          "MigrationWizard:RequestState": { wantUntrusted: true },
    153          "MigrationWizard:BeginMigration": { wantUntrusted: true },
    154          "MigrationWizard:RequestSafariPermissions": { wantUntrusted: true },
    155          "MigrationWizard:SelectManualPasswordFile": { wantUntrusted: true },
    156          "MigrationWizard:OpenAboutAddons": { wantUntrusted: true },
    157          "MigrationWizard:PermissionsNeeded": { wantUntrusted: true },
    158          "MigrationWizard:GetPermissions": { wantUntrusted: true },
    159          "MigrationWizard:OpenURL": { wantUntrusted: true },
    160        },
    161      },
    162 
    163      includeChrome: true,
    164      allFrames: true,
    165      matches: [
    166        "about:welcome",
    167        "about:welcome?*",
    168        "about:preferences",
    169        "about:settings",
    170        "chrome://browser/content/migration/migration-dialog-window.html",
    171        "chrome://browser/content/spotlight.html",
    172        "about:firefoxview",
    173      ],
    174    });
    175 
    176    ChromeUtils.defineLazyGetter(this, "IS_LINUX_SNAP_PACKAGE", () => {
    177      if (
    178        AppConstants.platform != "linux" ||
    179        !Cc["@mozilla.org/gio-service;1"]
    180      ) {
    181        return false;
    182      }
    183 
    184      let gIOSvc = Cc["@mozilla.org/gio-service;1"].getService(
    185        Ci.nsIGIOService
    186      );
    187      return gIOSvc.isRunningUnderSnap;
    188    });
    189  }
    190 
    191  resourceTypes = Object.freeze({
    192    ALL: 0x0000,
    193    /* 0x01 used to be used for settings, but was removed. */
    194    COOKIES: 0x0002,
    195    HISTORY: 0x0004,
    196    FORMDATA: 0x0008,
    197    PASSWORDS: 0x0010,
    198    BOOKMARKS: 0x0020,
    199    OTHERDATA: 0x0040,
    200    SESSION: 0x0080,
    201    PAYMENT_METHODS: 0x0100,
    202    EXTENSIONS: 0x0200,
    203  });
    204 
    205  /**
    206   * Helper for implementing simple asynchronous cases of migration resources'
    207   * ``migrate(aCallback)`` (see MigratorBase).  If your ``migrate`` method
    208   * just waits for some file to be read, for example, and then migrates
    209   * everything right away, you can wrap the async-function with this helper
    210   * and not worry about notifying the callback.
    211   *
    212   * @example
    213   * // For example, instead of writing:
    214   * setTimeout(function() {
    215   *   try {
    216   *     ....
    217   *     aCallback(true);
    218   *   }
    219   *   catch() {
    220   *     aCallback(false);
    221   *   }
    222   * }, 0);
    223   *
    224   * // You may write:
    225   * setTimeout(MigrationUtils.wrapMigrateFunction(function() {
    226   *   if (importingFromMosaic)
    227   *     throw Cr.NS_ERROR_UNEXPECTED;
    228   * }, aCallback), 0);
    229   *
    230   * // ... and aCallback will be called with aSuccess=false when importing
    231   * // from Mosaic, or with aSuccess=true otherwise.
    232   *
    233   * @param {Function} aFunction
    234   *   the function that will be called sometime later.  If aFunction
    235   *   throws when it's called, aCallback(false) is called, otherwise
    236   *   aCallback(true) is called.
    237   * @param {Function} aCallback
    238   *   the callback function passed to ``migrate``.
    239   * @returns {Function}
    240   *   the wrapped function.
    241   */
    242  wrapMigrateFunction(aFunction, aCallback) {
    243    return function () {
    244      let success = false;
    245      try {
    246        aFunction.apply(null, arguments);
    247        success = true;
    248      } catch (ex) {
    249        console.error(ex);
    250      }
    251      // Do not change this to call aCallback directly in try try & catch
    252      // blocks, because if aCallback throws, we may end up calling aCallback
    253      // twice.
    254      aCallback(success);
    255    };
    256  }
    257 
    258  /**
    259   * Gets localized string corresponding to l10n-id
    260   *
    261   * @param {string} aKey
    262   *   The key of the id of the localization to retrieve.
    263   * @param {object} [aArgs=undefined]
    264   *   An optional map of arguments to the id.
    265   * @returns {Promise<string>}
    266   *   A promise that resolves to the retrieved localization.
    267   */
    268  getLocalizedString(aKey, aArgs) {
    269    let l10n = getL10n();
    270    return l10n.formatValue(aKey, aArgs);
    271  }
    272 
    273  /**
    274   * Get all the rows corresponding to a select query from a database, without
    275   * requiring a lock on the database. If fetching data fails (because someone
    276   * else tried to write to the DB at the same time, for example), we will
    277   * retry the fetch after a 100ms timeout, up to 10 times.
    278   *
    279   * @param {string} path
    280   *   The file path to the database we want to open.
    281   * @param {string} description
    282   *   A developer-readable string identifying what kind of database we're
    283   *   trying to open.
    284   * @param {string} selectQuery
    285   *   The SELECT query to use to fetch the rows.
    286   * @param {Promise} [testDelayPromise]
    287   *   An optional promise to await for after the first loop, used in tests.
    288   *
    289   * @returns {Promise<object[]|Error>}
    290   *   A promise that resolves to an array of rows. The promise will be
    291   *   rejected if the read/fetch failed even after retrying.
    292   */
    293  getRowsFromDBWithoutLocks(
    294    path,
    295    description,
    296    selectQuery,
    297    testDelayPromise = null
    298  ) {
    299    let dbOptions = {
    300      readOnly: true,
    301      ignoreLockingMode: true,
    302      path,
    303    };
    304 
    305    const RETRYLIMIT = 10;
    306    const RETRYINTERVAL = 100;
    307    return (async function innerGetRows() {
    308      let rows = null;
    309      for (let retryCount = RETRYLIMIT; retryCount; retryCount--) {
    310        // Attempt to get the rows. If this succeeds, we will bail out of the loop,
    311        // close the database in a failsafe way, and pass the rows back.
    312        // If fetching the rows throws, we will wait RETRYINTERVAL ms
    313        // and try again. This will repeat a maximum of RETRYLIMIT times.
    314        let db;
    315        let didOpen = false;
    316        let previousExceptionMessage = null;
    317        try {
    318          db = await lazy.Sqlite.openConnection(dbOptions);
    319          didOpen = true;
    320          rows = await db.execute(selectQuery);
    321          break;
    322        } catch (ex) {
    323          if (previousExceptionMessage != ex.message) {
    324            console.error(ex);
    325          }
    326          previousExceptionMessage = ex.message;
    327          if (ex.name == "NS_ERROR_FILE_CORRUPTED") {
    328            break;
    329          }
    330        } finally {
    331          try {
    332            if (didOpen) {
    333              await db.close();
    334            }
    335          } catch (ex) {}
    336        }
    337        await Promise.all([
    338          new Promise(resolve => lazy.setTimeout(resolve, RETRYINTERVAL)),
    339          testDelayPromise,
    340        ]);
    341      }
    342      if (!rows) {
    343        throw new Error(
    344          "Couldn't get rows from the " + description + " database."
    345        );
    346      }
    347      return rows;
    348    })();
    349  }
    350 
    351  get #migrators() {
    352    if (!gMigrators) {
    353      gMigrators = new Map();
    354      for (let [symbol, { moduleURI, platforms }] of Object.entries(
    355        MIGRATOR_MODULES
    356      )) {
    357        if (platforms.includes(AppConstants.platform)) {
    358          let { [symbol]: migratorClass } =
    359            ChromeUtils.importESModule(moduleURI);
    360          if (gMigrators.has(migratorClass.key)) {
    361            console.error(
    362              "A pre-existing migrator exists with key " +
    363                `${migratorClass.key}. Not registering.`
    364            );
    365            continue;
    366          }
    367          gMigrators.set(migratorClass.key, new migratorClass());
    368        }
    369      }
    370    }
    371    return gMigrators;
    372  }
    373 
    374  get #fileMigrators() {
    375    if (!gFileMigrators) {
    376      gFileMigrators = new Map();
    377      for (let [symbol, { moduleURI }] of Object.entries(
    378        FILE_MIGRATOR_MODULES
    379      )) {
    380        let { [symbol]: migratorClass } = ChromeUtils.importESModule(moduleURI);
    381        if (gFileMigrators.has(migratorClass.key)) {
    382          console.error(
    383            "A pre-existing file migrator exists with key " +
    384              `${migratorClass.key}. Not registering.`
    385          );
    386          continue;
    387        }
    388        gFileMigrators.set(migratorClass.key, new migratorClass());
    389      }
    390    }
    391    return gFileMigrators;
    392  }
    393 
    394  forceExitSpinResolve() {
    395    gForceExitSpinResolve = true;
    396  }
    397 
    398  spinResolve(promise) {
    399    if (!(promise instanceof Promise)) {
    400      return promise;
    401    }
    402    let done = false;
    403    let result = null;
    404    let error = null;
    405    gForceExitSpinResolve = false;
    406    promise
    407      .catch(e => {
    408        error = e;
    409      })
    410      .then(r => {
    411        result = r;
    412        done = true;
    413      });
    414 
    415    Services.tm.spinEventLoopUntil(
    416      "MigrationUtils.sys.mjs:MU_spinResolve",
    417      () => done || gForceExitSpinResolve
    418    );
    419    if (!done) {
    420      throw new Error("Forcefully exited event loop.");
    421    } else if (error) {
    422      throw error;
    423    } else {
    424      return result;
    425    }
    426  }
    427 
    428  /**
    429   * Returns the migrator for the given source, if any data is available
    430   * for this source, or if permissions are required in order to read
    431   * data from this source. Returns null otherwise.
    432   *
    433   * @param {string} aKey
    434   *   Internal name of the migration source. See `availableMigratorKeys`
    435   *   for supported values by OS.
    436   * @returns {Promise<MigratorBase|null>}
    437   *   A profile migrator implementing nsIBrowserProfileMigrator, if it can
    438   *   import any data, null otherwise.
    439   */
    440  async getMigrator(aKey) {
    441    let migrator = this.#migrators.get(aKey);
    442    if (!migrator) {
    443      console.error(`Could not find a migrator class for key ${aKey}`);
    444      return null;
    445    }
    446 
    447    try {
    448      if (!migrator) {
    449        return null;
    450      }
    451 
    452      if (
    453        (await migrator.isSourceAvailable()) ||
    454        (!(await migrator.hasPermissions()) && migrator.canGetPermissions())
    455      ) {
    456        return migrator;
    457      }
    458 
    459      return null;
    460    } catch (ex) {
    461      console.error(ex);
    462      return null;
    463    }
    464  }
    465 
    466  getFileMigrator(aKey) {
    467    let migrator = this.#fileMigrators.get(aKey);
    468    if (!migrator) {
    469      console.error(`Could not find a file migrator class for key ${aKey}`);
    470      return null;
    471    }
    472    return migrator;
    473  }
    474 
    475  /**
    476   * Returns true if a migrator is registered with key aKey. No check is made
    477   * to determine if a profile exists that the migrator can migrate from.
    478   *
    479   * @param {string} aKey
    480   *   Internal name of the migration source. See `availableMigratorKeys`
    481   *   for supported values by OS.
    482   * @returns {boolean}
    483   */
    484  migratorExists(aKey) {
    485    return this.#migrators.has(aKey);
    486  }
    487 
    488  /**
    489   * Figure out what is the default browser, and if there is a migrator
    490   * for it, return that migrator's internal name.
    491   *
    492   * For the time being, the "internal name" of a migrator is its contract-id
    493   * trailer (e.g. ie for @mozilla.org/profile/migrator;1?app=browser&type=ie),
    494   * but it will soon be exposed properly.
    495   *
    496   * @returns {string}
    497   */
    498  getMigratorKeyForDefaultBrowser() {
    499    // Canary uses the same description as Chrome so we can't distinguish them.
    500    // Edge Beta on macOS uses "Microsoft Edge" with no "beta" indication.
    501    const APP_DESC_TO_KEY = {
    502      "Internet Explorer": "ie",
    503      "Microsoft Edge": "edge",
    504      Safari: "safari",
    505      Firefox: "firefox",
    506      Nightly: "firefox",
    507      Opera: "opera",
    508      Vivaldi: "vivaldi",
    509      "Opera GX": "opera-gx",
    510      "Brave Web Browser": "brave", // Windows, Linux
    511      Brave: "brave", // OS X
    512      "Google Chrome": "chrome", // Windows, Linux
    513      Chrome: "chrome", // OS X
    514      Chromium: "chromium", // Windows, OS X
    515      "Chromium Web Browser": "chromium", // Linux
    516      "360\u5b89\u5168\u6d4f\u89c8\u5668": "chromium-360se",
    517    };
    518 
    519    let key = "";
    520    try {
    521      let browserDesc = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
    522        .getService(Ci.nsIExternalProtocolService)
    523        .getApplicationDescription("http");
    524      key = APP_DESC_TO_KEY[browserDesc] || "";
    525      // Handle devedition, as well as "FirefoxNightly" on OS X.
    526      if (!key && browserDesc.startsWith("Firefox")) {
    527        key = "firefox";
    528      }
    529    } catch (ex) {
    530      console.error("Could not detect default browser: ", ex);
    531    }
    532 
    533    return key;
    534  }
    535 
    536  /**
    537   * True if we're in the process of a startup migration.
    538   *
    539   * @type {boolean}
    540   */
    541  get isStartupMigration() {
    542    return gProfileStartup != null;
    543  }
    544 
    545  /**
    546   * In the case of startup migration, this is set to the nsIProfileStartup
    547   * instance passed to ProfileMigrator's migrate.
    548   *
    549   * @see showMigrationWizard
    550   * @type {nsIProfileStartup|null}
    551   */
    552  get profileStartup() {
    553    return gProfileStartup;
    554  }
    555 
    556  /**
    557   * Show the migration wizard in about:preferences, or if there is not an existing
    558   * browser window open, in a new top-level dialog window.
    559   *
    560   * NB: If you add new consumers, please add a migration entry point constant to
    561   * MIGRATION_ENTRYPOINTS and supply that entrypoint with the entrypoint property
    562   * in the aOptions argument.
    563   *
    564   * @param {Window} [aOpener=null]
    565   *   optional; the window that asks to open the wizard.
    566   * @param {object} [aOptions=null]
    567   *   optional named arguments for the migration wizard.
    568   * @param {string} [aOptions.entrypoint=undefined]
    569   *   migration entry point constant. See MIGRATION_ENTRYPOINTS.
    570   * @param {string} [aOptions.migratorKey=undefined]
    571   *   The key for which migrator to use automatically. This is the key that is exposed
    572   *   as a static getter on the migrator class.
    573   * @param {MigratorBase} [aOptions.migrator=undefined]
    574   *   A migrator instance to use automatically.
    575   * @param {boolean} [aOptions.isStartupMigration=undefined]
    576   *   True if this is a startup migration.
    577   * @param {boolean} [aOptions.skipSourceSelection=undefined]
    578   *   True if the source selection page of the wizard should be skipped.
    579   * @param {string} [aOptions.profileId]
    580   *   An identifier for the profile to use when migrating.
    581   * @returns {Promise<undefined>}
    582   *   If an about:preferences tab can be opened, this will resolve when
    583   *   that tab has been switched to. Otherwise, this will resolve
    584   *   just after opening the top-level dialog window.
    585   */
    586  showMigrationWizard(aOpener, aOptions) {
    587    // When migration is kicked off from about:welcome, there are
    588    // a few different behaviors that we want to test, controlled
    589    // by a preference that is instrumented for Nimbus. The pref
    590    // has the following possible states:
    591    //
    592    // "autoclose":
    593    //   The user will be directed to the migration wizard in
    594    //   about:preferences, but once the wizard is dismissed,
    595    //   the tab will close.
    596    //
    597    // "standalone":
    598    //   The migration wizard will open in a new top-level content
    599    //   window.
    600    //
    601    // "default" / other
    602    //   The user will be directed to the migration wizard in
    603    //   about:preferences. The tab will not close once the
    604    //   user closes the wizard.
    605    let aboutWelcomeBehavior = Services.prefs.getCharPref(
    606      "browser.migrate.content-modal.about-welcome-behavior",
    607      "default"
    608    );
    609 
    610    let entrypoint = aOptions.entrypoint || this.MIGRATION_ENTRYPOINTS.UNKNOWN;
    611    Glean.browserMigration.entryPointCategorical[entrypoint].add(1);
    612 
    613    let openStandaloneWindow = blocking => {
    614      let features = "dialog,centerscreen,resizable=no";
    615 
    616      if (blocking) {
    617        features += ",modal";
    618      }
    619 
    620      Services.ww.openWindow(
    621        aOpener,
    622        "chrome://browser/content/migration/migration-dialog-window.html",
    623        "_blank",
    624        features,
    625        {
    626          options: aOptions,
    627        }
    628      );
    629      return Promise.resolve();
    630    };
    631 
    632    if (aOptions.isStartupMigration) {
    633      // Record that the uninstaller requested a profile refresh
    634      if (Services.env.get("MOZ_UNINSTALLER_PROFILE_REFRESH")) {
    635        Services.env.set("MOZ_UNINSTALLER_PROFILE_REFRESH", "");
    636        Glean.migration.uninstallerProfileRefresh.set(true);
    637      }
    638 
    639      openStandaloneWindow(true /* blocking */);
    640      return Promise.resolve();
    641    }
    642 
    643    if (aOpener?.openPreferences) {
    644      if (aOptions.entrypoint == this.MIGRATION_ENTRYPOINTS.NEWTAB) {
    645        if (aboutWelcomeBehavior == "autoclose") {
    646          return aOpener.openPreferences("general-migrate-autoclose");
    647        } else if (aboutWelcomeBehavior == "standalone") {
    648          openStandaloneWindow(false /* blocking */);
    649          return Promise.resolve();
    650        }
    651      }
    652      return aOpener.openPreferences("general-migrate");
    653    }
    654 
    655    // If somehow we failed to open about:preferences, fall back to opening
    656    // the top-level window.
    657    openStandaloneWindow(false /* blocking */);
    658    return Promise.resolve();
    659  }
    660 
    661  /**
    662   * Show the migration wizard for startup-migration.  This should only be
    663   * called by ProfileMigrator (see ProfileMigrator.js), which implements
    664   * nsIProfileMigrator. This runs asynchronously if we are running an
    665   * automigration.
    666   *
    667   * @param {nsIProfileStartup} aProfileStartup
    668   *   the nsIProfileStartup instance provided to ProfileMigrator.migrate.
    669   * @param {string|null} [aMigratorKey=null]
    670   *   If set, the migration wizard will import from the corresponding
    671   *   migrator, bypassing the source-selection page.  Otherwise, the
    672   *   source-selection page will be displayed, either with the default
    673   *   browser selected, if it could be detected and if there is a
    674   *   migrator for it, or with the first option selected as a fallback
    675   * @param {string|null} [aProfileToMigrate=null]
    676   *   If set, the migration wizard will import from the profile indicated.
    677   * @throws
    678   *   if aMigratorKey is invalid or if it points to a non-existent
    679   *   source.
    680   */
    681  startupMigration(aProfileStartup, aMigratorKey, aProfileToMigrate) {
    682    this.spinResolve(
    683      this.asyncStartupMigration(
    684        aProfileStartup,
    685        aMigratorKey,
    686        aProfileToMigrate
    687      )
    688    );
    689  }
    690 
    691  async asyncStartupMigration(
    692    aProfileStartup,
    693    aMigratorKey,
    694    aProfileToMigrate
    695  ) {
    696    if (!aProfileStartup) {
    697      throw new Error(
    698        "an profile-startup instance is required for startup-migration"
    699      );
    700    }
    701    gProfileStartup = aProfileStartup;
    702 
    703    let skipSourceSelection = false,
    704      migrator = null,
    705      migratorKey = "";
    706    if (aMigratorKey) {
    707      migrator = await this.getMigrator(aMigratorKey);
    708      if (!migrator) {
    709        // aMigratorKey must point to a valid source, so, if it doesn't
    710        // cleanup and throw.
    711        this.finishMigration();
    712        throw new Error(
    713          "startMigration was asked to open auto-migrate from " +
    714            "a non-existent source: " +
    715            aMigratorKey
    716        );
    717      }
    718      migratorKey = aMigratorKey;
    719      skipSourceSelection = true;
    720    } else {
    721      let defaultBrowserKey = this.getMigratorKeyForDefaultBrowser();
    722      if (defaultBrowserKey) {
    723        migrator = await this.getMigrator(defaultBrowserKey);
    724        if (migrator) {
    725          migratorKey = defaultBrowserKey;
    726        }
    727      }
    728    }
    729 
    730    if (!migrator) {
    731      let migrators = await Promise.all(
    732        this.availableMigratorKeys.map(key => this.getMigrator(key))
    733      );
    734      // If there's no migrator set so far, ensure that there is at least one
    735      // migrator available before opening the wizard.
    736      // Note that we don't need to check the default browser first, because
    737      // if that one existed we would have used it in the block above this one.
    738      if (!migrators.some(m => m)) {
    739        // None of the keys produced a usable migrator, so finish up here:
    740        this.finishMigration();
    741        return;
    742      }
    743    }
    744 
    745    let isRefresh =
    746      migrator &&
    747      skipSourceSelection &&
    748      migratorKey == AppConstants.MOZ_APP_NAME;
    749 
    750    let entrypoint = this.MIGRATION_ENTRYPOINTS.FIRSTRUN;
    751    if (isRefresh) {
    752      entrypoint = this.MIGRATION_ENTRYPOINTS.FXREFRESH;
    753    }
    754 
    755    this.showMigrationWizard(null, {
    756      entrypoint,
    757      migratorKey,
    758      migrator,
    759      isStartupMigration: !!aProfileStartup,
    760      skipSourceSelection,
    761      profileId: aProfileToMigrate,
    762    });
    763  }
    764 
    765  /**
    766   * This is only pseudo-private because some tests and helper functions
    767   * still expect to be able to directly access it.
    768   */
    769  _importQuantities = {
    770    bookmarks: 0,
    771    logins: 0,
    772    history: 0,
    773    cards: 0,
    774    extensions: 0,
    775  };
    776 
    777  getImportedCount(type) {
    778    if (!this._importQuantities.hasOwnProperty(type)) {
    779      throw new Error(
    780        `Unknown import data type "${type}" passed to getImportedCount`
    781      );
    782    }
    783    return this._importQuantities[type];
    784  }
    785 
    786  insertBookmarkWrapper(bookmark) {
    787    this._importQuantities.bookmarks++;
    788    let insertionPromise = lazy.PlacesUtils.bookmarks.insert(bookmark);
    789    if (!gKeepUndoData) {
    790      return insertionPromise;
    791    }
    792    // If we keep undo data, add a promise handler that stores the undo data once
    793    // the bookmark has been inserted in the DB, and then returns the bookmark.
    794    let { parentGuid } = bookmark;
    795    return insertionPromise.then(bm => {
    796      let { guid, lastModified, type } = bm;
    797      gUndoData.get("bookmarks").push({
    798        parentGuid,
    799        guid,
    800        lastModified,
    801        type,
    802      });
    803      return bm;
    804    });
    805  }
    806 
    807  insertManyBookmarksWrapper(bookmarks, parent) {
    808    let insertionPromise = lazy.PlacesUtils.bookmarks.insertTree({
    809      guid: parent,
    810      children: bookmarks,
    811    });
    812    return insertionPromise.then(
    813      insertedItems => {
    814        this._importQuantities.bookmarks += insertedItems.length;
    815        if (gKeepUndoData) {
    816          let bmData = gUndoData.get("bookmarks");
    817          for (let bm of insertedItems) {
    818            let { parentGuid, guid, lastModified, type } = bm;
    819            bmData.push({ parentGuid, guid, lastModified, type });
    820          }
    821        }
    822        if (parent == lazy.PlacesUtils.bookmarks.toolbarGuid) {
    823          lazy.PlacesUIUtils.maybeToggleBookmarkToolbarVisibility(
    824            true /* aForceVisible */
    825          ).catch(console.error);
    826        }
    827      },
    828      ex => console.error(ex)
    829    );
    830  }
    831 
    832  insertVisitsWrapper(pageInfos) {
    833    let now = new Date();
    834    // Ensure that none of the dates are in the future. If they are, rewrite
    835    // them to be now. This means we don't loose history entries, but they will
    836    // be valid for the history store.
    837    for (let pageInfo of pageInfos) {
    838      for (let visit of pageInfo.visits) {
    839        if (visit.date && visit.date > now) {
    840          visit.date = now;
    841        }
    842      }
    843    }
    844    this._importQuantities.history += pageInfos.length;
    845    if (gKeepUndoData) {
    846      this.#updateHistoryUndo(pageInfos);
    847    }
    848    return lazy.PlacesUtils.history.insertMany(pageInfos);
    849  }
    850 
    851  async insertLoginsWrapper(logins) {
    852    this._importQuantities.logins += logins.length;
    853    let inserted = await lazy.LoginHelper.maybeImportLogins(logins);
    854    // Note that this means that if we import a login that has a newer password
    855    // than we know about, we will update the login, and an undo of the import
    856    // will not revert this. This seems preferable over removing the login
    857    // outright or storing the old password in the undo file.
    858    if (gKeepUndoData) {
    859      for (let { guid, timePasswordChanged } of inserted) {
    860        gUndoData.get("logins").push({ guid, timePasswordChanged });
    861      }
    862    }
    863  }
    864 
    865  /**
    866   * Called by MigrationWizardParent during a migration to indicate that a
    867   * manual migration of logins occurred via import from a CSV / TSV file, and
    868   * should be counted towards the total number of imported logins.
    869   *
    870   * @param {number} totalLogins
    871   *   The number of logins imported manually from a CSV / TSV file.
    872   */
    873  notifyLoginsManuallyImported(totalLogins) {
    874    this._importQuantities.logins += totalLogins;
    875  }
    876 
    877  /**
    878   * Iterates through the favicons, sniffs for a mime type,
    879   * and uses the mime type to properly import the favicon.
    880   *
    881   * Note: You may not want to await on the returned promise, especially if by
    882   *       doing so there's risk of interrupting the migration of more critical
    883   *       data (e.g. bookmarks).
    884   *
    885   * @param {object[]} favicons
    886   *   An array of Objects with these properties:
    887   *     {Uint8Array} faviconData: The binary data of a favicon
    888   *     {nsIURI} uri: The URI of the associated page
    889   */
    890  async insertManyFavicons(favicons) {
    891    let sniffer = Cc["@mozilla.org/image/loader;1"].createInstance(
    892      Ci.nsIContentSniffer
    893    );
    894 
    895    for (let faviconDataItem of favicons) {
    896      try {
    897        // getMIMETypeFromContent throws error if could not get the mime type
    898        // from the data.
    899        let mimeType = sniffer.getMIMETypeFromContent(
    900          null,
    901          faviconDataItem.faviconData,
    902          faviconDataItem.faviconData.length
    903        );
    904 
    905        let dataURL = await new Promise((resolve, reject) => {
    906          let buffer = new Uint8ClampedArray(faviconDataItem.faviconData);
    907          let blob = new Blob([buffer], { type: mimeType });
    908          let reader = new FileReader();
    909          reader.addEventListener("load", () => resolve(reader.result));
    910          reader.addEventListener("error", reject);
    911          reader.readAsDataURL(blob);
    912        });
    913 
    914        let fakeFaviconURI = Services.io.newURI(
    915          "fake-favicon-uri:" + faviconDataItem.uri.spec
    916        );
    917        lazy.PlacesUtils.favicons
    918          .setFaviconForPage(
    919            faviconDataItem.uri,
    920            fakeFaviconURI,
    921            Services.io.newURI(dataURL)
    922          )
    923          .catch(console.warn);
    924      } catch (e) {
    925        // Even if error happens for favicon, continue the process.
    926        console.warn(e);
    927      }
    928    }
    929  }
    930 
    931  async insertCreditCardsWrapper(cards) {
    932    this._importQuantities.cards += cards.length;
    933    let { formAutofillStorage } = ChromeUtils.importESModule(
    934      "resource://autofill/FormAutofillStorage.sys.mjs"
    935    );
    936 
    937    await formAutofillStorage.initialize();
    938    for (let card of cards) {
    939      try {
    940        await formAutofillStorage.creditCards.add(card);
    941      } catch (e) {
    942        console.error("Failed to insert credit card due to error: ", e, card);
    943      }
    944    }
    945  }
    946 
    947  /**
    948   * Responsible for calling the AddonManager API that ultimately installs the
    949   * matched add-ons.
    950   *
    951   * @param {string} migratorKey a migrator key that we pass to
    952   *                             `AMBrowserExtensionsImport` as the "browser
    953   *                             identifier" used to match add-ons
    954   * @param {string[]} extensionIDs a list of extension IDs from another browser
    955   * @returns {(lazy.MigrationWizardConstants.PROGRESS_VALUE|string[])[]}
    956   *   An array whose first element is a `MigrationWizardConstants.PROGRESS_VALUE`
    957   *   and second element is an array of imported add-on ids.
    958   */
    959  async installExtensionsWrapper(migratorKey, extensionIDs) {
    960    const totalExtensions = extensionIDs.length;
    961 
    962    let importedAddonIDs = [];
    963    try {
    964      const result = await lazy.AMBrowserExtensionsImport.stageInstalls(
    965        migratorKey,
    966        extensionIDs
    967      );
    968      importedAddonIDs = result.importedAddonIDs;
    969    } catch (e) {
    970      console.error(`Failed to import extensions: ${e}`);
    971    }
    972 
    973    this._importQuantities.extensions += importedAddonIDs.length;
    974 
    975    if (!importedAddonIDs.length) {
    976      return [
    977        lazy.MigrationWizardConstants.PROGRESS_VALUE.WARNING,
    978        importedAddonIDs,
    979      ];
    980    }
    981    if (totalExtensions == importedAddonIDs.length) {
    982      return [
    983        lazy.MigrationWizardConstants.PROGRESS_VALUE.SUCCESS,
    984        importedAddonIDs,
    985      ];
    986    }
    987    return [
    988      lazy.MigrationWizardConstants.PROGRESS_VALUE.INFO,
    989      importedAddonIDs,
    990    ];
    991  }
    992 
    993  initializeUndoData() {
    994    gKeepUndoData = true;
    995    gUndoData = new Map([
    996      ["bookmarks", []],
    997      ["visits", []],
    998      ["logins", []],
    999    ]);
   1000  }
   1001 
   1002  async #postProcessUndoData(state) {
   1003    if (!state) {
   1004      return state;
   1005    }
   1006    let bookmarkFolders = state
   1007      .get("bookmarks")
   1008      .filter(b => b.type == lazy.PlacesUtils.bookmarks.TYPE_FOLDER);
   1009 
   1010    let bookmarkFolderData = [];
   1011    let bmPromises = bookmarkFolders.map(({ guid }) => {
   1012      // Ignore bookmarks where the promise doesn't resolve (ie that are missing)
   1013      // Also check that the bookmark fetch returns isn't null before adding it.
   1014      return lazy.PlacesUtils.bookmarks.fetch(guid).then(
   1015        bm => bm && bookmarkFolderData.push(bm),
   1016        () => {}
   1017      );
   1018    });
   1019 
   1020    await Promise.all(bmPromises);
   1021    let folderLMMap = new Map(
   1022      bookmarkFolderData.map(b => [b.guid, b.lastModified])
   1023    );
   1024    for (let bookmark of bookmarkFolders) {
   1025      let lastModified = folderLMMap.get(bookmark.guid);
   1026      // If the bookmark was deleted, the map will be returning null, so check:
   1027      if (lastModified) {
   1028        bookmark.lastModified = lastModified;
   1029      }
   1030    }
   1031    return state;
   1032  }
   1033 
   1034  stopAndRetrieveUndoData() {
   1035    let undoData = gUndoData;
   1036    gUndoData = null;
   1037    gKeepUndoData = false;
   1038    return this.#postProcessUndoData(undoData);
   1039  }
   1040 
   1041  #updateHistoryUndo(pageInfos) {
   1042    let visits = gUndoData.get("visits");
   1043    let visitMap = new Map(visits.map(v => [v.url, v]));
   1044    for (let pageInfo of pageInfos) {
   1045      let visitCount = pageInfo.visits.length;
   1046      let first, last;
   1047      if (visitCount > 1) {
   1048        let dates = pageInfo.visits.map(v => v.date);
   1049        first = Math.min.apply(Math, dates);
   1050        last = Math.max.apply(Math, dates);
   1051      } else {
   1052        first = last = pageInfo.visits[0].date;
   1053      }
   1054      let url = pageInfo.url;
   1055      if (url instanceof Ci.nsIURI) {
   1056        url = pageInfo.url.spec;
   1057      }
   1058 
   1059      if (!URL.canParse(url)) {
   1060        // This won't save and we won't need to 'undo' it, so ignore this URL.
   1061        continue;
   1062      }
   1063      if (!visitMap.has(url)) {
   1064        visitMap.set(url, { url, visitCount, first, last });
   1065      } else {
   1066        let currentData = visitMap.get(url);
   1067        currentData.visitCount += visitCount;
   1068        currentData.first = Math.min(currentData.first, first);
   1069        currentData.last = Math.max(currentData.last, last);
   1070      }
   1071    }
   1072    gUndoData.set("visits", Array.from(visitMap.values()));
   1073  }
   1074 
   1075  /**
   1076   * Cleans up references to migrators and nsIProfileInstance instances.
   1077   */
   1078  finishMigration() {
   1079    gMigrators = null;
   1080    gProfileStartup = null;
   1081    gL10n = null;
   1082  }
   1083 
   1084  get availableMigratorKeys() {
   1085    return [...this.#migrators.keys()];
   1086  }
   1087 
   1088  get availableFileMigrators() {
   1089    return [...this.#fileMigrators.values()];
   1090  }
   1091 
   1092  /**
   1093   * Enum for the entrypoint that is being used to start migration.
   1094   * Callers can use the MIGRATION_ENTRYPOINTS getter to use these.
   1095   *
   1096   * These values are what's written into the
   1097   * FX_MIGRATION_ENTRY_POINT_CATEGORICAL histogram after a migration.
   1098   *
   1099   * @see MIGRATION_ENTRYPOINTS
   1100   * @readonly
   1101   * @enum {string}
   1102   */
   1103  #MIGRATION_ENTRYPOINTS_ENUM = Object.freeze({
   1104    /** The entrypoint was not supplied */
   1105    UNKNOWN: "unknown",
   1106 
   1107    /** Migration is occurring at startup */
   1108    FIRSTRUN: "firstrun",
   1109 
   1110    /** Migration is occurring at after a profile refresh */
   1111    FXREFRESH: "fxrefresh",
   1112 
   1113    /** Migration is being started from the Library window */
   1114    PLACES: "places",
   1115 
   1116    /** Migration is being started from our password management UI */
   1117    PASSWORDS: "passwords",
   1118 
   1119    /** Migration is being started from the default about:home/about:newtab */
   1120    NEWTAB: "newtab",
   1121 
   1122    /** Migration is being started from the File menu */
   1123    FILE_MENU: "file_menu",
   1124 
   1125    /** Migration is being started from the Help menu */
   1126    HELP_MENU: "help_menu",
   1127 
   1128    /** Migration is being started from the Bookmarks Toolbar */
   1129    BOOKMARKS_TOOLBAR: "bookmarks_toolbar",
   1130 
   1131    /** Migration is being started from about:preferences */
   1132    PREFERENCES: "preferences",
   1133 
   1134    /** Migration is being started from about:firefoxview */
   1135    FIREFOX_VIEW: "firefox_view",
   1136  });
   1137 
   1138  /**
   1139   * Returns an enum that should be used to record the entrypoint for
   1140   * starting a migration.
   1141   *
   1142   * @returns {number}
   1143   */
   1144  get MIGRATION_ENTRYPOINTS() {
   1145    return this.#MIGRATION_ENTRYPOINTS_ENUM;
   1146  }
   1147 
   1148  /**
   1149   * Enum for the numeric value written to the FX_MIGRATION_SOURCE_BROWSER.
   1150   * histogram
   1151   *
   1152   * @see getSourceIdForTelemetry
   1153   * @readonly
   1154   * @enum {number}
   1155   */
   1156  #SOURCE_NAME_TO_ID_MAPPING_ENUM = Object.freeze({
   1157    nothing: 1,
   1158    firefox: 2,
   1159    edge: 3,
   1160    ie: 4,
   1161    chrome: 5,
   1162    "chrome-beta": 5,
   1163    "chrome-dev": 5,
   1164    chromium: 6,
   1165    canary: 7,
   1166    safari: 8,
   1167    "chromium-360se": 9,
   1168    "chromium-edge": 10,
   1169    "chromium-edge-beta": 10,
   1170    brave: 11,
   1171    opera: 12,
   1172    "opera-gx": 14,
   1173    vivaldi: 13,
   1174  });
   1175 
   1176  getSourceIdForTelemetry(sourceName) {
   1177    return this.#SOURCE_NAME_TO_ID_MAPPING_ENUM[sourceName] || 0;
   1178  }
   1179 
   1180  get HISTORY_MAX_AGE_IN_MILLISECONDS() {
   1181    return this.HISTORY_MAX_AGE_IN_DAYS * 24 * 60 * 60 * 1000;
   1182  }
   1183 
   1184  /**
   1185   * Determines whether or not the underlying platform supports creating
   1186   * native file pickers that can do folder selection, which is a
   1187   * pre-requisite for getting read-access permissions for data from other
   1188   * browsers that we can import from.
   1189   *
   1190   * @returns {Promise<boolean>}
   1191   */
   1192  canGetPermissionsOnPlatform() {
   1193    return lazy.gCanGetPermissionsOnPlatformPromise;
   1194  }
   1195 }
   1196 
   1197 const MigrationUtilsSingleton = new MigrationUtils();
   1198 
   1199 export { MigrationUtilsSingleton as MigrationUtils };