/* 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 { MigrationWizardConstants } from "chrome://browser/content/migration/migration-wizard-constants.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; XPCOMUtils.defineLazyPreferenceGetter( lazy, "SHOW_IMPORT_ALL_PREF", "browser.migrate.content-modal.import-all.enabled", false ); /** * This class is responsible for updating the state of a <migration-wizard> * component, and for listening for events from that component to perform * various migration functions. */ export class MigrationWizardChild extends JSWindowActorChild { #wizardEl = null; /** * Retrieves the list of browsers and profiles from the parent process, and then * puts the migration wizard onto the selection page showing the list that they * can import from. * * @param {boolean} [allowOnlyFileMigrators=null] * Set to true if showing the selection page is allowed if no browser migrators * are found. If not true, and no browser migrators are found, then the wizard * will be sent to the NO_BROWSERS_FOUND page. * @param {string} [migratorKey=null] * If set, this will automatically select the first associated migrator with that * migratorKey in the selector. If not set, the first item in the retrieved list * of migrators will be selected. * @param {string} [fileImportErrorMessage=null] * If set, this will display an error message below the browser / profile selector * indicating that something had previously gone wrong with an import of type * MIGRATOR_TYPES.FILE. */ async #populateMigrators( allowOnlyFileMigrators, migratorKey, fileImportErrorMessage ) { let migrators = await this.sendQuery("GetAvailableMigrators"); let hasBrowserMigrators = migrators.some(migrator => { return migrator.type == MigrationWizardConstants.MIGRATOR_TYPES.BROWSER; }); let hasFileMigrators = migrators.some(migrator => { return migrator.type == MigrationWizardConstants.MIGRATOR_TYPES.FILE; }); if (!hasBrowserMigrators && !allowOnlyFileMigrators) { this.setComponentState({ page: MigrationWizardConstants.PAGES.NO_BROWSERS_FOUND, hasFileMigrators, }); this.#sendTelemetryEvent("no_browsers_found"); } else { this.setComponentState({ migrators, page: MigrationWizardConstants.PAGES.SELECTION, showImportAll: lazy.SHOW_IMPORT_ALL_PREF, migratorKey, fileImportErrorMessage, }); } } /** * General event handler function for events dispatched from the * <migration-wizard> component. * * @param {Event} event * The DOM event being handled. * @returns {Promise} */ async handleEvent(event) { this.#wizardEl = event.target; switch (event.type) { case "MigrationWizard:RequestState": { this.#sendTelemetryEvent("opened"); await this.#requestState(event.detail?.allowOnlyFileMigrators); break; } case "MigrationWizard:BeginMigration": { let extraArgs = this.#recordBeginMigrationEvent(event.detail); let hasPermissions = await this.sendQuery("CheckPermissions", { key: event.detail.key, type: event.detail.type, }); if (!hasPermissions) { if (event.detail.key == "safari") { this.#sendTelemetryEvent("safari_perms"); this.setComponentState({ page: MigrationWizardConstants.PAGES.SAFARI_PERMISSION, }); } else { console.error( `A migrator with key ${event.detail.key} needs permissions, ` + "and no UI exists for that right now." ); } return; } await this.beginMigration(event.detail, extraArgs); break; } case "MigrationWizard:RequestSafariPermissions": { let success = await this.sendQuery("RequestSafariPermissions"); if (success) { let extraArgs = this.#constructExtraArgs(event.detail); await this.beginMigration(event.detail, extraArgs); } break; } case "MigrationWizard:SelectSafariPasswordFile": { let path = await this.sendQuery("SelectSafariPasswordFile"); if (path) { event.detail.safariPasswordFilePath = path; let passwordResourceIndex = event.detail.resourceTypes.indexOf( MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS ); event.detail.resourceTypes.splice(passwordResourceIndex, 1); let extraArgs = this.#constructExtraArgs(event.detail); await this.beginMigration(event.detail, extraArgs); } break; } case "MigrationWizard:OpenAboutAddons": { this.sendAsyncMessage("OpenAboutAddons"); break; } case "MigrationWizard:PermissionsNeeded": { // In theory, the migrator permissions might be requested on any // platform - but in practice, this only happens on Linux, so that's // why the event is named linux_perms. this.#sendTelemetryEvent("linux_perms", { migrator_key: event.detail.key, }); break; } case "MigrationWizard:GetPermissions": { let success = await this.sendQuery("GetPermissions", { key: event.detail.key, }); if (success) { await this.#requestState(true /* allowOnlyFileMigrators */); } break; } } } async #requestState(allowOnlyFileMigrators) { this.setComponentState({ page: MigrationWizardConstants.PAGES.LOADING, }); await this.#populateMigrators(allowOnlyFileMigrators); this.#wizardEl.dispatchEvent( new this.contentWindow.CustomEvent("MigrationWizard:Ready", { bubbles: true, }) ); } /** * Sends a message to the parent actor to record Event Telemetry. * * @param {string} type * The type of event being recorded. * @param {object} [args=null] * Optional extra_args to supply for the event. */ #sendTelemetryEvent(type, args) { this.sendAsyncMessage("RecordEvent", { type, args }); } /** * Constructs extra arguments to pass to some Event Telemetry based * on the MigrationDetails passed up from the MigrationWizard. * * See migration-wizard.mjs for a definition of MigrationDetails. * * @param {object} migrationDetails * A MigrationDetails object. * @returns {object} */ #constructExtraArgs(migrationDetails) { let extraArgs = { migrator_key: migrationDetails.key, history: "0", formdata: "0", passwords: "0", bookmarks: "0", payment_methods: "0", extensions: "0", other: 0, }; for (let type of migrationDetails.resourceTypes) { switch (type) { case MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY: { extraArgs.history = "1"; break; } case MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA: { extraArgs.formdata = "1"; break; } case MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS: { extraArgs.passwords = "1"; break; } case MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS: { extraArgs.bookmarks = "1"; break; } case MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.EXTENSIONS: { extraArgs.extensions = "1"; break; } case MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES .PAYMENT_METHODS: { extraArgs.payment_methods = "1"; break; } default: { extraArgs.other++; } } } // Event Telemetry extra arguments expect strings for every value, so // now we coerce our "other" count into a string. extraArgs.other = String(extraArgs.other); return extraArgs; } /** * This migration wizard combines a lot of steps (selecting the browser, profile, * resources, and starting the migration) into a single page. This helper method * records Event Telemetry for each of those actions at the same time when a * migration begins. * * This method returns the extra_args object that was constructed for the * resources_selected and migration_started event so that a * "migration_finished" event can use the same extra_args without * regenerating it. * * See migration-wizard.mjs for a definition of MigrationDetails. * * @param {object} migrationDetails * A MigrationDetails object. * @returns {object} */ #recordBeginMigrationEvent(migrationDetails) { this.#sendTelemetryEvent("browser_selected", { migrator_key: migrationDetails.key, }); if (migrationDetails.profile) { this.#sendTelemetryEvent("profile_selected", { migrator_key: migrationDetails.key, }); } let extraArgs = this.#constructExtraArgs(migrationDetails); extraArgs.configured = String(Number(migrationDetails.expandedDetails)); this.#sendTelemetryEvent("resources_selected", extraArgs); delete extraArgs.configured; this.#sendTelemetryEvent("migration_started", extraArgs); return extraArgs; } /** * Sends a message to the parent actor to attempt a migration. * * See migration-wizard.mjs for a definition of MigrationDetails. * * @param {object} migrationDetails * A MigrationDetails object. * @param {object} extraArgs * Extra argument object to pass to the Event Telemetry for finishing * the migration. * @returns {Promise<undefined>} * Returns a Promise that resolves after the parent responds to the migration * message. */ async beginMigration(migrationDetails, extraArgs) { if ( migrationDetails.key == "safari" && migrationDetails.resourceTypes.includes( MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS ) && !migrationDetails.safariPasswordFilePath ) { this.#sendTelemetryEvent("safari_password_file"); this.setComponentState({ page: MigrationWizardConstants.PAGES.SAFARI_PASSWORD_PERMISSION, }); return; } extraArgs = await this.sendQuery("Migrate", { migrationDetails, extraArgs, }); this.#sendTelemetryEvent("migration_finished", extraArgs); this.#wizardEl.dispatchEvent( new this.contentWindow.CustomEvent("MigrationWizard:DoneMigration", { bubbles: true, }) ); } /** * General message handler function for messages received from the * associated MigrationWizardParent JSWindowActor. * * @param {ReceiveMessageArgument} message * The message received from the MigrationWizardParent. */ receiveMessage(message) { switch (message.name) { case "UpdateProgress": { this.setComponentState({ page: MigrationWizardConstants.PAGES.PROGRESS, progress: message.data.progress, key: message.data.key, }); break; } case "UpdateFileImportProgress": { this.setComponentState({ page: MigrationWizardConstants.PAGES.FILE_IMPORT_PROGRESS, progress: message.data.progress, title: message.data.title, }); break; } case "FileImportProgressError": { this.#populateMigrators( true, message.data.migratorKey, message.data.fileImportErrorMessage ); break; } } } /** * Calls the `setState` method on the <migration-wizard> component. The * state is cloned into the execution scope of this.#wizardEl. * * @param {object} state The state object that a <migration-wizard> * component expects. See the documentation for the element's setState * method for more details. */ setComponentState(state) { if (!this.#wizardEl) { return; } // We waive XrayWrappers in the event that the element is embedded in // a document without system privileges, like about:welcome. Cu.waiveXrays(this.#wizardEl).setState( Cu.cloneInto( state, // ownerGlobal doesn't exist in content windows. // eslint-disable-next-line mozilla/use-ownerGlobal this.#wizardEl.ownerDocument.defaultView ) ); } }