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";
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",
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.
36 * Shared prototype for migrators.
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.
49 export class MigratorBase {
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.
58 throw new Error("MigratorBase.key must be overridden.");
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.
67 static get displayNameL10nID() {
68 throw new Error("MigratorBase.displayNameL10nID must be overridden.");
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
78 static get brandImage() {
79 return "chrome://global/skin/icons/defaultFavicon.svg";
83 * OVERRIDE IF AND ONLY IF the source supports multiple profiles.
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
90 * Only profiles from which data can be imported should be listed. Otherwise
91 * the behavior of the migration wizard isn't well-defined.
93 * For a single-profile source (e.g. safari, ie), this returns null,
94 * and not an empty array. That is the default implementation.
97 * @returns {object[]|null}
104 * MUST BE OVERRIDDEN.
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
110 * Each migration resource should provide:
111 * - a |type| getter, returning any of the migration resource types (see
112 * MigrationUtils.resourceTypes).
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
124 * Note: In the case of a simple asynchronous implementation, you may find
125 * MigrationUtils.wrapMigrateFunction handy for handling aCallback easily.
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.
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
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.
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
151 * @returns {Promise<MigratorResource[]>|MigratorResource[]}
153 // eslint-disable-next-line no-unused-vars
154 getResources(aProfile) {
155 throw new Error("getResources must be overridden");
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.
164 * If not overridden, the promise will resolve to the Unix epoch.
166 * @returns {Promise<Date>}
167 * A Promise that resolves to the last used date.
170 return Promise.resolve(new Date(0));
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.
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.
185 * true if the migrator is start-up only.
187 get startupOnlyMigrator() {
192 * Returns true if the migrator is configured to be enabled. This is
193 * controlled by the `browser.migrate.<BROWSER_KEY>.enabled` boolean
197 * true if the migrator should be shown in the migration wizard.
200 let key = this.constructor.key;
201 return Services.prefs.getBoolPref(`browser.migrate.${key}.enabled`, false);
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.
210 * @returns {Promise<boolean>}
212 async hasPermissions() {
213 return Promise.resolve(true);
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.
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.
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>}
233 // eslint-disable-next-line no-unused-vars
234 async getPermissions(win) {
235 return Promise.resolve(true);
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.
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.
248 async getMigrateData(aProfile) {
249 let resources = await this.#getMaybeCachedResources(aProfile);
253 let types = resources.map(r => r.type);
254 return types.reduce((a, b) => {
261 * @see MigrationUtils
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
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");
283 if (aItems != lazy.MigrationUtils.resourceTypes.ALL) {
284 resources = resources.filter(r => aItems & r.type);
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);
294 let getHistogramIdForResourceType = (resourceType, template) => {
295 if (resourceType == lazy.MigrationUtils.resourceTypes.HISTORY) {
296 return template.replace("*", "HISTORY");
298 if (resourceType == lazy.MigrationUtils.resourceTypes.BOOKMARKS) {
299 return template.replace("*", "BOOKMARKS");
301 if (resourceType == lazy.MigrationUtils.resourceTypes.PASSWORDS) {
302 return template.replace("*", "LOGINS");
307 let browserKey = this.constructor.key;
309 let maybeStartTelemetryStopwatch = resourceType => {
310 let histogramId = getHistogramIdForResourceType(
312 "FX_MIGRATION_*_IMPORT_MS"
315 TelemetryStopwatch.startKeyed(histogramId, browserKey);
320 let maybeStartResponsivenessMonitor = resourceType => {
321 let responsivenessMonitor;
322 let responsivenessHistogramId = getHistogramIdForResourceType(
324 "FX_MIGRATION_*_JANK_MS"
326 if (responsivenessHistogramId) {
327 responsivenessMonitor = new lazy.ResponsivenessMonitor();
329 return { responsivenessMonitor, responsivenessHistogramId };
332 let maybeFinishResponsivenessMonitor = (
333 responsivenessMonitor,
336 if (responsivenessMonitor) {
337 let accumulatedDelay = responsivenessMonitor.finish();
341 .getKeyedHistogramById(histogramId)
342 .add(browserKey, accumulatedDelay);
344 console.error(histogramId, ": ", ex);
350 let collectQuantityTelemetry = () => {
351 for (let resourceType of Object.keys(
352 lazy.MigrationUtils._importQuantities
355 "FX_MIGRATION_" + resourceType.toUpperCase() + "_QUANTITY";
358 .getKeyedHistogramById(histogramId)
361 lazy.MigrationUtils._importQuantities[resourceType]
364 console.error(histogramId, ": ", ex);
369 let collectMigrationTelemetry = resourceType => {
370 // We don't want to collect this if the migration is occurring due to a
372 if (this.constructor.key == lazy.FirefoxProfileMigrator.key) {
377 switch (resourceType) {
378 case lazy.MigrationUtils.resourceTypes.BOOKMARKS: {
379 prefKey = "browser.migrate.interactions.bookmarks";
382 case lazy.MigrationUtils.resourceTypes.HISTORY: {
383 prefKey = "browser.migrate.interactions.history";
386 case lazy.MigrationUtils.resourceTypes.PASSWORDS: {
387 prefKey = "browser.migrate.interactions.passwords";
396 Services.prefs.setBoolPref(prefKey, true);
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());
407 resourcesGroupedByItems.get(resource.type).add(resource);
410 if (resourcesGroupedByItems.size == 0) {
411 throw new Error("No items to import");
414 let notify = function (aMsg, aItemType) {
415 Services.obs.notifyObservers(null, aMsg, aItemType);
418 for (let resourceType of Object.keys(
419 lazy.MigrationUtils._importQuantities
421 lazy.MigrationUtils._importQuantities[resourceType] = 0;
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) {
441 ? "Migration:ItemAfterMigrate"
442 : "Migration:ItemError",
445 collectMigrationTelemetry(migrationType);
447 aProgressCallback(migrationType, itemSuccess, details);
449 resourcesGroupedByItems.delete(migrationType);
451 if (stopwatchHistogramId) {
452 TelemetryStopwatch.finishKeyed(
453 stopwatchHistogramId,
458 maybeFinishResponsivenessMonitor(
459 responsivenessMonitor,
460 responsivenessHistogramId
463 if (resourcesGroupedByItems.size == 0) {
464 collectQuantityTelemetry();
466 notify("Migration:Ended");
469 completeDeferred.resolve();
472 // If migrate throws, an error occurred, and the callback
473 // (itemMayBeDone) might haven't been called.
475 res.migrate(resourceDone);
481 await completeDeferred.promise;
482 await unblockMainThread();
488 lazy.MigrationUtils.isStartupMigration &&
489 !this.startupOnlyMigrator &&
490 Services.policies.isAllowed("defaultBookmarks")
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(
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",
509 source: lazy.PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
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(
520 TOPIC_PLACES_DEFAULTS_FINISHED
524 Services.obs.addObserver(
526 TOPIC_PLACES_DEFAULTS_FINISHED
529 browserGlue.observe(null, TOPIC_DID_IMPORT_BOOKMARKS, "");
530 await placesInitedPromise;
539 * Checks to see if one or more profiles exist for the browser that this
540 * migrator migrates from.
542 * @returns {Promise<boolean>}
543 * True if one or more profiles exists that this migrator can migrate
546 async isSourceAvailable() {
547 if (this.startupOnlyMigrator && !lazy.MigrationUtils.isStartupMigration) {
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.
556 let profiles = await this.getSourceProfiles();
558 let resources = await this.#getMaybeCachedResources("");
559 if (resources && resources.length) {
563 exists = !!profiles.length;
571 /*** PRIVATE STUFF - DO NOT OVERRIDE ***/
574 * Returns resources for a particular profile and then caches them for later
577 * @param {object|string} aProfile
578 * The profile that resources are being imported from.
579 * @returns {Promise<MigrationResource[]>}
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];
588 this._resourcesByProfile = {};
590 this._resourcesByProfile[profileKey] = await this.getResources(aProfile);
591 return this._resourcesByProfile[profileKey];