tor-browser

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

MigrationWizardParent.sys.mjs (29904B)


      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 { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs";
      7 import { E10SUtils } from "resource://gre/modules/E10SUtils.sys.mjs";
      8 
      9 const lazy = {};
     10 
     11 ChromeUtils.defineLazyGetter(lazy, "gFluentStrings", function () {
     12  return new Localization([
     13    "branding/brand.ftl",
     14    "browser/migrationWizard.ftl",
     15  ]);
     16 });
     17 
     18 ChromeUtils.defineESModuleGetters(lazy, {
     19  FirefoxProfileMigrator: "resource:///modules/FirefoxProfileMigrator.sys.mjs",
     20  InternalTestingProfileMigrator:
     21    "resource:///modules/InternalTestingProfileMigrator.sys.mjs",
     22  LoginCSVImport: "resource://gre/modules/LoginCSVImport.sys.mjs",
     23  MigrationWizardConstants:
     24    "chrome://browser/content/migration/migration-wizard-constants.mjs",
     25  PasswordFileMigrator: "resource:///modules/FileMigrators.sys.mjs",
     26 });
     27 
     28 if (AppConstants.platform == "macosx") {
     29  ChromeUtils.defineESModuleGetters(lazy, {
     30    SafariProfileMigrator: "resource:///modules/SafariProfileMigrator.sys.mjs",
     31  });
     32 }
     33 
     34 /**
     35 * Set to true once the first instance of MigrationWizardParent has received
     36 * a "GetAvailableMigrators" message.
     37 */
     38 let gHasOpenedBefore = false;
     39 
     40 /**
     41 * This class is responsible for communicating with MigrationUtils to do the
     42 * actual heavy-lifting of any kinds of migration work, based on messages from
     43 * the associated MigrationWizardChild.
     44 */
     45 export class MigrationWizardParent extends JSWindowActorParent {
     46  didDestroy() {
     47    Services.obs.notifyObservers(this, "MigrationWizard:Destroyed");
     48    MigrationUtils.finishMigration();
     49  }
     50 
     51  /**
     52   * General message handler function for messages received from the
     53   * associated MigrationWizardChild JSWindowActor.
     54   *
     55   * @param {ReceiveMessageArgument} message
     56   *   The message received from the MigrationWizardChild.
     57   * @returns {Promise}
     58   */
     59  async receiveMessage(message) {
     60    // Some belt-and-suspenders here, mainly because the migration-wizard
     61    // component can be embedded in less privileged content pages, so let's
     62    // make sure that any messages from content are coming from the privileged
     63    // about content process type.
     64    if (
     65      !this.browsingContext.currentWindowGlobal.isInProcess &&
     66      this.browsingContext.currentRemoteType !=
     67        E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE
     68    ) {
     69      throw new Error(
     70        "MigrationWizardParent: received message from the wrong content process type."
     71      );
     72    }
     73 
     74    switch (message.name) {
     75      case "GetAvailableMigrators": {
     76        if (!gHasOpenedBefore) {
     77          Glean.migration.timeToProduceMigratorList.start();
     78        }
     79 
     80        let availableMigrators = [];
     81        for (const key of MigrationUtils.availableMigratorKeys) {
     82          availableMigrators.push(this.#getMigratorAndProfiles(key));
     83        }
     84 
     85        // Wait for all getMigrator calls to resolve in parallel
     86        let results = await Promise.all(availableMigrators);
     87 
     88        for (const migrator of MigrationUtils.availableFileMigrators.values()) {
     89          results.push(await this.#serializeFileMigrator(migrator));
     90        }
     91 
     92        // Each migrator might give us a single MigratorProfileInstance,
     93        // or an Array of them, so we flatten them out and filter out
     94        // any that ended up going wrong and returning null from the
     95        // #getMigratorAndProfiles call.
     96        let filteredResults = results
     97          .flat()
     98          .filter(result => result)
     99          .sort((a, b) => {
    100            return b.lastModifiedDate - a.lastModifiedDate;
    101          });
    102 
    103        if (!gHasOpenedBefore) {
    104          gHasOpenedBefore = true;
    105          Glean.migration.timeToProduceMigratorList.stop();
    106        }
    107 
    108        return filteredResults;
    109      }
    110 
    111      case "Migrate": {
    112        let { migrationDetails, extraArgs } = message.data;
    113        if (
    114          migrationDetails.type ==
    115          lazy.MigrationWizardConstants.MIGRATOR_TYPES.BROWSER
    116        ) {
    117          return this.#doBrowserMigration(migrationDetails, extraArgs);
    118        } else if (
    119          migrationDetails.type ==
    120          lazy.MigrationWizardConstants.MIGRATOR_TYPES.FILE
    121        ) {
    122          let window = this.browsingContext.topChromeWindow;
    123          await this.#doFileMigration(window, migrationDetails.key);
    124          return extraArgs;
    125        }
    126        break;
    127      }
    128 
    129      case "CheckPermissions": {
    130        if (
    131          message.data.type ==
    132          lazy.MigrationWizardConstants.MIGRATOR_TYPES.BROWSER
    133        ) {
    134          let migrator = await MigrationUtils.getMigrator(message.data.key);
    135          return migrator.hasPermissions();
    136        }
    137        return true;
    138      }
    139 
    140      case "RequestSafariPermissions": {
    141        let safariMigrator = await MigrationUtils.getMigrator("safari");
    142        return safariMigrator.getPermissions(
    143          this.browsingContext.topChromeWindow
    144        );
    145      }
    146 
    147      case "SelectManualPasswordFile": {
    148        return this.#selectManualPasswordFile(
    149          this.browsingContext.topChromeWindow
    150        );
    151      }
    152 
    153      case "RecordEvent": {
    154        this.#recordEvent(message.data.type, message.data.args);
    155        break;
    156      }
    157 
    158      case "OpenAboutAddons": {
    159        let browser = this.browsingContext.topChromeWindow;
    160        this.#openAboutAddons(browser);
    161        break;
    162      }
    163 
    164      case "GetPermissions": {
    165        let migrator = await MigrationUtils.getMigrator(message.data.key);
    166        return migrator.getPermissions(this.browsingContext.topChromeWindow);
    167      }
    168 
    169      case "OpenURL": {
    170        let browser = this.browsingContext.topChromeWindow;
    171        this.#openURL(browser, message.data.url, message.data.where);
    172        break;
    173      }
    174    }
    175 
    176    return null;
    177  }
    178 
    179  /**
    180   * Used for recording telemetry in the migration wizard.
    181   *
    182   * @param {string} type
    183   *   The type of event being recorded.
    184   * @param {object} args
    185   *   The data to pass to telemetry when the event is recorded.
    186   */
    187  #recordEvent(type, args) {
    188    Glean.browserMigration[type + "Wizard"].record(args);
    189  }
    190 
    191  /**
    192   * Gets the FileMigrator associated with the passed in key, and then opens
    193   * a native file picker configured for that migrator. Once the user selects
    194   * a file from the native file picker, this is then passed to the
    195   * FileMigrator.migrate method.
    196   *
    197   * As the migration occurs, this will send UpdateProgress messages to the
    198   * MigrationWizardChild to show the beginning and then the ending state of
    199   * the migration.
    200   *
    201   * @param {DOMWindow} window
    202   *   The window that the native file picker should be associated with. This
    203   *   cannot be null. See nsIFilePicker.init for more details.
    204   * @param {string} key
    205   *   The unique identification key for a file migrator.
    206   * @returns {Promise<undefined>}
    207   *   Resolves once the file migrator's migrate method has resolved.
    208   */
    209  async #doFileMigration(window, key) {
    210    let fileMigrator = MigrationUtils.getFileMigrator(key);
    211    let filePickerConfig = await fileMigrator.getFilePickerConfig();
    212 
    213    let { result, path } = await new Promise(resolve => {
    214      let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
    215      fp.init(
    216        window.browsingContext,
    217        filePickerConfig.title,
    218        Ci.nsIFilePicker.modeOpen
    219      );
    220 
    221      for (let filter of filePickerConfig.filters) {
    222        fp.appendFilter(filter.title, filter.extensionPattern);
    223      }
    224      fp.appendFilters(Ci.nsIFilePicker.filterAll);
    225      fp.open(async fileOpenResult => {
    226        resolve({ result: fileOpenResult, path: fp.file.path });
    227      });
    228    });
    229 
    230    if (result == Ci.nsIFilePicker.returnCancel) {
    231      // If the user cancels out of the file picker, the migration wizard should
    232      // still be in the state that lets the user re-open the file picker if
    233      // they closed it by accident, so we don't have to do anything else here.
    234      return;
    235    }
    236 
    237    let progress = {};
    238    for (let resourceType of fileMigrator.displayedResourceTypes) {
    239      progress[resourceType] = {
    240        value: lazy.MigrationWizardConstants.PROGRESS_VALUE.LOADING,
    241        message: "",
    242      };
    243    }
    244 
    245    let [progressHeaderString, successHeaderString] =
    246      await lazy.gFluentStrings.formatValues([
    247        fileMigrator.progressHeaderL10nID,
    248        fileMigrator.successHeaderL10nID,
    249      ]);
    250 
    251    this.sendAsyncMessage("UpdateFileImportProgress", {
    252      title: progressHeaderString,
    253      progress,
    254    });
    255 
    256    let migrationResult;
    257    try {
    258      migrationResult = await fileMigrator.migrate(path);
    259    } catch (e) {
    260      this.sendAsyncMessage("FileImportProgressError", {
    261        migratorKey: key,
    262        fileImportErrorMessage: e.message,
    263      });
    264      return;
    265    }
    266 
    267    let successProgress = {};
    268    for (let resourceType in migrationResult) {
    269      successProgress[resourceType] = {
    270        value: lazy.MigrationWizardConstants.PROGRESS_VALUE.SUCCESS,
    271        message: migrationResult[resourceType],
    272      };
    273    }
    274    this.sendAsyncMessage("UpdateFileImportProgress", {
    275      title: successHeaderString,
    276      progress: successProgress,
    277    });
    278  }
    279 
    280  /**
    281   * Handles a request to open a native file picker to get the path to a
    282   * CSV file that contains passwords exported from another browser. The
    283   * returned path is in the form of a string, or `null` if the user cancelled
    284   * the native picker. We use this for browsers or platforms that do not
    285   * allow us to import passwords automatically.
    286   *
    287   * @param {DOMWindow} window
    288   *   The window that the native file picker should be associated with. This
    289   *   cannot be null. See nsIFilePicker.init for more details.
    290   * @returns {Promise<string|null>}
    291   */
    292  async #selectManualPasswordFile(window) {
    293    let fileMigrator = MigrationUtils.getFileMigrator(
    294      lazy.PasswordFileMigrator.key
    295    );
    296    let filePickerConfig = await fileMigrator.getFilePickerConfig();
    297 
    298    let { result, path } = await new Promise(resolve => {
    299      let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
    300      fp.init(
    301        window.browsingContext,
    302        filePickerConfig.title,
    303        Ci.nsIFilePicker.modeOpen
    304      );
    305 
    306      for (let filter of filePickerConfig.filters) {
    307        fp.appendFilter(filter.title, filter.extensionPattern);
    308      }
    309      fp.appendFilters(Ci.nsIFilePicker.filterAll);
    310      fp.open(async fileOpenResult => {
    311        resolve({ result: fileOpenResult, path: fp.file.path });
    312      });
    313    });
    314 
    315    if (result == Ci.nsIFilePicker.returnCancel) {
    316      // If the user cancels out of the file picker, the migration wizard should
    317      // still be in the state that lets the user re-open the file picker if
    318      // they closed it by accident, so we don't have to do anything else here.
    319      return null;
    320    }
    321 
    322    return path;
    323  }
    324 
    325  /**
    326   * Calls into MigrationUtils to perform a migration given the parameters
    327   * sent via the wizard.
    328   *
    329   * @param {MigrationDetails} migrationDetails
    330   *   See migration-wizard.mjs for a definition of MigrationDetails.
    331   * @param {object} extraArgs
    332   *   Extra argument object that will be passed to the Event Telemetry for
    333   *   finishing the migration. This was initialized in the child actor, and
    334   *   will be sent back down to it to write to Telemetry once migration
    335   *   completes.
    336   *
    337   * @returns {Promise<object>}
    338   *   Resolves once the Migration:Ended observer notification has fired,
    339   *   passing the extraArgs for Telemetry back with any relevant properties
    340   *   updated.
    341   */
    342  async #doBrowserMigration(migrationDetails, extraArgs) {
    343    Glean.browserMigration.sourceBrowser.accumulateSingleSample(
    344      MigrationUtils.getSourceIdForTelemetry(migrationDetails.key)
    345    );
    346 
    347    let migrator = await MigrationUtils.getMigrator(migrationDetails.key);
    348    let availableResourceTypes = await migrator.getMigrateData(
    349      migrationDetails.profile
    350    );
    351    let resourceTypesToMigrate = 0;
    352    let progress = {};
    353    let gleanMigrationUsage = Glean.browserMigration.usage;
    354 
    355    for (let resourceTypeName of migrationDetails.resourceTypes) {
    356      let resourceType = MigrationUtils.resourceTypes[resourceTypeName];
    357      if (availableResourceTypes & resourceType) {
    358        resourceTypesToMigrate |= resourceType;
    359        progress[resourceTypeName] = {
    360          value: lazy.MigrationWizardConstants.PROGRESS_VALUE.LOADING,
    361          message: "",
    362        };
    363 
    364        if (!migrationDetails.autoMigration) {
    365          gleanMigrationUsage[migrationDetails.key].accumulateSingleSample(
    366            Math.log2(resourceType)
    367          );
    368        }
    369      }
    370    }
    371 
    372    if (migrationDetails.manualPasswordFilePath) {
    373      // The caller supplied a password export file for another browser. We're
    374      // going to pretend that there was a PASSWORDS resource to represent the
    375      // state of importing from that file.
    376      progress[
    377        lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS
    378      ] = {
    379        value: lazy.MigrationWizardConstants.PROGRESS_VALUE.LOADING,
    380        message: "",
    381      };
    382 
    383      this.sendAsyncMessage("UpdateProgress", {
    384        key: migrationDetails.key,
    385        progress,
    386      });
    387 
    388      try {
    389        let summary = await lazy.LoginCSVImport.importFromCSV(
    390          migrationDetails.manualPasswordFilePath
    391        );
    392        let quantity = summary.filter(entry => entry.result == "added").length;
    393 
    394        MigrationUtils.notifyLoginsManuallyImported(quantity);
    395 
    396        progress[
    397          lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS
    398        ] = {
    399          value: lazy.MigrationWizardConstants.PROGRESS_VALUE.SUCCESS,
    400          message: await lazy.gFluentStrings.formatValue(
    401            "migration-wizard-progress-success-passwords",
    402            {
    403              quantity,
    404            }
    405          ),
    406        };
    407      } catch (e) {
    408        progress[
    409          lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS
    410        ] = {
    411          value: lazy.MigrationWizardConstants.PROGRESS_VALUE.WARNING,
    412          message: await lazy.gFluentStrings.formatValue(
    413            "migration-passwords-from-file-no-valid-data"
    414          ),
    415        };
    416      }
    417    }
    418 
    419    this.sendAsyncMessage("UpdateProgress", {
    420      key: migrationDetails.key,
    421      progress,
    422    });
    423 
    424    // It's possible that only a Safari password file path was sent up, and
    425    // there's nothing left to migrate, in which case we're done here.
    426    if (
    427      migrationDetails.manualPasswordFilePath &&
    428      !migrationDetails.resourceTypes.length
    429    ) {
    430      return extraArgs;
    431    }
    432 
    433    try {
    434      await migrator.migrate(
    435        resourceTypesToMigrate,
    436        false,
    437        migrationDetails.profile,
    438        async (resourceTypeNum, success, details) => {
    439          // Unfortunately, MigratorBase hands us the the numeric value of the
    440          // MigrationUtils.resourceType for this callback. For now, we'll just
    441          // do a look-up to map it to the right constant.
    442          let foundResourceTypeName;
    443          for (let resourceTypeName in MigrationUtils.resourceTypes) {
    444            if (
    445              MigrationUtils.resourceTypes[resourceTypeName] == resourceTypeNum
    446            ) {
    447              foundResourceTypeName = resourceTypeName;
    448              break;
    449            }
    450          }
    451 
    452          if (!foundResourceTypeName) {
    453            console.error(
    454              "Could not find a resource type for value: ",
    455              resourceTypeNum
    456            );
    457          } else {
    458            if (!success) {
    459              Glean.browserMigration.errors[
    460                migrationDetails.key
    461              ].accumulateSingleSample(Math.log2(resourceTypeNum));
    462            }
    463            if (
    464              foundResourceTypeName ==
    465              lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS
    466            ) {
    467              if (!success) {
    468                // did not match any extensions
    469                extraArgs.extensions =
    470                  lazy.MigrationWizardConstants.EXTENSIONS_IMPORT_RESULT.NONE_MATCHED;
    471                progress[foundResourceTypeName] = {
    472                  value: lazy.MigrationWizardConstants.PROGRESS_VALUE.WARNING,
    473                  message: await lazy.gFluentStrings.formatValue(
    474                    "migration-wizard-progress-no-matched-extensions"
    475                  ),
    476                  linkURL: Services.urlFormatter.formatURLPref(
    477                    "extensions.getAddons.link.url"
    478                  ),
    479                  linkText: await lazy.gFluentStrings.formatValue(
    480                    "migration-wizard-progress-extensions-addons-link"
    481                  ),
    482                };
    483              } else if (
    484                details?.progressValue ==
    485                lazy.MigrationWizardConstants.PROGRESS_VALUE.SUCCESS
    486              ) {
    487                // did match all extensions
    488                extraArgs.extensions =
    489                  lazy.MigrationWizardConstants.EXTENSIONS_IMPORT_RESULT.ALL_MATCHED;
    490                progress[foundResourceTypeName] = {
    491                  value: lazy.MigrationWizardConstants.PROGRESS_VALUE.SUCCESS,
    492                  message: await lazy.gFluentStrings.formatValue(
    493                    "migration-wizard-progress-success-extensions",
    494                    {
    495                      quantity: details.totalExtensions.length,
    496                    }
    497                  ),
    498                };
    499              } else if (
    500                details?.progressValue ==
    501                lazy.MigrationWizardConstants.PROGRESS_VALUE.INFO
    502              ) {
    503                // did match some extensions
    504                extraArgs.extensions =
    505                  lazy.MigrationWizardConstants.EXTENSIONS_IMPORT_RESULT.PARTIAL_MATCH;
    506                progress[foundResourceTypeName] = {
    507                  value: lazy.MigrationWizardConstants.PROGRESS_VALUE.INFO,
    508                  message: await lazy.gFluentStrings.formatValue(
    509                    "migration-wizard-progress-partial-success-extensions",
    510                    {
    511                      matched: details.importedExtensions.length,
    512                      quantity: details.totalExtensions.length,
    513                    }
    514                  ),
    515                  linkURL:
    516                    Services.urlFormatter.formatURLPref("app.support.baseURL") +
    517                    "import-data-another-browser",
    518                  linkText: await lazy.gFluentStrings.formatValue(
    519                    "migration-wizard-progress-extensions-support-link"
    520                  ),
    521                };
    522              }
    523            } else {
    524              progress[foundResourceTypeName] = {
    525                value: success
    526                  ? lazy.MigrationWizardConstants.PROGRESS_VALUE.SUCCESS
    527                  : lazy.MigrationWizardConstants.PROGRESS_VALUE.WARNING,
    528                message: await this.#getStringForImportQuantity(
    529                  migrationDetails.key,
    530                  foundResourceTypeName
    531                ),
    532              };
    533            }
    534            this.sendAsyncMessage("UpdateProgress", {
    535              key: migrationDetails.key,
    536              progress,
    537            });
    538          }
    539        }
    540      );
    541    } catch (e) {
    542      console.error(e);
    543    }
    544 
    545    return extraArgs;
    546  }
    547 
    548  /**
    549   * @typedef {object} MigratorProfileInstance
    550   *   An object that describes a single user profile (or the default
    551   *   user profile) for a particular migrator.
    552   * @property {string} key
    553   *   The unique identification key for a migrator.
    554   * @property {string} displayName
    555   *   The display name for the migrator that will be shown to the user
    556   *   in the wizard.
    557   * @property {string[]} resourceTypes
    558   *   An array of strings, where each string represents a resource type
    559   *   that can be imported for this migrator and profile. The strings
    560   *   should be one of the key values of
    561   *   MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.
    562   *
    563   *   Example: ["HISTORY", "FORMDATA", "PASSWORDS", "BOOKMARKS"]
    564   * @property {object|null} profile
    565   *   A description of the user profile that the migrator can import.
    566   * @property {string} profile.id
    567   *   A unique ID for the user profile.
    568   * @property {string} profile.name
    569   *   The display name for the user profile.
    570   */
    571 
    572  /**
    573   * Asynchronously fetches a migrator for a particular key, and then
    574   * also gets any user profiles that exist on for that migrator. Resolves
    575   * to null if something goes wrong getting information about the migrator
    576   * or any of the user profiles.
    577   *
    578   * @param {string} key
    579   *   The unique identification key for a migrator.
    580   * @returns {Promise<MigratorProfileInstance[]|null>}
    581   */
    582  async #getMigratorAndProfiles(key) {
    583    try {
    584      let migrator = await MigrationUtils.getMigrator(key);
    585      if (!migrator?.enabled) {
    586        return null;
    587      }
    588 
    589      if (!(await migrator.hasPermissions())) {
    590        // If we're unable to get permissions for this migrator, then we
    591        // just don't bother showing it.
    592        let permissionsPath = await migrator.canGetPermissions();
    593        if (!permissionsPath) {
    594          return null;
    595        }
    596        return this.#serializeMigratorAndProfile(
    597          migrator,
    598          null,
    599          false /* hasPermissions */,
    600          permissionsPath
    601        );
    602      }
    603 
    604      let sourceProfiles = await migrator.getSourceProfiles();
    605      if (Array.isArray(sourceProfiles)) {
    606        if (!sourceProfiles.length) {
    607          return null;
    608        }
    609 
    610        Glean.migration.discoveredMigrators[key].add(sourceProfiles.length);
    611 
    612        let result = [];
    613        for (let profile of sourceProfiles) {
    614          result.push(
    615            await this.#serializeMigratorAndProfile(migrator, profile)
    616          );
    617        }
    618        return result;
    619      }
    620 
    621      Glean.migration.discoveredMigrators[key].add(1);
    622      return this.#serializeMigratorAndProfile(migrator, sourceProfiles);
    623    } catch (e) {
    624      console.error(`Could not get migrator with key ${key}`, e);
    625    }
    626    return null;
    627  }
    628 
    629  /**
    630   * Asynchronously fetches information about what resource types can be
    631   * migrated for a particular migrator and user profile, and then packages
    632   * the migrator, user profile data, and resource type data into an object
    633   * that can be sent down to the MigrationWizardChild.
    634   *
    635   * @param {MigratorBase} migrator
    636   *   A migrator subclass of MigratorBase.
    637   * @param {object|null} profileObj
    638   *   The user profile object representing the profile to get information
    639   *   about. This object is usually gotten by calling getSourceProfiles on
    640   *   the migrator.
    641   * @param {boolean} [hasPermissions=true]
    642   *   Whether or not the migrator has permission to read the data for the
    643   *   other browser. It is expected that the caller will have already
    644   *   computed this by calling hasPermissions() on the migrator, and
    645   *   passing the result into this method. This is true by default.
    646   * @param {string} [permissionsPath=undefined]
    647   *   The path that the selected migrator needs read access to in order to
    648   *   do a migration, in the event that hasPermissions is false. This is
    649   *   undefined if hasPermissions is true.
    650   * @returns {Promise<MigratorProfileInstance>}
    651   */
    652  async #serializeMigratorAndProfile(
    653    migrator,
    654    profileObj,
    655    hasPermissions = true,
    656    permissionsPath
    657  ) {
    658    let [profileMigrationData, lastModifiedDate] = await Promise.all([
    659      migrator.getMigrateData(profileObj),
    660      migrator.getLastUsedDate(),
    661    ]);
    662 
    663    let availableResourceTypes = [];
    664 
    665    // Even if we don't have permissions, we'll show the resources available
    666    // for Safari. For Safari, the workflow is to request permissions only
    667    // after the resources have been selected.
    668    if (
    669      hasPermissions ||
    670      migrator.constructor.key == lazy.SafariProfileMigrator?.key
    671    ) {
    672      for (let resourceType in MigrationUtils.resourceTypes) {
    673        // Normally, we check each possible resourceType to see if we have one or
    674        // more corresponding resourceTypes in profileMigrationData.
    675        //
    676        // The exception is for passwords for Safari, and for Chrome on Windows,
    677        // where we cannot import passwords automatically, but we allow the user
    678        // to express that they'd like to import passwords from it anyways. We
    679        // use this to determine whether or not to show guidance on how to
    680        // manually import a passwords CSV file.
    681        if (
    682          profileMigrationData & MigrationUtils.resourceTypes[resourceType] ||
    683          (MigrationUtils.resourceTypes[resourceType] ==
    684            MigrationUtils.resourceTypes.PASSWORDS &&
    685            migrator.showsManualPasswordImport)
    686        ) {
    687          availableResourceTypes.push(resourceType);
    688        }
    689      }
    690    }
    691 
    692    let displayName;
    693 
    694    if (migrator.constructor.key == lazy.InternalTestingProfileMigrator.key) {
    695      // In the case of the InternalTestingProfileMigrator, which is never seen
    696      // by users outside of testing, we don't make our localization community
    697      // localize it's display name, and just display the ID instead.
    698      displayName = migrator.constructor.displayNameL10nID;
    699    } else {
    700      displayName = await lazy.gFluentStrings.formatValue(
    701        migrator.constructor.displayNameL10nID
    702      );
    703    }
    704 
    705    return {
    706      type: lazy.MigrationWizardConstants.MIGRATOR_TYPES.BROWSER,
    707      key: migrator.constructor.key,
    708      displayName,
    709      brandImage: migrator.constructor.brandImage,
    710      resourceTypes: availableResourceTypes,
    711      profile: profileObj,
    712      lastModifiedDate,
    713      hasPermissions,
    714      permissionsPath,
    715    };
    716  }
    717 
    718  /**
    719   * Returns the "success" string for a particular resource type after
    720   * migration has completed.
    721   *
    722   * @param {string} migratorKey
    723   *   The key for the migrator being used.
    724   * @param {string} resourceTypeStr
    725   *   A string mapping to one of the key values of
    726   *   MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.
    727   * @returns {Promise<string>}
    728   *   The success string for the resource type after migration has completed.
    729   */
    730  #getStringForImportQuantity(migratorKey, resourceTypeStr) {
    731    if (migratorKey == lazy.FirefoxProfileMigrator.key) {
    732      return "";
    733    }
    734 
    735    switch (resourceTypeStr) {
    736      case lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS: {
    737        let quantity = MigrationUtils.getImportedCount("bookmarks");
    738        let stringID = "migration-wizard-progress-success-bookmarks";
    739 
    740        if (
    741          lazy.MigrationWizardConstants.USES_FAVORITES.includes(migratorKey)
    742        ) {
    743          stringID = "migration-wizard-progress-success-favorites";
    744        }
    745 
    746        return lazy.gFluentStrings.formatValue(stringID, {
    747          quantity,
    748        });
    749      }
    750      case lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY: {
    751        return lazy.gFluentStrings.formatValue(
    752          "migration-wizard-progress-success-history",
    753          {
    754            maxAgeInDays: MigrationUtils.HISTORY_MAX_AGE_IN_DAYS,
    755          }
    756        );
    757      }
    758      case lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS: {
    759        let quantity = MigrationUtils.getImportedCount("logins");
    760        return lazy.gFluentStrings.formatValue(
    761          "migration-wizard-progress-success-passwords",
    762          {
    763            quantity,
    764          }
    765        );
    766      }
    767      case lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA: {
    768        return lazy.gFluentStrings.formatValue(
    769          "migration-wizard-progress-success-formdata"
    770        );
    771      }
    772      case lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES
    773        .PAYMENT_METHODS: {
    774        let quantity = MigrationUtils.getImportedCount("cards");
    775        return lazy.gFluentStrings.formatValue(
    776          "migration-wizard-progress-success-payment-methods",
    777          {
    778            quantity,
    779          }
    780        );
    781      }
    782      default: {
    783        return "";
    784      }
    785    }
    786  }
    787 
    788  /**
    789   * Returns a Promise that resolves to a serializable representation of a
    790   * FileMigrator for sending down to the MigrationWizard.
    791   *
    792   * @param {FileMigrator} fileMigrator
    793   *   The FileMigrator to serialize.
    794   * @returns {Promise<object|null>}
    795   *   The serializable representation of the FileMigrator, or null if the
    796   *   migrator is disabled.
    797   */
    798  async #serializeFileMigrator(fileMigrator) {
    799    if (!fileMigrator.enabled) {
    800      return null;
    801    }
    802 
    803    return {
    804      type: lazy.MigrationWizardConstants.MIGRATOR_TYPES.FILE,
    805      key: fileMigrator.constructor.key,
    806      displayName: await lazy.gFluentStrings.formatValue(
    807        fileMigrator.constructor.displayNameL10nID
    808      ),
    809      brandImage: fileMigrator.constructor.brandImage,
    810      resourceTypes: [],
    811    };
    812  }
    813 
    814  /**
    815   * Opens the about:addons page in a new background tab in the same window
    816   * as the passed browser.
    817   *
    818   * @param {Element} browser
    819   *   The browser element requesting that about:addons opens.
    820   */
    821  #openAboutAddons(browser) {
    822    let window = browser.ownerGlobal;
    823    window.openTrustedLinkIn("about:addons", "tab", { inBackground: true });
    824  }
    825 
    826  /**
    827   * Opens a url in a new background tab in the same window
    828   * as the passed browser.
    829   *
    830   * @param {Element} browser
    831   *   The browser element requesting that the URL opens in.
    832   * @param {string} url
    833   *   The URL that will be opened.
    834   * @param {string} where
    835   *   Where the URL will be opened. Defaults to current tab.
    836   */
    837  #openURL(browser, url, where) {
    838    let window = browser.ownerGlobal;
    839    window.openLinkIn(
    840      Services.urlFormatter.formatURL(url),
    841      where || "current",
    842      {
    843        private: false,
    844        triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
    845          {}
    846        ),
    847      }
    848    );
    849  }
    850 }