/* 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/. */ 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 lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { BookmarkHTMLUtils: "resource://gre/modules/BookmarkHTMLUtils.sys.mjs", FirefoxProfileMigrator: "resource:///modules/FirefoxProfileMigrator.sys.mjs", MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs", PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", ResponsivenessMonitor: "resource://gre/modules/ResponsivenessMonitor.sys.mjs", }); /** * @typedef {object} MigratorResource * A resource returned by a subclass of MigratorBase that can migrate * data to this browser. * @property {number} type * A bitfield with bits from MigrationUtils.resourceTypes flipped to indicate * what this resource represents. A resource can represent one or more types * of data, for example HISTORY and FORMDATA. * @property {Function} migrate * A function that will actually perform the migration of this resource's * data into this browser. */ /** * Shared prototype for migrators. * * To implement a migrator: * 1. Import this module. * 2. Create a subclass of MigratorBase for your new migrator. * 3. Override the `key` static getter with a unique identifier for the browser * that this migrator migrates from. * 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|. * 7. Add the migrator to the MIGRATOR_MODULES structure in MigrationUtils.sys.mjs. */ export class MigratorBase { /** * This must be overridden to return a simple string identifier for the * migrator, for example "firefox", "chrome", "opera-gx". This key is what * is used as an identifier when calling MigrationUtils.getMigrator. * * @type {string} */ static get key() { throw new Error("MigratorBase.key must be overridden."); } /** * This must be overridden to return a Fluent string ID mapping to the display * name for this migrator. These strings should be defined in migrationWizard.ftl. * * @type {string} */ static get displayNameL10nID() { throw new Error("MigratorBase.displayNameL10nID must be overridden."); } /** * This method should get overridden to return an icon url of the browser * to be imported from. By default, this will just use the default Favicon * image. * * @type {string} */ static get brandImage() { return "chrome://global/skin/icons/defaultFavicon.svg"; } /** * 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. * * @abstract * @returns {object[]|null} */ 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 resource types (see * MigrationUtils.resourceTypes). * * - a |migrate| method, taking two arguments, * aCallback(bool success, object details), for migrating the data for * this resource. It may do its job synchronously or asynchronously. * Either way, it must call aCallback(bool aSuccess, object details) * when it's done. In the case of an exception thrown from |migrate|, * it's taken as if aCallback(false, {}) is called. The details * argument is sometimes optional, but conditional on how the * migration wizard wants to display the migration state for the * resource. * * 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 MigrationUtils.resourceTypes, 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. * * @abstract * @param {object|string} _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). * @returns {Promise|MigratorResource[]} */ 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). * * If not overridden, the promise will resolve to the Unix epoch. * * @returns {Promise} * A Promise that resolves to the last used date. */ 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. * * @returns {boolean} * true if the migrator is start-up only. */ get startupOnlyMigrator() { return false; } /** * Returns true if the migrator is configured to be enabled. This is * controlled by the `browser.migrate..enabled` boolean * preference. * * @returns {boolean} * true if the migrator should be shown in the migration wizard. */ get enabled() { let key = this.constructor.key; return Services.prefs.getBoolPref(`browser.migrate.${key}.enabled`, false); } /** * Subclasses should implement this if special checks need to be made to determine * if certain permissions need to be requested before data can be imported. * The returned Promise resolves to true if the required permissions have * been granted and a migration could proceed. * * @returns {Promise} */ async hasPermissions() { return Promise.resolve(true); } /** * Subclasses should implement this if special permissions need to be * requested from the user or the operating system in order to perform * a migration with this MigratorBase. This will be called only if * hasPermissions resolves to false. * * The returned Promise will resolve to true if permissions were successfully * obtained, and false otherwise. Implementors should ensure that if a call * to getPermissions resolves to true, that the MigratorBase will be able to * get read access to all of the resources it needs to do a migration. * * @param {DOMWindow} _win * The top-level DOM window hosting the UI that is requesting the permission. * This can be used to, for example, anchor a file picker window to the * same window that is hosting the migration UI. * @returns {Promise} */ async getPermissions(_win) { return Promise.resolve(true); } /** * @returns {Promise} */ async canGetPermissions() { return Promise.resolve(false); } /** * This method returns a number that is the bitwise OR of all resource * types that are available in aProfile. See MigrationUtils.resourceTypes * for each resource type. * * @param {object|string} aProfile * The profile from which data may be imported, or an empty string * in the case of a single-profile migrator. * @returns {number} */ async 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); } /** * @see MigrationUtils * * @param {number} aItems * A bitfield with bits from MigrationUtils.resourceTypes flipped to indicate * what types of resources should be migrated. * @param {boolean} aStartup * True if this migration is occurring during startup. * @param {object|string} aProfile * The other browser profile that is being migrated from. * @param {Function|null} aProgressCallback * An optional callback that will be fired once a resourceType has finished * migrating. The callback will be passed the numeric representation of the * resource type followed by a boolean indicating whether or not the resource * was migrated successfully and optionally an object containing additional * details. */ async migrate(aItems, aStartup, aProfile, aProgressCallback = () => {}) { let resources = await this.#getMaybeCachedResources(aProfile); if (!resources.length) { throw new Error("migrate called for a non-existent source"); } if (aItems != lazy.MigrationUtils.resourceTypes.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 == lazy.MigrationUtils.resourceTypes.HISTORY) { return template.replace("*", "HISTORY"); } if (resourceType == lazy.MigrationUtils.resourceTypes.BOOKMARKS) { return template.replace("*", "BOOKMARKS"); } if (resourceType == lazy.MigrationUtils.resourceTypes.PASSWORDS) { return template.replace("*", "LOGINS"); } return null; }; let browserKey = this.constructor.key; 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 lazy.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) { console.error(histogramId, ": ", ex); } } } }; let collectQuantityTelemetry = () => { for (let resourceType of Object.keys( lazy.MigrationUtils._importQuantities )) { let histogramId = "FX_MIGRATION_" + resourceType.toUpperCase() + "_QUANTITY"; try { Services.telemetry .getKeyedHistogramById(histogramId) .add( browserKey, lazy.MigrationUtils._importQuantities[resourceType] ); } catch (ex) { console.error(histogramId, ": ", ex); } } }; let collectMigrationTelemetry = resourceType => { // We don't want to collect this if the migration is occurring due to a // profile refresh. if (this.constructor.key == lazy.FirefoxProfileMigrator.key) { return; } let prefKey = null; switch (resourceType) { case lazy.MigrationUtils.resourceTypes.BOOKMARKS: { prefKey = "browser.migrate.interactions.bookmarks"; break; } case lazy.MigrationUtils.resourceTypes.HISTORY: { prefKey = "browser.migrate.interactions.history"; break; } case lazy.MigrationUtils.resourceTypes.PASSWORDS: { prefKey = "browser.migrate.interactions.passwords"; break; } default: { return; } } if (prefKey) { Services.prefs.setBoolPref(prefKey, true); } }; // 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( lazy.MigrationUtils._importQuantities )) { lazy.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 = Promise.withResolvers(); let resourceDone = function (aSuccess, details) { itemResources.delete(res); itemSuccess |= aSuccess; if (itemResources.size == 0) { notify( itemSuccess ? "Migration:ItemAfterMigrate" : "Migration:ItemError", migrationType ); collectMigrationTelemetry(migrationType); aProgressCallback(migrationType, itemSuccess, details); 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) { console.error(ex); resourceDone(false); } await completeDeferred.promise; await unblockMainThread(); } } }; if ( lazy.MigrationUtils.isStartupMigration && !this.startupOnlyMigrator && Services.policies.isAllowed("defaultBookmarks") ) { lazy.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. await (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 lazy.BookmarkHTMLUtils.importFromURL( "chrome://browser/content/default-bookmarks.html", { replace: true, source: lazy.PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP, } ).catch(console.error); // 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; await doMigrate(); })(); return; } await doMigrate(); } /** * Checks to see if one or more profiles exist for the browser that this * migrator migrates from. * * @returns {Promise} * True if one or more profiles exists that this migrator can migrate * resources from. */ async isSourceAvailable() { if (this.startupOnlyMigrator && !lazy.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) { console.error(ex); } return exists; } /*** PRIVATE STUFF - DO NOT OVERRIDE ***/ /** * Returns resources for a particular profile and then caches them for later * lookups. * * @param {object|string} aProfile * The profile that resources are being imported from. * @returns {Promise} */ async #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]; } }