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