mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-27 06:43:32 +00:00
42e58d8700
Differential Revision: https://phabricator.services.mozilla.com/D225652
1194 lines
36 KiB
JavaScript
1194 lines
36 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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AMBrowserExtensionsImport: "resource://gre/modules/AddonManager.sys.mjs",
|
|
LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
|
|
PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs",
|
|
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
|
|
Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
|
|
setTimeout: "resource://gre/modules/Timer.sys.mjs",
|
|
MigrationWizardConstants:
|
|
"chrome://browser/content/migration/migration-wizard-constants.mjs",
|
|
});
|
|
|
|
ChromeUtils.defineLazyGetter(
|
|
lazy,
|
|
"gCanGetPermissionsOnPlatformPromise",
|
|
() => {
|
|
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
|
|
return fp.isModeSupported(Ci.nsIFilePicker.modeGetFolder);
|
|
}
|
|
);
|
|
|
|
var gMigrators = null;
|
|
var gFileMigrators = null;
|
|
var gProfileStartup = null;
|
|
var gL10n = null;
|
|
|
|
let gForceExitSpinResolve = false;
|
|
let gKeepUndoData = false;
|
|
let gUndoData = null;
|
|
|
|
function getL10n() {
|
|
if (!gL10n) {
|
|
gL10n = new Localization(["browser/migrationWizard.ftl"]);
|
|
}
|
|
return gL10n;
|
|
}
|
|
|
|
const MIGRATOR_MODULES = Object.freeze({
|
|
EdgeProfileMigrator: {
|
|
moduleURI: "resource:///modules/EdgeProfileMigrator.sys.mjs",
|
|
platforms: ["win"],
|
|
},
|
|
FirefoxProfileMigrator: {
|
|
moduleURI: "resource:///modules/FirefoxProfileMigrator.sys.mjs",
|
|
platforms: ["linux", "macosx", "win"],
|
|
},
|
|
IEProfileMigrator: {
|
|
moduleURI: "resource:///modules/IEProfileMigrator.sys.mjs",
|
|
platforms: ["win"],
|
|
},
|
|
SafariProfileMigrator: {
|
|
moduleURI: "resource:///modules/SafariProfileMigrator.sys.mjs",
|
|
platforms: ["macosx"],
|
|
},
|
|
|
|
// The following migrators are all variants of the ChromeProfileMigrator
|
|
|
|
BraveProfileMigrator: {
|
|
moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
|
|
platforms: ["linux", "macosx", "win"],
|
|
},
|
|
CanaryProfileMigrator: {
|
|
moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
|
|
platforms: ["macosx", "win"],
|
|
},
|
|
ChromeProfileMigrator: {
|
|
moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
|
|
platforms: ["linux", "macosx", "win"],
|
|
},
|
|
ChromeBetaMigrator: {
|
|
moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
|
|
platforms: ["linux", "win"],
|
|
},
|
|
ChromeDevMigrator: {
|
|
moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
|
|
platforms: ["linux"],
|
|
},
|
|
ChromiumProfileMigrator: {
|
|
moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
|
|
platforms: ["linux", "macosx", "win"],
|
|
},
|
|
Chromium360seMigrator: {
|
|
moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
|
|
platforms: ["win"],
|
|
},
|
|
ChromiumEdgeMigrator: {
|
|
moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
|
|
platforms: ["macosx", "win"],
|
|
},
|
|
ChromiumEdgeBetaMigrator: {
|
|
moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
|
|
platforms: ["macosx", "win"],
|
|
},
|
|
OperaProfileMigrator: {
|
|
moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
|
|
platforms: ["linux", "macosx", "win"],
|
|
},
|
|
VivaldiProfileMigrator: {
|
|
moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
|
|
platforms: ["linux", "macosx", "win"],
|
|
},
|
|
OperaGXProfileMigrator: {
|
|
moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
|
|
platforms: ["macosx", "win"],
|
|
},
|
|
|
|
InternalTestingProfileMigrator: {
|
|
moduleURI: "resource:///modules/InternalTestingProfileMigrator.sys.mjs",
|
|
platforms: ["linux", "macosx", "win"],
|
|
},
|
|
});
|
|
|
|
const FILE_MIGRATOR_MODULES = Object.freeze({
|
|
PasswordFileMigrator: {
|
|
moduleURI: "resource:///modules/FileMigrators.sys.mjs",
|
|
},
|
|
BookmarksFileMigrator: {
|
|
moduleURI: "resource:///modules/FileMigrators.sys.mjs",
|
|
},
|
|
});
|
|
|
|
/**
|
|
* The singleton MigrationUtils service. This service is the primary mechanism
|
|
* by which migrations from other browsers to this browser occur. The singleton
|
|
* instance of this class is exported from this module as `MigrationUtils`.
|
|
*/
|
|
class MigrationUtils {
|
|
constructor() {
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
this,
|
|
"HISTORY_MAX_AGE_IN_DAYS",
|
|
"browser.migrate.history.maxAgeInDays",
|
|
180
|
|
);
|
|
|
|
ChromeUtils.registerWindowActor("MigrationWizard", {
|
|
parent: {
|
|
esModuleURI: "resource:///actors/MigrationWizardParent.sys.mjs",
|
|
},
|
|
|
|
child: {
|
|
esModuleURI: "resource:///actors/MigrationWizardChild.sys.mjs",
|
|
events: {
|
|
"MigrationWizard:RequestState": { wantUntrusted: true },
|
|
"MigrationWizard:BeginMigration": { wantUntrusted: true },
|
|
"MigrationWizard:RequestSafariPermissions": { wantUntrusted: true },
|
|
"MigrationWizard:SelectSafariPasswordFile": { wantUntrusted: true },
|
|
"MigrationWizard:OpenAboutAddons": { wantUntrusted: true },
|
|
"MigrationWizard:PermissionsNeeded": { wantUntrusted: true },
|
|
"MigrationWizard:GetPermissions": { wantUntrusted: true },
|
|
"MigrationWizard:OpenURL": { wantUntrusted: true },
|
|
},
|
|
},
|
|
|
|
includeChrome: true,
|
|
allFrames: true,
|
|
matches: [
|
|
"about:welcome",
|
|
"about:welcome?*",
|
|
"about:preferences",
|
|
"about:settings",
|
|
"chrome://browser/content/migration/migration-dialog-window.html",
|
|
"chrome://browser/content/spotlight.html",
|
|
"about:firefoxview",
|
|
],
|
|
});
|
|
|
|
ChromeUtils.defineLazyGetter(this, "IS_LINUX_SNAP_PACKAGE", () => {
|
|
if (
|
|
AppConstants.platform != "linux" ||
|
|
!Cc["@mozilla.org/gio-service;1"]
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
let gIOSvc = Cc["@mozilla.org/gio-service;1"].getService(
|
|
Ci.nsIGIOService
|
|
);
|
|
return gIOSvc.isRunningUnderSnap;
|
|
});
|
|
}
|
|
|
|
resourceTypes = Object.freeze({
|
|
ALL: 0x0000,
|
|
/* 0x01 used to be used for settings, but was removed. */
|
|
COOKIES: 0x0002,
|
|
HISTORY: 0x0004,
|
|
FORMDATA: 0x0008,
|
|
PASSWORDS: 0x0010,
|
|
BOOKMARKS: 0x0020,
|
|
OTHERDATA: 0x0040,
|
|
SESSION: 0x0080,
|
|
PAYMENT_METHODS: 0x0100,
|
|
EXTENSIONS: 0x0200,
|
|
});
|
|
|
|
/**
|
|
* Helper for implementing simple asynchronous cases of migration resources'
|
|
* ``migrate(aCallback)`` (see MigratorBase). If your ``migrate`` method
|
|
* just waits for some file to be read, for example, and then migrates
|
|
* everything right away, you can wrap the async-function with this helper
|
|
* and not worry about notifying the callback.
|
|
*
|
|
* @example
|
|
* // For example, instead of writing:
|
|
* setTimeout(function() {
|
|
* try {
|
|
* ....
|
|
* aCallback(true);
|
|
* }
|
|
* catch() {
|
|
* aCallback(false);
|
|
* }
|
|
* }, 0);
|
|
*
|
|
* // You may write:
|
|
* setTimeout(MigrationUtils.wrapMigrateFunction(function() {
|
|
* if (importingFromMosaic)
|
|
* throw Cr.NS_ERROR_UNEXPECTED;
|
|
* }, aCallback), 0);
|
|
*
|
|
* // ... and aCallback will be called with aSuccess=false when importing
|
|
* // from Mosaic, or with aSuccess=true otherwise.
|
|
*
|
|
* @param {Function} aFunction
|
|
* the function that will be called sometime later. If aFunction
|
|
* throws when it's called, aCallback(false) is called, otherwise
|
|
* aCallback(true) is called.
|
|
* @param {Function} aCallback
|
|
* the callback function passed to ``migrate``.
|
|
* @returns {Function}
|
|
* the wrapped function.
|
|
*/
|
|
wrapMigrateFunction(aFunction, aCallback) {
|
|
return function () {
|
|
let success = false;
|
|
try {
|
|
aFunction.apply(null, arguments);
|
|
success = true;
|
|
} catch (ex) {
|
|
console.error(ex);
|
|
}
|
|
// Do not change this to call aCallback directly in try try & catch
|
|
// blocks, because if aCallback throws, we may end up calling aCallback
|
|
// twice.
|
|
aCallback(success);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Gets localized string corresponding to l10n-id
|
|
*
|
|
* @param {string} aKey
|
|
* The key of the id of the localization to retrieve.
|
|
* @param {object} [aArgs=undefined]
|
|
* An optional map of arguments to the id.
|
|
* @returns {Promise<string>}
|
|
* A promise that resolves to the retrieved localization.
|
|
*/
|
|
getLocalizedString(aKey, aArgs) {
|
|
let l10n = getL10n();
|
|
return l10n.formatValue(aKey, aArgs);
|
|
}
|
|
|
|
/**
|
|
* Get all the rows corresponding to a select query from a database, without
|
|
* requiring a lock on the database. If fetching data fails (because someone
|
|
* else tried to write to the DB at the same time, for example), we will
|
|
* retry the fetch after a 100ms timeout, up to 10 times.
|
|
*
|
|
* @param {string} path
|
|
* The file path to the database we want to open.
|
|
* @param {string} description
|
|
* A developer-readable string identifying what kind of database we're
|
|
* trying to open.
|
|
* @param {string} selectQuery
|
|
* The SELECT query to use to fetch the rows.
|
|
* @param {Promise} [testDelayPromise]
|
|
* An optional promise to await for after the first loop, used in tests.
|
|
*
|
|
* @returns {Promise<object[]|Error>}
|
|
* A promise that resolves to an array of rows. The promise will be
|
|
* rejected if the read/fetch failed even after retrying.
|
|
*/
|
|
getRowsFromDBWithoutLocks(
|
|
path,
|
|
description,
|
|
selectQuery,
|
|
testDelayPromise = null
|
|
) {
|
|
let dbOptions = {
|
|
readOnly: true,
|
|
ignoreLockingMode: true,
|
|
path,
|
|
};
|
|
|
|
const RETRYLIMIT = 10;
|
|
const RETRYINTERVAL = 100;
|
|
return (async function innerGetRows() {
|
|
let rows = null;
|
|
for (let retryCount = RETRYLIMIT; retryCount; retryCount--) {
|
|
// Attempt to get the rows. If this succeeds, we will bail out of the loop,
|
|
// close the database in a failsafe way, and pass the rows back.
|
|
// If fetching the rows throws, we will wait RETRYINTERVAL ms
|
|
// and try again. This will repeat a maximum of RETRYLIMIT times.
|
|
let db;
|
|
let didOpen = false;
|
|
let previousExceptionMessage = null;
|
|
try {
|
|
db = await lazy.Sqlite.openConnection(dbOptions);
|
|
didOpen = true;
|
|
rows = await db.execute(selectQuery);
|
|
break;
|
|
} catch (ex) {
|
|
if (previousExceptionMessage != ex.message) {
|
|
console.error(ex);
|
|
}
|
|
previousExceptionMessage = ex.message;
|
|
if (ex.name == "NS_ERROR_FILE_CORRUPTED") {
|
|
break;
|
|
}
|
|
} finally {
|
|
try {
|
|
if (didOpen) {
|
|
await db.close();
|
|
}
|
|
} catch (ex) {}
|
|
}
|
|
await Promise.all([
|
|
new Promise(resolve => lazy.setTimeout(resolve, RETRYINTERVAL)),
|
|
testDelayPromise,
|
|
]);
|
|
}
|
|
if (!rows) {
|
|
throw new Error(
|
|
"Couldn't get rows from the " + description + " database."
|
|
);
|
|
}
|
|
return rows;
|
|
})();
|
|
}
|
|
|
|
get #migrators() {
|
|
if (!gMigrators) {
|
|
gMigrators = new Map();
|
|
for (let [symbol, { moduleURI, platforms }] of Object.entries(
|
|
MIGRATOR_MODULES
|
|
)) {
|
|
if (platforms.includes(AppConstants.platform)) {
|
|
let { [symbol]: migratorClass } =
|
|
ChromeUtils.importESModule(moduleURI);
|
|
if (gMigrators.has(migratorClass.key)) {
|
|
console.error(
|
|
"A pre-existing migrator exists with key " +
|
|
`${migratorClass.key}. Not registering.`
|
|
);
|
|
continue;
|
|
}
|
|
gMigrators.set(migratorClass.key, new migratorClass());
|
|
}
|
|
}
|
|
}
|
|
return gMigrators;
|
|
}
|
|
|
|
get #fileMigrators() {
|
|
if (!gFileMigrators) {
|
|
gFileMigrators = new Map();
|
|
for (let [symbol, { moduleURI }] of Object.entries(
|
|
FILE_MIGRATOR_MODULES
|
|
)) {
|
|
let { [symbol]: migratorClass } = ChromeUtils.importESModule(moduleURI);
|
|
if (gFileMigrators.has(migratorClass.key)) {
|
|
console.error(
|
|
"A pre-existing file migrator exists with key " +
|
|
`${migratorClass.key}. Not registering.`
|
|
);
|
|
continue;
|
|
}
|
|
gFileMigrators.set(migratorClass.key, new migratorClass());
|
|
}
|
|
}
|
|
return gFileMigrators;
|
|
}
|
|
|
|
forceExitSpinResolve() {
|
|
gForceExitSpinResolve = true;
|
|
}
|
|
|
|
spinResolve(promise) {
|
|
if (!(promise instanceof Promise)) {
|
|
return promise;
|
|
}
|
|
let done = false;
|
|
let result = null;
|
|
let error = null;
|
|
gForceExitSpinResolve = false;
|
|
promise
|
|
.catch(e => {
|
|
error = e;
|
|
})
|
|
.then(r => {
|
|
result = r;
|
|
done = true;
|
|
});
|
|
|
|
Services.tm.spinEventLoopUntil(
|
|
"MigrationUtils.sys.mjs:MU_spinResolve",
|
|
() => done || gForceExitSpinResolve
|
|
);
|
|
if (!done) {
|
|
throw new Error("Forcefully exited event loop.");
|
|
} else if (error) {
|
|
throw error;
|
|
} else {
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the migrator for the given source, if any data is available
|
|
* for this source, or if permissions are required in order to read
|
|
* data from this source. Returns null otherwise.
|
|
*
|
|
* @param {string} aKey
|
|
* Internal name of the migration source. See `availableMigratorKeys`
|
|
* for supported values by OS.
|
|
* @returns {Promise<MigratorBase|null>}
|
|
* A profile migrator implementing nsIBrowserProfileMigrator, if it can
|
|
* import any data, null otherwise.
|
|
*/
|
|
async getMigrator(aKey) {
|
|
let migrator = this.#migrators.get(aKey);
|
|
if (!migrator) {
|
|
console.error(`Could not find a migrator class for key ${aKey}`);
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
if (!migrator) {
|
|
return null;
|
|
}
|
|
|
|
if (
|
|
(await migrator.isSourceAvailable()) ||
|
|
(!(await migrator.hasPermissions()) && migrator.canGetPermissions())
|
|
) {
|
|
return migrator;
|
|
}
|
|
|
|
return null;
|
|
} catch (ex) {
|
|
console.error(ex);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
getFileMigrator(aKey) {
|
|
let migrator = this.#fileMigrators.get(aKey);
|
|
if (!migrator) {
|
|
console.error(`Could not find a file migrator class for key ${aKey}`);
|
|
return null;
|
|
}
|
|
return migrator;
|
|
}
|
|
|
|
/**
|
|
* Returns true if a migrator is registered with key aKey. No check is made
|
|
* to determine if a profile exists that the migrator can migrate from.
|
|
*
|
|
* @param {string} aKey
|
|
* Internal name of the migration source. See `availableMigratorKeys`
|
|
* for supported values by OS.
|
|
* @returns {boolean}
|
|
*/
|
|
migratorExists(aKey) {
|
|
return this.#migrators.has(aKey);
|
|
}
|
|
|
|
/**
|
|
* Figure out what is the default browser, and if there is a migrator
|
|
* for it, return that migrator's internal name.
|
|
*
|
|
* For the time being, the "internal name" of a migrator is its contract-id
|
|
* trailer (e.g. ie for @mozilla.org/profile/migrator;1?app=browser&type=ie),
|
|
* but it will soon be exposed properly.
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
getMigratorKeyForDefaultBrowser() {
|
|
// Canary uses the same description as Chrome so we can't distinguish them.
|
|
// Edge Beta on macOS uses "Microsoft Edge" with no "beta" indication.
|
|
const APP_DESC_TO_KEY = {
|
|
"Internet Explorer": "ie",
|
|
"Microsoft Edge": "edge",
|
|
Safari: "safari",
|
|
Firefox: "firefox",
|
|
Nightly: "firefox",
|
|
Opera: "opera",
|
|
Vivaldi: "vivaldi",
|
|
"Opera GX": "opera-gx",
|
|
"Brave Web Browser": "brave", // Windows, Linux
|
|
Brave: "brave", // OS X
|
|
"Google Chrome": "chrome", // Windows, Linux
|
|
Chrome: "chrome", // OS X
|
|
Chromium: "chromium", // Windows, OS X
|
|
"Chromium Web Browser": "chromium", // Linux
|
|
"360\u5b89\u5168\u6d4f\u89c8\u5668": "chromium-360se",
|
|
};
|
|
|
|
let key = "";
|
|
try {
|
|
let browserDesc = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
|
|
.getService(Ci.nsIExternalProtocolService)
|
|
.getApplicationDescription("http");
|
|
key = APP_DESC_TO_KEY[browserDesc] || "";
|
|
// Handle devedition, as well as "FirefoxNightly" on OS X.
|
|
if (!key && browserDesc.startsWith("Firefox")) {
|
|
key = "firefox";
|
|
}
|
|
} catch (ex) {
|
|
console.error("Could not detect default browser: ", ex);
|
|
}
|
|
|
|
return key;
|
|
}
|
|
|
|
/**
|
|
* True if we're in the process of a startup migration.
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
get isStartupMigration() {
|
|
return gProfileStartup != null;
|
|
}
|
|
|
|
/**
|
|
* In the case of startup migration, this is set to the nsIProfileStartup
|
|
* instance passed to ProfileMigrator's migrate.
|
|
*
|
|
* @see showMigrationWizard
|
|
* @type {nsIProfileStartup|null}
|
|
*/
|
|
get profileStartup() {
|
|
return gProfileStartup;
|
|
}
|
|
|
|
/**
|
|
* Show the migration wizard in about:preferences, or if there is not an existing
|
|
* browser window open, in a new top-level dialog window.
|
|
*
|
|
* NB: If you add new consumers, please add a migration entry point constant to
|
|
* MIGRATION_ENTRYPOINTS and supply that entrypoint with the entrypoint property
|
|
* in the aOptions argument.
|
|
*
|
|
* @param {Window} [aOpener=null]
|
|
* optional; the window that asks to open the wizard.
|
|
* @param {object} [aOptions=null]
|
|
* optional named arguments for the migration wizard.
|
|
* @param {string} [aOptions.entrypoint=undefined]
|
|
* migration entry point constant. See MIGRATION_ENTRYPOINTS.
|
|
* @param {string} [aOptions.migratorKey=undefined]
|
|
* The key for which migrator to use automatically. This is the key that is exposed
|
|
* as a static getter on the migrator class.
|
|
* @param {MigratorBase} [aOptions.migrator=undefined]
|
|
* A migrator instance to use automatically.
|
|
* @param {boolean} [aOptions.isStartupMigration=undefined]
|
|
* True if this is a startup migration.
|
|
* @param {boolean} [aOptions.skipSourceSelection=undefined]
|
|
* True if the source selection page of the wizard should be skipped.
|
|
* @param {string} [aOptions.profileId]
|
|
* An identifier for the profile to use when migrating.
|
|
* @returns {Promise<undefined>}
|
|
* If an about:preferences tab can be opened, this will resolve when
|
|
* that tab has been switched to. Otherwise, this will resolve
|
|
* just after opening the top-level dialog window.
|
|
*/
|
|
showMigrationWizard(aOpener, aOptions) {
|
|
// When migration is kicked off from about:welcome, there are
|
|
// a few different behaviors that we want to test, controlled
|
|
// by a preference that is instrumented for Nimbus. The pref
|
|
// has the following possible states:
|
|
//
|
|
// "autoclose":
|
|
// The user will be directed to the migration wizard in
|
|
// about:preferences, but once the wizard is dismissed,
|
|
// the tab will close.
|
|
//
|
|
// "standalone":
|
|
// The migration wizard will open in a new top-level content
|
|
// window.
|
|
//
|
|
// "default" / other
|
|
// The user will be directed to the migration wizard in
|
|
// about:preferences. The tab will not close once the
|
|
// user closes the wizard.
|
|
let aboutWelcomeBehavior = Services.prefs.getCharPref(
|
|
"browser.migrate.content-modal.about-welcome-behavior",
|
|
"default"
|
|
);
|
|
|
|
let entrypoint = aOptions.entrypoint || this.MIGRATION_ENTRYPOINTS.UNKNOWN;
|
|
Services.telemetry
|
|
.getHistogramById("FX_MIGRATION_ENTRY_POINT_CATEGORICAL")
|
|
.add(entrypoint);
|
|
|
|
let openStandaloneWindow = blocking => {
|
|
let features = "dialog,centerscreen,resizable=no";
|
|
|
|
if (blocking) {
|
|
features += ",modal";
|
|
}
|
|
|
|
Services.ww.openWindow(
|
|
aOpener,
|
|
"chrome://browser/content/migration/migration-dialog-window.html",
|
|
"_blank",
|
|
features,
|
|
{
|
|
options: aOptions,
|
|
}
|
|
);
|
|
return Promise.resolve();
|
|
};
|
|
|
|
if (aOptions.isStartupMigration) {
|
|
// Record that the uninstaller requested a profile refresh
|
|
if (Services.env.get("MOZ_UNINSTALLER_PROFILE_REFRESH")) {
|
|
Services.env.set("MOZ_UNINSTALLER_PROFILE_REFRESH", "");
|
|
Glean.migration.uninstallerProfileRefresh.set(true);
|
|
}
|
|
|
|
openStandaloneWindow(true /* blocking */);
|
|
return Promise.resolve();
|
|
}
|
|
|
|
if (aOpener?.openPreferences) {
|
|
if (aOptions.entrypoint == this.MIGRATION_ENTRYPOINTS.NEWTAB) {
|
|
if (aboutWelcomeBehavior == "autoclose") {
|
|
return aOpener.openPreferences("general-migrate-autoclose");
|
|
} else if (aboutWelcomeBehavior == "standalone") {
|
|
openStandaloneWindow(false /* blocking */);
|
|
return Promise.resolve();
|
|
}
|
|
}
|
|
return aOpener.openPreferences("general-migrate");
|
|
}
|
|
|
|
// If somehow we failed to open about:preferences, fall back to opening
|
|
// the top-level window.
|
|
openStandaloneWindow(false /* blocking */);
|
|
return Promise.resolve();
|
|
}
|
|
|
|
/**
|
|
* Show the migration wizard for startup-migration. This should only be
|
|
* called by ProfileMigrator (see ProfileMigrator.js), which implements
|
|
* nsIProfileMigrator. This runs asynchronously if we are running an
|
|
* automigration.
|
|
*
|
|
* @param {nsIProfileStartup} aProfileStartup
|
|
* the nsIProfileStartup instance provided to ProfileMigrator.migrate.
|
|
* @param {string|null} [aMigratorKey=null]
|
|
* If set, the migration wizard will import from the corresponding
|
|
* migrator, bypassing the source-selection page. Otherwise, the
|
|
* source-selection page will be displayed, either with the default
|
|
* browser selected, if it could be detected and if there is a
|
|
* migrator for it, or with the first option selected as a fallback
|
|
* @param {string|null} [aProfileToMigrate=null]
|
|
* If set, the migration wizard will import from the profile indicated.
|
|
* @throws
|
|
* if aMigratorKey is invalid or if it points to a non-existent
|
|
* source.
|
|
*/
|
|
startupMigration(aProfileStartup, aMigratorKey, aProfileToMigrate) {
|
|
this.spinResolve(
|
|
this.asyncStartupMigration(
|
|
aProfileStartup,
|
|
aMigratorKey,
|
|
aProfileToMigrate
|
|
)
|
|
);
|
|
}
|
|
|
|
async asyncStartupMigration(
|
|
aProfileStartup,
|
|
aMigratorKey,
|
|
aProfileToMigrate
|
|
) {
|
|
if (!aProfileStartup) {
|
|
throw new Error(
|
|
"an profile-startup instance is required for startup-migration"
|
|
);
|
|
}
|
|
gProfileStartup = aProfileStartup;
|
|
|
|
let skipSourceSelection = false,
|
|
migrator = null,
|
|
migratorKey = "";
|
|
if (aMigratorKey) {
|
|
migrator = await this.getMigrator(aMigratorKey);
|
|
if (!migrator) {
|
|
// aMigratorKey must point to a valid source, so, if it doesn't
|
|
// cleanup and throw.
|
|
this.finishMigration();
|
|
throw new Error(
|
|
"startMigration was asked to open auto-migrate from " +
|
|
"a non-existent source: " +
|
|
aMigratorKey
|
|
);
|
|
}
|
|
migratorKey = aMigratorKey;
|
|
skipSourceSelection = true;
|
|
} else {
|
|
let defaultBrowserKey = this.getMigratorKeyForDefaultBrowser();
|
|
if (defaultBrowserKey) {
|
|
migrator = await this.getMigrator(defaultBrowserKey);
|
|
if (migrator) {
|
|
migratorKey = defaultBrowserKey;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!migrator) {
|
|
let migrators = await Promise.all(
|
|
this.availableMigratorKeys.map(key => this.getMigrator(key))
|
|
);
|
|
// If there's no migrator set so far, ensure that there is at least one
|
|
// migrator available before opening the wizard.
|
|
// Note that we don't need to check the default browser first, because
|
|
// if that one existed we would have used it in the block above this one.
|
|
if (!migrators.some(m => m)) {
|
|
// None of the keys produced a usable migrator, so finish up here:
|
|
this.finishMigration();
|
|
return;
|
|
}
|
|
}
|
|
|
|
let isRefresh =
|
|
migrator &&
|
|
skipSourceSelection &&
|
|
migratorKey == AppConstants.MOZ_APP_NAME;
|
|
|
|
let entrypoint = this.MIGRATION_ENTRYPOINTS.FIRSTRUN;
|
|
if (isRefresh) {
|
|
entrypoint = this.MIGRATION_ENTRYPOINTS.FXREFRESH;
|
|
}
|
|
|
|
this.showMigrationWizard(null, {
|
|
entrypoint,
|
|
migratorKey,
|
|
migrator,
|
|
isStartupMigration: !!aProfileStartup,
|
|
skipSourceSelection,
|
|
profileId: aProfileToMigrate,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* This is only pseudo-private because some tests and helper functions
|
|
* still expect to be able to directly access it.
|
|
*/
|
|
_importQuantities = {
|
|
bookmarks: 0,
|
|
logins: 0,
|
|
history: 0,
|
|
cards: 0,
|
|
extensions: 0,
|
|
};
|
|
|
|
getImportedCount(type) {
|
|
if (!this._importQuantities.hasOwnProperty(type)) {
|
|
throw new Error(
|
|
`Unknown import data type "${type}" passed to getImportedCount`
|
|
);
|
|
}
|
|
return this._importQuantities[type];
|
|
}
|
|
|
|
insertBookmarkWrapper(bookmark) {
|
|
this._importQuantities.bookmarks++;
|
|
let insertionPromise = lazy.PlacesUtils.bookmarks.insert(bookmark);
|
|
if (!gKeepUndoData) {
|
|
return insertionPromise;
|
|
}
|
|
// If we keep undo data, add a promise handler that stores the undo data once
|
|
// the bookmark has been inserted in the DB, and then returns the bookmark.
|
|
let { parentGuid } = bookmark;
|
|
return insertionPromise.then(bm => {
|
|
let { guid, lastModified, type } = bm;
|
|
gUndoData.get("bookmarks").push({
|
|
parentGuid,
|
|
guid,
|
|
lastModified,
|
|
type,
|
|
});
|
|
return bm;
|
|
});
|
|
}
|
|
|
|
insertManyBookmarksWrapper(bookmarks, parent) {
|
|
let insertionPromise = lazy.PlacesUtils.bookmarks.insertTree({
|
|
guid: parent,
|
|
children: bookmarks,
|
|
});
|
|
return insertionPromise.then(
|
|
insertedItems => {
|
|
this._importQuantities.bookmarks += insertedItems.length;
|
|
if (gKeepUndoData) {
|
|
let bmData = gUndoData.get("bookmarks");
|
|
for (let bm of insertedItems) {
|
|
let { parentGuid, guid, lastModified, type } = bm;
|
|
bmData.push({ parentGuid, guid, lastModified, type });
|
|
}
|
|
}
|
|
if (parent == lazy.PlacesUtils.bookmarks.toolbarGuid) {
|
|
lazy.PlacesUIUtils.maybeToggleBookmarkToolbarVisibility(
|
|
true /* aForceVisible */
|
|
).catch(console.error);
|
|
}
|
|
},
|
|
ex => console.error(ex)
|
|
);
|
|
}
|
|
|
|
insertVisitsWrapper(pageInfos) {
|
|
let now = new Date();
|
|
// Ensure that none of the dates are in the future. If they are, rewrite
|
|
// them to be now. This means we don't loose history entries, but they will
|
|
// be valid for the history store.
|
|
for (let pageInfo of pageInfos) {
|
|
for (let visit of pageInfo.visits) {
|
|
if (visit.date && visit.date > now) {
|
|
visit.date = now;
|
|
}
|
|
}
|
|
}
|
|
this._importQuantities.history += pageInfos.length;
|
|
if (gKeepUndoData) {
|
|
this.#updateHistoryUndo(pageInfos);
|
|
}
|
|
return lazy.PlacesUtils.history.insertMany(pageInfos);
|
|
}
|
|
|
|
async insertLoginsWrapper(logins) {
|
|
this._importQuantities.logins += logins.length;
|
|
let inserted = await lazy.LoginHelper.maybeImportLogins(logins);
|
|
// Note that this means that if we import a login that has a newer password
|
|
// than we know about, we will update the login, and an undo of the import
|
|
// will not revert this. This seems preferable over removing the login
|
|
// outright or storing the old password in the undo file.
|
|
if (gKeepUndoData) {
|
|
for (let { guid, timePasswordChanged } of inserted) {
|
|
gUndoData.get("logins").push({ guid, timePasswordChanged });
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Iterates through the favicons, sniffs for a mime type,
|
|
* and uses the mime type to properly import the favicon.
|
|
*
|
|
* Note: You may not want to await on the returned promise, especially if by
|
|
* doing so there's risk of interrupting the migration of more critical
|
|
* data (e.g. bookmarks).
|
|
*
|
|
* @param {object[]} favicons
|
|
* An array of Objects with these properties:
|
|
* {Uint8Array} faviconData: The binary data of a favicon
|
|
* {nsIURI} uri: The URI of the associated page
|
|
*/
|
|
async insertManyFavicons(favicons) {
|
|
let sniffer = Cc["@mozilla.org/image/loader;1"].createInstance(
|
|
Ci.nsIContentSniffer
|
|
);
|
|
|
|
for (let faviconDataItem of favicons) {
|
|
try {
|
|
// getMIMETypeFromContent throws error if could not get the mime type
|
|
// from the data.
|
|
let mimeType = sniffer.getMIMETypeFromContent(
|
|
null,
|
|
faviconDataItem.faviconData,
|
|
faviconDataItem.faviconData.length
|
|
);
|
|
|
|
let dataURL = await new Promise((resolve, reject) => {
|
|
let buffer = new Uint8ClampedArray(faviconDataItem.faviconData);
|
|
let blob = new Blob([buffer], { type: mimeType });
|
|
let reader = new FileReader();
|
|
reader.addEventListener("load", () => resolve(reader.result));
|
|
reader.addEventListener("error", reject);
|
|
reader.readAsDataURL(blob);
|
|
});
|
|
|
|
let fakeFaviconURI = Services.io.newURI(
|
|
"fake-favicon-uri:" + faviconDataItem.uri.spec
|
|
);
|
|
lazy.PlacesUtils.favicons
|
|
.setFaviconForPage(
|
|
faviconDataItem.uri,
|
|
fakeFaviconURI,
|
|
Services.io.newURI(dataURL)
|
|
)
|
|
.catch(console.warn);
|
|
} catch (e) {
|
|
// Even if error happens for favicon, continue the process.
|
|
console.warn(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
async insertCreditCardsWrapper(cards) {
|
|
this._importQuantities.cards += cards.length;
|
|
let { formAutofillStorage } = ChromeUtils.importESModule(
|
|
"resource://autofill/FormAutofillStorage.sys.mjs"
|
|
);
|
|
|
|
await formAutofillStorage.initialize();
|
|
for (let card of cards) {
|
|
try {
|
|
await formAutofillStorage.creditCards.add(card);
|
|
} catch (e) {
|
|
console.error("Failed to insert credit card due to error: ", e, card);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Responsible for calling the AddonManager API that ultimately installs the
|
|
* matched add-ons.
|
|
*
|
|
* @param {string} migratorKey a migrator key that we pass to
|
|
* `AMBrowserExtensionsImport` as the "browser
|
|
* identifier" used to match add-ons
|
|
* @param {string[]} extensionIDs a list of extension IDs from another browser
|
|
* @returns {(lazy.MigrationWizardConstants.PROGRESS_VALUE|string[])[]}
|
|
* An array whose first element is a `MigrationWizardConstants.PROGRESS_VALUE`
|
|
* and second element is an array of imported add-on ids.
|
|
*/
|
|
async installExtensionsWrapper(migratorKey, extensionIDs) {
|
|
const totalExtensions = extensionIDs.length;
|
|
|
|
let importedAddonIDs = [];
|
|
try {
|
|
const result = await lazy.AMBrowserExtensionsImport.stageInstalls(
|
|
migratorKey,
|
|
extensionIDs
|
|
);
|
|
importedAddonIDs = result.importedAddonIDs;
|
|
} catch (e) {
|
|
console.error(`Failed to import extensions: ${e}`);
|
|
}
|
|
|
|
this._importQuantities.extensions += importedAddonIDs.length;
|
|
|
|
if (!importedAddonIDs.length) {
|
|
return [
|
|
lazy.MigrationWizardConstants.PROGRESS_VALUE.WARNING,
|
|
importedAddonIDs,
|
|
];
|
|
}
|
|
if (totalExtensions == importedAddonIDs.length) {
|
|
return [
|
|
lazy.MigrationWizardConstants.PROGRESS_VALUE.SUCCESS,
|
|
importedAddonIDs,
|
|
];
|
|
}
|
|
return [
|
|
lazy.MigrationWizardConstants.PROGRESS_VALUE.INFO,
|
|
importedAddonIDs,
|
|
];
|
|
}
|
|
|
|
initializeUndoData() {
|
|
gKeepUndoData = true;
|
|
gUndoData = new Map([
|
|
["bookmarks", []],
|
|
["visits", []],
|
|
["logins", []],
|
|
]);
|
|
}
|
|
|
|
async #postProcessUndoData(state) {
|
|
if (!state) {
|
|
return state;
|
|
}
|
|
let bookmarkFolders = state
|
|
.get("bookmarks")
|
|
.filter(b => b.type == lazy.PlacesUtils.bookmarks.TYPE_FOLDER);
|
|
|
|
let bookmarkFolderData = [];
|
|
let bmPromises = bookmarkFolders.map(({ guid }) => {
|
|
// Ignore bookmarks where the promise doesn't resolve (ie that are missing)
|
|
// Also check that the bookmark fetch returns isn't null before adding it.
|
|
return lazy.PlacesUtils.bookmarks.fetch(guid).then(
|
|
bm => bm && bookmarkFolderData.push(bm),
|
|
() => {}
|
|
);
|
|
});
|
|
|
|
await Promise.all(bmPromises);
|
|
let folderLMMap = new Map(
|
|
bookmarkFolderData.map(b => [b.guid, b.lastModified])
|
|
);
|
|
for (let bookmark of bookmarkFolders) {
|
|
let lastModified = folderLMMap.get(bookmark.guid);
|
|
// If the bookmark was deleted, the map will be returning null, so check:
|
|
if (lastModified) {
|
|
bookmark.lastModified = lastModified;
|
|
}
|
|
}
|
|
return state;
|
|
}
|
|
|
|
stopAndRetrieveUndoData() {
|
|
let undoData = gUndoData;
|
|
gUndoData = null;
|
|
gKeepUndoData = false;
|
|
return this.#postProcessUndoData(undoData);
|
|
}
|
|
|
|
#updateHistoryUndo(pageInfos) {
|
|
let visits = gUndoData.get("visits");
|
|
let visitMap = new Map(visits.map(v => [v.url, v]));
|
|
for (let pageInfo of pageInfos) {
|
|
let visitCount = pageInfo.visits.length;
|
|
let first, last;
|
|
if (visitCount > 1) {
|
|
let dates = pageInfo.visits.map(v => v.date);
|
|
first = Math.min.apply(Math, dates);
|
|
last = Math.max.apply(Math, dates);
|
|
} else {
|
|
first = last = pageInfo.visits[0].date;
|
|
}
|
|
let url = pageInfo.url;
|
|
if (url instanceof Ci.nsIURI) {
|
|
url = pageInfo.url.spec;
|
|
} else if (typeof url != "string") {
|
|
pageInfo.url.href;
|
|
}
|
|
|
|
try {
|
|
new URL(url);
|
|
} catch (ex) {
|
|
// This won't save and we won't need to 'undo' it, so ignore this URL.
|
|
continue;
|
|
}
|
|
if (!visitMap.has(url)) {
|
|
visitMap.set(url, { url, visitCount, first, last });
|
|
} else {
|
|
let currentData = visitMap.get(url);
|
|
currentData.visitCount += visitCount;
|
|
currentData.first = Math.min(currentData.first, first);
|
|
currentData.last = Math.max(currentData.last, last);
|
|
}
|
|
}
|
|
gUndoData.set("visits", Array.from(visitMap.values()));
|
|
}
|
|
|
|
/**
|
|
* Cleans up references to migrators and nsIProfileInstance instances.
|
|
*/
|
|
finishMigration() {
|
|
gMigrators = null;
|
|
gProfileStartup = null;
|
|
gL10n = null;
|
|
}
|
|
|
|
get availableMigratorKeys() {
|
|
return [...this.#migrators.keys()];
|
|
}
|
|
|
|
get availableFileMigrators() {
|
|
return [...this.#fileMigrators.values()];
|
|
}
|
|
|
|
/**
|
|
* Enum for the entrypoint that is being used to start migration.
|
|
* Callers can use the MIGRATION_ENTRYPOINTS getter to use these.
|
|
*
|
|
* These values are what's written into the
|
|
* FX_MIGRATION_ENTRY_POINT_CATEGORICAL histogram after a migration.
|
|
*
|
|
* @see MIGRATION_ENTRYPOINTS
|
|
* @readonly
|
|
* @enum {string}
|
|
*/
|
|
#MIGRATION_ENTRYPOINTS_ENUM = Object.freeze({
|
|
/** The entrypoint was not supplied */
|
|
UNKNOWN: "unknown",
|
|
|
|
/** Migration is occurring at startup */
|
|
FIRSTRUN: "firstrun",
|
|
|
|
/** Migration is occurring at after a profile refresh */
|
|
FXREFRESH: "fxrefresh",
|
|
|
|
/** Migration is being started from the Library window */
|
|
PLACES: "places",
|
|
|
|
/** Migration is being started from our password management UI */
|
|
PASSWORDS: "passwords",
|
|
|
|
/** Migration is being started from the default about:home/about:newtab */
|
|
NEWTAB: "newtab",
|
|
|
|
/** Migration is being started from the File menu */
|
|
FILE_MENU: "file_menu",
|
|
|
|
/** Migration is being started from the Help menu */
|
|
HELP_MENU: "help_menu",
|
|
|
|
/** Migration is being started from the Bookmarks Toolbar */
|
|
BOOKMARKS_TOOLBAR: "bookmarks_toolbar",
|
|
|
|
/** Migration is being started from about:preferences */
|
|
PREFERENCES: "preferences",
|
|
|
|
/** Migration is being started from about:firefoxview */
|
|
FIREFOX_VIEW: "firefox_view",
|
|
});
|
|
|
|
/**
|
|
* Returns an enum that should be used to record the entrypoint for
|
|
* starting a migration.
|
|
*
|
|
* @returns {number}
|
|
*/
|
|
get MIGRATION_ENTRYPOINTS() {
|
|
return this.#MIGRATION_ENTRYPOINTS_ENUM;
|
|
}
|
|
|
|
/**
|
|
* Enum for the numeric value written to the FX_MIGRATION_SOURCE_BROWSER.
|
|
* histogram
|
|
*
|
|
* @see getSourceIdForTelemetry
|
|
* @readonly
|
|
* @enum {number}
|
|
*/
|
|
#SOURCE_NAME_TO_ID_MAPPING_ENUM = Object.freeze({
|
|
nothing: 1,
|
|
firefox: 2,
|
|
edge: 3,
|
|
ie: 4,
|
|
chrome: 5,
|
|
"chrome-beta": 5,
|
|
"chrome-dev": 5,
|
|
chromium: 6,
|
|
canary: 7,
|
|
safari: 8,
|
|
"chromium-360se": 9,
|
|
"chromium-edge": 10,
|
|
"chromium-edge-beta": 10,
|
|
brave: 11,
|
|
opera: 12,
|
|
"opera-gx": 14,
|
|
vivaldi: 13,
|
|
});
|
|
|
|
getSourceIdForTelemetry(sourceName) {
|
|
return this.#SOURCE_NAME_TO_ID_MAPPING_ENUM[sourceName] || 0;
|
|
}
|
|
|
|
get HISTORY_MAX_AGE_IN_MILLISECONDS() {
|
|
return this.HISTORY_MAX_AGE_IN_DAYS * 24 * 60 * 60 * 1000;
|
|
}
|
|
|
|
/**
|
|
* Determines whether or not the underlying platform supports creating
|
|
* native file pickers that can do folder selection, which is a
|
|
* pre-requisite for getting read-access permissions for data from other
|
|
* browsers that we can import from.
|
|
*
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
canGetPermissionsOnPlatform() {
|
|
return lazy.gCanGetPermissionsOnPlatformPromise;
|
|
}
|
|
}
|
|
|
|
const MigrationUtilsSingleton = new MigrationUtils();
|
|
|
|
export { MigrationUtilsSingleton as MigrationUtils };
|