/* 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} * 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.browsingContext, 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} */ 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.browsingContext, 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} * 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} */ 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} */ 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} * 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} * 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 }); } }