/* 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 { 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": { if (!gHasOpenedBefore) { Glean.migration.timeToProduceMigratorList.start(); } 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; }); if (!gHasOpenedBefore) { gHasOpenedBefore = true; Glean.migration.timeToProduceMigratorList.stop(); } 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 "SelectManualPasswordFile": { return this.#selectManualPasswordFile( this.browsingContext.topChromeWindow ); } case "RecordEvent": { this.#recordEvent(message.data.type, message.data.args); break; } case "OpenAboutAddons": { let browser = this.browsingContext.topChromeWindow; this.#openAboutAddons(browser); break; } case "GetPermissions": { let migrator = await MigrationUtils.getMigrator(message.data.key); return migrator.getPermissions(this.browsingContext.topChromeWindow); } case "OpenURL": { let browser = this.browsingContext.topChromeWindow; this.#openURL(browser, message.data.url, message.data.where); break; } } 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) { Glean.browserMigration[type + "Wizard"].record(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 another browser. The * returned path is in the form of a string, or `null` if the user cancelled * the native picker. We use this for browsers or platforms that do not * allow us to import passwords automatically. * * @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 #selectManualPasswordFile(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) { Glean.browserMigration.sourceBrowser.accumulateSingleSample( MigrationUtils.getSourceIdForTelemetry(migrationDetails.key) ); let migrator = await MigrationUtils.getMigrator(migrationDetails.key); let availableResourceTypes = await migrator.getMigrateData( migrationDetails.profile ); let resourceTypesToMigrate = 0; let progress = {}; let gleanMigrationUsage = Glean.browserMigration.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) { gleanMigrationUsage[migrationDetails.key].accumulateSingleSample( Math.log2(resourceType) ); } } } if (migrationDetails.manualPasswordFilePath) { // The caller supplied a password export file for another browser. We're // going to pretend that there was a PASSWORDS resource 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.manualPasswordFilePath ); let quantity = summary.filter(entry => entry.result == "added").length; MigrationUtils.notifyLoginsManuallyImported(quantity); 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.manualPasswordFilePath && !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) { Glean.browserMigration.errors[ migrationDetails.key ].accumulateSingleSample(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; } Glean.migration.discoveredMigrators[key].add(sourceProfiles.length); let result = []; for (let profile of sourceProfiles) { result.push( await this.#serializeMigratorAndProfile(migrator, profile) ); } return result; } Glean.migration.discoveredMigrators[key].add(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 passwords for Safari, and for Chrome on Windows, // where we cannot import passwords automatically, but we allow the user // to express that they'd like to import passwords from it anyways. We // use this to determine whether or not to show guidance on how to // manually import a passwords CSV file. if ( profileMigrationData & MigrationUtils.resourceTypes[resourceType] || (MigrationUtils.resourceTypes[resourceType] == MigrationUtils.resourceTypes.PASSWORDS && migrator.showsManualPasswordImport) ) { 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 }); } /** * Opens a url in a new background tab in the same window * as the passed browser. * * @param {Element} browser * The browser element requesting that the URL opens in. * @param {string} url * The URL that will be opened. * @param {string} where * Where the URL will be opened. Defaults to current tab. */ #openURL(browser, url, where) { let window = browser.ownerGlobal; window.openLinkIn( Services.urlFormatter.formatURL(url), where || "current", { private: false, triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal( {} ), csp: null, } ); } }