Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const TOPIC_WILL_IMPORT_BOOKMARKS =
"initial-migration-will-import-default-bookmarks";
const TOPIC_DID_IMPORT_BOOKMARKS =
"initial-migration-did-import-default-bookmarks";
const TOPIC_PLACES_DEFAULTS_FINISHED = "places-browser-init-complete";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
});
/**
* @typedef {object} MigratorResource
* A resource returned by a subclass of MigratorBase that can migrate
* data to this browser.
* @property {number} type
* A bitfield with bits from MigrationUtils.resourceTypes flipped to indicate
* what this resource represents. A resource can represent one or more types
* of data, for example HISTORY and FORMDATA.
* @property {Function} migrate
* A function that will actually perform the migration of this resource's
* data into this browser.
*/
/**
* Shared prototype for migrators.
*
* To implement a migrator:
* 1. Import this module.
* 2. Create a subclass of MigratorBase for your new migrator.
* 3. Override the `key` static getter with a unique identifier for the browser
* that this migrator migrates from.
* 4. If the migrator supports multiple profiles, override the sourceProfiles
* Here we default for single-profile migrator.
* 5. Implement getResources(aProfile) (see below).
* 6. For startup-only migrators, override |startupOnlyMigrator|.
* 7. Add the migrator to the MIGRATOR_MODULES structure in MigrationUtils.sys.mjs.
*/
export class MigratorBase {
/**
* This must be overridden to return a simple string identifier for the
* migrator, for example "firefox", "chrome", "opera-gx". This key is what
* is used as an identifier when calling MigrationUtils.getMigrator.
*
* @type {string}
*/
static get key() {
throw new Error("MigratorBase.key must be overridden.");
}
/**
* This must be overridden to return a Fluent string ID mapping to the display
* name for this migrator. These strings should be defined in migrationWizard.ftl.
*
* @type {string}
*/
static get displayNameL10nID() {
throw new Error("MigratorBase.displayNameL10nID must be overridden.");
}
/**
* This method should get overridden to return an icon url of the browser
* to be imported from. By default, this will just use the default Favicon
* image.
*
* @type {string}
*/
static get brandImage() {
}
/**
* OVERRIDE IF AND ONLY IF the source supports multiple profiles.
*
* Returns array of profile objects from which data may be imported. The object
* should have the following keys:
* id - a unique string identifier for the profile
* name - a pretty name to display to the user in the UI
*
* Only profiles from which data can be imported should be listed. Otherwise
* the behavior of the migration wizard isn't well-defined.
*
* For a single-profile source (e.g. safari, ie), this returns null,
* and not an empty array. That is the default implementation.
*
* @abstract
* @returns {object[]|null}
*/
getSourceProfiles() {
return null;
}
/**
* MUST BE OVERRIDDEN.
*
* Returns an array of "migration resources" objects for the given profile,
* or for the "default" profile, if the migrator does not support multiple
* profiles.
*
* Each migration resource should provide:
* - a |type| getter, returning any of the migration resource types (see
* MigrationUtils.resourceTypes).
*
* - a |migrate| method, taking two arguments,
* aCallback(bool success, object details), for migrating the data for
* this resource. It may do its job synchronously or asynchronously.
* Either way, it must call aCallback(bool aSuccess, object details)
* when it's done. In the case of an exception thrown from |migrate|,
* it's taken as if aCallback(false, {}) is called. The details
* argument is sometimes optional, but conditional on how the
* migration wizard wants to display the migration state for the
* resource.
*
* Note: In the case of a simple asynchronous implementation, you may find
* MigrationUtils.wrapMigrateFunction handy for handling aCallback easily.
*
* For each migration type listed in MigrationUtils.resourceTypes, multiple
* migration resources may be provided. This practice is useful when the
* data for a certain migration type is independently stored in few
* locations. For example, the mac version of Safari stores its "reading list"
* bookmarks in a separate property list.
*
* Note that the importation of a particular migration type is reported as
* successful if _any_ of its resources succeeded to import (that is, called,
* |aCallback(true, {})|). However, completion-status for a particular migration
* type is reported to the UI only once all of its migrators have called
* aCallback.
*
* NOTE: The returned array should only include resources from which data
* can be imported. So, for example, before adding a resource for the
* BOOKMARKS migration type, you should check if you should check that the
* bookmarks file exists.
*
* @abstract
* @param {object|string} aProfile
* The profile from which data may be imported, or an empty string
* in the case of a single-profile migrator.
* In the case of multiple-profiles migrator, it is guaranteed that
* aProfile is a value returned by the sourceProfiles getter (see
* above).
* @returns {Promise<MigratorResource[]>|MigratorResource[]}
*/
// eslint-disable-next-line no-unused-vars
getResources(aProfile) {
throw new Error("getResources must be overridden");
}
/**
* OVERRIDE in order to provide an estimate of when the last time was
* that somebody used the browser. It is OK that this is somewhat fuzzy -
* history may not be available (or be wiped or not present due to e.g.
* incognito mode).
*
* If not overridden, the promise will resolve to the Unix epoch.
*
* @returns {Promise<Date>}
* A Promise that resolves to the last used date.
*/
getLastUsedDate() {
return Promise.resolve(new Date(0));
}
/**
* OVERRIDE IF AND ONLY IF the migrator is a startup-only migrator (For now,
* that is just the Firefox migrator, see bug 737381). Default: false.
*
* Startup-only migrators are different in two ways:
* - they may only be used during startup.
* - the user-profile is half baked during migration. The folder exists,
* but it's only accessible through MigrationUtils.profileStartup.
* The migrator can call MigrationUtils.profileStartup.doStartup
* at any point in order to initialize the profile.
*
* @returns {boolean}
* true if the migrator is start-up only.
*/
get startupOnlyMigrator() {
return false;
}
/**
* Returns true if the migrator is configured to be enabled. This is
* controlled by the `browser.migrate.<BROWSER_KEY>.enabled` boolean
* preference.
*
* @returns {boolean}
* true if the migrator should be shown in the migration wizard.
*/
get enabled() {
let key = this.constructor.key;
return Services.prefs.getBoolPref(`browser.migrate.${key}.enabled`, false);
}
/**
* Subclasses should implement this if special checks need to be made to determine
* if certain permissions need to be requested before data can be imported.
* The returned Promise resolves to true if the required permissions have
* been granted and a migration could proceed.
*
* @returns {Promise<boolean>}
*/
async hasPermissions() {
return Promise.resolve(true);
}
/**
* Subclasses should implement this if special permissions need to be
* requested from the user or the operating system in order to perform
* a migration with this MigratorBase. This will be called only if
* hasPermissions resolves to false.
*
* The returned Promise will resolve to true if permissions were successfully
* obtained, and false otherwise. Implementors should ensure that if a call
* to getPermissions resolves to true, that the MigratorBase will be able to
* get read access to all of the resources it needs to do a migration.
*
* @param {DOMWindow} win
* The top-level DOM window hosting the UI that is requesting the permission.
* This can be used to, for example, anchor a file picker window to the
* same window that is hosting the migration UI.
* @returns {Promise<boolean>}
*/
// eslint-disable-next-line no-unused-vars
async getPermissions(win) {
return Promise.resolve(true);
}
/**
* @returns {Promise<boolean|string>}
*/
async canGetPermissions() {
return Promise.resolve(false);
}
/**
* This method returns a number that is the bitwise OR of all resource
* types that are available in aProfile. See MigrationUtils.resourceTypes
* for each resource type.
*
* @param {object|string} aProfile
* The profile from which data may be imported, or an empty string
* in the case of a single-profile migrator.
* @returns {number}
*/
async getMigrateData(aProfile) {
let resources = await this.#getMaybeCachedResources(aProfile);
if (!resources) {
return 0;
}
let types = resources.map(r => r.type);
return types.reduce((a, b) => {
a |= b;
return a;
}, 0);
}
/**
* @see MigrationUtils
*
* @param {number} aItems
* A bitfield with bits from MigrationUtils.resourceTypes flipped to indicate
* what types of resources should be migrated.
* @param {boolean} aStartup
* True if this migration is occurring during startup.
* @param {object|string} aProfile
* The other browser profile that is being migrated from.
* @param {Function|null} aProgressCallback
* An optional callback that will be fired once a resourceType has finished
* migrating. The callback will be passed the numeric representation of the
* resource type followed by a boolean indicating whether or not the resource
* was migrated successfully and optionally an object containing additional
* details.
*/
async migrate(aItems, aStartup, aProfile, aProgressCallback = () => {}) {
let resources = await this.#getMaybeCachedResources(aProfile);
if (!resources.length) {
throw new Error("migrate called for a non-existent source");
}
if (aItems != lazy.MigrationUtils.resourceTypes.ALL) {
resources = resources.filter(r => aItems & r.type);
}
// Used to periodically give back control to the main-thread loop.
let unblockMainThread = function () {
return new Promise(resolve => {
Services.tm.dispatchToMainThread(resolve);
});
};
let getHistogramIdForResourceType = (resourceType, template) => {
if (resourceType == lazy.MigrationUtils.resourceTypes.HISTORY) {
return template.replace("*", "HISTORY");
}
if (resourceType == lazy.MigrationUtils.resourceTypes.BOOKMARKS) {
return template.replace("*", "BOOKMARKS");
}
if (resourceType == lazy.MigrationUtils.resourceTypes.PASSWORDS) {
return template.replace("*", "LOGINS");
}
return null;
};
let browserKey = this.constructor.key;
let maybeStartTelemetryStopwatch = resourceType => {
let histogramId = getHistogramIdForResourceType(
resourceType,
"FX_MIGRATION_*_IMPORT_MS"
);
if (histogramId) {
TelemetryStopwatch.startKeyed(histogramId, browserKey);
}
return histogramId;
};
let maybeStartResponsivenessMonitor = resourceType => {
let responsivenessMonitor;
let responsivenessHistogramId = getHistogramIdForResourceType(
resourceType,
"FX_MIGRATION_*_JANK_MS"
);
if (responsivenessHistogramId) {
responsivenessMonitor = new lazy.ResponsivenessMonitor();
}
return { responsivenessMonitor, responsivenessHistogramId };
};
let maybeFinishResponsivenessMonitor = (
responsivenessMonitor,
histogramId
) => {
if (responsivenessMonitor) {
let accumulatedDelay = responsivenessMonitor.finish();
if (histogramId) {
try {
Services.telemetry
.getKeyedHistogramById(histogramId)
.add(browserKey, accumulatedDelay);
} catch (ex) {
console.error(histogramId, ": ", ex);
}
}
}
};
let collectQuantityTelemetry = () => {
for (let resourceType of Object.keys(
lazy.MigrationUtils._importQuantities
)) {
let histogramId =
"FX_MIGRATION_" + resourceType.toUpperCase() + "_QUANTITY";
try {
Services.telemetry
.getKeyedHistogramById(histogramId)
.add(
browserKey,
lazy.MigrationUtils._importQuantities[resourceType]
);
} catch (ex) {
console.error(histogramId, ": ", ex);
}
}
};
let collectMigrationTelemetry = resourceType => {
// We don't want to collect this if the migration is occurring due to a
// profile refresh.
if (this.constructor.key == lazy.FirefoxProfileMigrator.key) {
return;
}
let prefKey = null;
switch (resourceType) {
case lazy.MigrationUtils.resourceTypes.BOOKMARKS: {
prefKey = "browser.migrate.interactions.bookmarks";
break;
}
case lazy.MigrationUtils.resourceTypes.HISTORY: {
prefKey = "browser.migrate.interactions.history";
break;
}
case lazy.MigrationUtils.resourceTypes.PASSWORDS: {
prefKey = "browser.migrate.interactions.passwords";
break;
}
default: {
return;
}
}
if (prefKey) {
Services.prefs.setBoolPref(prefKey, true);
}
};
// Called either directly or through the bookmarks import callback.
let doMigrate = async function () {
let resourcesGroupedByItems = new Map();
resources.forEach(function (resource) {
if (!resourcesGroupedByItems.has(resource.type)) {
resourcesGroupedByItems.set(resource.type, new Set());
}
resourcesGroupedByItems.get(resource.type).add(resource);
});
if (resourcesGroupedByItems.size == 0) {
throw new Error("No items to import");
}
let notify = function (aMsg, aItemType) {
Services.obs.notifyObservers(null, aMsg, aItemType);
};
for (let resourceType of Object.keys(
lazy.MigrationUtils._importQuantities
)) {
lazy.MigrationUtils._importQuantities[resourceType] = 0;
}
notify("Migration:Started");
for (let [migrationType, itemResources] of resourcesGroupedByItems) {
notify("Migration:ItemBeforeMigrate", migrationType);
let stopwatchHistogramId = maybeStartTelemetryStopwatch(migrationType);
let { responsivenessMonitor, responsivenessHistogramId } =
maybeStartResponsivenessMonitor(migrationType);
let itemSuccess = false;
for (let res of itemResources) {
let completeDeferred = Promise.withResolvers();
let resourceDone = function (aSuccess, details) {
itemResources.delete(res);
itemSuccess |= aSuccess;
if (itemResources.size == 0) {
notify(
itemSuccess
? "Migration:ItemAfterMigrate"
: "Migration:ItemError",
migrationType
);
collectMigrationTelemetry(migrationType);
aProgressCallback(migrationType, itemSuccess, details);
resourcesGroupedByItems.delete(migrationType);
if (stopwatchHistogramId) {
TelemetryStopwatch.finishKeyed(
stopwatchHistogramId,
browserKey
);
}
maybeFinishResponsivenessMonitor(
responsivenessMonitor,
responsivenessHistogramId
);
if (resourcesGroupedByItems.size == 0) {
collectQuantityTelemetry();
notify("Migration:Ended");
}
}
completeDeferred.resolve();
};
// If migrate throws, an error occurred, and the callback
// (itemMayBeDone) might haven't been called.
try {
res.migrate(resourceDone);
} catch (ex) {
console.error(ex);
resourceDone(false);
}
await completeDeferred.promise;
await unblockMainThread();
}
}
};
if (
lazy.MigrationUtils.isStartupMigration &&
!this.startupOnlyMigrator &&
Services.policies.isAllowed("defaultBookmarks")
) {
lazy.MigrationUtils.profileStartup.doStartup();
// First import the default bookmarks.
// Note: We do not need to do so for the Firefox migrator
// (=startupOnlyMigrator), as it just copies over the places database
// from another profile.
await (async function () {
// Tell nsBrowserGlue we're importing default bookmarks.
let browserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService(
Ci.nsIObserver
);
browserGlue.observe(null, TOPIC_WILL_IMPORT_BOOKMARKS, "");
// Import the default bookmarks. We ignore whether or not we succeed.
await lazy.BookmarkHTMLUtils.importFromURL(
{
replace: true,
source: lazy.PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
}
).catch(console.error);
// We'll tell nsBrowserGlue we've imported bookmarks, but before that
// we need to make sure we're going to know when it's finished
// initializing places:
let placesInitedPromise = new Promise(resolve => {
let onPlacesInited = function () {
Services.obs.removeObserver(
onPlacesInited,
TOPIC_PLACES_DEFAULTS_FINISHED
);
resolve();
};
Services.obs.addObserver(
onPlacesInited,
TOPIC_PLACES_DEFAULTS_FINISHED
);
});
browserGlue.observe(null, TOPIC_DID_IMPORT_BOOKMARKS, "");
await placesInitedPromise;
await doMigrate();
})();
return;
}
await doMigrate();
}
/**
* Checks to see if one or more profiles exist for the browser that this
* migrator migrates from.
*
* @returns {Promise<boolean>}
* True if one or more profiles exists that this migrator can migrate
* resources from.
*/
async isSourceAvailable() {
if (this.startupOnlyMigrator && !lazy.MigrationUtils.isStartupMigration) {
return false;
}
// For a single-profile source, check if any data is available.
// For multiple-profiles source, make sure that at least one
// profile is available.
let exists = false;
try {
let profiles = await this.getSourceProfiles();
if (!profiles) {
let resources = await this.#getMaybeCachedResources("");
if (resources && resources.length) {
exists = true;
}
} else {
exists = !!profiles.length;
}
} catch (ex) {
console.error(ex);
}
return exists;
}
/*** PRIVATE STUFF - DO NOT OVERRIDE ***/
/**
* Returns resources for a particular profile and then caches them for later
* lookups.
*
* @param {object|string} aProfile
* The profile that resources are being imported from.
* @returns {Promise<MigrationResource[]>}
*/
async #getMaybeCachedResources(aProfile) {
let profileKey = aProfile ? aProfile.id : "";
if (this._resourcesByProfile) {
if (profileKey in this._resourcesByProfile) {
return this._resourcesByProfile[profileKey];
}
} else {
this._resourcesByProfile = {};
}
this._resourcesByProfile[profileKey] = await this.getResources(aProfile);
return this._resourcesByProfile[profileKey];
}
}