Bug 1802961 - Convert MigratorPrototype into an ES6 class and move into its own ESM as MigratorBase to be subclassed. r=NeilDeakin
Differential Revision: https://phabricator.services.mozilla.com/D163257
This commit is contained in:
@@ -2,22 +2,13 @@
|
||||
* 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";
|
||||
|
||||
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
||||
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
BookmarkHTMLUtils: "resource://gre/modules/BookmarkHTMLUtils.sys.mjs",
|
||||
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
|
||||
PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs",
|
||||
PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
|
||||
ResponsivenessMonitor: "resource://gre/modules/ResponsivenessMonitor.sys.mjs",
|
||||
Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
|
||||
setTimeout: "resource://gre/modules/Timer.sys.mjs",
|
||||
WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs",
|
||||
@@ -44,475 +35,12 @@ function getL10n() {
|
||||
return gL10n;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} MigratorResource
|
||||
* A resource returned by a subclass of MigratorPrototype that can migrate
|
||||
* data to this browser.
|
||||
* @property {number} type
|
||||
* A bitfield with bits from nsIBrowserProfileMigrator 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, implementing nsIBrowserProfileMigrator.
|
||||
*
|
||||
* To implement a migrator:
|
||||
* 1. Import this module.
|
||||
* 2. Create the prototype for the migrator, extending MigratorPrototype.
|
||||
* Namely: MosaicMigrator.prototype = Object.create(MigratorPrototype);
|
||||
* 3. Set classDescription, contractID and classID for your migrator, and set
|
||||
* NSGetFactory appropriately.
|
||||
* 4. If the migrator supports multiple profiles, override the sourceProfiles
|
||||
* Here we default for single-profile migrator.
|
||||
* 5. Implement getResources(aProfile) (see below).
|
||||
* 6. For startup-only migrators, override |startupOnlyMigrator|.
|
||||
*/
|
||||
export var MigratorPrototype = {
|
||||
QueryInterface: ChromeUtils.generateQI(["nsIBrowserProfileMigrator"]),
|
||||
|
||||
/**
|
||||
* OVERRIDE IF AND ONLY IF the source supports multiple profiles.
|
||||
*
|
||||
* Returns array of profile objects from which data may be imported. The object
|
||||
* should have the following keys:
|
||||
* id - a unique string identifier for the profile
|
||||
* name - a pretty name to display to the user in the UI
|
||||
*
|
||||
* Only profiles from which data can be imported should be listed. Otherwise
|
||||
* the behavior of the migration wizard isn't well-defined.
|
||||
*
|
||||
* For a single-profile source (e.g. safari, ie), this returns null,
|
||||
* and not an empty array. That is the default implementation.
|
||||
*
|
||||
* @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 types (see
|
||||
* nsIBrowserProfileMigrator).
|
||||
*
|
||||
* - a |migrate| method, taking a single argument, aCallback(bool success),
|
||||
* for migrating the data for this resource. It may do its job
|
||||
* synchronously or asynchronously. Either way, it must call
|
||||
* aCallback(bool aSuccess) when it's done. In the case of an exception
|
||||
* thrown from |migrate|, it's taken as if aCallback(false) is called.
|
||||
*
|
||||
* Note: In the case of a simple asynchronous implementation, you may find
|
||||
* MigrationUtils.wrapMigrateFunction handy for handling aCallback easily.
|
||||
*
|
||||
* For each migration type listed in nsIBrowserProfileMigrator, multiple
|
||||
* migration resources may be provided. This practice is useful when the
|
||||
* data for a certain migration type is independently stored in few
|
||||
* locations. For example, the mac version of Safari stores its "reading list"
|
||||
* bookmarks in a separate property list.
|
||||
*
|
||||
* Note that the importation of a particular migration type is reported as
|
||||
* successful if _any_ of its resources succeeded to import (that is, called,
|
||||
* |aCallback(true)|). However, completion-status for a particular migration
|
||||
* type is reported to the UI only once all of its migrators have called
|
||||
* aCallback.
|
||||
*
|
||||
* NOTE: The returned array should only include resources from which data
|
||||
* can be imported. So, for example, before adding a resource for the
|
||||
* BOOKMARKS migration type, you should check if you should check that the
|
||||
* bookmarks file exists.
|
||||
*
|
||||
* @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).
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
getResources: function MP_getResources(aProfile) {
|
||||
throw new Error("getResources must be overridden");
|
||||
},
|
||||
|
||||
/**
|
||||
* OVERRIDE in order to provide an estimate of when the last time was
|
||||
* that somebody used the browser. It is OK that this is somewhat fuzzy -
|
||||
* history may not be available (or be wiped or not present due to e.g.
|
||||
* incognito mode).
|
||||
*
|
||||
* 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.getBrowserKey();
|
||||
return Services.prefs.getBoolPref(`browser.migrate.${key}.enabled`, false);
|
||||
},
|
||||
|
||||
/**
|
||||
* DO NOT OVERRIDE - After deCOMing migration, the UI will just call
|
||||
* getResources.
|
||||
*
|
||||
* @see nsIBrowserProfileMigrator
|
||||
* @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 {MigratorResource[]}
|
||||
*/
|
||||
getMigrateData: async function MP_getMigrateData(aProfile) {
|
||||
let resources = await this._getMaybeCachedResources(aProfile);
|
||||
if (!resources) {
|
||||
return 0;
|
||||
}
|
||||
let types = resources.map(r => r.type);
|
||||
return types.reduce((a, b) => {
|
||||
a |= b;
|
||||
return a;
|
||||
}, 0);
|
||||
},
|
||||
|
||||
getBrowserKey: function MP_getBrowserKey() {
|
||||
return this.contractID.match(/\=([^\=]+)$/)[1];
|
||||
},
|
||||
|
||||
/**
|
||||
* DO NOT OVERRIDE - After deCOMing migration, the UI will just call
|
||||
* migrate for each resource.
|
||||
*
|
||||
* @see nsIBrowserProfileMigrator
|
||||
* @param {number} aItems
|
||||
* A bitfield with bits from nsIBrowserProfileMigrator 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.
|
||||
*/
|
||||
migrate: async function MP_migrate(aItems, aStartup, aProfile) {
|
||||
let resources = await this._getMaybeCachedResources(aProfile);
|
||||
if (!resources.length) {
|
||||
throw new Error("migrate called for a non-existent source");
|
||||
}
|
||||
|
||||
if (aItems != Ci.nsIBrowserProfileMigrator.ALL) {
|
||||
resources = resources.filter(r => aItems & r.type);
|
||||
}
|
||||
|
||||
// Used to periodically give back control to the main-thread loop.
|
||||
let unblockMainThread = function() {
|
||||
return new Promise(resolve => {
|
||||
Services.tm.dispatchToMainThread(resolve);
|
||||
});
|
||||
};
|
||||
|
||||
let getHistogramIdForResourceType = (resourceType, template) => {
|
||||
if (resourceType == MigrationUtils.resourceTypes.HISTORY) {
|
||||
return template.replace("*", "HISTORY");
|
||||
}
|
||||
if (resourceType == MigrationUtils.resourceTypes.BOOKMARKS) {
|
||||
return template.replace("*", "BOOKMARKS");
|
||||
}
|
||||
if (resourceType == MigrationUtils.resourceTypes.PASSWORDS) {
|
||||
return template.replace("*", "LOGINS");
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
let browserKey = this.getBrowserKey();
|
||||
|
||||
let maybeStartTelemetryStopwatch = resourceType => {
|
||||
let histogramId = getHistogramIdForResourceType(
|
||||
resourceType,
|
||||
"FX_MIGRATION_*_IMPORT_MS"
|
||||
);
|
||||
if (histogramId) {
|
||||
TelemetryStopwatch.startKeyed(histogramId, browserKey);
|
||||
}
|
||||
return histogramId;
|
||||
};
|
||||
|
||||
let maybeStartResponsivenessMonitor = resourceType => {
|
||||
let responsivenessMonitor;
|
||||
let responsivenessHistogramId = getHistogramIdForResourceType(
|
||||
resourceType,
|
||||
"FX_MIGRATION_*_JANK_MS"
|
||||
);
|
||||
if (responsivenessHistogramId) {
|
||||
responsivenessMonitor = new 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) {
|
||||
Cu.reportError(histogramId + ": " + ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let collectQuantityTelemetry = () => {
|
||||
for (let resourceType of Object.keys(MigrationUtils._importQuantities)) {
|
||||
let histogramId =
|
||||
"FX_MIGRATION_" + resourceType.toUpperCase() + "_QUANTITY";
|
||||
try {
|
||||
Services.telemetry
|
||||
.getKeyedHistogramById(histogramId)
|
||||
.add(browserKey, MigrationUtils._importQuantities[resourceType]);
|
||||
} catch (ex) {
|
||||
Cu.reportError(histogramId + ": " + ex);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Called either directly or through the bookmarks import callback.
|
||||
let doMigrate = async function() {
|
||||
let resourcesGroupedByItems = new Map();
|
||||
resources.forEach(function(resource) {
|
||||
if (!resourcesGroupedByItems.has(resource.type)) {
|
||||
resourcesGroupedByItems.set(resource.type, new Set());
|
||||
}
|
||||
resourcesGroupedByItems.get(resource.type).add(resource);
|
||||
});
|
||||
|
||||
if (resourcesGroupedByItems.size == 0) {
|
||||
throw new Error("No items to import");
|
||||
}
|
||||
|
||||
let notify = function(aMsg, aItemType) {
|
||||
Services.obs.notifyObservers(null, aMsg, aItemType);
|
||||
};
|
||||
|
||||
for (let resourceType of Object.keys(MigrationUtils._importQuantities)) {
|
||||
MigrationUtils._importQuantities[resourceType] = 0;
|
||||
}
|
||||
notify("Migration:Started");
|
||||
for (let [migrationType, itemResources] of resourcesGroupedByItems) {
|
||||
notify("Migration:ItemBeforeMigrate", migrationType);
|
||||
|
||||
let stopwatchHistogramId = maybeStartTelemetryStopwatch(migrationType);
|
||||
|
||||
let {
|
||||
responsivenessMonitor,
|
||||
responsivenessHistogramId,
|
||||
} = maybeStartResponsivenessMonitor(migrationType);
|
||||
|
||||
let itemSuccess = false;
|
||||
for (let res of itemResources) {
|
||||
let completeDeferred = lazy.PromiseUtils.defer();
|
||||
let resourceDone = function(aSuccess) {
|
||||
itemResources.delete(res);
|
||||
itemSuccess |= aSuccess;
|
||||
if (itemResources.size == 0) {
|
||||
notify(
|
||||
itemSuccess
|
||||
? "Migration:ItemAfterMigrate"
|
||||
: "Migration:ItemError",
|
||||
migrationType
|
||||
);
|
||||
resourcesGroupedByItems.delete(migrationType);
|
||||
|
||||
if (stopwatchHistogramId) {
|
||||
TelemetryStopwatch.finishKeyed(
|
||||
stopwatchHistogramId,
|
||||
browserKey
|
||||
);
|
||||
}
|
||||
|
||||
maybeFinishResponsivenessMonitor(
|
||||
responsivenessMonitor,
|
||||
responsivenessHistogramId
|
||||
);
|
||||
|
||||
if (resourcesGroupedByItems.size == 0) {
|
||||
collectQuantityTelemetry();
|
||||
notify("Migration:Ended");
|
||||
}
|
||||
}
|
||||
completeDeferred.resolve();
|
||||
};
|
||||
|
||||
// If migrate throws, an error occurred, and the callback
|
||||
// (itemMayBeDone) might haven't been called.
|
||||
try {
|
||||
res.migrate(resourceDone);
|
||||
} catch (ex) {
|
||||
Cu.reportError(ex);
|
||||
resourceDone(false);
|
||||
}
|
||||
|
||||
await completeDeferred.promise;
|
||||
await unblockMainThread();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
MigrationUtils.isStartupMigration &&
|
||||
!this.startupOnlyMigrator &&
|
||||
Services.policies.isAllowed("defaultBookmarks")
|
||||
) {
|
||||
MigrationUtils.profileStartup.doStartup();
|
||||
// First import the default bookmarks.
|
||||
// Note: We do not need to do so for the Firefox migrator
|
||||
// (=startupOnlyMigrator), as it just copies over the places database
|
||||
// from another profile.
|
||||
(async function() {
|
||||
// Tell nsBrowserGlue we're importing default bookmarks.
|
||||
let browserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService(
|
||||
Ci.nsIObserver
|
||||
);
|
||||
browserGlue.observe(null, TOPIC_WILL_IMPORT_BOOKMARKS, "");
|
||||
|
||||
// Import the default bookmarks. We ignore whether or not we succeed.
|
||||
await lazy.BookmarkHTMLUtils.importFromURL(
|
||||
"chrome://browser/content/default-bookmarks.html",
|
||||
{
|
||||
replace: true,
|
||||
source: lazy.PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
|
||||
}
|
||||
).catch(Cu.reportError);
|
||||
|
||||
// We'll tell nsBrowserGlue we've imported bookmarks, but before that
|
||||
// we need to make sure we're going to know when it's finished
|
||||
// initializing places:
|
||||
let placesInitedPromise = new Promise(resolve => {
|
||||
let onPlacesInited = function() {
|
||||
Services.obs.removeObserver(
|
||||
onPlacesInited,
|
||||
TOPIC_PLACES_DEFAULTS_FINISHED
|
||||
);
|
||||
resolve();
|
||||
};
|
||||
Services.obs.addObserver(
|
||||
onPlacesInited,
|
||||
TOPIC_PLACES_DEFAULTS_FINISHED
|
||||
);
|
||||
});
|
||||
browserGlue.observe(null, TOPIC_DID_IMPORT_BOOKMARKS, "");
|
||||
await placesInitedPromise;
|
||||
doMigrate();
|
||||
})();
|
||||
return;
|
||||
}
|
||||
doMigrate();
|
||||
},
|
||||
|
||||
/**
|
||||
* DO NOT OVERRIDE - After deCOMing migration, this code
|
||||
* won't be part of the migrator itself.
|
||||
*
|
||||
* @see nsIBrowserProfileMigrator
|
||||
*/
|
||||
async isSourceAvailable() {
|
||||
if (this.startupOnlyMigrator && !MigrationUtils.isStartupMigration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For a single-profile source, check if any data is available.
|
||||
// For multiple-profiles source, make sure that at least one
|
||||
// profile is available.
|
||||
let exists = false;
|
||||
try {
|
||||
let profiles = await this.getSourceProfiles();
|
||||
if (!profiles) {
|
||||
let resources = await this._getMaybeCachedResources("");
|
||||
if (resources && resources.length) {
|
||||
exists = true;
|
||||
}
|
||||
} else {
|
||||
exists = !!profiles.length;
|
||||
}
|
||||
} catch (ex) {
|
||||
Cu.reportError(ex);
|
||||
}
|
||||
return exists;
|
||||
},
|
||||
|
||||
/*** PRIVATE STUFF - DO NOT OVERRIDE ***/
|
||||
|
||||
/**
|
||||
* 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[]>}
|
||||
*/
|
||||
_getMaybeCachedResources: async function PMB__getMaybeCachedResources(
|
||||
aProfile
|
||||
) {
|
||||
let profileKey = aProfile ? aProfile.id : "";
|
||||
if (this._resourcesByProfile) {
|
||||
if (profileKey in this._resourcesByProfile) {
|
||||
return this._resourcesByProfile[profileKey];
|
||||
}
|
||||
} else {
|
||||
this._resourcesByProfile = {};
|
||||
}
|
||||
this._resourcesByProfile[profileKey] = await this.getResources(aProfile);
|
||||
return this._resourcesByProfile[profileKey];
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The singleton MigrationUtils service. This service is the primary mechanism
|
||||
* by which migrations from other browsers to this browser occur. The singleton
|
||||
* instance of this class is exported from this module as `MigrationUtils`.
|
||||
*/
|
||||
class MigrationUtilsSingleton {
|
||||
class MigrationUtils {
|
||||
resourceTypes = Object.freeze({
|
||||
COOKIES: Ci.nsIBrowserProfileMigrator.COOKIES,
|
||||
HISTORY: Ci.nsIBrowserProfileMigrator.HISTORY,
|
||||
@@ -525,7 +53,7 @@ class MigrationUtilsSingleton {
|
||||
|
||||
/**
|
||||
* Helper for implementing simple asynchronous cases of migration resources'
|
||||
* |migrate(aCallback)| (see MigratorPrototype). If your |migrate| method
|
||||
* |migrate(aCallback)| (see MigratorBase). If your |migrate| method
|
||||
* just waits for some file to be read, for example, and then migrates
|
||||
* everything right away, you can wrap the async-function with this helper
|
||||
* and not worry about notifying the callback.
|
||||
@@ -711,7 +239,7 @@ class MigrationUtilsSingleton {
|
||||
* Internal name of the migration source. See `availableMigratorKeys`
|
||||
* for supported values by OS.
|
||||
*
|
||||
* @returns {MigratorPrototype}
|
||||
* @returns {MigratorBase}
|
||||
* A profile migrator implementing nsIBrowserProfileMigrator, if it can
|
||||
* import any data, null otherwise.
|
||||
*/
|
||||
@@ -1420,4 +948,6 @@ class MigrationUtilsSingleton {
|
||||
}
|
||||
}
|
||||
|
||||
export const MigrationUtils = new MigrationUtilsSingleton();
|
||||
const MigrationUtilsSingleton = new MigrationUtils();
|
||||
|
||||
export { MigrationUtilsSingleton as MigrationUtils };
|
||||
|
||||
Reference in New Issue
Block a user