This is needed for Ubuntu Linux when Firefox is installed as a Snap package. Under those conditions, Firefox doesn't have the ability to read the contents of the other browsers' data directories. We work around by showing the user a native file picker and instructing them to browse to that data directory in folder picker mode - doing so gives the Firefox process access to the folder (through a temporary symlink). Another restriction we must overcome is Sqlite's inability to read from Sqlite databases through that symlinked folder. We work around this by copying the databases we're importing from to the system temporary directory, querying it, and then deleting those copies. Differential Revision: https://phabricator.services.mozilla.com/D188874
391 lines
12 KiB
JavaScript
391 lines
12 KiB
JavaScript
/* 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: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
|
|
)
|
|
);
|
|
}
|
|
}
|