summaryrefslogtreecommitdiffstats
path: root/browser/components/migration/MigrationUtils.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/migration/MigrationUtils.jsm')
-rw-r--r--browser/components/migration/MigrationUtils.jsm1326
1 files changed, 1326 insertions, 0 deletions
diff --git a/browser/components/migration/MigrationUtils.jsm b/browser/components/migration/MigrationUtils.jsm
new file mode 100644
index 0000000000..8d695f9001
--- /dev/null
+++ b/browser/components/migration/MigrationUtils.jsm
@@ -0,0 +1,1326 @@
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["MigrationUtils", "MigratorPrototype"];
+
+const TOPIC_WILL_IMPORT_BOOKMARKS =
+ "initial-migration-will-import-default-bookmarks";
+const TOPIC_DID_IMPORT_BOOKMARKS =
+ "initial-migration-did-import-default-bookmarks";
+const TOPIC_PLACES_DEFAULTS_FINISHED = "places-browser-init-complete";
+
+const { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "BookmarkHTMLUtils",
+ "resource://gre/modules/BookmarkHTMLUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "PromiseUtils",
+ "resource://gre/modules/PromiseUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "ResponsivenessMonitor",
+ "resource://gre/modules/ResponsivenessMonitor.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "Sqlite",
+ "resource://gre/modules/Sqlite.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "WindowsRegistry",
+ "resource://gre/modules/WindowsRegistry.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "setTimeout",
+ "resource://gre/modules/Timer.jsm"
+);
+
+var gMigrators = null;
+var gProfileStartup = null;
+var gL10n = null;
+var gPreviousDefaultBrowserKey = "";
+
+let gForceExitSpinResolve = false;
+let gKeepUndoData = false;
+let gUndoData = null;
+
+XPCOMUtils.defineLazyGetter(this, "gAvailableMigratorKeys", function() {
+ if (AppConstants.platform == "win") {
+ return [
+ "firefox",
+ "edge",
+ "ie",
+ "chrome",
+ "chromium-edge",
+ "chromium-edge-beta",
+ "chrome-beta",
+ "chromium",
+ "360se",
+ "canary",
+ ];
+ }
+ if (AppConstants.platform == "macosx") {
+ return [
+ "firefox",
+ "safari",
+ "chrome",
+ "chromium-edge",
+ "chromium-edge-beta",
+ "chromium",
+ "canary",
+ ];
+ }
+ if (AppConstants.XP_UNIX) {
+ return ["firefox", "chrome", "chrome-beta", "chrome-dev", "chromium"];
+ }
+ return [];
+});
+
+function getL10n() {
+ if (!gL10n) {
+ gL10n = new Localization(["browser/migration.ftl"]);
+ }
+ return gL10n;
+}
+
+/**
+ * Shared prototype for migrators, implementing nsIBrowserProfileMigrator.
+ *
+ * To implement a migrator:
+ * 1. Import this module.
+ * 2. Create the prototype for the migrator, extending MigratorPrototype.
+ * Namely: MosaicMigrator.prototype = Object.create(MigratorPrototype);
+ * 3. Set classDescription, contractID and classID for your migrator, and set
+ * NSGetFactory appropriately.
+ * 4. If the migrator supports multiple profiles, override the sourceProfiles
+ * Here we default for single-profile migrator.
+ * 5. Implement getResources(aProfile) (see below).
+ * 6. For startup-only migrators, override |startupOnlyMigrator|.
+ */
+var MigratorPrototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIBrowserProfileMigrator"]),
+
+ /**
+ * OVERRIDE IF AND ONLY IF the source supports multiple profiles.
+ *
+ * Returns array of profile objects from which data may be imported. The object
+ * should have the following keys:
+ * id - a unique string identifier for the profile
+ * name - a pretty name to display to the user in the UI
+ *
+ * Only profiles from which data can be imported should be listed. Otherwise
+ * the behavior of the migration wizard isn't well-defined.
+ *
+ * For a single-profile source (e.g. safari, ie), this returns null,
+ * and not an empty array. That is the default implementation.
+ */
+ getSourceProfiles() {
+ return null;
+ },
+
+ /**
+ * MUST BE OVERRIDDEN.
+ *
+ * Returns an array of "migration resources" objects for the given profile,
+ * or for the "default" profile, if the migrator does not support multiple
+ * profiles.
+ *
+ * Each migration resource should provide:
+ * - a |type| getter, returning any of the migration types (see
+ * nsIBrowserProfileMigrator).
+ *
+ * - a |migrate| method, taking a single argument, aCallback(bool success),
+ * for migrating the data for this resource. It may do its job
+ * synchronously or asynchronously. Either way, it must call
+ * aCallback(bool aSuccess) when it's done. In the case of an exception
+ * thrown from |migrate|, it's taken as if aCallback(false) is called.
+ *
+ * Note: In the case of a simple asynchronous implementation, you may find
+ * MigrationUtils.wrapMigrateFunction handy for handling aCallback easily.
+ *
+ * For each migration type listed in nsIBrowserProfileMigrator, multiple
+ * migration resources may be provided. This practice is useful when the
+ * data for a certain migration type is independently stored in few
+ * locations. For example, the mac version of Safari stores its "reading list"
+ * bookmarks in a separate property list.
+ *
+ * Note that the importation of a particular migration type is reported as
+ * successful if _any_ of its resources succeeded to import (that is, called,
+ * |aCallback(true)|). However, completion-status for a particular migration
+ * type is reported to the UI only once all of its migrators have called
+ * aCallback.
+ *
+ * @note The returned array should only include resources from which data
+ * can be imported. So, for example, before adding a resource for the
+ * BOOKMARKS migration type, you should check if you should check that the
+ * bookmarks file exists.
+ *
+ * @param aProfile
+ * The profile from which data may be imported, or an empty string
+ * in the case of a single-profile migrator.
+ * In the case of multiple-profiles migrator, it is guaranteed that
+ * aProfile is a value returned by the sourceProfiles getter (see
+ * above).
+ */
+ getResources: function MP_getResources(/* aProfile */) {
+ throw new Error("getResources must be overridden");
+ },
+
+ /**
+ * OVERRIDE in order to provide an estimate of when the last time was
+ * that somebody used the browser. It is OK that this is somewhat fuzzy -
+ * history may not be available (or be wiped or not present due to e.g.
+ * incognito mode).
+ *
+ * @return a Promise that resolves to the last used date.
+ *
+ * @note If not overridden, the promise will resolve to the unix epoch.
+ */
+ getLastUsedDate() {
+ return Promise.resolve(new Date(0));
+ },
+
+ /**
+ * OVERRIDE IF AND ONLY IF the migrator is a startup-only migrator (For now,
+ * that is just the Firefox migrator, see bug 737381). Default: false.
+ *
+ * Startup-only migrators are different in two ways:
+ * - they may only be used during startup.
+ * - the user-profile is half baked during migration. The folder exists,
+ * but it's only accessible through MigrationUtils.profileStartup.
+ * The migrator can call MigrationUtils.profileStartup.doStartup
+ * at any point in order to initialize the profile.
+ */
+ get startupOnlyMigrator() {
+ return false;
+ },
+
+ /**
+ * Override if the data to migrate is locked/in-use and the user should
+ * probably shutdown the source browser.
+ */
+ get sourceLocked() {
+ return false;
+ },
+
+ /**
+ * DO NOT OVERRIDE - After deCOMing migration, the UI will just call
+ * getResources.
+ *
+ * @see nsIBrowserProfileMigrator
+ */
+ getMigrateData: async function MP_getMigrateData(aProfile) {
+ let resources = await this._getMaybeCachedResources(aProfile);
+ if (!resources) {
+ return 0;
+ }
+ let types = resources.map(r => r.type);
+ return types.reduce((a, b) => {
+ a |= b;
+ return a;
+ }, 0);
+ },
+
+ getBrowserKey: function MP_getBrowserKey() {
+ return this.contractID.match(/\=([^\=]+)$/)[1];
+ },
+
+ /**
+ * DO NOT OVERRIDE - After deCOMing migration, the UI will just call
+ * migrate for each resource.
+ *
+ * @see nsIBrowserProfileMigrator
+ */
+ migrate: async function MP_migrate(aItems, aStartup, aProfile) {
+ let resources = await this._getMaybeCachedResources(aProfile);
+ if (!resources.length) {
+ throw new Error("migrate called for a non-existent source");
+ }
+
+ if (aItems != Ci.nsIBrowserProfileMigrator.ALL) {
+ resources = resources.filter(r => aItems & r.type);
+ }
+
+ // Used to periodically give back control to the main-thread loop.
+ let unblockMainThread = function() {
+ return new Promise(resolve => {
+ Services.tm.dispatchToMainThread(resolve);
+ });
+ };
+
+ let getHistogramIdForResourceType = (resourceType, template) => {
+ if (resourceType == MigrationUtils.resourceTypes.HISTORY) {
+ return template.replace("*", "HISTORY");
+ }
+ if (resourceType == MigrationUtils.resourceTypes.BOOKMARKS) {
+ return template.replace("*", "BOOKMARKS");
+ }
+ if (resourceType == MigrationUtils.resourceTypes.PASSWORDS) {
+ return template.replace("*", "LOGINS");
+ }
+ return null;
+ };
+
+ let browserKey = this.getBrowserKey();
+
+ let maybeStartTelemetryStopwatch = resourceType => {
+ let histogramId = getHistogramIdForResourceType(
+ resourceType,
+ "FX_MIGRATION_*_IMPORT_MS"
+ );
+ if (histogramId) {
+ TelemetryStopwatch.startKeyed(histogramId, browserKey);
+ }
+ return histogramId;
+ };
+
+ let maybeStartResponsivenessMonitor = resourceType => {
+ let responsivenessMonitor;
+ let responsivenessHistogramId = getHistogramIdForResourceType(
+ resourceType,
+ "FX_MIGRATION_*_JANK_MS"
+ );
+ if (responsivenessHistogramId) {
+ responsivenessMonitor = new ResponsivenessMonitor();
+ }
+ return { responsivenessMonitor, responsivenessHistogramId };
+ };
+
+ let maybeFinishResponsivenessMonitor = (
+ responsivenessMonitor,
+ histogramId
+ ) => {
+ if (responsivenessMonitor) {
+ let accumulatedDelay = responsivenessMonitor.finish();
+ if (histogramId) {
+ try {
+ Services.telemetry
+ .getKeyedHistogramById(histogramId)
+ .add(browserKey, accumulatedDelay);
+ } catch (ex) {
+ Cu.reportError(histogramId + ": " + ex);
+ }
+ }
+ }
+ };
+
+ let collectQuantityTelemetry = () => {
+ for (let resourceType of Object.keys(MigrationUtils._importQuantities)) {
+ let histogramId =
+ "FX_MIGRATION_" + resourceType.toUpperCase() + "_QUANTITY";
+ try {
+ Services.telemetry
+ .getKeyedHistogramById(histogramId)
+ .add(browserKey, MigrationUtils._importQuantities[resourceType]);
+ } catch (ex) {
+ Cu.reportError(histogramId + ": " + ex);
+ }
+ }
+ };
+
+ // Called either directly or through the bookmarks import callback.
+ let doMigrate = async function() {
+ let resourcesGroupedByItems = new Map();
+ resources.forEach(function(resource) {
+ if (!resourcesGroupedByItems.has(resource.type)) {
+ resourcesGroupedByItems.set(resource.type, new Set());
+ }
+ resourcesGroupedByItems.get(resource.type).add(resource);
+ });
+
+ if (resourcesGroupedByItems.size == 0) {
+ throw new Error("No items to import");
+ }
+
+ let notify = function(aMsg, aItemType) {
+ Services.obs.notifyObservers(null, aMsg, aItemType);
+ };
+
+ for (let resourceType of Object.keys(MigrationUtils._importQuantities)) {
+ MigrationUtils._importQuantities[resourceType] = 0;
+ }
+ notify("Migration:Started");
+ for (let [migrationType, itemResources] of resourcesGroupedByItems) {
+ notify("Migration:ItemBeforeMigrate", migrationType);
+
+ let stopwatchHistogramId = maybeStartTelemetryStopwatch(migrationType);
+
+ let {
+ responsivenessMonitor,
+ responsivenessHistogramId,
+ } = maybeStartResponsivenessMonitor(migrationType);
+
+ let itemSuccess = false;
+ for (let res of itemResources) {
+ let completeDeferred = PromiseUtils.defer();
+ let resourceDone = function(aSuccess) {
+ itemResources.delete(res);
+ itemSuccess |= aSuccess;
+ if (itemResources.size == 0) {
+ notify(
+ itemSuccess
+ ? "Migration:ItemAfterMigrate"
+ : "Migration:ItemError",
+ migrationType
+ );
+ resourcesGroupedByItems.delete(migrationType);
+
+ if (stopwatchHistogramId) {
+ TelemetryStopwatch.finishKeyed(
+ stopwatchHistogramId,
+ browserKey
+ );
+ }
+
+ maybeFinishResponsivenessMonitor(
+ responsivenessMonitor,
+ responsivenessHistogramId
+ );
+
+ if (resourcesGroupedByItems.size == 0) {
+ collectQuantityTelemetry();
+ notify("Migration:Ended");
+ }
+ }
+ completeDeferred.resolve();
+ };
+
+ // If migrate throws, an error occurred, and the callback
+ // (itemMayBeDone) might haven't been called.
+ try {
+ res.migrate(resourceDone);
+ } catch (ex) {
+ Cu.reportError(ex);
+ resourceDone(false);
+ }
+
+ await completeDeferred.promise;
+ await unblockMainThread();
+ }
+ }
+ };
+
+ if (
+ MigrationUtils.isStartupMigration &&
+ !this.startupOnlyMigrator &&
+ Services.policies.isAllowed("defaultBookmarks")
+ ) {
+ MigrationUtils.profileStartup.doStartup();
+ // First import the default bookmarks.
+ // Note: We do not need to do so for the Firefox migrator
+ // (=startupOnlyMigrator), as it just copies over the places database
+ // from another profile.
+ (async function() {
+ // Tell nsBrowserGlue we're importing default bookmarks.
+ let browserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService(
+ Ci.nsIObserver
+ );
+ browserGlue.observe(null, TOPIC_WILL_IMPORT_BOOKMARKS, "");
+
+ // Import the default bookmarks. We ignore whether or not we succeed.
+ await BookmarkHTMLUtils.importFromURL(
+ "chrome://browser/locale/bookmarks.html",
+ {
+ replace: true,
+ source: PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
+ }
+ ).catch(Cu.reportError);
+
+ // We'll tell nsBrowserGlue we've imported bookmarks, but before that
+ // we need to make sure we're going to know when it's finished
+ // initializing places:
+ let placesInitedPromise = new Promise(resolve => {
+ let onPlacesInited = function() {
+ Services.obs.removeObserver(
+ onPlacesInited,
+ TOPIC_PLACES_DEFAULTS_FINISHED
+ );
+ resolve();
+ };
+ Services.obs.addObserver(
+ onPlacesInited,
+ TOPIC_PLACES_DEFAULTS_FINISHED
+ );
+ });
+ browserGlue.observe(null, TOPIC_DID_IMPORT_BOOKMARKS, "");
+ await placesInitedPromise;
+ doMigrate();
+ })();
+ return;
+ }
+ doMigrate();
+ },
+
+ /**
+ * DO NOT OVERRIDE - After deCOMing migration, this code
+ * won't be part of the migrator itself.
+ *
+ * @see nsIBrowserProfileMigrator
+ */
+ async isSourceAvailable() {
+ if (this.startupOnlyMigrator && !MigrationUtils.isStartupMigration) {
+ return false;
+ }
+
+ // For a single-profile source, check if any data is available.
+ // For multiple-profiles source, make sure that at least one
+ // profile is available.
+ let exists = false;
+ try {
+ let profiles = await this.getSourceProfiles();
+ if (!profiles) {
+ let resources = await this._getMaybeCachedResources("");
+ if (resources && resources.length) {
+ exists = true;
+ }
+ } else {
+ exists = !!profiles.length;
+ }
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ return exists;
+ },
+
+ /** * PRIVATE STUFF - DO NOT OVERRIDE ***/
+ _getMaybeCachedResources: async function PMB__getMaybeCachedResources(
+ aProfile
+ ) {
+ let profileKey = aProfile ? aProfile.id : "";
+ if (this._resourcesByProfile) {
+ if (profileKey in this._resourcesByProfile) {
+ return this._resourcesByProfile[profileKey];
+ }
+ } else {
+ this._resourcesByProfile = {};
+ }
+ this._resourcesByProfile[profileKey] = await this.getResources(aProfile);
+ return this._resourcesByProfile[profileKey];
+ },
+};
+
+var MigrationUtils = Object.seal({
+ resourceTypes: {
+ COOKIES: Ci.nsIBrowserProfileMigrator.COOKIES,
+ HISTORY: Ci.nsIBrowserProfileMigrator.HISTORY,
+ FORMDATA: Ci.nsIBrowserProfileMigrator.FORMDATA,
+ PASSWORDS: Ci.nsIBrowserProfileMigrator.PASSWORDS,
+ BOOKMARKS: Ci.nsIBrowserProfileMigrator.BOOKMARKS,
+ OTHERDATA: Ci.nsIBrowserProfileMigrator.OTHERDATA,
+ SESSION: Ci.nsIBrowserProfileMigrator.SESSION,
+ },
+
+ /**
+ * Helper for implementing simple asynchronous cases of migration resources'
+ * |migrate(aCallback)| (see MigratorPrototype). 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.
+ *
+ * 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 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 aCallback
+ * the callback function passed to |migrate|.
+ * @return the wrapped function.
+ */
+ wrapMigrateFunction: function MU_wrapMigrateFunction(aFunction, aCallback) {
+ return function() {
+ let success = false;
+ try {
+ aFunction.apply(null, arguments);
+ success = true;
+ } catch (ex) {
+ Cu.reportError(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 aKey
+ * The key of the id of the localization to retrieve.
+ * @param aArgs
+ * [optional] map of arguments to the id.
+ * @return A promise that resolves to the retrieved localization.
+ */
+ getLocalizedString: function MU_getLocalizedString(aKey, aArgs) {
+ let l10n = getL10n();
+ return l10n.formatValue(aKey, aArgs);
+ },
+
+ _getLocalePropertyForBrowser(browserId) {
+ switch (browserId) {
+ case "chromium-edge":
+ case "edge":
+ return "source-name-edge";
+ case "ie":
+ return "source-name-ie";
+ case "safari":
+ return "source-name-safari";
+ case "canary":
+ return "source-name-canary";
+ case "chrome":
+ return "source-name-chrome";
+ case "chrome-beta":
+ return "source-name-chrome-beta";
+ case "chrome-dev":
+ return "source-name-chrome-dev";
+ case "chromium":
+ return "source-name-chromium";
+ case "chromium-edge-beta":
+ return "source-name-chromium-edge-beta";
+ case "firefox":
+ return "source-name-firefox";
+ case "360se":
+ return "source-name-360se";
+ }
+ return null;
+ },
+
+ /**
+ * Helper for creating a folder for imported bookmarks from a particular
+ * migration source. The folder is created at the end of the given folder.
+ *
+ * @param sourceNameStr
+ * the source name (first letter capitalized). This is used
+ * for reading the localized source name from the migration
+ * bundle (e.g. if aSourceNameStr is Mosaic, this will try to read
+ * sourceNameMosaic from the migration bundle).
+ * @param parentGuid
+ * the GUID of the folder in which the new folder should be created.
+ * @return the GUID of the new folder.
+ */
+ async createImportedBookmarksFolder(sourceNameStr, parentGuid) {
+ let source = await this.getLocalizedString(
+ "source-name-" + sourceNameStr.toLowerCase()
+ );
+ let title = await this.getLocalizedString("imported-bookmarks-source", {
+ source,
+ });
+ return (
+ await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid,
+ title,
+ })
+ ).guid;
+ },
+
+ /**
+ * 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 path
+ * the file path to the database we want to open.
+ * @param description
+ * a developer-readable string identifying what kind of database we're
+ * trying to open.
+ * @param selectQuery
+ * the SELECT query to use to fetch the rows.
+ *
+ * @return 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) {
+ 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 && !rows; 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 exceptionSeen;
+ try {
+ db = await Sqlite.openConnection(dbOptions);
+ didOpen = true;
+ rows = await db.execute(selectQuery);
+ } catch (ex) {
+ if (!exceptionSeen) {
+ Cu.reportError(ex);
+ }
+ exceptionSeen = ex;
+ } finally {
+ try {
+ if (didOpen) {
+ await db.close();
+ }
+ } catch (ex) {}
+ }
+ if (exceptionSeen) {
+ await new Promise(resolve => setTimeout(resolve, RETRYINTERVAL));
+ }
+ }
+ if (!rows) {
+ throw new Error(
+ "Couldn't get rows from the " + description + " database."
+ );
+ }
+ return rows;
+ })();
+ },
+
+ get _migrators() {
+ if (!gMigrators) {
+ gMigrators = new Map();
+ }
+ return gMigrators;
+ },
+
+ forceExitSpinResolve: function MU_forceExitSpinResolve() {
+ gForceExitSpinResolve = true;
+ },
+
+ spinResolve: function MU_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(() => 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 null otherwise.
+ *
+ * @param aKey internal name of the migration source.
+ * See `gAvailableMigratorKeys` for supported values by OS.
+ *
+ * If null is returned, either no data can be imported
+ * for the given migrator, or aMigratorKey is invalid (e.g. ie on mac,
+ * or mosaic everywhere). This method should be used rather than direct
+ * getService for future compatibility (see bug 718280).
+ *
+ * @return profile migrator implementing nsIBrowserProfileMigrator, if it can
+ * import any data, null otherwise.
+ */
+ getMigrator: async function MU_getMigrator(aKey) {
+ let migrator = null;
+ if (this._migrators.has(aKey)) {
+ migrator = this._migrators.get(aKey);
+ } else {
+ try {
+ migrator = Cc[
+ "@mozilla.org/profile/migrator;1?app=browser&type=" + aKey
+ ].createInstance(Ci.nsIBrowserProfileMigrator);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ this._migrators.set(aKey, migrator);
+ }
+
+ try {
+ return migrator && (await migrator.isSourceAvailable()) ? migrator : null;
+ } catch (ex) {
+ Cu.reportError(ex);
+ return null;
+ }
+ },
+
+ /**
+ * 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.
+ */
+ 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",
+ "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": "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) {
+ Cu.reportError("Could not detect default browser: " + ex);
+ }
+
+ // "firefox" is the least useful entry here, and might just be because we've set
+ // ourselves as the default (on Windows 7 and below). In that case, check if we
+ // have a registry key that tells us where to go:
+ if (
+ key == "firefox" &&
+ AppConstants.isPlatformAndVersionAtMost("win", "6.2")
+ ) {
+ // Because we remove the registry key, reading the registry key only works once.
+ // We save the value for subsequent calls to avoid hard-to-trace bugs when multiple
+ // consumers ask for this key.
+ if (gPreviousDefaultBrowserKey) {
+ key = gPreviousDefaultBrowserKey;
+ } else {
+ // We didn't have a saved value, so check the registry.
+ const kRegPath = "Software\\Mozilla\\Firefox";
+ let oldDefault = WindowsRegistry.readRegKey(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ kRegPath,
+ "OldDefaultBrowserCommand"
+ );
+ if (oldDefault) {
+ // Remove the key:
+ WindowsRegistry.removeRegKey(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ kRegPath,
+ "OldDefaultBrowserCommand"
+ );
+ try {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(
+ Ci.nsILocalFileWin
+ );
+ file.initWithCommandLine(oldDefault);
+ key =
+ APP_DESC_TO_KEY[file.getVersionInfoField("FileDescription")] ||
+ key;
+ // Save the value for future callers.
+ gPreviousDefaultBrowserKey = key;
+ } catch (ex) {
+ Cu.reportError(
+ "Could not convert old default browser value to description."
+ );
+ }
+ }
+ }
+ }
+ return key;
+ },
+
+ // Whether or not we're in the process of startup migration
+ 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
+ */
+ get profileStartup() {
+ return gProfileStartup;
+ },
+
+ /**
+ * Show the migration wizard. On mac, this may just focus the wizard if it's
+ * already running, in which case aOpener and aParams are ignored.
+ *
+ * @param {Window} [aOpener]
+ * optional; the window that asks to open the wizard.
+ * @param {Array} [aParams]
+ * optional arguments for the migration wizard, in the form of an array
+ * This is passed as-is for the params argument of
+ * nsIWindowWatcher.openWindow. The array elements we expect are, in
+ * order:
+ * - {Number} migration entry point constant (see below)
+ * - {String} source browser identifier
+ * - {nsIBrowserProfileMigrator} actual migrator object
+ * - {Boolean} whether this is a startup migration
+ * - {Boolean} whether to skip the 'source' page
+ * - {String} an identifier for the profile to use when migrating
+ * NB: If you add new consumers, please add a migration entry point
+ * constant below, and specify at least the first element of the array
+ * (the migration entry point for purposes of telemetry).
+ */
+ showMigrationWizard: function MU_showMigrationWizard(aOpener, aParams) {
+ let features = "chrome,dialog,modal,centerscreen,titlebar,resizable=no";
+ if (AppConstants.platform == "macosx" && !this.isStartupMigration) {
+ let win = Services.wm.getMostRecentWindow("Browser:MigrationWizard");
+ if (win) {
+ win.focus();
+ return;
+ }
+ // On mac, the migration wiazrd should only be modal in the case of
+ // startup-migration.
+ features = "centerscreen,chrome,resizable=no";
+ }
+
+ // nsIWindowWatcher doesn't deal with raw arrays, so we convert the input
+ let params;
+ if (Array.isArray(aParams)) {
+ params = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+ for (let item of aParams) {
+ let comtaminatedVal;
+ if (item && item instanceof Ci.nsISupports) {
+ comtaminatedVal = item;
+ } else {
+ switch (typeof item) {
+ case "boolean":
+ comtaminatedVal = Cc[
+ "@mozilla.org/supports-PRBool;1"
+ ].createInstance(Ci.nsISupportsPRBool);
+ comtaminatedVal.data = item;
+ break;
+ case "number":
+ comtaminatedVal = Cc[
+ "@mozilla.org/supports-PRUint32;1"
+ ].createInstance(Ci.nsISupportsPRUint32);
+ comtaminatedVal.data = item;
+ break;
+ case "string":
+ comtaminatedVal = Cc[
+ "@mozilla.org/supports-cstring;1"
+ ].createInstance(Ci.nsISupportsCString);
+ comtaminatedVal.data = item;
+ break;
+
+ case "undefined":
+ case "object":
+ if (!item) {
+ comtaminatedVal = null;
+ break;
+ }
+ /* intentionally falling through to error out here for
+ non-null/undefined things: */
+ default:
+ throw new Error(
+ "Unexpected parameter type " + typeof item + ": " + item
+ );
+ }
+ }
+ params.appendElement(comtaminatedVal);
+ }
+ } else {
+ params = aParams;
+ }
+
+ Services.ww.openWindow(
+ aOpener,
+ "chrome://browser/content/migration/migration.xhtml",
+ "_blank",
+ features,
+ params
+ );
+ },
+
+ /**
+ * 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 aProfileStartup
+ * the nsIProfileStartup instance provided to ProfileMigrator.migrate.
+ * @param [optional] aMigratorKey
+ * 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
+ * (The first option is hardcoded to be the most common browser for
+ * the OS we run on. See migration.xhtml).
+ * @param [optional] aProfileToMigrate
+ * 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: function MU_startupMigrator(
+ aProfileStartup,
+ aMigratorKey,
+ aProfileToMigrate
+ ) {
+ this.spinResolve(
+ this.asyncStartupMigration(
+ aProfileStartup,
+ aMigratorKey,
+ aProfileToMigrate
+ )
+ );
+ },
+
+ asyncStartupMigration: async function MU_asyncStartupMigrator(
+ aProfileStartup,
+ aMigratorKey,
+ aProfileToMigrate
+ ) {
+ if (!aProfileStartup) {
+ throw new Error(
+ "an profile-startup instance is required for startup-migration"
+ );
+ }
+ gProfileStartup = aProfileStartup;
+
+ let skipSourcePage = 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;
+ skipSourcePage = true;
+ } else {
+ let defaultBrowserKey = this.getMigratorKeyForDefaultBrowser();
+ if (defaultBrowserKey) {
+ migrator = await this.getMigrator(defaultBrowserKey);
+ if (migrator) {
+ migratorKey = defaultBrowserKey;
+ }
+ }
+ }
+
+ if (!migrator) {
+ let migrators = await Promise.all(
+ gAvailableMigratorKeys.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 && skipSourcePage && migratorKey == AppConstants.MOZ_APP_NAME;
+
+ let migrationEntryPoint = this.MIGRATION_ENTRYPOINT_FIRSTRUN;
+ if (isRefresh) {
+ migrationEntryPoint = this.MIGRATION_ENTRYPOINT_FXREFRESH;
+ }
+
+ let params = [
+ migrationEntryPoint,
+ migratorKey,
+ migrator,
+ aProfileStartup,
+ skipSourcePage,
+ aProfileToMigrate,
+ ];
+ this.showMigrationWizard(null, params);
+ },
+
+ _importQuantities: {
+ bookmarks: 0,
+ logins: 0,
+ history: 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 = 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 = 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 });
+ }
+ }
+ },
+ ex => Cu.reportError(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 PlacesUtils.history.insertMany(pageInfos);
+ },
+
+ async insertLoginsWrapper(logins) {
+ this._importQuantities.logins += logins.length;
+ let inserted = await 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 });
+ }
+ }
+ },
+
+ 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 == 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 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: function MU_finishMigration() {
+ gMigrators = null;
+ gProfileStartup = null;
+ gL10n = null;
+ },
+
+ gAvailableMigratorKeys,
+
+ MIGRATION_ENTRYPOINT_UNKNOWN: 0,
+ MIGRATION_ENTRYPOINT_FIRSTRUN: 1,
+ MIGRATION_ENTRYPOINT_FXREFRESH: 2,
+ MIGRATION_ENTRYPOINT_PLACES: 3,
+ MIGRATION_ENTRYPOINT_PASSWORDS: 4,
+ MIGRATION_ENTRYPOINT_NEWTAB: 5,
+ MIGRATION_ENTRYPOINT_FILE_MENU: 6,
+ MIGRATION_ENTRYPOINT_HELP_MENU: 7,
+ MIGRATION_ENTRYPOINT_BOOKMARKS_TOOLBAR: 8,
+
+ _sourceNameToIdMapping: {
+ nothing: 1,
+ firefox: 2,
+ edge: 3,
+ ie: 4,
+ chrome: 5,
+ "chrome-beta": 5,
+ "chrome-dev": 5,
+ chromium: 6,
+ canary: 7,
+ safari: 8,
+ "360se": 9,
+ "chromium-edge": 10,
+ "chromium-edge-beta": 10,
+ },
+ getSourceIdForTelemetry(sourceName) {
+ return this._sourceNameToIdMapping[sourceName] || 0;
+ },
+
+ /* Enum of locations where bookmarks were found in the
+ source browser that we import from */
+ SOURCE_BOOKMARK_ROOTS_BOOKMARKS_TOOLBAR: 1,
+ SOURCE_BOOKMARK_ROOTS_BOOKMARKS_MENU: 2,
+ SOURCE_BOOKMARK_ROOTS_READING_LIST: 4,
+ SOURCE_BOOKMARK_ROOTS_UNFILED: 8,
+});