Backed out 2 changesets (bug 1818237) for causing mochitest/xpc failures on browser...
[gecko.git] / browser / components / migration / MigratorBase.sys.mjs
blobdd02bec8c1a9caeeb7b21e8f52d123835a7c84a9
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 const TOPIC_WILL_IMPORT_BOOKMARKS =
6   "initial-migration-will-import-default-bookmarks";
7 const TOPIC_DID_IMPORT_BOOKMARKS =
8   "initial-migration-did-import-default-bookmarks";
9 const TOPIC_PLACES_DEFAULTS_FINISHED = "places-browser-init-complete";
11 const lazy = {};
13 ChromeUtils.defineESModuleGetters(lazy, {
14   BookmarkHTMLUtils: "resource://gre/modules/BookmarkHTMLUtils.sys.mjs",
15   FirefoxProfileMigrator: "resource:///modules/FirefoxProfileMigrator.sys.mjs",
16   MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs",
17   PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
18   PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
19   ResponsivenessMonitor: "resource://gre/modules/ResponsivenessMonitor.sys.mjs",
20 });
22 /**
23  * @typedef {object} MigratorResource
24  *   A resource returned by a subclass of MigratorBase that can migrate
25  *   data to this browser.
26  * @property {number} type
27  *   A bitfield with bits from MigrationUtils.resourceTypes flipped to indicate
28  *   what this resource represents. A resource can represent one or more types
29  *   of data, for example HISTORY and FORMDATA.
30  * @property {Function} migrate
31  *   A function that will actually perform the migration of this resource's
32  *   data into this browser.
33  */
35 /**
36  * Shared prototype for migrators.
37  *
38  * To implement a migrator:
39  * 1. Import this module.
40  * 2. Create a subclass of MigratorBase for your new migrator.
41  * 3. Override the `key` static getter with a unique identifier for the browser
42  *    that this migrator migrates from.
43  * 4. If the migrator supports multiple profiles, override the sourceProfiles
44  *    Here we default for single-profile migrator.
45  * 5. Implement getResources(aProfile) (see below).
46  * 6. For startup-only migrators, override |startupOnlyMigrator|.
47  * 7. Add the migrator to the MIGRATOR_MODULES structure in MigrationUtils.sys.mjs.
48  */
49 export class MigratorBase {
50   /**
51    * This must be overridden to return a simple string identifier for the
52    * migrator, for example "firefox", "chrome", "opera-gx". This key is what
53    * is used as an identifier when calling MigrationUtils.getMigrator.
54    *
55    * @type {string}
56    */
57   static get key() {
58     throw new Error("MigratorBase.key must be overridden.");
59   }
61   /**
62    * This must be overridden to return a Fluent string ID mapping to the display
63    * name for this migrator. These strings should be defined in migrationWizard.ftl.
64    *
65    * @type {string}
66    */
67   static get displayNameL10nID() {
68     throw new Error("MigratorBase.displayNameL10nID must be overridden.");
69   }
71   /**
72    * This method should get overridden to return an icon url of the browser
73    * to be imported from. By default, this will just use the default Favicon
74    * image.
75    *
76    * @type {string}
77    */
78   static get brandImage() {
79     return "chrome://global/skin/icons/defaultFavicon.svg";
80   }
82   /**
83    * OVERRIDE IF AND ONLY IF the source supports multiple profiles.
84    *
85    * Returns array of profile objects from which data may be imported. The object
86    * should have the following keys:
87    *   id - a unique string identifier for the profile
88    *   name - a pretty name to display to the user in the UI
89    *
90    * Only profiles from which data can be imported should be listed.  Otherwise
91    * the behavior of the migration wizard isn't well-defined.
92    *
93    * For a single-profile source (e.g. safari, ie), this returns null,
94    * and not an empty array.  That is the default implementation.
95    *
96    * @abstract
97    * @returns {object[]|null}
98    */
99   getSourceProfiles() {
100     return null;
101   }
103   /**
104    * MUST BE OVERRIDDEN.
105    *
106    * Returns an array of "migration resources" objects for the given profile,
107    * or for the "default" profile, if the migrator does not support multiple
108    * profiles.
109    *
110    * Each migration resource should provide:
111    * - a |type| getter, returning any of the migration resource types (see
112    *   MigrationUtils.resourceTypes).
113    *
114    * - a |migrate| method, taking two arguments,
115    *   aCallback(bool success, object details), for migrating the data for
116    *   this resource.  It may do its job synchronously or asynchronously.
117    *   Either way, it must call aCallback(bool aSuccess, object details)
118    *   when it's done.  In the case of an exception thrown from |migrate|,
119    *   it's taken as if aCallback(false, {}) is called. The details
120    *   argument is sometimes optional, but conditional on how the
121    *   migration wizard wants to display the migration state for the
122    *   resource.
123    *
124    *   Note: In the case of a simple asynchronous implementation, you may find
125    *   MigrationUtils.wrapMigrateFunction handy for handling aCallback easily.
126    *
127    * For each migration type listed in MigrationUtils.resourceTypes, multiple
128    * migration resources may be provided.  This practice is useful when the
129    * data for a certain migration type is independently stored in few
130    * locations.  For example, the mac version of Safari stores its "reading list"
131    * bookmarks in a separate property list.
132    *
133    * Note that the importation of a particular migration type is reported as
134    * successful if _any_ of its resources succeeded to import (that is, called,
135    * |aCallback(true, {})|).  However, completion-status for a particular migration
136    * type is reported to the UI only once all of its migrators have called
137    * aCallback.
138    *
139    * NOTE: The returned array should only include resources from which data
140    * can be imported.  So, for example, before adding a resource for the
141    * BOOKMARKS migration type, you should check if you should check that the
142    * bookmarks file exists.
143    *
144    * @abstract
145    * @param {object|string} aProfile
146    *  The profile from which data may be imported, or an empty string
147    *  in the case of a single-profile migrator.
148    *  In the case of multiple-profiles migrator, it is guaranteed that
149    *  aProfile is a value returned by the sourceProfiles getter (see
150    *  above).
151    * @returns {Promise<MigratorResource[]>|MigratorResource[]}
152    */
153   // eslint-disable-next-line no-unused-vars
154   getResources(aProfile) {
155     throw new Error("getResources must be overridden");
156   }
158   /**
159    * OVERRIDE in order to provide an estimate of when the last time was
160    * that somebody used the browser. It is OK that this is somewhat fuzzy -
161    * history may not be available (or be wiped or not present due to e.g.
162    * incognito mode).
163    *
164    * If not overridden, the promise will resolve to the Unix epoch.
165    *
166    * @returns {Promise<Date>}
167    *   A Promise that resolves to the last used date.
168    */
169   getLastUsedDate() {
170     return Promise.resolve(new Date(0));
171   }
173   /**
174    * OVERRIDE IF AND ONLY IF the migrator is a startup-only migrator (For now,
175    * that is just the Firefox migrator, see bug 737381).  Default: false.
176    *
177    * Startup-only migrators are different in two ways:
178    * - they may only be used during startup.
179    * - the user-profile is half baked during migration.  The folder exists,
180    *   but it's only accessible through MigrationUtils.profileStartup.
181    *   The migrator can call MigrationUtils.profileStartup.doStartup
182    *   at any point in order to initialize the profile.
183    *
184    * @returns {boolean}
185    *   true if the migrator is start-up only.
186    */
187   get startupOnlyMigrator() {
188     return false;
189   }
191   /**
192    * Returns true if the migrator is configured to be enabled. This is
193    * controlled by the `browser.migrate.<BROWSER_KEY>.enabled` boolean
194    * preference.
195    *
196    * @returns {boolean}
197    *   true if the migrator should be shown in the migration wizard.
198    */
199   get enabled() {
200     let key = this.constructor.key;
201     return Services.prefs.getBoolPref(`browser.migrate.${key}.enabled`, false);
202   }
204   /**
205    * Subclasses should implement this if special checks need to be made to determine
206    * if certain permissions need to be requested before data can be imported.
207    * The returned Promise resolves to true if the required permissions have
208    * been granted and a migration could proceed.
209    *
210    * @returns {Promise<boolean>}
211    */
212   async hasPermissions() {
213     return Promise.resolve(true);
214   }
216   /**
217    * Subclasses should implement this if special permissions need to be
218    * requested from the user or the operating system in order to perform
219    * a migration with this MigratorBase. This will be called only if
220    * hasPermissions resolves to false.
221    *
222    * The returned Promise will resolve to true if permissions were successfully
223    * obtained, and false otherwise. Implementors should ensure that if a call
224    * to getPermissions resolves to true, that the MigratorBase will be able to
225    * get read access to all of the resources it needs to do a migration.
226    *
227    * @param {DOMWindow} win
228    *   The top-level DOM window hosting the UI that is requesting the permission.
229    *   This can be used to, for example, anchor a file picker window to the
230    *   same window that is hosting the migration UI.
231    * @returns {Promise<boolean>}
232    */
233   // eslint-disable-next-line no-unused-vars
234   async getPermissions(win) {
235     return Promise.resolve(true);
236   }
238   /**
239    * This method returns a number that is the bitwise OR of all resource
240    * types that are available in aProfile. See MigrationUtils.resourceTypes
241    * for each resource type.
242    *
243    * @param {object|string} aProfile
244    *   The profile from which data may be imported, or an empty string
245    *   in the case of a single-profile migrator.
246    * @returns {number}
247    */
248   async getMigrateData(aProfile) {
249     let resources = await this.#getMaybeCachedResources(aProfile);
250     if (!resources) {
251       return 0;
252     }
253     let types = resources.map(r => r.type);
254     return types.reduce((a, b) => {
255       a |= b;
256       return a;
257     }, 0);
258   }
260   /**
261    * @see MigrationUtils
262    *
263    * @param {number} aItems
264    *   A bitfield with bits from MigrationUtils.resourceTypes flipped to indicate
265    *   what types of resources should be migrated.
266    * @param {boolean} aStartup
267    *   True if this migration is occurring during startup.
268    * @param {object|string} aProfile
269    *   The other browser profile that is being migrated from.
270    * @param {Function|null} aProgressCallback
271    *   An optional callback that will be fired once a resourceType has finished
272    *   migrating. The callback will be passed the numeric representation of the
273    *   resource type followed by a boolean indicating whether or not the resource
274    *   was migrated successfully and optionally an object containing additional
275    *   details.
276    */
277   async migrate(aItems, aStartup, aProfile, aProgressCallback = () => {}) {
278     let resources = await this.#getMaybeCachedResources(aProfile);
279     if (!resources.length) {
280       throw new Error("migrate called for a non-existent source");
281     }
283     if (aItems != lazy.MigrationUtils.resourceTypes.ALL) {
284       resources = resources.filter(r => aItems & r.type);
285     }
287     // Used to periodically give back control to the main-thread loop.
288     let unblockMainThread = function () {
289       return new Promise(resolve => {
290         Services.tm.dispatchToMainThread(resolve);
291       });
292     };
294     let getHistogramIdForResourceType = (resourceType, template) => {
295       if (resourceType == lazy.MigrationUtils.resourceTypes.HISTORY) {
296         return template.replace("*", "HISTORY");
297       }
298       if (resourceType == lazy.MigrationUtils.resourceTypes.BOOKMARKS) {
299         return template.replace("*", "BOOKMARKS");
300       }
301       if (resourceType == lazy.MigrationUtils.resourceTypes.PASSWORDS) {
302         return template.replace("*", "LOGINS");
303       }
304       return null;
305     };
307     let browserKey = this.constructor.key;
309     let maybeStartTelemetryStopwatch = resourceType => {
310       let histogramId = getHistogramIdForResourceType(
311         resourceType,
312         "FX_MIGRATION_*_IMPORT_MS"
313       );
314       if (histogramId) {
315         TelemetryStopwatch.startKeyed(histogramId, browserKey);
316       }
317       return histogramId;
318     };
320     let maybeStartResponsivenessMonitor = resourceType => {
321       let responsivenessMonitor;
322       let responsivenessHistogramId = getHistogramIdForResourceType(
323         resourceType,
324         "FX_MIGRATION_*_JANK_MS"
325       );
326       if (responsivenessHistogramId) {
327         responsivenessMonitor = new lazy.ResponsivenessMonitor();
328       }
329       return { responsivenessMonitor, responsivenessHistogramId };
330     };
332     let maybeFinishResponsivenessMonitor = (
333       responsivenessMonitor,
334       histogramId
335     ) => {
336       if (responsivenessMonitor) {
337         let accumulatedDelay = responsivenessMonitor.finish();
338         if (histogramId) {
339           try {
340             Services.telemetry
341               .getKeyedHistogramById(histogramId)
342               .add(browserKey, accumulatedDelay);
343           } catch (ex) {
344             console.error(histogramId, ": ", ex);
345           }
346         }
347       }
348     };
350     let collectQuantityTelemetry = () => {
351       for (let resourceType of Object.keys(
352         lazy.MigrationUtils._importQuantities
353       )) {
354         let histogramId =
355           "FX_MIGRATION_" + resourceType.toUpperCase() + "_QUANTITY";
356         try {
357           Services.telemetry
358             .getKeyedHistogramById(histogramId)
359             .add(
360               browserKey,
361               lazy.MigrationUtils._importQuantities[resourceType]
362             );
363         } catch (ex) {
364           console.error(histogramId, ": ", ex);
365         }
366       }
367     };
369     let collectMigrationTelemetry = resourceType => {
370       // We don't want to collect this if the migration is occurring due to a
371       // profile refresh.
372       if (this.constructor.key == lazy.FirefoxProfileMigrator.key) {
373         return;
374       }
376       let prefKey = null;
377       switch (resourceType) {
378         case lazy.MigrationUtils.resourceTypes.BOOKMARKS: {
379           prefKey = "browser.migrate.interactions.bookmarks";
380           break;
381         }
382         case lazy.MigrationUtils.resourceTypes.HISTORY: {
383           prefKey = "browser.migrate.interactions.history";
384           break;
385         }
386         case lazy.MigrationUtils.resourceTypes.PASSWORDS: {
387           prefKey = "browser.migrate.interactions.passwords";
388           break;
389         }
390         default: {
391           return;
392         }
393       }
395       if (prefKey) {
396         Services.prefs.setBoolPref(prefKey, true);
397       }
398     };
400     // Called either directly or through the bookmarks import callback.
401     let doMigrate = async function () {
402       let resourcesGroupedByItems = new Map();
403       resources.forEach(function (resource) {
404         if (!resourcesGroupedByItems.has(resource.type)) {
405           resourcesGroupedByItems.set(resource.type, new Set());
406         }
407         resourcesGroupedByItems.get(resource.type).add(resource);
408       });
410       if (resourcesGroupedByItems.size == 0) {
411         throw new Error("No items to import");
412       }
414       let notify = function (aMsg, aItemType) {
415         Services.obs.notifyObservers(null, aMsg, aItemType);
416       };
418       for (let resourceType of Object.keys(
419         lazy.MigrationUtils._importQuantities
420       )) {
421         lazy.MigrationUtils._importQuantities[resourceType] = 0;
422       }
423       notify("Migration:Started");
424       for (let [migrationType, itemResources] of resourcesGroupedByItems) {
425         notify("Migration:ItemBeforeMigrate", migrationType);
427         let stopwatchHistogramId = maybeStartTelemetryStopwatch(migrationType);
429         let { responsivenessMonitor, responsivenessHistogramId } =
430           maybeStartResponsivenessMonitor(migrationType);
432         let itemSuccess = false;
433         for (let res of itemResources) {
434           let completeDeferred = lazy.PromiseUtils.defer();
435           let resourceDone = function (aSuccess, details) {
436             itemResources.delete(res);
437             itemSuccess |= aSuccess;
438             if (itemResources.size == 0) {
439               notify(
440                 itemSuccess
441                   ? "Migration:ItemAfterMigrate"
442                   : "Migration:ItemError",
443                 migrationType
444               );
445               collectMigrationTelemetry(migrationType);
447               aProgressCallback(migrationType, itemSuccess, details);
449               resourcesGroupedByItems.delete(migrationType);
451               if (stopwatchHistogramId) {
452                 TelemetryStopwatch.finishKeyed(
453                   stopwatchHistogramId,
454                   browserKey
455                 );
456               }
458               maybeFinishResponsivenessMonitor(
459                 responsivenessMonitor,
460                 responsivenessHistogramId
461               );
463               if (resourcesGroupedByItems.size == 0) {
464                 collectQuantityTelemetry();
466                 notify("Migration:Ended");
467               }
468             }
469             completeDeferred.resolve();
470           };
472           // If migrate throws, an error occurred, and the callback
473           // (itemMayBeDone) might haven't been called.
474           try {
475             res.migrate(resourceDone);
476           } catch (ex) {
477             console.error(ex);
478             resourceDone(false);
479           }
481           await completeDeferred.promise;
482           await unblockMainThread();
483         }
484       }
485     };
487     if (
488       lazy.MigrationUtils.isStartupMigration &&
489       !this.startupOnlyMigrator &&
490       Services.policies.isAllowed("defaultBookmarks")
491     ) {
492       lazy.MigrationUtils.profileStartup.doStartup();
493       // First import the default bookmarks.
494       // Note: We do not need to do so for the Firefox migrator
495       // (=startupOnlyMigrator), as it just copies over the places database
496       // from another profile.
497       await (async function () {
498         // Tell nsBrowserGlue we're importing default bookmarks.
499         let browserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService(
500           Ci.nsIObserver
501         );
502         browserGlue.observe(null, TOPIC_WILL_IMPORT_BOOKMARKS, "");
504         // Import the default bookmarks. We ignore whether or not we succeed.
505         await lazy.BookmarkHTMLUtils.importFromURL(
506           "chrome://browser/content/default-bookmarks.html",
507           {
508             replace: true,
509             source: lazy.PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
510           }
511         ).catch(console.error);
513         // We'll tell nsBrowserGlue we've imported bookmarks, but before that
514         // we need to make sure we're going to know when it's finished
515         // initializing places:
516         let placesInitedPromise = new Promise(resolve => {
517           let onPlacesInited = function () {
518             Services.obs.removeObserver(
519               onPlacesInited,
520               TOPIC_PLACES_DEFAULTS_FINISHED
521             );
522             resolve();
523           };
524           Services.obs.addObserver(
525             onPlacesInited,
526             TOPIC_PLACES_DEFAULTS_FINISHED
527           );
528         });
529         browserGlue.observe(null, TOPIC_DID_IMPORT_BOOKMARKS, "");
530         await placesInitedPromise;
531         await doMigrate();
532       })();
533       return;
534     }
535     await doMigrate();
536   }
538   /**
539    * Checks to see if one or more profiles exist for the browser that this
540    * migrator migrates from.
541    *
542    * @returns {Promise<boolean>}
543    *   True if one or more profiles exists that this migrator can migrate
544    *   resources from.
545    */
546   async isSourceAvailable() {
547     if (this.startupOnlyMigrator && !lazy.MigrationUtils.isStartupMigration) {
548       return false;
549     }
551     // For a single-profile source, check if any data is available.
552     // For multiple-profiles source, make sure that at least one
553     // profile is available.
554     let exists = false;
555     try {
556       let profiles = await this.getSourceProfiles();
557       if (!profiles) {
558         let resources = await this.#getMaybeCachedResources("");
559         if (resources && resources.length) {
560           exists = true;
561         }
562       } else {
563         exists = !!profiles.length;
564       }
565     } catch (ex) {
566       console.error(ex);
567     }
568     return exists;
569   }
571   /*** PRIVATE STUFF - DO NOT OVERRIDE ***/
573   /**
574    * Returns resources for a particular profile and then caches them for later
575    * lookups.
576    *
577    * @param {object|string} aProfile
578    *   The profile that resources are being imported from.
579    * @returns {Promise<MigrationResource[]>}
580    */
581   async #getMaybeCachedResources(aProfile) {
582     let profileKey = aProfile ? aProfile.id : "";
583     if (this._resourcesByProfile) {
584       if (profileKey in this._resourcesByProfile) {
585         return this._resourcesByProfile[profileKey];
586       }
587     } else {
588       this._resourcesByProfile = {};
589     }
590     this._resourcesByProfile[profileKey] = await this.getResources(aProfile);
591     return this._resourcesByProfile[profileKey];
592   }