diff options
Diffstat (limited to 'browser/components/migration/MigrationWizardParent.sys.mjs')
-rw-r--r-- | browser/components/migration/MigrationWizardParent.sys.mjs | 834 |
1 files changed, 834 insertions, 0 deletions
diff --git a/browser/components/migration/MigrationWizardParent.sys.mjs b/browser/components/migration/MigrationWizardParent.sys.mjs new file mode 100644 index 0000000000..deb0e89007 --- /dev/null +++ b/browser/components/migration/MigrationWizardParent.sys.mjs @@ -0,0 +1,834 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs"; +import { E10SUtils } from "resource://gre/modules/E10SUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineLazyGetter(lazy, "gFluentStrings", function () { + return new Localization([ + "branding/brand.ftl", + "browser/migrationWizard.ftl", + ]); +}); + +ChromeUtils.defineESModuleGetters(lazy, { + FirefoxProfileMigrator: "resource:///modules/FirefoxProfileMigrator.sys.mjs", + InternalTestingProfileMigrator: + "resource:///modules/InternalTestingProfileMigrator.sys.mjs", + LoginCSVImport: "resource://gre/modules/LoginCSVImport.sys.mjs", + MigrationWizardConstants: + "chrome://browser/content/migration/migration-wizard-constants.mjs", + PasswordFileMigrator: "resource:///modules/FileMigrators.sys.mjs", +}); + +if (AppConstants.platform == "macosx") { + ChromeUtils.defineESModuleGetters(lazy, { + SafariProfileMigrator: "resource:///modules/SafariProfileMigrator.sys.mjs", + }); +} + +/** + * Set to true once the first instance of MigrationWizardParent has received + * a "GetAvailableMigrators" message. + */ +let gHasOpenedBefore = false; + +/** + * This class is responsible for communicating with MigrationUtils to do the + * actual heavy-lifting of any kinds of migration work, based on messages from + * the associated MigrationWizardChild. + */ +export class MigrationWizardParent extends JSWindowActorParent { + constructor() { + super(); + Services.telemetry.setEventRecordingEnabled("browser.migration", true); + } + + didDestroy() { + Services.obs.notifyObservers(this, "MigrationWizard:Destroyed"); + MigrationUtils.finishMigration(); + } + + /** + * General message handler function for messages received from the + * associated MigrationWizardChild JSWindowActor. + * + * @param {ReceiveMessageArgument} message + * The message received from the MigrationWizardChild. + * @returns {Promise} + */ + async receiveMessage(message) { + // Some belt-and-suspenders here, mainly because the migration-wizard + // component can be embedded in less privileged content pages, so let's + // make sure that any messages from content are coming from the privileged + // about content process type. + if ( + !this.browsingContext.currentWindowGlobal.isInProcess && + this.browsingContext.currentRemoteType != + E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE + ) { + throw new Error( + "MigrationWizardParent: received message from the wrong content process type." + ); + } + + switch (message.name) { + case "GetAvailableMigrators": { + let start = Cu.now(); + + let availableMigrators = []; + for (const key of MigrationUtils.availableMigratorKeys) { + availableMigrators.push(this.#getMigratorAndProfiles(key)); + } + + // Wait for all getMigrator calls to resolve in parallel + let results = await Promise.all(availableMigrators); + + for (const migrator of MigrationUtils.availableFileMigrators.values()) { + results.push(await this.#serializeFileMigrator(migrator)); + } + + // Each migrator might give us a single MigratorProfileInstance, + // or an Array of them, so we flatten them out and filter out + // any that ended up going wrong and returning null from the + // #getMigratorAndProfiles call. + let filteredResults = results + .flat() + .filter(result => result) + .sort((a, b) => { + return b.lastModifiedDate - a.lastModifiedDate; + }); + + let elapsed = Cu.now() - start; + if (!gHasOpenedBefore) { + gHasOpenedBefore = true; + Services.telemetry.scalarSet( + "migration.time_to_produce_migrator_list", + elapsed + ); + } + + return filteredResults; + } + + case "Migrate": { + let { migrationDetails, extraArgs } = message.data; + if ( + migrationDetails.type == + lazy.MigrationWizardConstants.MIGRATOR_TYPES.BROWSER + ) { + return this.#doBrowserMigration(migrationDetails, extraArgs); + } else if ( + migrationDetails.type == + lazy.MigrationWizardConstants.MIGRATOR_TYPES.FILE + ) { + let window = this.browsingContext.topChromeWindow; + await this.#doFileMigration(window, migrationDetails.key); + return extraArgs; + } + break; + } + + case "CheckPermissions": { + if ( + message.data.type == + lazy.MigrationWizardConstants.MIGRATOR_TYPES.BROWSER + ) { + let migrator = await MigrationUtils.getMigrator(message.data.key); + return migrator.hasPermissions(); + } + return true; + } + + case "RequestSafariPermissions": { + let safariMigrator = await MigrationUtils.getMigrator("safari"); + return safariMigrator.getPermissions( + this.browsingContext.topChromeWindow + ); + } + + case "SelectSafariPasswordFile": { + return this.#selectSafariPasswordFile( + this.browsingContext.topChromeWindow + ); + } + + case "RecordEvent": { + this.#recordEvent(message.data.type, message.data.args); + break; + } + + case "OpenAboutAddons": { + let browser = this.browsingContext.top.embedderElement; + this.#openAboutAddons(browser); + break; + } + + case "GetPermissions": { + let migrator = await MigrationUtils.getMigrator(message.data.key); + return migrator.getPermissions(this.browsingContext.topChromeWindow); + } + } + + return null; + } + + /** + * Used for recording telemetry in the migration wizard. + * + * @param {string} type + * The type of event being recorded. + * @param {object} args + * The data to pass to telemetry when the event is recorded. + */ + #recordEvent(type, args = null) { + Services.telemetry.recordEvent( + "browser.migration", + type, + "wizard", + null, + args + ); + } + + /** + * Gets the FileMigrator associated with the passed in key, and then opens + * a native file picker configured for that migrator. Once the user selects + * a file from the native file picker, this is then passed to the + * FileMigrator.migrate method. + * + * As the migration occurs, this will send UpdateProgress messages to the + * MigrationWizardChild to show the beginning and then the ending state of + * the migration. + * + * @param {DOMWindow} window + * The window that the native file picker should be associated with. This + * cannot be null. See nsIFilePicker.init for more details. + * @param {string} key + * The unique identification key for a file migrator. + * @returns {Promise<undefined>} + * Resolves once the file migrator's migrate method has resolved. + */ + async #doFileMigration(window, key) { + let fileMigrator = MigrationUtils.getFileMigrator(key); + let filePickerConfig = await fileMigrator.getFilePickerConfig(); + + let { result, path } = await new Promise(resolve => { + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + fp.init(window, filePickerConfig.title, Ci.nsIFilePicker.modeOpen); + + for (let filter of filePickerConfig.filters) { + fp.appendFilter(filter.title, filter.extensionPattern); + } + fp.appendFilters(Ci.nsIFilePicker.filterAll); + fp.open(async fileOpenResult => { + resolve({ result: fileOpenResult, path: fp.file.path }); + }); + }); + + if (result == Ci.nsIFilePicker.returnCancel) { + // If the user cancels out of the file picker, the migration wizard should + // still be in the state that lets the user re-open the file picker if + // they closed it by accident, so we don't have to do anything else here. + return; + } + + let progress = {}; + for (let resourceType of fileMigrator.displayedResourceTypes) { + progress[resourceType] = { + value: lazy.MigrationWizardConstants.PROGRESS_VALUE.LOADING, + message: "", + }; + } + + let [progressHeaderString, successHeaderString] = + await lazy.gFluentStrings.formatValues([ + fileMigrator.progressHeaderL10nID, + fileMigrator.successHeaderL10nID, + ]); + + this.sendAsyncMessage("UpdateFileImportProgress", { + title: progressHeaderString, + progress, + }); + + let migrationResult; + try { + migrationResult = await fileMigrator.migrate(path); + } catch (e) { + this.sendAsyncMessage("FileImportProgressError", { + migratorKey: key, + fileImportErrorMessage: e.message, + }); + return; + } + + let successProgress = {}; + for (let resourceType in migrationResult) { + successProgress[resourceType] = { + value: lazy.MigrationWizardConstants.PROGRESS_VALUE.SUCCESS, + message: migrationResult[resourceType], + }; + } + this.sendAsyncMessage("UpdateFileImportProgress", { + title: successHeaderString, + progress: successProgress, + }); + } + + /** + * Handles a request to open a native file picker to get the path to a + * CSV file that contains passwords exported from Safari. The returned + * path is in the form of a string, or `null` if the user cancelled the + * native picker. + * + * @param {DOMWindow} window + * The window that the native file picker should be associated with. This + * cannot be null. See nsIFilePicker.init for more details. + * @returns {Promise<string|null>} + */ + async #selectSafariPasswordFile(window) { + let fileMigrator = MigrationUtils.getFileMigrator( + lazy.PasswordFileMigrator.key + ); + let filePickerConfig = await fileMigrator.getFilePickerConfig(); + + let { result, path } = await new Promise(resolve => { + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + fp.init(window, filePickerConfig.title, Ci.nsIFilePicker.modeOpen); + + for (let filter of filePickerConfig.filters) { + fp.appendFilter(filter.title, filter.extensionPattern); + } + fp.appendFilters(Ci.nsIFilePicker.filterAll); + fp.open(async fileOpenResult => { + resolve({ result: fileOpenResult, path: fp.file.path }); + }); + }); + + if (result == Ci.nsIFilePicker.returnCancel) { + // If the user cancels out of the file picker, the migration wizard should + // still be in the state that lets the user re-open the file picker if + // they closed it by accident, so we don't have to do anything else here. + return null; + } + + return path; + } + + /** + * Calls into MigrationUtils to perform a migration given the parameters + * sent via the wizard. + * + * @param {MigrationDetails} migrationDetails + * See migration-wizard.mjs for a definition of MigrationDetails. + * @param {object} extraArgs + * Extra argument object that will be passed to the Event Telemetry for + * finishing the migration. This was initialized in the child actor, and + * will be sent back down to it to write to Telemetry once migration + * completes. + * + * @returns {Promise<object>} + * Resolves once the Migration:Ended observer notification has fired, + * passing the extraArgs for Telemetry back with any relevant properties + * updated. + */ + async #doBrowserMigration(migrationDetails, extraArgs) { + Services.telemetry + .getHistogramById("FX_MIGRATION_SOURCE_BROWSER") + .add(MigrationUtils.getSourceIdForTelemetry(migrationDetails.key)); + + let migrator = await MigrationUtils.getMigrator(migrationDetails.key); + let availableResourceTypes = await migrator.getMigrateData( + migrationDetails.profile + ); + let resourceTypesToMigrate = 0; + let progress = {}; + let migrationUsageHist = + Services.telemetry.getKeyedHistogramById("FX_MIGRATION_USAGE"); + + for (let resourceTypeName of migrationDetails.resourceTypes) { + let resourceType = MigrationUtils.resourceTypes[resourceTypeName]; + if (availableResourceTypes & resourceType) { + resourceTypesToMigrate |= resourceType; + progress[resourceTypeName] = { + value: lazy.MigrationWizardConstants.PROGRESS_VALUE.LOADING, + message: "", + }; + + if (!migrationDetails.autoMigration) { + migrationUsageHist.add(migrationDetails.key, Math.log2(resourceType)); + } + } + } + + if ( + migrationDetails.key == lazy.SafariProfileMigrator?.key && + migrationDetails.safariPasswordFilePath + ) { + // The caller supplied a password export file for Safari. We're going to + // pretend that there was a PASSWORDS resource for Safari to represent + // the state of importing from that file. + progress[ + lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS + ] = { + value: lazy.MigrationWizardConstants.PROGRESS_VALUE.LOADING, + message: "", + }; + + this.sendAsyncMessage("UpdateProgress", { + key: migrationDetails.key, + progress, + }); + + try { + let summary = await lazy.LoginCSVImport.importFromCSV( + migrationDetails.safariPasswordFilePath + ); + let quantity = summary.filter(entry => entry.result == "added").length; + + progress[ + lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS + ] = { + value: lazy.MigrationWizardConstants.PROGRESS_VALUE.SUCCESS, + message: await lazy.gFluentStrings.formatValue( + "migration-wizard-progress-success-passwords", + { + quantity, + } + ), + }; + } catch (e) { + progress[ + lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS + ] = { + value: lazy.MigrationWizardConstants.PROGRESS_VALUE.WARNING, + message: await lazy.gFluentStrings.formatValue( + "migration-passwords-from-file-no-valid-data" + ), + }; + } + } + + this.sendAsyncMessage("UpdateProgress", { + key: migrationDetails.key, + progress, + }); + + // It's possible that only a Safari password file path was sent up, and + // there's nothing left to migrate, in which case we're done here. + if ( + migrationDetails.safariPasswordFilePath && + !migrationDetails.resourceTypes.length + ) { + return extraArgs; + } + + try { + await migrator.migrate( + resourceTypesToMigrate, + false, + migrationDetails.profile, + async (resourceTypeNum, success, details) => { + // Unfortunately, MigratorBase hands us the the numeric value of the + // MigrationUtils.resourceType for this callback. For now, we'll just + // do a look-up to map it to the right constant. + let foundResourceTypeName; + for (let resourceTypeName in MigrationUtils.resourceTypes) { + if ( + MigrationUtils.resourceTypes[resourceTypeName] == resourceTypeNum + ) { + foundResourceTypeName = resourceTypeName; + break; + } + } + + if (!foundResourceTypeName) { + console.error( + "Could not find a resource type for value: ", + resourceTypeNum + ); + } else { + if (!success) { + Services.telemetry + .getKeyedHistogramById("FX_MIGRATION_ERRORS") + .add(migrationDetails.key, Math.log2(resourceTypeNum)); + } + if ( + foundResourceTypeName == + lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS + ) { + if (!success) { + // did not match any extensions + extraArgs.extensions = + lazy.MigrationWizardConstants.EXTENSIONS_IMPORT_RESULT.NONE_MATCHED; + progress[foundResourceTypeName] = { + value: lazy.MigrationWizardConstants.PROGRESS_VALUE.WARNING, + message: await lazy.gFluentStrings.formatValue( + "migration-wizard-progress-no-matched-extensions" + ), + linkURL: Services.urlFormatter.formatURLPref( + "extensions.getAddons.link.url" + ), + linkText: await lazy.gFluentStrings.formatValue( + "migration-wizard-progress-extensions-addons-link" + ), + }; + } else if ( + details?.progressValue == + lazy.MigrationWizardConstants.PROGRESS_VALUE.SUCCESS + ) { + // did match all extensions + extraArgs.extensions = + lazy.MigrationWizardConstants.EXTENSIONS_IMPORT_RESULT.ALL_MATCHED; + progress[foundResourceTypeName] = { + value: lazy.MigrationWizardConstants.PROGRESS_VALUE.SUCCESS, + message: await lazy.gFluentStrings.formatValue( + "migration-wizard-progress-success-extensions", + { + quantity: details.totalExtensions.length, + } + ), + }; + } else if ( + details?.progressValue == + lazy.MigrationWizardConstants.PROGRESS_VALUE.INFO + ) { + // did match some extensions + extraArgs.extensions = + lazy.MigrationWizardConstants.EXTENSIONS_IMPORT_RESULT.PARTIAL_MATCH; + progress[foundResourceTypeName] = { + value: lazy.MigrationWizardConstants.PROGRESS_VALUE.INFO, + message: await lazy.gFluentStrings.formatValue( + "migration-wizard-progress-partial-success-extensions", + { + matched: details.importedExtensions.length, + quantity: details.totalExtensions.length, + } + ), + linkURL: + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "import-data-another-browser", + linkText: await lazy.gFluentStrings.formatValue( + "migration-wizard-progress-extensions-support-link" + ), + }; + } + } else { + progress[foundResourceTypeName] = { + value: success + ? lazy.MigrationWizardConstants.PROGRESS_VALUE.SUCCESS + : lazy.MigrationWizardConstants.PROGRESS_VALUE.WARNING, + message: await this.#getStringForImportQuantity( + migrationDetails.key, + foundResourceTypeName + ), + }; + } + this.sendAsyncMessage("UpdateProgress", { + key: migrationDetails.key, + progress, + }); + } + } + ); + } catch (e) { + console.error(e); + } + + return extraArgs; + } + + /** + * @typedef {object} MigratorProfileInstance + * An object that describes a single user profile (or the default + * user profile) for a particular migrator. + * @property {string} key + * The unique identification key for a migrator. + * @property {string} displayName + * The display name for the migrator that will be shown to the user + * in the wizard. + * @property {string[]} resourceTypes + * An array of strings, where each string represents a resource type + * that can be imported for this migrator and profile. The strings + * should be one of the key values of + * MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES. + * + * Example: ["HISTORY", "FORMDATA", "PASSWORDS", "BOOKMARKS"] + * @property {object|null} profile + * A description of the user profile that the migrator can import. + * @property {string} profile.id + * A unique ID for the user profile. + * @property {string} profile.name + * The display name for the user profile. + */ + + /** + * Asynchronously fetches a migrator for a particular key, and then + * also gets any user profiles that exist on for that migrator. Resolves + * to null if something goes wrong getting information about the migrator + * or any of the user profiles. + * + * @param {string} key + * The unique identification key for a migrator. + * @returns {Promise<MigratorProfileInstance[]|null>} + */ + async #getMigratorAndProfiles(key) { + try { + let migrator = await MigrationUtils.getMigrator(key); + if (!migrator?.enabled) { + return null; + } + + if (!(await migrator.hasPermissions())) { + // If we're unable to get permissions for this migrator, then we + // just don't bother showing it. + let permissionsPath = await migrator.canGetPermissions(); + if (!permissionsPath) { + return null; + } + return this.#serializeMigratorAndProfile( + migrator, + null, + false /* hasPermissions */, + permissionsPath + ); + } + + let sourceProfiles = await migrator.getSourceProfiles(); + if (Array.isArray(sourceProfiles)) { + if (!sourceProfiles.length) { + return null; + } + + Services.telemetry.keyedScalarAdd( + "migration.discovered_migrators", + key, + sourceProfiles.length + ); + + let result = []; + for (let profile of sourceProfiles) { + result.push( + await this.#serializeMigratorAndProfile(migrator, profile) + ); + } + return result; + } + + Services.telemetry.keyedScalarAdd( + "migration.discovered_migrators", + key, + 1 + ); + return this.#serializeMigratorAndProfile(migrator, sourceProfiles); + } catch (e) { + console.error(`Could not get migrator with key ${key}`, e); + } + return null; + } + + /** + * Asynchronously fetches information about what resource types can be + * migrated for a particular migrator and user profile, and then packages + * the migrator, user profile data, and resource type data into an object + * that can be sent down to the MigrationWizardChild. + * + * @param {MigratorBase} migrator + * A migrator subclass of MigratorBase. + * @param {object|null} profileObj + * The user profile object representing the profile to get information + * about. This object is usually gotten by calling getSourceProfiles on + * the migrator. + * @param {boolean} [hasPermissions=true] + * Whether or not the migrator has permission to read the data for the + * other browser. It is expected that the caller will have already + * computed this by calling hasPermissions() on the migrator, and + * passing the result into this method. This is true by default. + * @param {string} [permissionsPath=undefined] + * The path that the selected migrator needs read access to in order to + * do a migration, in the event that hasPermissions is false. This is + * undefined if hasPermissions is true. + * @returns {Promise<MigratorProfileInstance>} + */ + async #serializeMigratorAndProfile( + migrator, + profileObj, + hasPermissions = true, + permissionsPath + ) { + let [profileMigrationData, lastModifiedDate] = await Promise.all([ + migrator.getMigrateData(profileObj), + migrator.getLastUsedDate(), + ]); + + let availableResourceTypes = []; + + // Even if we don't have permissions, we'll show the resources available + // for Safari. For Safari, the workflow is to request permissions only + // after the resources have been selected. + if ( + hasPermissions || + migrator.constructor.key == lazy.SafariProfileMigrator?.key + ) { + for (let resourceType in MigrationUtils.resourceTypes) { + // Normally, we check each possible resourceType to see if we have one or + // more corresponding resourceTypes in profileMigrationData. The exception + // is for Safari, where the migrator does not expose a PASSWORDS resource + // type, but we allow the user to express that they'd like to import + // passwords from it anyways. This is because the Safari migration flow is + // special, and allows the user to import passwords from a file exported + // from Safari. + if ( + profileMigrationData & MigrationUtils.resourceTypes[resourceType] || + (migrator.constructor.key == lazy.SafariProfileMigrator?.key && + MigrationUtils.resourceTypes[resourceType] == + MigrationUtils.resourceTypes.PASSWORDS && + Services.prefs.getBoolPref( + "signon.management.page.fileImport.enabled", + false + )) + ) { + availableResourceTypes.push(resourceType); + } + } + } + + let displayName; + + if (migrator.constructor.key == lazy.InternalTestingProfileMigrator.key) { + // In the case of the InternalTestingProfileMigrator, which is never seen + // by users outside of testing, we don't make our localization community + // localize it's display name, and just display the ID instead. + displayName = migrator.constructor.displayNameL10nID; + } else { + displayName = await lazy.gFluentStrings.formatValue( + migrator.constructor.displayNameL10nID + ); + } + + return { + type: lazy.MigrationWizardConstants.MIGRATOR_TYPES.BROWSER, + key: migrator.constructor.key, + displayName, + brandImage: migrator.constructor.brandImage, + resourceTypes: availableResourceTypes, + profile: profileObj, + lastModifiedDate, + hasPermissions, + permissionsPath, + }; + } + + /** + * Returns the "success" string for a particular resource type after + * migration has completed. + * + * @param {string} migratorKey + * The key for the migrator being used. + * @param {string} resourceTypeStr + * A string mapping to one of the key values of + * MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES. + * @returns {Promise<string>} + * The success string for the resource type after migration has completed. + */ + #getStringForImportQuantity(migratorKey, resourceTypeStr) { + if (migratorKey == lazy.FirefoxProfileMigrator.key) { + return ""; + } + + switch (resourceTypeStr) { + case lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS: { + let quantity = MigrationUtils.getImportedCount("bookmarks"); + let stringID = "migration-wizard-progress-success-bookmarks"; + + if ( + lazy.MigrationWizardConstants.USES_FAVORITES.includes(migratorKey) + ) { + stringID = "migration-wizard-progress-success-favorites"; + } + + return lazy.gFluentStrings.formatValue(stringID, { + quantity, + }); + } + case lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY: { + return lazy.gFluentStrings.formatValue( + "migration-wizard-progress-success-history", + { + maxAgeInDays: MigrationUtils.HISTORY_MAX_AGE_IN_DAYS, + } + ); + } + case lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS: { + let quantity = MigrationUtils.getImportedCount("logins"); + return lazy.gFluentStrings.formatValue( + "migration-wizard-progress-success-passwords", + { + quantity, + } + ); + } + case lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA: { + return lazy.gFluentStrings.formatValue( + "migration-wizard-progress-success-formdata" + ); + } + case lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES + .PAYMENT_METHODS: { + let quantity = MigrationUtils.getImportedCount("cards"); + return lazy.gFluentStrings.formatValue( + "migration-wizard-progress-success-payment-methods", + { + quantity, + } + ); + } + default: { + return ""; + } + } + } + + /** + * Returns a Promise that resolves to a serializable representation of a + * FileMigrator for sending down to the MigrationWizard. + * + * @param {FileMigrator} fileMigrator + * The FileMigrator to serialize. + * @returns {Promise<object|null>} + * The serializable representation of the FileMigrator, or null if the + * migrator is disabled. + */ + async #serializeFileMigrator(fileMigrator) { + if (!fileMigrator.enabled) { + return null; + } + + return { + type: lazy.MigrationWizardConstants.MIGRATOR_TYPES.FILE, + key: fileMigrator.constructor.key, + displayName: await lazy.gFluentStrings.formatValue( + fileMigrator.constructor.displayNameL10nID + ), + brandImage: fileMigrator.constructor.brandImage, + resourceTypes: [], + }; + } + + /** + * Opens the about:addons page in a new background tab in the same window + * as the passed browser. + * + * @param {Element} browser + * The browser element requesting that about:addons opens. + */ + #openAboutAddons(browser) { + let window = browser.ownerGlobal; + window.openTrustedLinkIn("about:addons", "tab", { inBackground: true }); + } +} |