summaryrefslogtreecommitdiffstats
path: root/browser/components/migration/MigrationWizardParent.sys.mjs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
commit9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /browser/components/migration/MigrationWizardParent.sys.mjs
parentInitial commit. (diff)
downloadthunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.tar.xz
thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/migration/MigrationWizardParent.sys.mjs')
-rw-r--r--browser/components/migration/MigrationWizardParent.sys.mjs651
1 files changed, 651 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..488f29b47a
--- /dev/null
+++ b/browser/components/migration/MigrationWizardParent.sys.mjs
@@ -0,0 +1,651 @@
+/* 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";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyGetter(lazy, "gFluentStrings", function () {
+ return new Localization([
+ "branding/brand.ftl",
+ "browser/migrationWizard.ftl",
+ ]);
+});
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ InternalTestingProfileMigrator:
+ "resource:///modules/InternalTestingProfileMigrator.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",
+ });
+}
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ LoginCSVImport: "resource://gre/modules/LoginCSVImport.jsm",
+});
+
+/**
+ * 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");
+ }
+
+ /**
+ * 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 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;
+ });
+
+ for (let result of filteredResults) {
+ Services.telemetry.keyedScalarAdd(
+ "migration.discovered_migrators",
+ result.key,
+ 1
+ );
+ }
+ return filteredResults;
+ }
+
+ case "Migrate": {
+ if (
+ message.data.type ==
+ lazy.MigrationWizardConstants.MIGRATOR_TYPES.BROWSER
+ ) {
+ await this.#doBrowserMigration(
+ message.data.key,
+ message.data.resourceTypes,
+ message.data.profile,
+ message.data.safariPasswordFilePath
+ );
+ } else if (
+ message.data.type == lazy.MigrationWizardConstants.MIGRATOR_TYPES.FILE
+ ) {
+ let window = this.browsingContext.topChromeWindow;
+ await this.#doFileMigration(window, message.data.key);
+ }
+ 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;
+ }
+ }
+
+ 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] = {
+ inProgress: true,
+ message: "",
+ };
+ }
+
+ let [progressHeaderString, successHeaderString] =
+ await lazy.gFluentStrings.formatValues([
+ fileMigrator.progressHeaderL10nID,
+ fileMigrator.successHeaderL10nID,
+ ]);
+
+ this.sendAsyncMessage("UpdateFileImportProgress", {
+ title: progressHeaderString,
+ progress,
+ });
+ let migrationResult = await fileMigrator.migrate(path);
+ let successProgress = {};
+ for (let resourceType in migrationResult) {
+ successProgress[resourceType] = {
+ inProgress: false,
+ 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 {string} migratorKey
+ * The unique identification key for a migrator.
+ * @param {string[]} resourceTypeNames
+ * 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.
+ * @param {object|null} profileObj
+ * A description of the user profile that the migrator can import.
+ * @param {string} profileObj.id
+ * A unique ID for the user profile.
+ * @param {string} profileObj.name
+ * The display name for the user profile.
+ * @param {string} [safariPasswordFilePath=null]
+ * An optional string argument that points to the path of a passwords
+ * export file from Safari. This file will have password imported from if
+ * supplied. This argument is ignored if the migratorKey is not for the
+ * Safari browser.
+ * @returns {Promise<undefined>}
+ * Resolves once the Migration:Ended observer notification has fired.
+ */
+ async #doBrowserMigration(
+ migratorKey,
+ resourceTypeNames,
+ profileObj,
+ safariPasswordFilePath = null
+ ) {
+ let migrator = await MigrationUtils.getMigrator(migratorKey);
+ let availableResourceTypes = await migrator.getMigrateData(profileObj);
+ let resourceTypesToMigrate = 0;
+ let progress = {};
+
+ for (let resourceTypeName of resourceTypeNames) {
+ let resourceType = MigrationUtils.resourceTypes[resourceTypeName];
+ if (availableResourceTypes & resourceType) {
+ resourceTypesToMigrate |= resourceType;
+ progress[resourceTypeName] = {
+ inProgress: true,
+ message: "",
+ };
+ }
+ }
+
+ if (
+ migratorKey == lazy.SafariProfileMigrator?.key &&
+ 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
+ ] = {
+ inProgress: true,
+ message: "",
+ };
+
+ this.sendAsyncMessage("UpdateProgress", { key: migratorKey, progress });
+
+ let summary = await lazy.LoginCSVImport.importFromCSV(
+ safariPasswordFilePath
+ );
+ let quantity = summary.filter(entry => entry.result == "added").length;
+
+ progress[
+ lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS
+ ] = {
+ inProgress: false,
+ message: await lazy.gFluentStrings.formatValue(
+ "migration-wizard-progress-success-passwords",
+ {
+ quantity,
+ }
+ ),
+ };
+ }
+
+ this.sendAsyncMessage("UpdateProgress", { key: migratorKey, 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 (safariPasswordFilePath && !resourceTypeNames.length) {
+ return;
+ }
+
+ try {
+ await migrator.migrate(
+ resourceTypesToMigrate,
+ false,
+ profileObj,
+ async resourceTypeNum => {
+ // 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 {
+ // For now, we ignore errors in migration, and simply display
+ // the success state.
+ progress[foundResourceTypeName] = {
+ inProgress: false,
+ message: await this.#getStringForImportQuantity(
+ migratorKey,
+ foundResourceTypeName
+ ),
+ };
+ this.sendAsyncMessage("UpdateProgress", {
+ key: migratorKey,
+ progress,
+ });
+ }
+ }
+ );
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ /**
+ * @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;
+ }
+
+ let sourceProfiles = await migrator.getSourceProfiles();
+ if (Array.isArray(sourceProfiles)) {
+ if (!sourceProfiles.length) {
+ return null;
+ }
+
+ let result = [];
+ for (let profile of sourceProfiles) {
+ result.push(
+ await this.#serializeMigratorAndProfile(migrator, profile)
+ );
+ }
+ return result;
+ }
+ 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.
+ * @returns {Promise<MigratorProfileInstance>}
+ */
+ async #serializeMigratorAndProfile(migrator, profileObj) {
+ let [profileMigrationData, lastModifiedDate] = await Promise.all([
+ migrator.getMigrateData(profileObj),
+ migrator.getLastUsedDate(),
+ ]);
+
+ let availableResourceTypes = [];
+
+ 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,
+ };
+ }
+
+ /**
+ * 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) {
+ 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: [],
+ };
+ }
+}