Bug 1944416: Restore individual tabs from closed groups in closed windows r=dao,sessi...
[gecko.git] / browser / components / migration / MigrationUtils.sys.mjs
blob8c3deb37646ed7c02130fa271546f6a409ccf53e
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
8 const lazy = {};
10 ChromeUtils.defineESModuleGetters(lazy, {
11   AMBrowserExtensionsImport: "resource://gre/modules/AddonManager.sys.mjs",
12   LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
13   PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs",
14   PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
15   Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
16   setTimeout: "resource://gre/modules/Timer.sys.mjs",
17   MigrationWizardConstants:
18     "chrome://browser/content/migration/migration-wizard-constants.mjs",
19 });
21 ChromeUtils.defineLazyGetter(
22   lazy,
23   "gCanGetPermissionsOnPlatformPromise",
24   () => {
25     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
26     return fp.isModeSupported(Ci.nsIFilePicker.modeGetFolder);
27   }
30 var gMigrators = null;
31 var gFileMigrators = null;
32 var gProfileStartup = null;
33 var gL10n = null;
35 let gForceExitSpinResolve = false;
36 let gKeepUndoData = false;
37 let gUndoData = null;
39 function getL10n() {
40   if (!gL10n) {
41     gL10n = new Localization(["browser/migrationWizard.ftl"]);
42   }
43   return gL10n;
46 const MIGRATOR_MODULES = Object.freeze({
47   EdgeProfileMigrator: {
48     moduleURI: "resource:///modules/EdgeProfileMigrator.sys.mjs",
49     platforms: ["win"],
50   },
51   FirefoxProfileMigrator: {
52     moduleURI: "resource:///modules/FirefoxProfileMigrator.sys.mjs",
53     platforms: ["linux", "macosx", "win"],
54   },
55   IEProfileMigrator: {
56     moduleURI: "resource:///modules/IEProfileMigrator.sys.mjs",
57     platforms: ["win"],
58   },
59   SafariProfileMigrator: {
60     moduleURI: "resource:///modules/SafariProfileMigrator.sys.mjs",
61     platforms: ["macosx"],
62   },
64   // The following migrators are all variants of the ChromeProfileMigrator
66   BraveProfileMigrator: {
67     moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
68     platforms: ["linux", "macosx", "win"],
69   },
70   CanaryProfileMigrator: {
71     moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
72     platforms: ["macosx", "win"],
73   },
74   ChromeProfileMigrator: {
75     moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
76     platforms: ["linux", "macosx", "win"],
77   },
78   ChromeBetaMigrator: {
79     moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
80     platforms: ["linux", "win"],
81   },
82   ChromeDevMigrator: {
83     moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
84     platforms: ["linux"],
85   },
86   ChromiumProfileMigrator: {
87     moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
88     platforms: ["linux", "macosx", "win"],
89   },
90   Chromium360seMigrator: {
91     moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
92     platforms: ["win"],
93   },
94   ChromiumEdgeMigrator: {
95     moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
96     platforms: ["macosx", "win"],
97   },
98   ChromiumEdgeBetaMigrator: {
99     moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
100     platforms: ["macosx", "win"],
101   },
102   OperaProfileMigrator: {
103     moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
104     platforms: ["linux", "macosx", "win"],
105   },
106   VivaldiProfileMigrator: {
107     moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
108     platforms: ["linux", "macosx", "win"],
109   },
110   OperaGXProfileMigrator: {
111     moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs",
112     platforms: ["macosx", "win"],
113   },
115   InternalTestingProfileMigrator: {
116     moduleURI: "resource:///modules/InternalTestingProfileMigrator.sys.mjs",
117     platforms: ["linux", "macosx", "win"],
118   },
121 const FILE_MIGRATOR_MODULES = Object.freeze({
122   PasswordFileMigrator: {
123     moduleURI: "resource:///modules/FileMigrators.sys.mjs",
124   },
125   BookmarksFileMigrator: {
126     moduleURI: "resource:///modules/FileMigrators.sys.mjs",
127   },
131  * The singleton MigrationUtils service. This service is the primary mechanism
132  * by which migrations from other browsers to this browser occur. The singleton
133  * instance of this class is exported from this module as `MigrationUtils`.
134  */
135 class MigrationUtils {
136   constructor() {
137     XPCOMUtils.defineLazyPreferenceGetter(
138       this,
139       "HISTORY_MAX_AGE_IN_DAYS",
140       "browser.migrate.history.maxAgeInDays",
141       180
142     );
144     ChromeUtils.registerWindowActor("MigrationWizard", {
145       parent: {
146         esModuleURI: "resource:///actors/MigrationWizardParent.sys.mjs",
147       },
149       child: {
150         esModuleURI: "resource:///actors/MigrationWizardChild.sys.mjs",
151         events: {
152           "MigrationWizard:RequestState": { wantUntrusted: true },
153           "MigrationWizard:BeginMigration": { wantUntrusted: true },
154           "MigrationWizard:RequestSafariPermissions": { wantUntrusted: true },
155           "MigrationWizard:SelectSafariPasswordFile": { wantUntrusted: true },
156           "MigrationWizard:OpenAboutAddons": { wantUntrusted: true },
157           "MigrationWizard:PermissionsNeeded": { wantUntrusted: true },
158           "MigrationWizard:GetPermissions": { wantUntrusted: true },
159           "MigrationWizard:OpenURL": { wantUntrusted: true },
160         },
161       },
163       includeChrome: true,
164       allFrames: true,
165       matches: [
166         "about:welcome",
167         "about:welcome?*",
168         "about:preferences",
169         "about:settings",
170         "chrome://browser/content/migration/migration-dialog-window.html",
171         "chrome://browser/content/spotlight.html",
172         "about:firefoxview",
173       ],
174     });
176     ChromeUtils.defineLazyGetter(this, "IS_LINUX_SNAP_PACKAGE", () => {
177       if (
178         AppConstants.platform != "linux" ||
179         !Cc["@mozilla.org/gio-service;1"]
180       ) {
181         return false;
182       }
184       let gIOSvc = Cc["@mozilla.org/gio-service;1"].getService(
185         Ci.nsIGIOService
186       );
187       return gIOSvc.isRunningUnderSnap;
188     });
189   }
191   resourceTypes = Object.freeze({
192     ALL: 0x0000,
193     /* 0x01 used to be used for settings, but was removed. */
194     COOKIES: 0x0002,
195     HISTORY: 0x0004,
196     FORMDATA: 0x0008,
197     PASSWORDS: 0x0010,
198     BOOKMARKS: 0x0020,
199     OTHERDATA: 0x0040,
200     SESSION: 0x0080,
201     PAYMENT_METHODS: 0x0100,
202     EXTENSIONS: 0x0200,
203   });
205   /**
206    * Helper for implementing simple asynchronous cases of migration resources'
207    * ``migrate(aCallback)`` (see MigratorBase).  If your ``migrate`` method
208    * just waits for some file to be read, for example, and then migrates
209    * everything right away, you can wrap the async-function with this helper
210    * and not worry about notifying the callback.
211    *
212    * @example
213    * // For example, instead of writing:
214    * setTimeout(function() {
215    *   try {
216    *     ....
217    *     aCallback(true);
218    *   }
219    *   catch() {
220    *     aCallback(false);
221    *   }
222    * }, 0);
223    *
224    * // You may write:
225    * setTimeout(MigrationUtils.wrapMigrateFunction(function() {
226    *   if (importingFromMosaic)
227    *     throw Cr.NS_ERROR_UNEXPECTED;
228    * }, aCallback), 0);
229    *
230    * // ... and aCallback will be called with aSuccess=false when importing
231    * // from Mosaic, or with aSuccess=true otherwise.
232    *
233    * @param {Function} aFunction
234    *   the function that will be called sometime later.  If aFunction
235    *   throws when it's called, aCallback(false) is called, otherwise
236    *   aCallback(true) is called.
237    * @param {Function} aCallback
238    *   the callback function passed to ``migrate``.
239    * @returns {Function}
240    *   the wrapped function.
241    */
242   wrapMigrateFunction(aFunction, aCallback) {
243     return function () {
244       let success = false;
245       try {
246         aFunction.apply(null, arguments);
247         success = true;
248       } catch (ex) {
249         console.error(ex);
250       }
251       // Do not change this to call aCallback directly in try try & catch
252       // blocks, because if aCallback throws, we may end up calling aCallback
253       // twice.
254       aCallback(success);
255     };
256   }
258   /**
259    * Gets localized string corresponding to l10n-id
260    *
261    * @param {string} aKey
262    *   The key of the id of the localization to retrieve.
263    * @param {object} [aArgs=undefined]
264    *   An optional map of arguments to the id.
265    * @returns {Promise<string>}
266    *   A promise that resolves to the retrieved localization.
267    */
268   getLocalizedString(aKey, aArgs) {
269     let l10n = getL10n();
270     return l10n.formatValue(aKey, aArgs);
271   }
273   /**
274    * Get all the rows corresponding to a select query from a database, without
275    * requiring a lock on the database. If fetching data fails (because someone
276    * else tried to write to the DB at the same time, for example), we will
277    * retry the fetch after a 100ms timeout, up to 10 times.
278    *
279    * @param {string} path
280    *   The file path to the database we want to open.
281    * @param {string} description
282    *   A developer-readable string identifying what kind of database we're
283    *   trying to open.
284    * @param {string} selectQuery
285    *   The SELECT query to use to fetch the rows.
286    * @param {Promise} [testDelayPromise]
287    *   An optional promise to await for after the first loop, used in tests.
288    *
289    * @returns {Promise<object[]|Error>}
290    *   A promise that resolves to an array of rows. The promise will be
291    *   rejected if the read/fetch failed even after retrying.
292    */
293   getRowsFromDBWithoutLocks(
294     path,
295     description,
296     selectQuery,
297     testDelayPromise = null
298   ) {
299     let dbOptions = {
300       readOnly: true,
301       ignoreLockingMode: true,
302       path,
303     };
305     const RETRYLIMIT = 10;
306     const RETRYINTERVAL = 100;
307     return (async function innerGetRows() {
308       let rows = null;
309       for (let retryCount = RETRYLIMIT; retryCount; retryCount--) {
310         // Attempt to get the rows. If this succeeds, we will bail out of the loop,
311         // close the database in a failsafe way, and pass the rows back.
312         // If fetching the rows throws, we will wait RETRYINTERVAL ms
313         // and try again. This will repeat a maximum of RETRYLIMIT times.
314         let db;
315         let didOpen = false;
316         let previousExceptionMessage = null;
317         try {
318           db = await lazy.Sqlite.openConnection(dbOptions);
319           didOpen = true;
320           rows = await db.execute(selectQuery);
321           break;
322         } catch (ex) {
323           if (previousExceptionMessage != ex.message) {
324             console.error(ex);
325           }
326           previousExceptionMessage = ex.message;
327           if (ex.name == "NS_ERROR_FILE_CORRUPTED") {
328             break;
329           }
330         } finally {
331           try {
332             if (didOpen) {
333               await db.close();
334             }
335           } catch (ex) {}
336         }
337         await Promise.all([
338           new Promise(resolve => lazy.setTimeout(resolve, RETRYINTERVAL)),
339           testDelayPromise,
340         ]);
341       }
342       if (!rows) {
343         throw new Error(
344           "Couldn't get rows from the " + description + " database."
345         );
346       }
347       return rows;
348     })();
349   }
351   get #migrators() {
352     if (!gMigrators) {
353       gMigrators = new Map();
354       for (let [symbol, { moduleURI, platforms }] of Object.entries(
355         MIGRATOR_MODULES
356       )) {
357         if (platforms.includes(AppConstants.platform)) {
358           let { [symbol]: migratorClass } =
359             ChromeUtils.importESModule(moduleURI);
360           if (gMigrators.has(migratorClass.key)) {
361             console.error(
362               "A pre-existing migrator exists with key " +
363                 `${migratorClass.key}. Not registering.`
364             );
365             continue;
366           }
367           gMigrators.set(migratorClass.key, new migratorClass());
368         }
369       }
370     }
371     return gMigrators;
372   }
374   get #fileMigrators() {
375     if (!gFileMigrators) {
376       gFileMigrators = new Map();
377       for (let [symbol, { moduleURI }] of Object.entries(
378         FILE_MIGRATOR_MODULES
379       )) {
380         let { [symbol]: migratorClass } = ChromeUtils.importESModule(moduleURI);
381         if (gFileMigrators.has(migratorClass.key)) {
382           console.error(
383             "A pre-existing file migrator exists with key " +
384               `${migratorClass.key}. Not registering.`
385           );
386           continue;
387         }
388         gFileMigrators.set(migratorClass.key, new migratorClass());
389       }
390     }
391     return gFileMigrators;
392   }
394   forceExitSpinResolve() {
395     gForceExitSpinResolve = true;
396   }
398   spinResolve(promise) {
399     if (!(promise instanceof Promise)) {
400       return promise;
401     }
402     let done = false;
403     let result = null;
404     let error = null;
405     gForceExitSpinResolve = false;
406     promise
407       .catch(e => {
408         error = e;
409       })
410       .then(r => {
411         result = r;
412         done = true;
413       });
415     Services.tm.spinEventLoopUntil(
416       "MigrationUtils.sys.mjs:MU_spinResolve",
417       () => done || gForceExitSpinResolve
418     );
419     if (!done) {
420       throw new Error("Forcefully exited event loop.");
421     } else if (error) {
422       throw error;
423     } else {
424       return result;
425     }
426   }
428   /**
429    * Returns the migrator for the given source, if any data is available
430    * for this source, or if permissions are required in order to read
431    * data from this source. Returns null otherwise.
432    *
433    * @param {string} aKey
434    *   Internal name of the migration source. See `availableMigratorKeys`
435    *   for supported values by OS.
436    * @returns {Promise<MigratorBase|null>}
437    *   A profile migrator implementing nsIBrowserProfileMigrator, if it can
438    *   import any data, null otherwise.
439    */
440   async getMigrator(aKey) {
441     let migrator = this.#migrators.get(aKey);
442     if (!migrator) {
443       console.error(`Could not find a migrator class for key ${aKey}`);
444       return null;
445     }
447     try {
448       if (!migrator) {
449         return null;
450       }
452       if (
453         (await migrator.isSourceAvailable()) ||
454         (!(await migrator.hasPermissions()) && migrator.canGetPermissions())
455       ) {
456         return migrator;
457       }
459       return null;
460     } catch (ex) {
461       console.error(ex);
462       return null;
463     }
464   }
466   getFileMigrator(aKey) {
467     let migrator = this.#fileMigrators.get(aKey);
468     if (!migrator) {
469       console.error(`Could not find a file migrator class for key ${aKey}`);
470       return null;
471     }
472     return migrator;
473   }
475   /**
476    * Returns true if a migrator is registered with key aKey. No check is made
477    * to determine if a profile exists that the migrator can migrate from.
478    *
479    * @param {string} aKey
480    *   Internal name of the migration source. See `availableMigratorKeys`
481    *   for supported values by OS.
482    * @returns {boolean}
483    */
484   migratorExists(aKey) {
485     return this.#migrators.has(aKey);
486   }
488   /**
489    * Figure out what is the default browser, and if there is a migrator
490    * for it, return that migrator's internal name.
491    *
492    * For the time being, the "internal name" of a migrator is its contract-id
493    * trailer (e.g. ie for @mozilla.org/profile/migrator;1?app=browser&type=ie),
494    * but it will soon be exposed properly.
495    *
496    * @returns {string}
497    */
498   getMigratorKeyForDefaultBrowser() {
499     // Canary uses the same description as Chrome so we can't distinguish them.
500     // Edge Beta on macOS uses "Microsoft Edge" with no "beta" indication.
501     const APP_DESC_TO_KEY = {
502       "Internet Explorer": "ie",
503       "Microsoft Edge": "edge",
504       Safari: "safari",
505       Firefox: "firefox",
506       Nightly: "firefox",
507       Opera: "opera",
508       Vivaldi: "vivaldi",
509       "Opera GX": "opera-gx",
510       "Brave Web Browser": "brave", // Windows, Linux
511       Brave: "brave", // OS X
512       "Google Chrome": "chrome", // Windows, Linux
513       Chrome: "chrome", // OS X
514       Chromium: "chromium", // Windows, OS X
515       "Chromium Web Browser": "chromium", // Linux
516       "360\u5b89\u5168\u6d4f\u89c8\u5668": "chromium-360se",
517     };
519     let key = "";
520     try {
521       let browserDesc = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
522         .getService(Ci.nsIExternalProtocolService)
523         .getApplicationDescription("http");
524       key = APP_DESC_TO_KEY[browserDesc] || "";
525       // Handle devedition, as well as "FirefoxNightly" on OS X.
526       if (!key && browserDesc.startsWith("Firefox")) {
527         key = "firefox";
528       }
529     } catch (ex) {
530       console.error("Could not detect default browser: ", ex);
531     }
533     return key;
534   }
536   /**
537    * True if we're in the process of a startup migration.
538    *
539    * @type {boolean}
540    */
541   get isStartupMigration() {
542     return gProfileStartup != null;
543   }
545   /**
546    * In the case of startup migration, this is set to the nsIProfileStartup
547    * instance passed to ProfileMigrator's migrate.
548    *
549    * @see showMigrationWizard
550    * @type {nsIProfileStartup|null}
551    */
552   get profileStartup() {
553     return gProfileStartup;
554   }
556   /**
557    * Show the migration wizard in about:preferences, or if there is not an existing
558    * browser window open, in a new top-level dialog window.
559    *
560    * NB: If you add new consumers, please add a migration entry point constant to
561    * MIGRATION_ENTRYPOINTS and supply that entrypoint with the entrypoint property
562    * in the aOptions argument.
563    *
564    * @param {Window} [aOpener=null]
565    *   optional; the window that asks to open the wizard.
566    * @param {object} [aOptions=null]
567    *   optional named arguments for the migration wizard.
568    * @param {string} [aOptions.entrypoint=undefined]
569    *   migration entry point constant. See MIGRATION_ENTRYPOINTS.
570    * @param {string} [aOptions.migratorKey=undefined]
571    *   The key for which migrator to use automatically. This is the key that is exposed
572    *   as a static getter on the migrator class.
573    * @param {MigratorBase} [aOptions.migrator=undefined]
574    *   A migrator instance to use automatically.
575    * @param {boolean} [aOptions.isStartupMigration=undefined]
576    *   True if this is a startup migration.
577    * @param {boolean} [aOptions.skipSourceSelection=undefined]
578    *   True if the source selection page of the wizard should be skipped.
579    * @param {string} [aOptions.profileId]
580    *   An identifier for the profile to use when migrating.
581    * @returns {Promise<undefined>}
582    *   If an about:preferences tab can be opened, this will resolve when
583    *   that tab has been switched to. Otherwise, this will resolve
584    *   just after opening the top-level dialog window.
585    */
586   showMigrationWizard(aOpener, aOptions) {
587     // When migration is kicked off from about:welcome, there are
588     // a few different behaviors that we want to test, controlled
589     // by a preference that is instrumented for Nimbus. The pref
590     // has the following possible states:
591     //
592     // "autoclose":
593     //   The user will be directed to the migration wizard in
594     //   about:preferences, but once the wizard is dismissed,
595     //   the tab will close.
596     //
597     // "standalone":
598     //   The migration wizard will open in a new top-level content
599     //   window.
600     //
601     // "default" / other
602     //   The user will be directed to the migration wizard in
603     //   about:preferences. The tab will not close once the
604     //   user closes the wizard.
605     let aboutWelcomeBehavior = Services.prefs.getCharPref(
606       "browser.migrate.content-modal.about-welcome-behavior",
607       "default"
608     );
610     let entrypoint = aOptions.entrypoint || this.MIGRATION_ENTRYPOINTS.UNKNOWN;
611     Glean.browserMigration.entryPointCategorical[entrypoint].add(1);
613     let openStandaloneWindow = blocking => {
614       let features = "dialog,centerscreen,resizable=no";
616       if (blocking) {
617         features += ",modal";
618       }
620       Services.ww.openWindow(
621         aOpener,
622         "chrome://browser/content/migration/migration-dialog-window.html",
623         "_blank",
624         features,
625         {
626           options: aOptions,
627         }
628       );
629       return Promise.resolve();
630     };
632     if (aOptions.isStartupMigration) {
633       // Record that the uninstaller requested a profile refresh
634       if (Services.env.get("MOZ_UNINSTALLER_PROFILE_REFRESH")) {
635         Services.env.set("MOZ_UNINSTALLER_PROFILE_REFRESH", "");
636         Glean.migration.uninstallerProfileRefresh.set(true);
637       }
639       openStandaloneWindow(true /* blocking */);
640       return Promise.resolve();
641     }
643     if (aOpener?.openPreferences) {
644       if (aOptions.entrypoint == this.MIGRATION_ENTRYPOINTS.NEWTAB) {
645         if (aboutWelcomeBehavior == "autoclose") {
646           return aOpener.openPreferences("general-migrate-autoclose");
647         } else if (aboutWelcomeBehavior == "standalone") {
648           openStandaloneWindow(false /* blocking */);
649           return Promise.resolve();
650         }
651       }
652       return aOpener.openPreferences("general-migrate");
653     }
655     // If somehow we failed to open about:preferences, fall back to opening
656     // the top-level window.
657     openStandaloneWindow(false /* blocking */);
658     return Promise.resolve();
659   }
661   /**
662    * Show the migration wizard for startup-migration.  This should only be
663    * called by ProfileMigrator (see ProfileMigrator.js), which implements
664    * nsIProfileMigrator. This runs asynchronously if we are running an
665    * automigration.
666    *
667    * @param {nsIProfileStartup} aProfileStartup
668    *   the nsIProfileStartup instance provided to ProfileMigrator.migrate.
669    * @param {string|null} [aMigratorKey=null]
670    *   If set, the migration wizard will import from the corresponding
671    *   migrator, bypassing the source-selection page.  Otherwise, the
672    *   source-selection page will be displayed, either with the default
673    *   browser selected, if it could be detected and if there is a
674    *   migrator for it, or with the first option selected as a fallback
675    * @param {string|null} [aProfileToMigrate=null]
676    *   If set, the migration wizard will import from the profile indicated.
677    * @throws
678    *   if aMigratorKey is invalid or if it points to a non-existent
679    *   source.
680    */
681   startupMigration(aProfileStartup, aMigratorKey, aProfileToMigrate) {
682     this.spinResolve(
683       this.asyncStartupMigration(
684         aProfileStartup,
685         aMigratorKey,
686         aProfileToMigrate
687       )
688     );
689   }
691   async asyncStartupMigration(
692     aProfileStartup,
693     aMigratorKey,
694     aProfileToMigrate
695   ) {
696     if (!aProfileStartup) {
697       throw new Error(
698         "an profile-startup instance is required for startup-migration"
699       );
700     }
701     gProfileStartup = aProfileStartup;
703     let skipSourceSelection = false,
704       migrator = null,
705       migratorKey = "";
706     if (aMigratorKey) {
707       migrator = await this.getMigrator(aMigratorKey);
708       if (!migrator) {
709         // aMigratorKey must point to a valid source, so, if it doesn't
710         // cleanup and throw.
711         this.finishMigration();
712         throw new Error(
713           "startMigration was asked to open auto-migrate from " +
714             "a non-existent source: " +
715             aMigratorKey
716         );
717       }
718       migratorKey = aMigratorKey;
719       skipSourceSelection = true;
720     } else {
721       let defaultBrowserKey = this.getMigratorKeyForDefaultBrowser();
722       if (defaultBrowserKey) {
723         migrator = await this.getMigrator(defaultBrowserKey);
724         if (migrator) {
725           migratorKey = defaultBrowserKey;
726         }
727       }
728     }
730     if (!migrator) {
731       let migrators = await Promise.all(
732         this.availableMigratorKeys.map(key => this.getMigrator(key))
733       );
734       // If there's no migrator set so far, ensure that there is at least one
735       // migrator available before opening the wizard.
736       // Note that we don't need to check the default browser first, because
737       // if that one existed we would have used it in the block above this one.
738       if (!migrators.some(m => m)) {
739         // None of the keys produced a usable migrator, so finish up here:
740         this.finishMigration();
741         return;
742       }
743     }
745     let isRefresh =
746       migrator &&
747       skipSourceSelection &&
748       migratorKey == AppConstants.MOZ_APP_NAME;
750     let entrypoint = this.MIGRATION_ENTRYPOINTS.FIRSTRUN;
751     if (isRefresh) {
752       entrypoint = this.MIGRATION_ENTRYPOINTS.FXREFRESH;
753     }
755     this.showMigrationWizard(null, {
756       entrypoint,
757       migratorKey,
758       migrator,
759       isStartupMigration: !!aProfileStartup,
760       skipSourceSelection,
761       profileId: aProfileToMigrate,
762     });
763   }
765   /**
766    * This is only pseudo-private because some tests and helper functions
767    * still expect to be able to directly access it.
768    */
769   _importQuantities = {
770     bookmarks: 0,
771     logins: 0,
772     history: 0,
773     cards: 0,
774     extensions: 0,
775   };
777   getImportedCount(type) {
778     if (!this._importQuantities.hasOwnProperty(type)) {
779       throw new Error(
780         `Unknown import data type "${type}" passed to getImportedCount`
781       );
782     }
783     return this._importQuantities[type];
784   }
786   insertBookmarkWrapper(bookmark) {
787     this._importQuantities.bookmarks++;
788     let insertionPromise = lazy.PlacesUtils.bookmarks.insert(bookmark);
789     if (!gKeepUndoData) {
790       return insertionPromise;
791     }
792     // If we keep undo data, add a promise handler that stores the undo data once
793     // the bookmark has been inserted in the DB, and then returns the bookmark.
794     let { parentGuid } = bookmark;
795     return insertionPromise.then(bm => {
796       let { guid, lastModified, type } = bm;
797       gUndoData.get("bookmarks").push({
798         parentGuid,
799         guid,
800         lastModified,
801         type,
802       });
803       return bm;
804     });
805   }
807   insertManyBookmarksWrapper(bookmarks, parent) {
808     let insertionPromise = lazy.PlacesUtils.bookmarks.insertTree({
809       guid: parent,
810       children: bookmarks,
811     });
812     return insertionPromise.then(
813       insertedItems => {
814         this._importQuantities.bookmarks += insertedItems.length;
815         if (gKeepUndoData) {
816           let bmData = gUndoData.get("bookmarks");
817           for (let bm of insertedItems) {
818             let { parentGuid, guid, lastModified, type } = bm;
819             bmData.push({ parentGuid, guid, lastModified, type });
820           }
821         }
822         if (parent == lazy.PlacesUtils.bookmarks.toolbarGuid) {
823           lazy.PlacesUIUtils.maybeToggleBookmarkToolbarVisibility(
824             true /* aForceVisible */
825           ).catch(console.error);
826         }
827       },
828       ex => console.error(ex)
829     );
830   }
832   insertVisitsWrapper(pageInfos) {
833     let now = new Date();
834     // Ensure that none of the dates are in the future. If they are, rewrite
835     // them to be now. This means we don't loose history entries, but they will
836     // be valid for the history store.
837     for (let pageInfo of pageInfos) {
838       for (let visit of pageInfo.visits) {
839         if (visit.date && visit.date > now) {
840           visit.date = now;
841         }
842       }
843     }
844     this._importQuantities.history += pageInfos.length;
845     if (gKeepUndoData) {
846       this.#updateHistoryUndo(pageInfos);
847     }
848     return lazy.PlacesUtils.history.insertMany(pageInfos);
849   }
851   async insertLoginsWrapper(logins) {
852     this._importQuantities.logins += logins.length;
853     let inserted = await lazy.LoginHelper.maybeImportLogins(logins);
854     // Note that this means that if we import a login that has a newer password
855     // than we know about, we will update the login, and an undo of the import
856     // will not revert this. This seems preferable over removing the login
857     // outright or storing the old password in the undo file.
858     if (gKeepUndoData) {
859       for (let { guid, timePasswordChanged } of inserted) {
860         gUndoData.get("logins").push({ guid, timePasswordChanged });
861       }
862     }
863   }
865   /**
866    * Iterates through the favicons, sniffs for a mime type,
867    * and uses the mime type to properly import the favicon.
868    *
869    * Note: You may not want to await on the returned promise, especially if by
870    *       doing so there's risk of interrupting the migration of more critical
871    *       data (e.g. bookmarks).
872    *
873    * @param {object[]} favicons
874    *   An array of Objects with these properties:
875    *     {Uint8Array} faviconData: The binary data of a favicon
876    *     {nsIURI} uri: The URI of the associated page
877    */
878   async insertManyFavicons(favicons) {
879     let sniffer = Cc["@mozilla.org/image/loader;1"].createInstance(
880       Ci.nsIContentSniffer
881     );
883     for (let faviconDataItem of favicons) {
884       try {
885         // getMIMETypeFromContent throws error if could not get the mime type
886         // from the data.
887         let mimeType = sniffer.getMIMETypeFromContent(
888           null,
889           faviconDataItem.faviconData,
890           faviconDataItem.faviconData.length
891         );
893         let dataURL = await new Promise((resolve, reject) => {
894           let buffer = new Uint8ClampedArray(faviconDataItem.faviconData);
895           let blob = new Blob([buffer], { type: mimeType });
896           let reader = new FileReader();
897           reader.addEventListener("load", () => resolve(reader.result));
898           reader.addEventListener("error", reject);
899           reader.readAsDataURL(blob);
900         });
902         let fakeFaviconURI = Services.io.newURI(
903           "fake-favicon-uri:" + faviconDataItem.uri.spec
904         );
905         lazy.PlacesUtils.favicons
906           .setFaviconForPage(
907             faviconDataItem.uri,
908             fakeFaviconURI,
909             Services.io.newURI(dataURL)
910           )
911           .catch(console.warn);
912       } catch (e) {
913         // Even if error happens for favicon, continue the process.
914         console.warn(e);
915       }
916     }
917   }
919   async insertCreditCardsWrapper(cards) {
920     this._importQuantities.cards += cards.length;
921     let { formAutofillStorage } = ChromeUtils.importESModule(
922       "resource://autofill/FormAutofillStorage.sys.mjs"
923     );
925     await formAutofillStorage.initialize();
926     for (let card of cards) {
927       try {
928         await formAutofillStorage.creditCards.add(card);
929       } catch (e) {
930         console.error("Failed to insert credit card due to error: ", e, card);
931       }
932     }
933   }
935   /**
936    * Responsible for calling the AddonManager API that ultimately installs the
937    * matched add-ons.
938    *
939    * @param {string} migratorKey a migrator key that we pass to
940    *                             `AMBrowserExtensionsImport` as the "browser
941    *                             identifier" used to match add-ons
942    * @param {string[]} extensionIDs a list of extension IDs from another browser
943    * @returns {(lazy.MigrationWizardConstants.PROGRESS_VALUE|string[])[]}
944    *   An array whose first element is a `MigrationWizardConstants.PROGRESS_VALUE`
945    *   and second element is an array of imported add-on ids.
946    */
947   async installExtensionsWrapper(migratorKey, extensionIDs) {
948     const totalExtensions = extensionIDs.length;
950     let importedAddonIDs = [];
951     try {
952       const result = await lazy.AMBrowserExtensionsImport.stageInstalls(
953         migratorKey,
954         extensionIDs
955       );
956       importedAddonIDs = result.importedAddonIDs;
957     } catch (e) {
958       console.error(`Failed to import extensions: ${e}`);
959     }
961     this._importQuantities.extensions += importedAddonIDs.length;
963     if (!importedAddonIDs.length) {
964       return [
965         lazy.MigrationWizardConstants.PROGRESS_VALUE.WARNING,
966         importedAddonIDs,
967       ];
968     }
969     if (totalExtensions == importedAddonIDs.length) {
970       return [
971         lazy.MigrationWizardConstants.PROGRESS_VALUE.SUCCESS,
972         importedAddonIDs,
973       ];
974     }
975     return [
976       lazy.MigrationWizardConstants.PROGRESS_VALUE.INFO,
977       importedAddonIDs,
978     ];
979   }
981   initializeUndoData() {
982     gKeepUndoData = true;
983     gUndoData = new Map([
984       ["bookmarks", []],
985       ["visits", []],
986       ["logins", []],
987     ]);
988   }
990   async #postProcessUndoData(state) {
991     if (!state) {
992       return state;
993     }
994     let bookmarkFolders = state
995       .get("bookmarks")
996       .filter(b => b.type == lazy.PlacesUtils.bookmarks.TYPE_FOLDER);
998     let bookmarkFolderData = [];
999     let bmPromises = bookmarkFolders.map(({ guid }) => {
1000       // Ignore bookmarks where the promise doesn't resolve (ie that are missing)
1001       // Also check that the bookmark fetch returns isn't null before adding it.
1002       return lazy.PlacesUtils.bookmarks.fetch(guid).then(
1003         bm => bm && bookmarkFolderData.push(bm),
1004         () => {}
1005       );
1006     });
1008     await Promise.all(bmPromises);
1009     let folderLMMap = new Map(
1010       bookmarkFolderData.map(b => [b.guid, b.lastModified])
1011     );
1012     for (let bookmark of bookmarkFolders) {
1013       let lastModified = folderLMMap.get(bookmark.guid);
1014       // If the bookmark was deleted, the map will be returning null, so check:
1015       if (lastModified) {
1016         bookmark.lastModified = lastModified;
1017       }
1018     }
1019     return state;
1020   }
1022   stopAndRetrieveUndoData() {
1023     let undoData = gUndoData;
1024     gUndoData = null;
1025     gKeepUndoData = false;
1026     return this.#postProcessUndoData(undoData);
1027   }
1029   #updateHistoryUndo(pageInfos) {
1030     let visits = gUndoData.get("visits");
1031     let visitMap = new Map(visits.map(v => [v.url, v]));
1032     for (let pageInfo of pageInfos) {
1033       let visitCount = pageInfo.visits.length;
1034       let first, last;
1035       if (visitCount > 1) {
1036         let dates = pageInfo.visits.map(v => v.date);
1037         first = Math.min.apply(Math, dates);
1038         last = Math.max.apply(Math, dates);
1039       } else {
1040         first = last = pageInfo.visits[0].date;
1041       }
1042       let url = pageInfo.url;
1043       if (url instanceof Ci.nsIURI) {
1044         url = pageInfo.url.spec;
1045       } else if (typeof url != "string") {
1046         pageInfo.url.href;
1047       }
1049       try {
1050         new URL(url);
1051       } catch (ex) {
1052         // This won't save and we won't need to 'undo' it, so ignore this URL.
1053         continue;
1054       }
1055       if (!visitMap.has(url)) {
1056         visitMap.set(url, { url, visitCount, first, last });
1057       } else {
1058         let currentData = visitMap.get(url);
1059         currentData.visitCount += visitCount;
1060         currentData.first = Math.min(currentData.first, first);
1061         currentData.last = Math.max(currentData.last, last);
1062       }
1063     }
1064     gUndoData.set("visits", Array.from(visitMap.values()));
1065   }
1067   /**
1068    * Cleans up references to migrators and nsIProfileInstance instances.
1069    */
1070   finishMigration() {
1071     gMigrators = null;
1072     gProfileStartup = null;
1073     gL10n = null;
1074   }
1076   get availableMigratorKeys() {
1077     return [...this.#migrators.keys()];
1078   }
1080   get availableFileMigrators() {
1081     return [...this.#fileMigrators.values()];
1082   }
1084   /**
1085    * Enum for the entrypoint that is being used to start migration.
1086    * Callers can use the MIGRATION_ENTRYPOINTS getter to use these.
1087    *
1088    * These values are what's written into the
1089    * FX_MIGRATION_ENTRY_POINT_CATEGORICAL histogram after a migration.
1090    *
1091    * @see MIGRATION_ENTRYPOINTS
1092    * @readonly
1093    * @enum {string}
1094    */
1095   #MIGRATION_ENTRYPOINTS_ENUM = Object.freeze({
1096     /** The entrypoint was not supplied */
1097     UNKNOWN: "unknown",
1099     /** Migration is occurring at startup */
1100     FIRSTRUN: "firstrun",
1102     /** Migration is occurring at after a profile refresh */
1103     FXREFRESH: "fxrefresh",
1105     /** Migration is being started from the Library window */
1106     PLACES: "places",
1108     /** Migration is being started from our password management UI */
1109     PASSWORDS: "passwords",
1111     /** Migration is being started from the default about:home/about:newtab */
1112     NEWTAB: "newtab",
1114     /** Migration is being started from the File menu */
1115     FILE_MENU: "file_menu",
1117     /** Migration is being started from the Help menu */
1118     HELP_MENU: "help_menu",
1120     /** Migration is being started from the Bookmarks Toolbar */
1121     BOOKMARKS_TOOLBAR: "bookmarks_toolbar",
1123     /** Migration is being started from about:preferences */
1124     PREFERENCES: "preferences",
1126     /** Migration is being started from about:firefoxview */
1127     FIREFOX_VIEW: "firefox_view",
1128   });
1130   /**
1131    * Returns an enum that should be used to record the entrypoint for
1132    * starting a migration.
1133    *
1134    * @returns {number}
1135    */
1136   get MIGRATION_ENTRYPOINTS() {
1137     return this.#MIGRATION_ENTRYPOINTS_ENUM;
1138   }
1140   /**
1141    * Enum for the numeric value written to the FX_MIGRATION_SOURCE_BROWSER.
1142    * histogram
1143    *
1144    * @see getSourceIdForTelemetry
1145    * @readonly
1146    * @enum {number}
1147    */
1148   #SOURCE_NAME_TO_ID_MAPPING_ENUM = Object.freeze({
1149     nothing: 1,
1150     firefox: 2,
1151     edge: 3,
1152     ie: 4,
1153     chrome: 5,
1154     "chrome-beta": 5,
1155     "chrome-dev": 5,
1156     chromium: 6,
1157     canary: 7,
1158     safari: 8,
1159     "chromium-360se": 9,
1160     "chromium-edge": 10,
1161     "chromium-edge-beta": 10,
1162     brave: 11,
1163     opera: 12,
1164     "opera-gx": 14,
1165     vivaldi: 13,
1166   });
1168   getSourceIdForTelemetry(sourceName) {
1169     return this.#SOURCE_NAME_TO_ID_MAPPING_ENUM[sourceName] || 0;
1170   }
1172   get HISTORY_MAX_AGE_IN_MILLISECONDS() {
1173     return this.HISTORY_MAX_AGE_IN_DAYS * 24 * 60 * 60 * 1000;
1174   }
1176   /**
1177    * Determines whether or not the underlying platform supports creating
1178    * native file pickers that can do folder selection, which is a
1179    * pre-requisite for getting read-access permissions for data from other
1180    * browsers that we can import from.
1181    *
1182    * @returns {Promise<boolean>}
1183    */
1184   canGetPermissionsOnPlatform() {
1185     return lazy.gCanGetPermissionsOnPlatformPromise;
1186   }
1189 const MigrationUtilsSingleton = new MigrationUtils();
1191 export { MigrationUtilsSingleton as MigrationUtils };