diff options
Diffstat (limited to 'browser/components/migration/MigrationUtils.sys.mjs')
-rw-r--r-- | browser/components/migration/MigrationUtils.sys.mjs | 1171 |
1 files changed, 1171 insertions, 0 deletions
diff --git a/browser/components/migration/MigrationUtils.sys.mjs b/browser/components/migration/MigrationUtils.sys.mjs new file mode 100644 index 0000000000..57a957d527 --- /dev/null +++ b/browser/components/migration/MigrationUtils.sys.mjs @@ -0,0 +1,1171 @@ +/* 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, { + 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", + WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +var gMigrators = null; +var gFileMigrators = null; +var gProfileStartup = null; +var gL10n = null; +var gPreviousDefaultBrowserKey = ""; + +let gForceExitSpinResolve = false; +let gKeepUndoData = false; +let gUndoData = null; + +function getL10n() { + if (!gL10n) { + gL10n = new Localization(["browser/migration.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 + ); + } + + 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, + }); + + /** + * 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.jsm: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 null otherwise. + * + * 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). + * + * @param {string} aKey + * Internal name of the migration source. See `availableMigratorKeys` + * for supported values by OS. + * + * @returns {MigratorBase} + * 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 { + return migrator && (await migrator.isSourceAvailable()) ? migrator : 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); + } + + // "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 = lazy.WindowsRegistry.readRegKey( + Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + kRegPath, + "OldDefaultBrowserCommand" + ); + if (oldDefault) { + // Remove the key: + lazy.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) { + console.error( + "Could not convert old default browser value to description." + ); + } + } + } + } + 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. On mac, this may just focus the wizard if it's + * already running, in which case aOpener and aOptions are ignored. + * + * 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 the new content-modal migration dialog is enabled and 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 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. + // + // "legacy": + // The legacy migration wizard will open, even if the new migration + // wizard is enabled by default. + // + // "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 aboutWelcomeLegacyBehavior = + aboutWelcomeBehavior == "legacy" && + aOptions.entrypoint == this.MIGRATION_ENTRYPOINTS.NEWTAB; + + if ( + Services.prefs.getBoolPref( + "browser.migrate.content-modal.enabled", + false + ) && + !aOptions?.isStartupMigration && + !aboutWelcomeLegacyBehavior + ) { + let entrypoint = + aOptions.entrypoint || this.MIGRATION_ENTRYPOINTS.UNKNOWN; + Services.telemetry + .getHistogramById("FX_MIGRATION_ENTRY_POINT_CATEGORICAL") + .add(entrypoint); + + let openStandaloneWindow = () => { + const FEATURES = "dialog,centerscreen,resizable=no"; + const win = Services.ww.openWindow( + aOpener, + "chrome://browser/content/migration/migration-dialog-window.html", + "_blank", + FEATURES, + { + onResize: () => { + win.sizeToContent(); + }, + options: aOptions, + } + ); + return Promise.resolve(); + }; + + if (aOptions.isStartupMigration) { + openStandaloneWindow(); + 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(); + 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(); + return Promise.resolve(); + } + // Legacy migration dialog + const DIALOG_URL = "chrome://browser/content/migration/migration.xhtml"; + 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 Promise.resolve(); + } + // On mac, the migration wiazrd should only be modal in the case of + // startup-migration. + features = "centerscreen,chrome,resizable=no"; + } + Services.ww.openWindow(aOpener, DIALOG_URL, "_blank", features, aOptions); + 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 + * (The first option is hardcoded to be the most common browser for + * the OS we run on. See migration.xhtml). + * @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, + }; + + 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. + * + * @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 + */ + insertManyFavicons(favicons) { + let sniffer = Cc["@mozilla.org/image/loader;1"].createInstance( + Ci.nsIContentSniffer + ); + for (let faviconDataItem of favicons) { + let mimeType = sniffer.getMIMETypeFromContent( + null, + faviconDataItem.faviconData, + faviconDataItem.faviconData.length + ); + let fakeFaviconURI = Services.io.newURI( + "fake-favicon-uri:" + faviconDataItem.uri.spec + ); + lazy.PlacesUtils.favicons.replaceFaviconData( + fakeFaviconURI, + faviconDataItem.faviconData, + mimeType + ); + lazy.PlacesUtils.favicons.setAndFetchFaviconForPage( + faviconDataItem.uri, + fakeFaviconURI, + true, + lazy.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + null, + Services.scriptSecurityManager.getSystemPrincipal() + ); + } + } + + 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); + } + } + } + + 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 + * histogram after a migration. + * + * @see MIGRATION_ENTRYPOINTS + * @readonly + * @enum {number} + */ + #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", + }); + + /** + * 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; + } + + /** + * Translates an entrypoint string into the proper numeric value for the legacy + * FX_MIGRATION_ENTRY_POINT histogram. + * + * @param {string} entrypoint + * The entrypoint to translate from MIGRATION_ENTRYPOINTS. + * @returns {number} + * The numeric value for the legacy FX_MIGRATION_ENTRY_POINT histogram. + */ + getLegacyMigrationEntrypoint(entrypoint) { + switch (entrypoint) { + case this.MIGRATION_ENTRYPOINTS.FIRSTRUN: { + return 1; + } + case this.MIGRATION_ENTRYPOINTS.FXREFRESH: { + return 2; + } + case this.MIGRATION_ENTRYPOINTS.PLACES: { + return 3; + } + case this.MIGRATION_ENTRYPOINTS.PASSWORDS: { + return 4; + } + case this.MIGRATION_ENTRYPOINTS.NEWTAB: { + return 5; + } + case this.MIGRATION_ENTRYPOINTS.FILE_MENU: { + return 6; + } + case this.MIGRATION_ENTRYPOINTS.HELP_MENU: { + return 7; + } + case this.MIGRATION_ENTRYPOINTS.BOOKMARKS_TOOLBAR: { + return 8; + } + case this.MIGRATION_ENTRYPOINTS.PREFERENCES: { + return 9; + } + case this.MIGRATION_ENTRYPOINTS.UNKNOWN: + // Intentional fall-through + default: { + return 0; // Unknown + } + } + } + + /** + * Enum for the numeric value written to the FX_MIGRATION_SOURCE_BROWSER, + * and FX_STARTUP_MIGRATION_EXISTING_DEFAULT_BROWSER histograms. + * + * @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; + } +} + +const MigrationUtilsSingleton = new MigrationUtils(); + +export { MigrationUtilsSingleton as MigrationUtils }; |