forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			1283 lines
		
	
	
	
		
			40 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1283 lines
		
	
	
	
		
			40 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/. */
 | |
| 
 | |
| "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",
 | |
|       "brave",
 | |
|       "chrome",
 | |
|       "chromium-edge",
 | |
|       "chromium-edge-beta",
 | |
|       "chrome-beta",
 | |
|       "chromium",
 | |
|       "chromium-360se",
 | |
|       "canary",
 | |
|     ];
 | |
|   }
 | |
|   if (AppConstants.platform == "macosx") {
 | |
|     return [
 | |
|       "firefox",
 | |
|       "safari",
 | |
|       "brave",
 | |
|       "chrome",
 | |
|       "chromium-edge",
 | |
|       "chromium-edge-beta",
 | |
|       "chromium",
 | |
|       "canary",
 | |
|     ];
 | |
|   }
 | |
|   if (AppConstants.XP_UNIX) {
 | |
|     return [
 | |
|       "firefox",
 | |
|       "brave",
 | |
|       "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/content/default-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);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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 previousException = { message: null };
 | |
|         try {
 | |
|           db = await Sqlite.openConnection(dbOptions);
 | |
|           didOpen = true;
 | |
|           rows = await db.execute(selectQuery);
 | |
|         } catch (ex) {
 | |
|           if (previousException.message != ex.message) {
 | |
|             Cu.reportError(ex);
 | |
|           }
 | |
|           previousException = ex;
 | |
|         } finally {
 | |
|           try {
 | |
|             if (didOpen) {
 | |
|               await db.close();
 | |
|             }
 | |
|           } catch (ex) {}
 | |
|         }
 | |
|         if (previousException) {
 | |
|           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(
 | |
|       "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.
 | |
|    *
 | |
|    * @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",
 | |
|       "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) {
 | |
|       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,
 | |
|     "chromium-360se": 9,
 | |
|     "chromium-edge": 10,
 | |
|     "chromium-edge-beta": 10,
 | |
|     brave: 11,
 | |
|   },
 | |
|   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,
 | |
| });
 | 
