MigrationUtils.sys.mjs (37588B)
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 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 7 8 const lazy = {}; 9 10 ChromeUtils.defineESModuleGetters(lazy, { 11 AMBrowserExtensionsImport: "resource://gre/modules/AddonManager.sys.mjs", 12 LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", 13 PlacesUIUtils: "moz-src:///browser/components/places/PlacesUIUtils.sys.mjs", 14 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 15 Sqlite: "resource://gre/modules/Sqlite.sys.mjs", 16 setTimeout: "resource://gre/modules/Timer.sys.mjs", 17 MigrationWizardConstants: 18 "chrome://browser/content/migration/migration-wizard-constants.mjs", 19 }); 20 21 ChromeUtils.defineLazyGetter( 22 lazy, 23 "gCanGetPermissionsOnPlatformPromise", 24 () => { 25 let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); 26 return fp.isModeSupported(Ci.nsIFilePicker.modeGetFolder); 27 } 28 ); 29 30 var gMigrators = null; 31 var gFileMigrators = null; 32 var gProfileStartup = null; 33 var gL10n = null; 34 35 let gForceExitSpinResolve = false; 36 let gKeepUndoData = false; 37 let gUndoData = null; 38 39 function getL10n() { 40 if (!gL10n) { 41 gL10n = new Localization(["browser/migrationWizard.ftl"]); 42 } 43 return gL10n; 44 } 45 46 const MIGRATOR_MODULES = Object.freeze({ 47 EdgeProfileMigrator: { 48 moduleURI: "resource:///modules/EdgeProfileMigrator.sys.mjs", 49 platforms: ["win"], 50 }, 51 FirefoxProfileMigrator: { 52 moduleURI: "resource:///modules/FirefoxProfileMigrator.sys.mjs", 53 platforms: ["linux", "macosx", "win"], 54 }, 55 IEProfileMigrator: { 56 moduleURI: "resource:///modules/IEProfileMigrator.sys.mjs", 57 platforms: ["win"], 58 }, 59 SafariProfileMigrator: { 60 moduleURI: "resource:///modules/SafariProfileMigrator.sys.mjs", 61 platforms: ["macosx"], 62 }, 63 64 // The following migrators are all variants of the ChromeProfileMigrator 65 66 BraveProfileMigrator: { 67 moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs", 68 platforms: ["linux", "macosx", "win"], 69 }, 70 CanaryProfileMigrator: { 71 moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs", 72 platforms: ["macosx", "win"], 73 }, 74 ChromeProfileMigrator: { 75 moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs", 76 platforms: ["linux", "macosx", "win"], 77 }, 78 ChromeBetaMigrator: { 79 moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs", 80 platforms: ["linux", "win"], 81 }, 82 ChromeDevMigrator: { 83 moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs", 84 platforms: ["linux"], 85 }, 86 ChromiumProfileMigrator: { 87 moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs", 88 platforms: ["linux", "macosx", "win"], 89 }, 90 Chromium360seMigrator: { 91 moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs", 92 platforms: ["win"], 93 }, 94 ChromiumEdgeMigrator: { 95 moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs", 96 platforms: ["macosx", "win"], 97 }, 98 ChromiumEdgeBetaMigrator: { 99 moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs", 100 platforms: ["macosx", "win"], 101 }, 102 OperaProfileMigrator: { 103 moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs", 104 platforms: ["linux", "macosx", "win"], 105 }, 106 VivaldiProfileMigrator: { 107 moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs", 108 platforms: ["linux", "macosx", "win"], 109 }, 110 OperaGXProfileMigrator: { 111 moduleURI: "resource:///modules/ChromeProfileMigrator.sys.mjs", 112 platforms: ["macosx", "win"], 113 }, 114 115 InternalTestingProfileMigrator: { 116 moduleURI: "resource:///modules/InternalTestingProfileMigrator.sys.mjs", 117 platforms: ["linux", "macosx", "win"], 118 }, 119 }); 120 121 const FILE_MIGRATOR_MODULES = Object.freeze({ 122 PasswordFileMigrator: { 123 moduleURI: "resource:///modules/FileMigrators.sys.mjs", 124 }, 125 BookmarksFileMigrator: { 126 moduleURI: "resource:///modules/FileMigrators.sys.mjs", 127 }, 128 }); 129 130 /** 131 * The singleton MigrationUtils service. This service is the primary mechanism 132 * by which migrations from other browsers to this browser occur. The singleton 133 * instance of this class is exported from this module as `MigrationUtils`. 134 */ 135 class MigrationUtils { 136 constructor() { 137 XPCOMUtils.defineLazyPreferenceGetter( 138 this, 139 "HISTORY_MAX_AGE_IN_DAYS", 140 "browser.migrate.history.maxAgeInDays", 141 180 142 ); 143 144 ChromeUtils.registerWindowActor("MigrationWizard", { 145 parent: { 146 esModuleURI: "resource:///actors/MigrationWizardParent.sys.mjs", 147 }, 148 149 child: { 150 esModuleURI: "resource:///actors/MigrationWizardChild.sys.mjs", 151 events: { 152 "MigrationWizard:RequestState": { wantUntrusted: true }, 153 "MigrationWizard:BeginMigration": { wantUntrusted: true }, 154 "MigrationWizard:RequestSafariPermissions": { wantUntrusted: true }, 155 "MigrationWizard:SelectManualPasswordFile": { wantUntrusted: true }, 156 "MigrationWizard:OpenAboutAddons": { wantUntrusted: true }, 157 "MigrationWizard:PermissionsNeeded": { wantUntrusted: true }, 158 "MigrationWizard:GetPermissions": { wantUntrusted: true }, 159 "MigrationWizard:OpenURL": { wantUntrusted: true }, 160 }, 161 }, 162 163 includeChrome: true, 164 allFrames: true, 165 matches: [ 166 "about:welcome", 167 "about:welcome?*", 168 "about:preferences", 169 "about:settings", 170 "chrome://browser/content/migration/migration-dialog-window.html", 171 "chrome://browser/content/spotlight.html", 172 "about:firefoxview", 173 ], 174 }); 175 176 ChromeUtils.defineLazyGetter(this, "IS_LINUX_SNAP_PACKAGE", () => { 177 if ( 178 AppConstants.platform != "linux" || 179 !Cc["@mozilla.org/gio-service;1"] 180 ) { 181 return false; 182 } 183 184 let gIOSvc = Cc["@mozilla.org/gio-service;1"].getService( 185 Ci.nsIGIOService 186 ); 187 return gIOSvc.isRunningUnderSnap; 188 }); 189 } 190 191 resourceTypes = Object.freeze({ 192 ALL: 0x0000, 193 /* 0x01 used to be used for settings, but was removed. */ 194 COOKIES: 0x0002, 195 HISTORY: 0x0004, 196 FORMDATA: 0x0008, 197 PASSWORDS: 0x0010, 198 BOOKMARKS: 0x0020, 199 OTHERDATA: 0x0040, 200 SESSION: 0x0080, 201 PAYMENT_METHODS: 0x0100, 202 EXTENSIONS: 0x0200, 203 }); 204 205 /** 206 * Helper for implementing simple asynchronous cases of migration resources' 207 * ``migrate(aCallback)`` (see MigratorBase). If your ``migrate`` method 208 * just waits for some file to be read, for example, and then migrates 209 * everything right away, you can wrap the async-function with this helper 210 * and not worry about notifying the callback. 211 * 212 * @example 213 * // For example, instead of writing: 214 * setTimeout(function() { 215 * try { 216 * .... 217 * aCallback(true); 218 * } 219 * catch() { 220 * aCallback(false); 221 * } 222 * }, 0); 223 * 224 * // You may write: 225 * setTimeout(MigrationUtils.wrapMigrateFunction(function() { 226 * if (importingFromMosaic) 227 * throw Cr.NS_ERROR_UNEXPECTED; 228 * }, aCallback), 0); 229 * 230 * // ... and aCallback will be called with aSuccess=false when importing 231 * // from Mosaic, or with aSuccess=true otherwise. 232 * 233 * @param {Function} aFunction 234 * the function that will be called sometime later. If aFunction 235 * throws when it's called, aCallback(false) is called, otherwise 236 * aCallback(true) is called. 237 * @param {Function} aCallback 238 * the callback function passed to ``migrate``. 239 * @returns {Function} 240 * the wrapped function. 241 */ 242 wrapMigrateFunction(aFunction, aCallback) { 243 return function () { 244 let success = false; 245 try { 246 aFunction.apply(null, arguments); 247 success = true; 248 } catch (ex) { 249 console.error(ex); 250 } 251 // Do not change this to call aCallback directly in try try & catch 252 // blocks, because if aCallback throws, we may end up calling aCallback 253 // twice. 254 aCallback(success); 255 }; 256 } 257 258 /** 259 * Gets localized string corresponding to l10n-id 260 * 261 * @param {string} aKey 262 * The key of the id of the localization to retrieve. 263 * @param {object} [aArgs=undefined] 264 * An optional map of arguments to the id. 265 * @returns {Promise<string>} 266 * A promise that resolves to the retrieved localization. 267 */ 268 getLocalizedString(aKey, aArgs) { 269 let l10n = getL10n(); 270 return l10n.formatValue(aKey, aArgs); 271 } 272 273 /** 274 * Get all the rows corresponding to a select query from a database, without 275 * requiring a lock on the database. If fetching data fails (because someone 276 * else tried to write to the DB at the same time, for example), we will 277 * retry the fetch after a 100ms timeout, up to 10 times. 278 * 279 * @param {string} path 280 * The file path to the database we want to open. 281 * @param {string} description 282 * A developer-readable string identifying what kind of database we're 283 * trying to open. 284 * @param {string} selectQuery 285 * The SELECT query to use to fetch the rows. 286 * @param {Promise} [testDelayPromise] 287 * An optional promise to await for after the first loop, used in tests. 288 * 289 * @returns {Promise<object[]|Error>} 290 * A promise that resolves to an array of rows. The promise will be 291 * rejected if the read/fetch failed even after retrying. 292 */ 293 getRowsFromDBWithoutLocks( 294 path, 295 description, 296 selectQuery, 297 testDelayPromise = null 298 ) { 299 let dbOptions = { 300 readOnly: true, 301 ignoreLockingMode: true, 302 path, 303 }; 304 305 const RETRYLIMIT = 10; 306 const RETRYINTERVAL = 100; 307 return (async function innerGetRows() { 308 let rows = null; 309 for (let retryCount = RETRYLIMIT; retryCount; retryCount--) { 310 // Attempt to get the rows. If this succeeds, we will bail out of the loop, 311 // close the database in a failsafe way, and pass the rows back. 312 // If fetching the rows throws, we will wait RETRYINTERVAL ms 313 // and try again. This will repeat a maximum of RETRYLIMIT times. 314 let db; 315 let didOpen = false; 316 let previousExceptionMessage = null; 317 try { 318 db = await lazy.Sqlite.openConnection(dbOptions); 319 didOpen = true; 320 rows = await db.execute(selectQuery); 321 break; 322 } catch (ex) { 323 if (previousExceptionMessage != ex.message) { 324 console.error(ex); 325 } 326 previousExceptionMessage = ex.message; 327 if (ex.name == "NS_ERROR_FILE_CORRUPTED") { 328 break; 329 } 330 } finally { 331 try { 332 if (didOpen) { 333 await db.close(); 334 } 335 } catch (ex) {} 336 } 337 await Promise.all([ 338 new Promise(resolve => lazy.setTimeout(resolve, RETRYINTERVAL)), 339 testDelayPromise, 340 ]); 341 } 342 if (!rows) { 343 throw new Error( 344 "Couldn't get rows from the " + description + " database." 345 ); 346 } 347 return rows; 348 })(); 349 } 350 351 get #migrators() { 352 if (!gMigrators) { 353 gMigrators = new Map(); 354 for (let [symbol, { moduleURI, platforms }] of Object.entries( 355 MIGRATOR_MODULES 356 )) { 357 if (platforms.includes(AppConstants.platform)) { 358 let { [symbol]: migratorClass } = 359 ChromeUtils.importESModule(moduleURI); 360 if (gMigrators.has(migratorClass.key)) { 361 console.error( 362 "A pre-existing migrator exists with key " + 363 `${migratorClass.key}. Not registering.` 364 ); 365 continue; 366 } 367 gMigrators.set(migratorClass.key, new migratorClass()); 368 } 369 } 370 } 371 return gMigrators; 372 } 373 374 get #fileMigrators() { 375 if (!gFileMigrators) { 376 gFileMigrators = new Map(); 377 for (let [symbol, { moduleURI }] of Object.entries( 378 FILE_MIGRATOR_MODULES 379 )) { 380 let { [symbol]: migratorClass } = ChromeUtils.importESModule(moduleURI); 381 if (gFileMigrators.has(migratorClass.key)) { 382 console.error( 383 "A pre-existing file migrator exists with key " + 384 `${migratorClass.key}. Not registering.` 385 ); 386 continue; 387 } 388 gFileMigrators.set(migratorClass.key, new migratorClass()); 389 } 390 } 391 return gFileMigrators; 392 } 393 394 forceExitSpinResolve() { 395 gForceExitSpinResolve = true; 396 } 397 398 spinResolve(promise) { 399 if (!(promise instanceof Promise)) { 400 return promise; 401 } 402 let done = false; 403 let result = null; 404 let error = null; 405 gForceExitSpinResolve = false; 406 promise 407 .catch(e => { 408 error = e; 409 }) 410 .then(r => { 411 result = r; 412 done = true; 413 }); 414 415 Services.tm.spinEventLoopUntil( 416 "MigrationUtils.sys.mjs:MU_spinResolve", 417 () => done || gForceExitSpinResolve 418 ); 419 if (!done) { 420 throw new Error("Forcefully exited event loop."); 421 } else if (error) { 422 throw error; 423 } else { 424 return result; 425 } 426 } 427 428 /** 429 * Returns the migrator for the given source, if any data is available 430 * for this source, or if permissions are required in order to read 431 * data from this source. Returns null otherwise. 432 * 433 * @param {string} aKey 434 * Internal name of the migration source. See `availableMigratorKeys` 435 * for supported values by OS. 436 * @returns {Promise<MigratorBase|null>} 437 * A profile migrator implementing nsIBrowserProfileMigrator, if it can 438 * import any data, null otherwise. 439 */ 440 async getMigrator(aKey) { 441 let migrator = this.#migrators.get(aKey); 442 if (!migrator) { 443 console.error(`Could not find a migrator class for key ${aKey}`); 444 return null; 445 } 446 447 try { 448 if (!migrator) { 449 return null; 450 } 451 452 if ( 453 (await migrator.isSourceAvailable()) || 454 (!(await migrator.hasPermissions()) && migrator.canGetPermissions()) 455 ) { 456 return migrator; 457 } 458 459 return null; 460 } catch (ex) { 461 console.error(ex); 462 return null; 463 } 464 } 465 466 getFileMigrator(aKey) { 467 let migrator = this.#fileMigrators.get(aKey); 468 if (!migrator) { 469 console.error(`Could not find a file migrator class for key ${aKey}`); 470 return null; 471 } 472 return migrator; 473 } 474 475 /** 476 * Returns true if a migrator is registered with key aKey. No check is made 477 * to determine if a profile exists that the migrator can migrate from. 478 * 479 * @param {string} aKey 480 * Internal name of the migration source. See `availableMigratorKeys` 481 * for supported values by OS. 482 * @returns {boolean} 483 */ 484 migratorExists(aKey) { 485 return this.#migrators.has(aKey); 486 } 487 488 /** 489 * Figure out what is the default browser, and if there is a migrator 490 * for it, return that migrator's internal name. 491 * 492 * For the time being, the "internal name" of a migrator is its contract-id 493 * trailer (e.g. ie for @mozilla.org/profile/migrator;1?app=browser&type=ie), 494 * but it will soon be exposed properly. 495 * 496 * @returns {string} 497 */ 498 getMigratorKeyForDefaultBrowser() { 499 // Canary uses the same description as Chrome so we can't distinguish them. 500 // Edge Beta on macOS uses "Microsoft Edge" with no "beta" indication. 501 const APP_DESC_TO_KEY = { 502 "Internet Explorer": "ie", 503 "Microsoft Edge": "edge", 504 Safari: "safari", 505 Firefox: "firefox", 506 Nightly: "firefox", 507 Opera: "opera", 508 Vivaldi: "vivaldi", 509 "Opera GX": "opera-gx", 510 "Brave Web Browser": "brave", // Windows, Linux 511 Brave: "brave", // OS X 512 "Google Chrome": "chrome", // Windows, Linux 513 Chrome: "chrome", // OS X 514 Chromium: "chromium", // Windows, OS X 515 "Chromium Web Browser": "chromium", // Linux 516 "360\u5b89\u5168\u6d4f\u89c8\u5668": "chromium-360se", 517 }; 518 519 let key = ""; 520 try { 521 let browserDesc = Cc["@mozilla.org/uriloader/external-protocol-service;1"] 522 .getService(Ci.nsIExternalProtocolService) 523 .getApplicationDescription("http"); 524 key = APP_DESC_TO_KEY[browserDesc] || ""; 525 // Handle devedition, as well as "FirefoxNightly" on OS X. 526 if (!key && browserDesc.startsWith("Firefox")) { 527 key = "firefox"; 528 } 529 } catch (ex) { 530 console.error("Could not detect default browser: ", ex); 531 } 532 533 return key; 534 } 535 536 /** 537 * True if we're in the process of a startup migration. 538 * 539 * @type {boolean} 540 */ 541 get isStartupMigration() { 542 return gProfileStartup != null; 543 } 544 545 /** 546 * In the case of startup migration, this is set to the nsIProfileStartup 547 * instance passed to ProfileMigrator's migrate. 548 * 549 * @see showMigrationWizard 550 * @type {nsIProfileStartup|null} 551 */ 552 get profileStartup() { 553 return gProfileStartup; 554 } 555 556 /** 557 * Show the migration wizard in about:preferences, or if there is not an existing 558 * browser window open, in a new top-level dialog window. 559 * 560 * NB: If you add new consumers, please add a migration entry point constant to 561 * MIGRATION_ENTRYPOINTS and supply that entrypoint with the entrypoint property 562 * in the aOptions argument. 563 * 564 * @param {Window} [aOpener=null] 565 * optional; the window that asks to open the wizard. 566 * @param {object} [aOptions=null] 567 * optional named arguments for the migration wizard. 568 * @param {string} [aOptions.entrypoint=undefined] 569 * migration entry point constant. See MIGRATION_ENTRYPOINTS. 570 * @param {string} [aOptions.migratorKey=undefined] 571 * The key for which migrator to use automatically. This is the key that is exposed 572 * as a static getter on the migrator class. 573 * @param {MigratorBase} [aOptions.migrator=undefined] 574 * A migrator instance to use automatically. 575 * @param {boolean} [aOptions.isStartupMigration=undefined] 576 * True if this is a startup migration. 577 * @param {boolean} [aOptions.skipSourceSelection=undefined] 578 * True if the source selection page of the wizard should be skipped. 579 * @param {string} [aOptions.profileId] 580 * An identifier for the profile to use when migrating. 581 * @returns {Promise<undefined>} 582 * If an about:preferences tab can be opened, this will resolve when 583 * that tab has been switched to. Otherwise, this will resolve 584 * just after opening the top-level dialog window. 585 */ 586 showMigrationWizard(aOpener, aOptions) { 587 // When migration is kicked off from about:welcome, there are 588 // a few different behaviors that we want to test, controlled 589 // by a preference that is instrumented for Nimbus. The pref 590 // has the following possible states: 591 // 592 // "autoclose": 593 // The user will be directed to the migration wizard in 594 // about:preferences, but once the wizard is dismissed, 595 // the tab will close. 596 // 597 // "standalone": 598 // The migration wizard will open in a new top-level content 599 // window. 600 // 601 // "default" / other 602 // The user will be directed to the migration wizard in 603 // about:preferences. The tab will not close once the 604 // user closes the wizard. 605 let aboutWelcomeBehavior = Services.prefs.getCharPref( 606 "browser.migrate.content-modal.about-welcome-behavior", 607 "default" 608 ); 609 610 let entrypoint = aOptions.entrypoint || this.MIGRATION_ENTRYPOINTS.UNKNOWN; 611 Glean.browserMigration.entryPointCategorical[entrypoint].add(1); 612 613 let openStandaloneWindow = blocking => { 614 let features = "dialog,centerscreen,resizable=no"; 615 616 if (blocking) { 617 features += ",modal"; 618 } 619 620 Services.ww.openWindow( 621 aOpener, 622 "chrome://browser/content/migration/migration-dialog-window.html", 623 "_blank", 624 features, 625 { 626 options: aOptions, 627 } 628 ); 629 return Promise.resolve(); 630 }; 631 632 if (aOptions.isStartupMigration) { 633 // Record that the uninstaller requested a profile refresh 634 if (Services.env.get("MOZ_UNINSTALLER_PROFILE_REFRESH")) { 635 Services.env.set("MOZ_UNINSTALLER_PROFILE_REFRESH", ""); 636 Glean.migration.uninstallerProfileRefresh.set(true); 637 } 638 639 openStandaloneWindow(true /* blocking */); 640 return Promise.resolve(); 641 } 642 643 if (aOpener?.openPreferences) { 644 if (aOptions.entrypoint == this.MIGRATION_ENTRYPOINTS.NEWTAB) { 645 if (aboutWelcomeBehavior == "autoclose") { 646 return aOpener.openPreferences("general-migrate-autoclose"); 647 } else if (aboutWelcomeBehavior == "standalone") { 648 openStandaloneWindow(false /* blocking */); 649 return Promise.resolve(); 650 } 651 } 652 return aOpener.openPreferences("general-migrate"); 653 } 654 655 // If somehow we failed to open about:preferences, fall back to opening 656 // the top-level window. 657 openStandaloneWindow(false /* blocking */); 658 return Promise.resolve(); 659 } 660 661 /** 662 * Show the migration wizard for startup-migration. This should only be 663 * called by ProfileMigrator (see ProfileMigrator.js), which implements 664 * nsIProfileMigrator. This runs asynchronously if we are running an 665 * automigration. 666 * 667 * @param {nsIProfileStartup} aProfileStartup 668 * the nsIProfileStartup instance provided to ProfileMigrator.migrate. 669 * @param {string|null} [aMigratorKey=null] 670 * If set, the migration wizard will import from the corresponding 671 * migrator, bypassing the source-selection page. Otherwise, the 672 * source-selection page will be displayed, either with the default 673 * browser selected, if it could be detected and if there is a 674 * migrator for it, or with the first option selected as a fallback 675 * @param {string|null} [aProfileToMigrate=null] 676 * If set, the migration wizard will import from the profile indicated. 677 * @throws 678 * if aMigratorKey is invalid or if it points to a non-existent 679 * source. 680 */ 681 startupMigration(aProfileStartup, aMigratorKey, aProfileToMigrate) { 682 this.spinResolve( 683 this.asyncStartupMigration( 684 aProfileStartup, 685 aMigratorKey, 686 aProfileToMigrate 687 ) 688 ); 689 } 690 691 async asyncStartupMigration( 692 aProfileStartup, 693 aMigratorKey, 694 aProfileToMigrate 695 ) { 696 if (!aProfileStartup) { 697 throw new Error( 698 "an profile-startup instance is required for startup-migration" 699 ); 700 } 701 gProfileStartup = aProfileStartup; 702 703 let skipSourceSelection = false, 704 migrator = null, 705 migratorKey = ""; 706 if (aMigratorKey) { 707 migrator = await this.getMigrator(aMigratorKey); 708 if (!migrator) { 709 // aMigratorKey must point to a valid source, so, if it doesn't 710 // cleanup and throw. 711 this.finishMigration(); 712 throw new Error( 713 "startMigration was asked to open auto-migrate from " + 714 "a non-existent source: " + 715 aMigratorKey 716 ); 717 } 718 migratorKey = aMigratorKey; 719 skipSourceSelection = true; 720 } else { 721 let defaultBrowserKey = this.getMigratorKeyForDefaultBrowser(); 722 if (defaultBrowserKey) { 723 migrator = await this.getMigrator(defaultBrowserKey); 724 if (migrator) { 725 migratorKey = defaultBrowserKey; 726 } 727 } 728 } 729 730 if (!migrator) { 731 let migrators = await Promise.all( 732 this.availableMigratorKeys.map(key => this.getMigrator(key)) 733 ); 734 // If there's no migrator set so far, ensure that there is at least one 735 // migrator available before opening the wizard. 736 // Note that we don't need to check the default browser first, because 737 // if that one existed we would have used it in the block above this one. 738 if (!migrators.some(m => m)) { 739 // None of the keys produced a usable migrator, so finish up here: 740 this.finishMigration(); 741 return; 742 } 743 } 744 745 let isRefresh = 746 migrator && 747 skipSourceSelection && 748 migratorKey == AppConstants.MOZ_APP_NAME; 749 750 let entrypoint = this.MIGRATION_ENTRYPOINTS.FIRSTRUN; 751 if (isRefresh) { 752 entrypoint = this.MIGRATION_ENTRYPOINTS.FXREFRESH; 753 } 754 755 this.showMigrationWizard(null, { 756 entrypoint, 757 migratorKey, 758 migrator, 759 isStartupMigration: !!aProfileStartup, 760 skipSourceSelection, 761 profileId: aProfileToMigrate, 762 }); 763 } 764 765 /** 766 * This is only pseudo-private because some tests and helper functions 767 * still expect to be able to directly access it. 768 */ 769 _importQuantities = { 770 bookmarks: 0, 771 logins: 0, 772 history: 0, 773 cards: 0, 774 extensions: 0, 775 }; 776 777 getImportedCount(type) { 778 if (!this._importQuantities.hasOwnProperty(type)) { 779 throw new Error( 780 `Unknown import data type "${type}" passed to getImportedCount` 781 ); 782 } 783 return this._importQuantities[type]; 784 } 785 786 insertBookmarkWrapper(bookmark) { 787 this._importQuantities.bookmarks++; 788 let insertionPromise = lazy.PlacesUtils.bookmarks.insert(bookmark); 789 if (!gKeepUndoData) { 790 return insertionPromise; 791 } 792 // If we keep undo data, add a promise handler that stores the undo data once 793 // the bookmark has been inserted in the DB, and then returns the bookmark. 794 let { parentGuid } = bookmark; 795 return insertionPromise.then(bm => { 796 let { guid, lastModified, type } = bm; 797 gUndoData.get("bookmarks").push({ 798 parentGuid, 799 guid, 800 lastModified, 801 type, 802 }); 803 return bm; 804 }); 805 } 806 807 insertManyBookmarksWrapper(bookmarks, parent) { 808 let insertionPromise = lazy.PlacesUtils.bookmarks.insertTree({ 809 guid: parent, 810 children: bookmarks, 811 }); 812 return insertionPromise.then( 813 insertedItems => { 814 this._importQuantities.bookmarks += insertedItems.length; 815 if (gKeepUndoData) { 816 let bmData = gUndoData.get("bookmarks"); 817 for (let bm of insertedItems) { 818 let { parentGuid, guid, lastModified, type } = bm; 819 bmData.push({ parentGuid, guid, lastModified, type }); 820 } 821 } 822 if (parent == lazy.PlacesUtils.bookmarks.toolbarGuid) { 823 lazy.PlacesUIUtils.maybeToggleBookmarkToolbarVisibility( 824 true /* aForceVisible */ 825 ).catch(console.error); 826 } 827 }, 828 ex => console.error(ex) 829 ); 830 } 831 832 insertVisitsWrapper(pageInfos) { 833 let now = new Date(); 834 // Ensure that none of the dates are in the future. If they are, rewrite 835 // them to be now. This means we don't loose history entries, but they will 836 // be valid for the history store. 837 for (let pageInfo of pageInfos) { 838 for (let visit of pageInfo.visits) { 839 if (visit.date && visit.date > now) { 840 visit.date = now; 841 } 842 } 843 } 844 this._importQuantities.history += pageInfos.length; 845 if (gKeepUndoData) { 846 this.#updateHistoryUndo(pageInfos); 847 } 848 return lazy.PlacesUtils.history.insertMany(pageInfos); 849 } 850 851 async insertLoginsWrapper(logins) { 852 this._importQuantities.logins += logins.length; 853 let inserted = await lazy.LoginHelper.maybeImportLogins(logins); 854 // Note that this means that if we import a login that has a newer password 855 // than we know about, we will update the login, and an undo of the import 856 // will not revert this. This seems preferable over removing the login 857 // outright or storing the old password in the undo file. 858 if (gKeepUndoData) { 859 for (let { guid, timePasswordChanged } of inserted) { 860 gUndoData.get("logins").push({ guid, timePasswordChanged }); 861 } 862 } 863 } 864 865 /** 866 * Called by MigrationWizardParent during a migration to indicate that a 867 * manual migration of logins occurred via import from a CSV / TSV file, and 868 * should be counted towards the total number of imported logins. 869 * 870 * @param {number} totalLogins 871 * The number of logins imported manually from a CSV / TSV file. 872 */ 873 notifyLoginsManuallyImported(totalLogins) { 874 this._importQuantities.logins += totalLogins; 875 } 876 877 /** 878 * Iterates through the favicons, sniffs for a mime type, 879 * and uses the mime type to properly import the favicon. 880 * 881 * Note: You may not want to await on the returned promise, especially if by 882 * doing so there's risk of interrupting the migration of more critical 883 * data (e.g. bookmarks). 884 * 885 * @param {object[]} favicons 886 * An array of Objects with these properties: 887 * {Uint8Array} faviconData: The binary data of a favicon 888 * {nsIURI} uri: The URI of the associated page 889 */ 890 async insertManyFavicons(favicons) { 891 let sniffer = Cc["@mozilla.org/image/loader;1"].createInstance( 892 Ci.nsIContentSniffer 893 ); 894 895 for (let faviconDataItem of favicons) { 896 try { 897 // getMIMETypeFromContent throws error if could not get the mime type 898 // from the data. 899 let mimeType = sniffer.getMIMETypeFromContent( 900 null, 901 faviconDataItem.faviconData, 902 faviconDataItem.faviconData.length 903 ); 904 905 let dataURL = await new Promise((resolve, reject) => { 906 let buffer = new Uint8ClampedArray(faviconDataItem.faviconData); 907 let blob = new Blob([buffer], { type: mimeType }); 908 let reader = new FileReader(); 909 reader.addEventListener("load", () => resolve(reader.result)); 910 reader.addEventListener("error", reject); 911 reader.readAsDataURL(blob); 912 }); 913 914 let fakeFaviconURI = Services.io.newURI( 915 "fake-favicon-uri:" + faviconDataItem.uri.spec 916 ); 917 lazy.PlacesUtils.favicons 918 .setFaviconForPage( 919 faviconDataItem.uri, 920 fakeFaviconURI, 921 Services.io.newURI(dataURL) 922 ) 923 .catch(console.warn); 924 } catch (e) { 925 // Even if error happens for favicon, continue the process. 926 console.warn(e); 927 } 928 } 929 } 930 931 async insertCreditCardsWrapper(cards) { 932 this._importQuantities.cards += cards.length; 933 let { formAutofillStorage } = ChromeUtils.importESModule( 934 "resource://autofill/FormAutofillStorage.sys.mjs" 935 ); 936 937 await formAutofillStorage.initialize(); 938 for (let card of cards) { 939 try { 940 await formAutofillStorage.creditCards.add(card); 941 } catch (e) { 942 console.error("Failed to insert credit card due to error: ", e, card); 943 } 944 } 945 } 946 947 /** 948 * Responsible for calling the AddonManager API that ultimately installs the 949 * matched add-ons. 950 * 951 * @param {string} migratorKey a migrator key that we pass to 952 * `AMBrowserExtensionsImport` as the "browser 953 * identifier" used to match add-ons 954 * @param {string[]} extensionIDs a list of extension IDs from another browser 955 * @returns {(lazy.MigrationWizardConstants.PROGRESS_VALUE|string[])[]} 956 * An array whose first element is a `MigrationWizardConstants.PROGRESS_VALUE` 957 * and second element is an array of imported add-on ids. 958 */ 959 async installExtensionsWrapper(migratorKey, extensionIDs) { 960 const totalExtensions = extensionIDs.length; 961 962 let importedAddonIDs = []; 963 try { 964 const result = await lazy.AMBrowserExtensionsImport.stageInstalls( 965 migratorKey, 966 extensionIDs 967 ); 968 importedAddonIDs = result.importedAddonIDs; 969 } catch (e) { 970 console.error(`Failed to import extensions: ${e}`); 971 } 972 973 this._importQuantities.extensions += importedAddonIDs.length; 974 975 if (!importedAddonIDs.length) { 976 return [ 977 lazy.MigrationWizardConstants.PROGRESS_VALUE.WARNING, 978 importedAddonIDs, 979 ]; 980 } 981 if (totalExtensions == importedAddonIDs.length) { 982 return [ 983 lazy.MigrationWizardConstants.PROGRESS_VALUE.SUCCESS, 984 importedAddonIDs, 985 ]; 986 } 987 return [ 988 lazy.MigrationWizardConstants.PROGRESS_VALUE.INFO, 989 importedAddonIDs, 990 ]; 991 } 992 993 initializeUndoData() { 994 gKeepUndoData = true; 995 gUndoData = new Map([ 996 ["bookmarks", []], 997 ["visits", []], 998 ["logins", []], 999 ]); 1000 } 1001 1002 async #postProcessUndoData(state) { 1003 if (!state) { 1004 return state; 1005 } 1006 let bookmarkFolders = state 1007 .get("bookmarks") 1008 .filter(b => b.type == lazy.PlacesUtils.bookmarks.TYPE_FOLDER); 1009 1010 let bookmarkFolderData = []; 1011 let bmPromises = bookmarkFolders.map(({ guid }) => { 1012 // Ignore bookmarks where the promise doesn't resolve (ie that are missing) 1013 // Also check that the bookmark fetch returns isn't null before adding it. 1014 return lazy.PlacesUtils.bookmarks.fetch(guid).then( 1015 bm => bm && bookmarkFolderData.push(bm), 1016 () => {} 1017 ); 1018 }); 1019 1020 await Promise.all(bmPromises); 1021 let folderLMMap = new Map( 1022 bookmarkFolderData.map(b => [b.guid, b.lastModified]) 1023 ); 1024 for (let bookmark of bookmarkFolders) { 1025 let lastModified = folderLMMap.get(bookmark.guid); 1026 // If the bookmark was deleted, the map will be returning null, so check: 1027 if (lastModified) { 1028 bookmark.lastModified = lastModified; 1029 } 1030 } 1031 return state; 1032 } 1033 1034 stopAndRetrieveUndoData() { 1035 let undoData = gUndoData; 1036 gUndoData = null; 1037 gKeepUndoData = false; 1038 return this.#postProcessUndoData(undoData); 1039 } 1040 1041 #updateHistoryUndo(pageInfos) { 1042 let visits = gUndoData.get("visits"); 1043 let visitMap = new Map(visits.map(v => [v.url, v])); 1044 for (let pageInfo of pageInfos) { 1045 let visitCount = pageInfo.visits.length; 1046 let first, last; 1047 if (visitCount > 1) { 1048 let dates = pageInfo.visits.map(v => v.date); 1049 first = Math.min.apply(Math, dates); 1050 last = Math.max.apply(Math, dates); 1051 } else { 1052 first = last = pageInfo.visits[0].date; 1053 } 1054 let url = pageInfo.url; 1055 if (url instanceof Ci.nsIURI) { 1056 url = pageInfo.url.spec; 1057 } 1058 1059 if (!URL.canParse(url)) { 1060 // This won't save and we won't need to 'undo' it, so ignore this URL. 1061 continue; 1062 } 1063 if (!visitMap.has(url)) { 1064 visitMap.set(url, { url, visitCount, first, last }); 1065 } else { 1066 let currentData = visitMap.get(url); 1067 currentData.visitCount += visitCount; 1068 currentData.first = Math.min(currentData.first, first); 1069 currentData.last = Math.max(currentData.last, last); 1070 } 1071 } 1072 gUndoData.set("visits", Array.from(visitMap.values())); 1073 } 1074 1075 /** 1076 * Cleans up references to migrators and nsIProfileInstance instances. 1077 */ 1078 finishMigration() { 1079 gMigrators = null; 1080 gProfileStartup = null; 1081 gL10n = null; 1082 } 1083 1084 get availableMigratorKeys() { 1085 return [...this.#migrators.keys()]; 1086 } 1087 1088 get availableFileMigrators() { 1089 return [...this.#fileMigrators.values()]; 1090 } 1091 1092 /** 1093 * Enum for the entrypoint that is being used to start migration. 1094 * Callers can use the MIGRATION_ENTRYPOINTS getter to use these. 1095 * 1096 * These values are what's written into the 1097 * FX_MIGRATION_ENTRY_POINT_CATEGORICAL histogram after a migration. 1098 * 1099 * @see MIGRATION_ENTRYPOINTS 1100 * @readonly 1101 * @enum {string} 1102 */ 1103 #MIGRATION_ENTRYPOINTS_ENUM = Object.freeze({ 1104 /** The entrypoint was not supplied */ 1105 UNKNOWN: "unknown", 1106 1107 /** Migration is occurring at startup */ 1108 FIRSTRUN: "firstrun", 1109 1110 /** Migration is occurring at after a profile refresh */ 1111 FXREFRESH: "fxrefresh", 1112 1113 /** Migration is being started from the Library window */ 1114 PLACES: "places", 1115 1116 /** Migration is being started from our password management UI */ 1117 PASSWORDS: "passwords", 1118 1119 /** Migration is being started from the default about:home/about:newtab */ 1120 NEWTAB: "newtab", 1121 1122 /** Migration is being started from the File menu */ 1123 FILE_MENU: "file_menu", 1124 1125 /** Migration is being started from the Help menu */ 1126 HELP_MENU: "help_menu", 1127 1128 /** Migration is being started from the Bookmarks Toolbar */ 1129 BOOKMARKS_TOOLBAR: "bookmarks_toolbar", 1130 1131 /** Migration is being started from about:preferences */ 1132 PREFERENCES: "preferences", 1133 1134 /** Migration is being started from about:firefoxview */ 1135 FIREFOX_VIEW: "firefox_view", 1136 }); 1137 1138 /** 1139 * Returns an enum that should be used to record the entrypoint for 1140 * starting a migration. 1141 * 1142 * @returns {number} 1143 */ 1144 get MIGRATION_ENTRYPOINTS() { 1145 return this.#MIGRATION_ENTRYPOINTS_ENUM; 1146 } 1147 1148 /** 1149 * Enum for the numeric value written to the FX_MIGRATION_SOURCE_BROWSER. 1150 * histogram 1151 * 1152 * @see getSourceIdForTelemetry 1153 * @readonly 1154 * @enum {number} 1155 */ 1156 #SOURCE_NAME_TO_ID_MAPPING_ENUM = Object.freeze({ 1157 nothing: 1, 1158 firefox: 2, 1159 edge: 3, 1160 ie: 4, 1161 chrome: 5, 1162 "chrome-beta": 5, 1163 "chrome-dev": 5, 1164 chromium: 6, 1165 canary: 7, 1166 safari: 8, 1167 "chromium-360se": 9, 1168 "chromium-edge": 10, 1169 "chromium-edge-beta": 10, 1170 brave: 11, 1171 opera: 12, 1172 "opera-gx": 14, 1173 vivaldi: 13, 1174 }); 1175 1176 getSourceIdForTelemetry(sourceName) { 1177 return this.#SOURCE_NAME_TO_ID_MAPPING_ENUM[sourceName] || 0; 1178 } 1179 1180 get HISTORY_MAX_AGE_IN_MILLISECONDS() { 1181 return this.HISTORY_MAX_AGE_IN_DAYS * 24 * 60 * 60 * 1000; 1182 } 1183 1184 /** 1185 * Determines whether or not the underlying platform supports creating 1186 * native file pickers that can do folder selection, which is a 1187 * pre-requisite for getting read-access permissions for data from other 1188 * browsers that we can import from. 1189 * 1190 * @returns {Promise<boolean>} 1191 */ 1192 canGetPermissionsOnPlatform() { 1193 return lazy.gCanGetPermissionsOnPlatformPromise; 1194 } 1195 } 1196 1197 const MigrationUtilsSingleton = new MigrationUtils(); 1198 1199 export { MigrationUtilsSingleton as MigrationUtils };