ChromeProfileMigrator.sys.mjs (38638B)
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- 2 * vim: sw=2 ts=2 sts=2 et */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 const AUTH_TYPE = { 8 SCHEME_HTML: 0, 9 SCHEME_BASIC: 1, 10 SCHEME_DIGEST: 2, 11 }; 12 13 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 14 import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs"; 15 import { MigratorBase } from "resource:///modules/MigratorBase.sys.mjs"; 16 17 const lazy = {}; 18 19 ChromeUtils.defineESModuleGetters(lazy, { 20 ChromeMigrationUtils: "resource:///modules/ChromeMigrationUtils.sys.mjs", 21 FormHistory: "resource://gre/modules/FormHistory.sys.mjs", 22 NetUtil: "resource://gre/modules/NetUtil.sys.mjs", 23 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 24 Qihoo360seMigrationUtils: "resource:///modules/360seMigrationUtils.sys.mjs", 25 MigrationWizardConstants: 26 "chrome://browser/content/migration/migration-wizard-constants.mjs", 27 }); 28 29 /** 30 * Converts an array of chrome bookmark objects into one our own places code 31 * understands. 32 * 33 * @param {object[]} items Chrome Bookmark items to be inserted on this parent 34 * @param {set} bookmarkURLAccumulator Accumulate all imported bookmark urls to be used for importing favicons 35 * @param {Function} errorAccumulator function that gets called with any errors 36 * thrown so we don't drop them on the floor. 37 * @returns {object[]} 38 */ 39 function convertBookmarks(items, bookmarkURLAccumulator, errorAccumulator) { 40 let itemsToInsert = []; 41 for (let item of items) { 42 try { 43 if (item.type == "url") { 44 if (item.url.trim().startsWith("chrome:")) { 45 // Skip invalid internal URIs. Creating an actual URI always reports 46 // messages to the console because Gecko has its own concept of how 47 // chrome:// URIs should be formed, so we avoid doing that. 48 continue; 49 } 50 if (item.url.trim().startsWith("edge:")) { 51 // Don't import internal Microsoft Edge URIs as they won't resolve within Firefox. 52 continue; 53 } 54 itemsToInsert.push({ url: item.url, title: item.name }); 55 bookmarkURLAccumulator.add({ url: item.url }); 56 } else if (item.type == "folder") { 57 let folderItem = { 58 type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER, 59 title: item.name, 60 }; 61 folderItem.children = convertBookmarks( 62 item.children, 63 bookmarkURLAccumulator, 64 errorAccumulator 65 ); 66 itemsToInsert.push(folderItem); 67 } 68 } catch (ex) { 69 console.error(ex); 70 errorAccumulator(ex); 71 } 72 } 73 return itemsToInsert; 74 } 75 76 /** 77 * Chrome profile migrator. This can also be used as a parent class for 78 * migrators for browsers that are variants of Chrome. 79 */ 80 export class ChromeProfileMigrator extends MigratorBase { 81 /** 82 * On Ubuntu Linux, when the browser is installed as a Snap package, 83 * we must request permission to read data from other browsers. We 84 * make that request by opening up a native file picker in folder 85 * selection mode and instructing the user to navigate to the folder 86 * that the other browser's user data resides in. 87 * 88 * For Snap packages, this gives the browser read access - but it does 89 * so through a temporary symlink that does not match the original user 90 * data path. Effectively, the user data directory is remapped to a 91 * temporary location on the file system. We record these remaps here, 92 * keyed on the original data directory. 93 * 94 * @type {Map<string, string>} 95 */ 96 #dataPathRemappings = new Map(); 97 98 static get key() { 99 return "chrome"; 100 } 101 102 static get displayNameL10nID() { 103 return "migration-wizard-migrator-display-name-chrome"; 104 } 105 106 static get brandImage() { 107 return "chrome://browser/content/migration/brands/chrome.png"; 108 } 109 110 get _chromeUserDataPathSuffix() { 111 return "Chrome"; 112 } 113 114 async hasPermissions() { 115 let dataPath = await this._getChromeUserDataPathIfExists(); 116 if (!dataPath) { 117 return true; 118 } 119 120 let localStatePath = PathUtils.join(dataPath, "Local State"); 121 try { 122 // Read one byte since on snap we can check existence even without being able 123 // to read the file. 124 await IOUtils.read(localStatePath, { maxBytes: 1 }); 125 return true; 126 } catch (ex) { 127 console.error("No permissions for local state folder."); 128 } 129 return false; 130 } 131 132 async getPermissions(win) { 133 // Get the original path to the user data and ignore any existing remapping. 134 // This allows us to set a new remapping if the user navigates the platforms 135 // filepicker to a different directory on a second permission request attempt. 136 let originalDataPath = await this._getChromeUserDataPathIfExists( 137 true /* noRemapping */ 138 ); 139 // Keep prompting the user until they pick something that grants us access 140 // to Chrome's local state directory. 141 while (!(await this.hasPermissions())) { 142 let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); 143 fp.init(win?.browsingContext, "", Ci.nsIFilePicker.modeGetFolder); 144 fp.filterIndex = 1; 145 // Now wait for the filepicker to open and close. If the user picks 146 // the local state folder, the OS should grant us read access to everything 147 // inside, so we don't need to check or do anything else with what's 148 // returned by the filepicker. 149 let result = await new Promise(resolve => fp.open(resolve)); 150 // Bail if the user cancels the dialog: 151 if (result == Ci.nsIFilePicker.returnCancel) { 152 return false; 153 } 154 155 let file = fp.file; 156 if (file && file.path != originalDataPath) { 157 this.#dataPathRemappings.set(originalDataPath, file.path); 158 } 159 } 160 return true; 161 } 162 163 async canGetPermissions() { 164 if ( 165 !Services.prefs.getBoolPref( 166 "browser.migrate.chrome.get_permissions.enabled" 167 ) 168 ) { 169 return false; 170 } 171 172 if (await MigrationUtils.canGetPermissionsOnPlatform()) { 173 let dataPath = await this._getChromeUserDataPathIfExists(); 174 if (dataPath) { 175 let localStatePath = PathUtils.join(dataPath, "Local State"); 176 if (await IOUtils.exists(localStatePath)) { 177 return dataPath; 178 } 179 } 180 } 181 return false; 182 } 183 184 /** 185 * For Chrome on Windows, we show a specialized flow for importing passwords 186 * from a CSV file. 187 * 188 * @returns {boolean} 189 */ 190 get showsManualPasswordImport() { 191 return AppConstants.platform == "win" && this.constructor.key == "chrome"; 192 } 193 194 _keychainServiceName = "Chrome Safe Storage"; 195 196 _keychainAccountName = "Chrome"; 197 198 /** 199 * Returns a Promise that resolves to the data path containing the 200 * Local State and profile directories for this browser. 201 * 202 * @param {boolean} [noRemapping=false] 203 * Set to true to bypass any remapping that might have occurred on 204 * platforms where the data path changes once permission has been 205 * granted. 206 * @returns {Promise<string>} 207 */ 208 async _getChromeUserDataPathIfExists(noRemapping = false) { 209 if (this._chromeUserDataPath) { 210 // Skip looking up any remapping if `noRemapping` was passed. This is 211 // helpful if the caller needs create a new remapping and overwrite 212 // an old remapping, as "real" user data path is used as a key for 213 // the remapping. 214 if (noRemapping) { 215 return this._chromeUserDataPath; 216 } 217 218 let remappedPath = this.#dataPathRemappings.get(this._chromeUserDataPath); 219 return remappedPath || this._chromeUserDataPath; 220 } 221 let path = await lazy.ChromeMigrationUtils.getDataPath( 222 this._chromeUserDataPathSuffix 223 ); 224 let exists = path && (await IOUtils.exists(path)); 225 if (exists) { 226 this._chromeUserDataPath = path; 227 } else { 228 this._chromeUserDataPath = null; 229 } 230 return this._chromeUserDataPath; 231 } 232 233 async getResources(aProfile) { 234 if (!(await this.hasPermissions())) { 235 return []; 236 } 237 238 let chromeUserDataPath = await this._getChromeUserDataPathIfExists(); 239 if (chromeUserDataPath) { 240 let profileFolder = chromeUserDataPath; 241 if (aProfile) { 242 profileFolder = PathUtils.join(chromeUserDataPath, aProfile.id); 243 } 244 if (await IOUtils.exists(profileFolder)) { 245 let possibleResourcePromises = [ 246 GetBookmarksResource(profileFolder, this.constructor.key), 247 GetHistoryResource(profileFolder), 248 GetFormdataResource(profileFolder), 249 GetExtensionsResource(aProfile.id, this.constructor.key), 250 ]; 251 if (lazy.ChromeMigrationUtils.supportsLoginsForPlatform) { 252 possibleResourcePromises.push( 253 this._GetPasswordsResource(profileFolder), 254 this._GetPaymentMethodsResource(profileFolder, this.constructor.key) 255 ); 256 } 257 258 // Some of these Promises might reject due to things like database 259 // corruptions. We absorb those rejections here and filter them 260 // out so that we only try to import the resources that don't appear 261 // corrupted. 262 let possibleResources = await Promise.allSettled( 263 possibleResourcePromises 264 ); 265 return possibleResources 266 .filter(promise => { 267 return promise.status == "fulfilled" && promise.value !== null; 268 }) 269 .map(promise => promise.value); 270 } 271 } 272 return []; 273 } 274 275 async getLastUsedDate() { 276 let sourceProfiles = await this.getSourceProfiles(); 277 if (!sourceProfiles) { 278 return new Date(0); 279 } 280 let chromeUserDataPath = await this._getChromeUserDataPathIfExists(); 281 if (!chromeUserDataPath) { 282 return new Date(0); 283 } 284 let datePromises = sourceProfiles.map(async profile => { 285 let basePath = PathUtils.join(chromeUserDataPath, profile.id); 286 let fileDatePromises = ["Bookmarks", "History", "Cookies"].map( 287 async leafName => { 288 let path = PathUtils.join(basePath, leafName); 289 let info = await IOUtils.stat(path).catch(() => null); 290 return info ? info.lastModified : 0; 291 } 292 ); 293 let dates = await Promise.all(fileDatePromises); 294 return Math.max(...dates); 295 }); 296 let datesOuter = await Promise.all(datePromises); 297 datesOuter.push(0); 298 return new Date(Math.max(...datesOuter)); 299 } 300 301 async getSourceProfiles() { 302 if ("__sourceProfiles" in this) { 303 return this.__sourceProfiles; 304 } 305 306 let chromeUserDataPath = await this._getChromeUserDataPathIfExists(); 307 if (!chromeUserDataPath) { 308 return []; 309 } 310 311 let localState; 312 let profiles = []; 313 try { 314 localState = await lazy.ChromeMigrationUtils.getLocalState( 315 this._chromeUserDataPathSuffix, 316 chromeUserDataPath 317 ); 318 let info_cache = localState.profile.info_cache; 319 for (let profileFolderName in info_cache) { 320 profiles.push({ 321 id: profileFolderName, 322 name: info_cache[profileFolderName].name || profileFolderName, 323 }); 324 } 325 } catch (e) { 326 // Avoid reporting NotFoundErrors from trying to get local state. 327 if (localState || e.name != "NotFoundError") { 328 console.error("Error detecting Chrome profiles: ", e); 329 } 330 331 // If we didn't have permission to read the local state, return the 332 // empty array. The user might have the opportunity to request 333 // permission using `hasPermission` and `getPermission`. 334 if (e.name == "NotAllowedError") { 335 return []; 336 } 337 338 // If we weren't able to detect any profiles above, fallback to the Default profile. 339 let defaultProfilePath = PathUtils.join(chromeUserDataPath, "Default"); 340 if (await IOUtils.exists(defaultProfilePath)) { 341 profiles = [ 342 { 343 id: "Default", 344 name: "Default", 345 }, 346 ]; 347 } 348 } 349 350 let profileResources = await Promise.all( 351 profiles.map(async profile => ({ 352 profile, 353 resources: await this.getResources(profile), 354 })) 355 ); 356 357 // Only list profiles from which any data can be imported 358 this.__sourceProfiles = profileResources 359 .filter(({ resources }) => { 360 return resources && !!resources.length; 361 }, this) 362 .map(({ profile }) => profile); 363 return this.__sourceProfiles; 364 } 365 366 async _GetPasswordsResource(aProfileFolder) { 367 let loginPath = PathUtils.join(aProfileFolder, "Login Data"); 368 if (!(await IOUtils.exists(loginPath))) { 369 return null; 370 } 371 372 let tempFilePath = null; 373 if (MigrationUtils.IS_LINUX_SNAP_PACKAGE) { 374 tempFilePath = await IOUtils.createUniqueFile( 375 PathUtils.tempDir, 376 "Login Data" 377 ); 378 await IOUtils.copy(loginPath, tempFilePath); 379 loginPath = tempFilePath; 380 } 381 382 let { 383 _chromeUserDataPathSuffix, 384 _keychainServiceName, 385 _keychainAccountName, 386 _keychainMockPassphrase = null, 387 } = this; 388 389 let countQuery = `SELECT COUNT(*) FROM logins WHERE blacklisted_by_user = 0`; 390 391 let countRows = await MigrationUtils.getRowsFromDBWithoutLocks( 392 loginPath, 393 "Chrome passwords", 394 countQuery 395 ); 396 397 if (!countRows[0].getResultByName("COUNT(*)")) { 398 return null; 399 } 400 401 return { 402 type: MigrationUtils.resourceTypes.PASSWORDS, 403 404 async migrate(aCallback) { 405 let rows = await MigrationUtils.getRowsFromDBWithoutLocks( 406 loginPath, 407 "Chrome passwords", 408 `SELECT origin_url, action_url, username_element, username_value, 409 password_element, password_value, signon_realm, scheme, date_created, 410 times_used FROM logins WHERE blacklisted_by_user = 0` 411 ) 412 .catch(ex => { 413 console.error(ex); 414 aCallback(false); 415 }) 416 .finally(() => { 417 return tempFilePath && IOUtils.remove(tempFilePath); 418 }); 419 420 // If the promise was rejected we will have already called aCallback, 421 // so we can just return here. 422 if (!rows) { 423 return; 424 } 425 426 // If there are no relevant rows, return before initializing crypto and 427 // thus prompting for Keychain access on macOS. 428 if (!rows.length) { 429 aCallback(true); 430 return; 431 } 432 433 let loginCrypto; 434 try { 435 if (AppConstants.platform == "win") { 436 let { ChromeWindowsLoginCrypto } = ChromeUtils.importESModule( 437 "resource:///modules/ChromeWindowsLoginCrypto.sys.mjs" 438 ); 439 loginCrypto = new ChromeWindowsLoginCrypto( 440 _chromeUserDataPathSuffix 441 ); 442 } else if (AppConstants.platform == "macosx") { 443 let { ChromeMacOSLoginCrypto } = ChromeUtils.importESModule( 444 "resource:///modules/ChromeMacOSLoginCrypto.sys.mjs" 445 ); 446 loginCrypto = new ChromeMacOSLoginCrypto( 447 _keychainServiceName, 448 _keychainAccountName, 449 _keychainMockPassphrase 450 ); 451 } else { 452 aCallback(false); 453 return; 454 } 455 } catch (ex) { 456 // Handle the user canceling Keychain access or other OSCrypto errors. 457 console.error(ex); 458 aCallback(false); 459 return; 460 } 461 462 let logins = []; 463 let fallbackCreationDate = new Date(); 464 const kValidSchemes = new Set(["https", "http", "ftp"]); 465 for (let row of rows) { 466 try { 467 let origin_url = lazy.NetUtil.newURI( 468 row.getResultByName("origin_url") 469 ); 470 // Ignore entries for non-http(s)/ftp URLs because we likely can't 471 // use them anyway. 472 if (!kValidSchemes.has(origin_url.scheme)) { 473 continue; 474 } 475 let loginInfo = { 476 username: row.getResultByName("username_value"), 477 password: await loginCrypto.decryptData( 478 row.getResultByName("password_value"), 479 null 480 ), 481 origin: origin_url.prePath, 482 formActionOrigin: null, 483 httpRealm: null, 484 usernameElement: row.getResultByName("username_element"), 485 passwordElement: row.getResultByName("password_element"), 486 timeCreated: lazy.ChromeMigrationUtils.chromeTimeToDate( 487 row.getResultByName("date_created") + 0, 488 fallbackCreationDate 489 ).getTime(), 490 timesUsed: row.getResultByName("times_used") + 0, 491 }; 492 493 switch (row.getResultByName("scheme")) { 494 case AUTH_TYPE.SCHEME_HTML: { 495 let action_url = row.getResultByName("action_url"); 496 if (!action_url) { 497 // If there is no action_url, store the wildcard "" value. 498 // See the `formActionOrigin` IDL comments. 499 loginInfo.formActionOrigin = ""; 500 break; 501 } 502 let action_uri = lazy.NetUtil.newURI(action_url); 503 if (!kValidSchemes.has(action_uri.scheme)) { 504 continue; // This continues the outer for loop. 505 } 506 loginInfo.formActionOrigin = action_uri.prePath; 507 break; 508 } 509 case AUTH_TYPE.SCHEME_BASIC: 510 case AUTH_TYPE.SCHEME_DIGEST: 511 // signon_realm format is URIrealm, so we need remove URI 512 loginInfo.httpRealm = row 513 .getResultByName("signon_realm") 514 .substring(loginInfo.origin.length + 1); 515 break; 516 default: 517 throw new Error( 518 "Login data scheme type not supported: " + 519 row.getResultByName("scheme") 520 ); 521 } 522 logins.push(loginInfo); 523 } catch (e) { 524 console.error(e); 525 } 526 } 527 try { 528 if (logins.length) { 529 await MigrationUtils.insertLoginsWrapper(logins); 530 } 531 } catch (e) { 532 console.error(e); 533 } 534 if (crypto.finalize) { 535 crypto.finalize(); 536 } 537 aCallback(true); 538 }, 539 }; 540 } 541 async _GetPaymentMethodsResource(aProfileFolder, aBrowserKey = "chrome") { 542 if ( 543 !Services.prefs.getBoolPref( 544 "browser.migrate.chrome.payment_methods.enabled", 545 false 546 ) 547 ) { 548 return null; 549 } 550 551 // We no longer support importing payment methods from Chrome or Edge on 552 // Windows. 553 if ( 554 AppConstants.platform == "win" && 555 (aBrowserKey == "chrome" || aBrowserKey == "chromium-edge") 556 ) { 557 return null; 558 } 559 560 let paymentMethodsPath = PathUtils.join(aProfileFolder, "Web Data"); 561 562 if (!(await IOUtils.exists(paymentMethodsPath))) { 563 return null; 564 } 565 566 let tempFilePath = null; 567 if (MigrationUtils.IS_LINUX_SNAP_PACKAGE) { 568 tempFilePath = await IOUtils.createUniqueFile( 569 PathUtils.tempDir, 570 "Web Data" 571 ); 572 await IOUtils.copy(paymentMethodsPath, tempFilePath); 573 paymentMethodsPath = tempFilePath; 574 } 575 576 let rows = await MigrationUtils.getRowsFromDBWithoutLocks( 577 paymentMethodsPath, 578 "Chrome Credit Cards", 579 "SELECT name_on_card, card_number_encrypted, expiration_month, expiration_year FROM credit_cards" 580 ) 581 .catch(ex => { 582 console.error(ex); 583 }) 584 .finally(() => { 585 return tempFilePath && IOUtils.remove(tempFilePath); 586 }); 587 588 if (!rows?.length) { 589 return null; 590 } 591 592 let { 593 _chromeUserDataPathSuffix, 594 _keychainServiceName, 595 _keychainAccountName, 596 _keychainMockPassphrase = null, 597 } = this; 598 599 return { 600 type: MigrationUtils.resourceTypes.PAYMENT_METHODS, 601 602 async migrate(aCallback) { 603 let loginCrypto; 604 try { 605 if (AppConstants.platform == "win") { 606 let { ChromeWindowsLoginCrypto } = ChromeUtils.importESModule( 607 "resource:///modules/ChromeWindowsLoginCrypto.sys.mjs" 608 ); 609 loginCrypto = new ChromeWindowsLoginCrypto( 610 _chromeUserDataPathSuffix 611 ); 612 } else if (AppConstants.platform == "macosx") { 613 let { ChromeMacOSLoginCrypto } = ChromeUtils.importESModule( 614 "resource:///modules/ChromeMacOSLoginCrypto.sys.mjs" 615 ); 616 loginCrypto = new ChromeMacOSLoginCrypto( 617 _keychainServiceName, 618 _keychainAccountName, 619 _keychainMockPassphrase 620 ); 621 } else { 622 aCallback(false); 623 return; 624 } 625 } catch (ex) { 626 // Handle the user canceling Keychain access or other OSCrypto errors. 627 console.error(ex); 628 aCallback(false); 629 return; 630 } 631 632 let cards = []; 633 for (let row of rows) { 634 try { 635 cards.push({ 636 "cc-name": row.getResultByName("name_on_card"), 637 "cc-number": await loginCrypto.decryptData( 638 row.getResultByName("card_number_encrypted"), 639 null 640 ), 641 "cc-exp-month": parseInt( 642 row.getResultByName("expiration_month"), 643 10 644 ), 645 "cc-exp-year": parseInt( 646 row.getResultByName("expiration_year"), 647 10 648 ), 649 }); 650 } catch (e) { 651 console.error(e); 652 } 653 } 654 655 await MigrationUtils.insertCreditCardsWrapper(cards); 656 aCallback(true); 657 }, 658 }; 659 } 660 } 661 662 async function GetBookmarksResource(aProfileFolder, aBrowserKey) { 663 let bookmarksPath = PathUtils.join(aProfileFolder, "Bookmarks"); 664 let faviconsPath = PathUtils.join(aProfileFolder, "Favicons"); 665 666 if (aBrowserKey === "chromium-360se") { 667 let localState = {}; 668 try { 669 localState = await lazy.ChromeMigrationUtils.getLocalState("360 SE"); 670 } catch (ex) { 671 console.error(ex); 672 } 673 674 let alternativeBookmarks = 675 await lazy.Qihoo360seMigrationUtils.getAlternativeBookmarks({ 676 bookmarksPath, 677 localState, 678 }); 679 if (alternativeBookmarks.resource) { 680 return alternativeBookmarks.resource; 681 } 682 683 bookmarksPath = alternativeBookmarks.path; 684 } 685 686 if (!(await IOUtils.exists(bookmarksPath))) { 687 return null; 688 } 689 690 let tempFilePath = null; 691 if (MigrationUtils.IS_LINUX_SNAP_PACKAGE) { 692 tempFilePath = await IOUtils.createUniqueFile( 693 PathUtils.tempDir, 694 "Favicons" 695 ); 696 await IOUtils.copy(faviconsPath, tempFilePath); 697 faviconsPath = tempFilePath; 698 } 699 700 // check to read JSON bookmarks structure and see if any bookmarks exist else return null 701 // Parse Chrome bookmark file that is JSON format 702 let bookmarkJSON = await IOUtils.readJSON(bookmarksPath); 703 let other = bookmarkJSON.roots.other.children.length; 704 let bookmarkBar = bookmarkJSON.roots.bookmark_bar.children.length; 705 let synced = bookmarkJSON.roots.synced.children.length; 706 707 if (!other && !bookmarkBar && !synced) { 708 return null; 709 } 710 return { 711 type: MigrationUtils.resourceTypes.BOOKMARKS, 712 713 migrate(aCallback) { 714 return (async function () { 715 let gotErrors = false; 716 let errorGatherer = function () { 717 gotErrors = true; 718 }; 719 720 let faviconRows = []; 721 try { 722 faviconRows = await MigrationUtils.getRowsFromDBWithoutLocks( 723 faviconsPath, 724 "Chrome Bookmark Favicons", 725 `select fav.id, fav.url, map.page_url, bit.image_data FROM favicons as fav 726 INNER JOIN favicon_bitmaps bit ON (fav.id = bit.icon_id) 727 INNER JOIN icon_mapping map ON (map.icon_id = bit.icon_id)` 728 ); 729 } catch (ex) { 730 console.error(ex); 731 } finally { 732 if (tempFilePath) { 733 await IOUtils.remove(tempFilePath); 734 } 735 } 736 737 // Create Hashmap for favicons 738 let faviconMap = new Map(); 739 for (let faviconRow of faviconRows) { 740 // First, try to normalize the URI: 741 try { 742 let uri = lazy.NetUtil.newURI( 743 faviconRow.getResultByName("page_url") 744 ); 745 faviconMap.set(uri.spec, { 746 faviconData: faviconRow.getResultByName("image_data"), 747 uri, 748 }); 749 } catch (e) { 750 // Couldn't parse the URI, so just skip it. 751 continue; 752 } 753 } 754 755 let roots = bookmarkJSON.roots; 756 let bookmarkURLAccumulator = new Set(); 757 758 // Importing bookmark bar items 759 if (roots.bookmark_bar.children && roots.bookmark_bar.children.length) { 760 // Toolbar 761 let parentGuid = lazy.PlacesUtils.bookmarks.toolbarGuid; 762 let bookmarks = convertBookmarks( 763 roots.bookmark_bar.children, 764 bookmarkURLAccumulator, 765 errorGatherer 766 ); 767 await MigrationUtils.insertManyBookmarksWrapper( 768 bookmarks, 769 parentGuid 770 ); 771 } 772 773 // Importing Other Bookmarks items 774 if (roots.other.children && roots.other.children.length) { 775 // Other Bookmarks 776 let parentGuid = lazy.PlacesUtils.bookmarks.unfiledGuid; 777 let bookmarks = convertBookmarks( 778 roots.other.children, 779 bookmarkURLAccumulator, 780 errorGatherer 781 ); 782 await MigrationUtils.insertManyBookmarksWrapper( 783 bookmarks, 784 parentGuid 785 ); 786 } 787 788 // Importing synced Bookmarks items 789 if (roots.synced.children && roots.synced.children.length) { 790 // Synced Bookmarks 791 let parentGuid = lazy.PlacesUtils.bookmarks.unfiledGuid; 792 let bookmarks = convertBookmarks( 793 roots.synced.children, 794 bookmarkURLAccumulator, 795 errorGatherer 796 ); 797 await MigrationUtils.insertManyBookmarksWrapper( 798 bookmarks, 799 parentGuid 800 ); 801 } 802 803 // Find all favicons with associated bookmarks 804 let favicons = []; 805 for (let bookmark of bookmarkURLAccumulator) { 806 try { 807 let uri = lazy.NetUtil.newURI(bookmark.url); 808 let favicon = faviconMap.get(uri.spec); 809 if (favicon) { 810 favicons.push(favicon); 811 } 812 } catch (e) { 813 // Couldn't parse the bookmark URI, so just skip 814 continue; 815 } 816 } 817 818 // Import Bookmark Favicons 819 MigrationUtils.insertManyFavicons(favicons).catch(console.error); 820 821 if (gotErrors) { 822 throw new Error("The migration included errors."); 823 } 824 })().then( 825 () => aCallback(true), 826 () => aCallback(false) 827 ); 828 }, 829 }; 830 } 831 832 async function GetHistoryResource(aProfileFolder) { 833 let historyPath = PathUtils.join(aProfileFolder, "History"); 834 if (!(await IOUtils.exists(historyPath))) { 835 return null; 836 } 837 838 let tempFilePath = null; 839 if (MigrationUtils.IS_LINUX_SNAP_PACKAGE) { 840 tempFilePath = await IOUtils.createUniqueFile(PathUtils.tempDir, "History"); 841 await IOUtils.copy(historyPath, tempFilePath); 842 historyPath = tempFilePath; 843 } 844 845 let countQuery = "SELECT COUNT(*) FROM urls WHERE hidden = 0"; 846 847 let countRows = await MigrationUtils.getRowsFromDBWithoutLocks( 848 historyPath, 849 "Chrome history", 850 countQuery 851 ); 852 if (!countRows[0].getResultByName("COUNT(*)")) { 853 return null; 854 } 855 return { 856 type: MigrationUtils.resourceTypes.HISTORY, 857 858 migrate(aCallback) { 859 (async function () { 860 const LIMIT = Services.prefs.getIntPref( 861 "browser.migrate.chrome.history.limit" 862 ); 863 864 let query = 865 "SELECT url, title, last_visit_time, typed_count FROM urls WHERE hidden = 0"; 866 let maxAge = lazy.ChromeMigrationUtils.dateToChromeTime( 867 Date.now() - MigrationUtils.HISTORY_MAX_AGE_IN_MILLISECONDS 868 ); 869 query += " AND last_visit_time > " + maxAge; 870 871 if (LIMIT) { 872 query += " ORDER BY last_visit_time DESC LIMIT " + LIMIT; 873 } 874 875 let rows; 876 try { 877 rows = await MigrationUtils.getRowsFromDBWithoutLocks( 878 historyPath, 879 "Chrome history", 880 query 881 ); 882 } finally { 883 if (tempFilePath) { 884 await IOUtils.remove(tempFilePath); 885 } 886 } 887 888 let pageInfos = []; 889 let fallbackVisitDate = new Date(); 890 for (let row of rows) { 891 try { 892 // if having typed_count, we changes transition type to typed. 893 let transition = lazy.PlacesUtils.history.TRANSITIONS.LINK; 894 if (row.getResultByName("typed_count") > 0) { 895 transition = lazy.PlacesUtils.history.TRANSITIONS.TYPED; 896 } 897 898 pageInfos.push({ 899 title: row.getResultByName("title"), 900 url: new URL(row.getResultByName("url")), 901 visits: [ 902 { 903 transition, 904 date: lazy.ChromeMigrationUtils.chromeTimeToDate( 905 row.getResultByName("last_visit_time"), 906 fallbackVisitDate 907 ), 908 }, 909 ], 910 }); 911 } catch (e) { 912 console.error(e); 913 } 914 } 915 916 if (pageInfos.length) { 917 await MigrationUtils.insertVisitsWrapper(pageInfos); 918 } 919 })().then( 920 () => { 921 aCallback(true); 922 }, 923 ex => { 924 console.error(ex); 925 aCallback(false); 926 } 927 ); 928 }, 929 }; 930 } 931 932 async function GetFormdataResource(aProfileFolder) { 933 let formdataPath = PathUtils.join(aProfileFolder, "Web Data"); 934 if (!(await IOUtils.exists(formdataPath))) { 935 return null; 936 } 937 let countQuery = "SELECT COUNT(*) FROM autofill"; 938 939 let tempFilePath = null; 940 if (MigrationUtils.IS_LINUX_SNAP_PACKAGE) { 941 tempFilePath = await IOUtils.createUniqueFile( 942 PathUtils.tempDir, 943 "Web Data" 944 ); 945 await IOUtils.copy(formdataPath, tempFilePath); 946 formdataPath = tempFilePath; 947 } 948 949 let countRows = await MigrationUtils.getRowsFromDBWithoutLocks( 950 formdataPath, 951 "Chrome formdata", 952 countQuery 953 ); 954 if (!countRows[0].getResultByName("COUNT(*)")) { 955 return null; 956 } 957 return { 958 type: MigrationUtils.resourceTypes.FORMDATA, 959 960 async migrate(aCallback) { 961 let query = 962 "SELECT name, value, count, date_created, date_last_used FROM autofill"; 963 let rows; 964 965 try { 966 rows = await MigrationUtils.getRowsFromDBWithoutLocks( 967 formdataPath, 968 "Chrome formdata", 969 query 970 ); 971 } finally { 972 if (tempFilePath) { 973 await IOUtils.remove(tempFilePath); 974 } 975 } 976 977 let addOps = []; 978 for (let row of rows) { 979 try { 980 let fieldname = row.getResultByName("name"); 981 let value = row.getResultByName("value"); 982 if (fieldname && value) { 983 addOps.push({ 984 op: "add", 985 fieldname, 986 value, 987 timesUsed: row.getResultByName("count"), 988 firstUsed: row.getResultByName("date_created") * 1000, 989 lastUsed: row.getResultByName("date_last_used") * 1000, 990 }); 991 } 992 } catch (e) { 993 console.error(e); 994 } 995 } 996 997 try { 998 await lazy.FormHistory.update(addOps); 999 } catch (e) { 1000 console.error(e); 1001 aCallback(false); 1002 return; 1003 } 1004 1005 aCallback(true); 1006 }, 1007 }; 1008 } 1009 1010 async function GetExtensionsResource(aProfileId, aBrowserKey = "chrome") { 1011 if ( 1012 !Services.prefs.getBoolPref( 1013 "browser.migrate.chrome.extensions.enabled", 1014 false 1015 ) 1016 ) { 1017 return null; 1018 } 1019 let extensions = await lazy.ChromeMigrationUtils.getExtensionList(aProfileId); 1020 if (!extensions.length || aBrowserKey !== "chrome") { 1021 return null; 1022 } 1023 1024 return { 1025 type: MigrationUtils.resourceTypes.EXTENSIONS, 1026 async migrate(callback) { 1027 let ids = extensions.map(extension => extension.id); 1028 let [progressValue, importedExtensions] = 1029 await MigrationUtils.installExtensionsWrapper(aBrowserKey, ids); 1030 let details = { 1031 progressValue, 1032 totalExtensions: extensions, 1033 importedExtensions, 1034 }; 1035 if ( 1036 progressValue == lazy.MigrationWizardConstants.PROGRESS_VALUE.INFO || 1037 progressValue == lazy.MigrationWizardConstants.PROGRESS_VALUE.SUCCESS 1038 ) { 1039 callback(true, details); 1040 } else { 1041 callback(false); 1042 } 1043 }, 1044 }; 1045 } 1046 1047 /** 1048 * Chromium migrator 1049 */ 1050 export class ChromiumProfileMigrator extends ChromeProfileMigrator { 1051 static get key() { 1052 return "chromium"; 1053 } 1054 1055 static get displayNameL10nID() { 1056 return "migration-wizard-migrator-display-name-chromium"; 1057 } 1058 1059 static get brandImage() { 1060 return "chrome://browser/content/migration/brands/chromium.png"; 1061 } 1062 1063 _chromeUserDataPathSuffix = "Chromium"; 1064 _keychainServiceName = "Chromium Safe Storage"; 1065 _keychainAccountName = "Chromium"; 1066 } 1067 1068 /** 1069 * Chrome Canary 1070 * Not available on Linux 1071 */ 1072 export class CanaryProfileMigrator extends ChromeProfileMigrator { 1073 static get key() { 1074 return "canary"; 1075 } 1076 1077 static get displayNameL10nID() { 1078 return "migration-wizard-migrator-display-name-canary"; 1079 } 1080 1081 static get brandImage() { 1082 return "chrome://browser/content/migration/brands/canary.png"; 1083 } 1084 1085 get _chromeUserDataPathSuffix() { 1086 return "Canary"; 1087 } 1088 1089 get _keychainServiceName() { 1090 return "Chromium Safe Storage"; 1091 } 1092 1093 get _keychainAccountName() { 1094 return "Chromium"; 1095 } 1096 } 1097 1098 /** 1099 * Chrome Dev - Linux only (not available in Mac and Windows) 1100 */ 1101 export class ChromeDevMigrator extends ChromeProfileMigrator { 1102 static get key() { 1103 return "chrome-dev"; 1104 } 1105 1106 static get displayNameL10nID() { 1107 return "migration-wizard-migrator-display-name-chrome-dev"; 1108 } 1109 1110 _chromeUserDataPathSuffix = "Chrome Dev"; 1111 _keychainServiceName = "Chromium Safe Storage"; 1112 _keychainAccountName = "Chromium"; 1113 } 1114 1115 /** 1116 * Chrome Beta migrator 1117 */ 1118 export class ChromeBetaMigrator extends ChromeProfileMigrator { 1119 static get key() { 1120 return "chrome-beta"; 1121 } 1122 1123 static get displayNameL10nID() { 1124 return "migration-wizard-migrator-display-name-chrome-beta"; 1125 } 1126 1127 _chromeUserDataPathSuffix = "Chrome Beta"; 1128 _keychainServiceName = "Chromium Safe Storage"; 1129 _keychainAccountName = "Chromium"; 1130 } 1131 1132 /** 1133 * Brave migrator 1134 */ 1135 export class BraveProfileMigrator extends ChromeProfileMigrator { 1136 static get key() { 1137 return "brave"; 1138 } 1139 1140 static get displayNameL10nID() { 1141 return "migration-wizard-migrator-display-name-brave"; 1142 } 1143 1144 static get brandImage() { 1145 return "chrome://browser/content/migration/brands/brave.png"; 1146 } 1147 1148 _chromeUserDataPathSuffix = "Brave"; 1149 _keychainServiceName = "Brave Browser Safe Storage"; 1150 _keychainAccountName = "Brave Browser"; 1151 } 1152 1153 /** 1154 * Edge (Chromium-based) migrator 1155 */ 1156 export class ChromiumEdgeMigrator extends ChromeProfileMigrator { 1157 static get key() { 1158 return "chromium-edge"; 1159 } 1160 1161 static get displayNameL10nID() { 1162 return "migration-wizard-migrator-display-name-chromium-edge"; 1163 } 1164 1165 static get brandImage() { 1166 return "chrome://browser/content/migration/brands/edge.png"; 1167 } 1168 1169 _chromeUserDataPathSuffix = "Edge"; 1170 _keychainServiceName = "Microsoft Edge Safe Storage"; 1171 _keychainAccountName = "Microsoft Edge"; 1172 } 1173 1174 /** 1175 * Edge Beta (Chromium-based) migrator 1176 */ 1177 export class ChromiumEdgeBetaMigrator extends ChromeProfileMigrator { 1178 static get key() { 1179 return "chromium-edge-beta"; 1180 } 1181 1182 static get displayNameL10nID() { 1183 return "migration-wizard-migrator-display-name-chromium-edge-beta"; 1184 } 1185 1186 static get brandImage() { 1187 return "chrome://browser/content/migration/brands/edgebeta.png"; 1188 } 1189 1190 _chromeUserDataPathSuffix = "Edge Beta"; 1191 _keychainServiceName = "Microsoft Edge Safe Storage"; 1192 _keychainAccountName = "Microsoft Edge"; 1193 } 1194 1195 /** 1196 * Chromium 360 migrator 1197 */ 1198 export class Chromium360seMigrator extends ChromeProfileMigrator { 1199 static get key() { 1200 return "chromium-360se"; 1201 } 1202 1203 static get displayNameL10nID() { 1204 return "migration-wizard-migrator-display-name-chromium-360se"; 1205 } 1206 1207 static get brandImage() { 1208 return "chrome://browser/content/migration/brands/360.png"; 1209 } 1210 1211 _chromeUserDataPathSuffix = "360 SE"; 1212 _keychainServiceName = "Microsoft Edge Safe Storage"; 1213 _keychainAccountName = "Microsoft Edge"; 1214 } 1215 1216 /** 1217 * Opera migrator 1218 */ 1219 export class OperaProfileMigrator extends ChromeProfileMigrator { 1220 static get key() { 1221 return "opera"; 1222 } 1223 1224 static get displayNameL10nID() { 1225 return "migration-wizard-migrator-display-name-opera"; 1226 } 1227 1228 static get brandImage() { 1229 return "chrome://browser/content/migration/brands/opera.png"; 1230 } 1231 1232 _chromeUserDataPathSuffix = "Opera"; 1233 _keychainServiceName = "Opera Safe Storage"; 1234 _keychainAccountName = "Opera"; 1235 1236 async getSourceProfiles() { 1237 let detectedProfiles = await super.getSourceProfiles(); 1238 if (Array.isArray(detectedProfiles) && !detectedProfiles.length) { 1239 // We might be attempting from a version of Opera that doesn't support 1240 // profiles yet, so try returning null to see if the profile data 1241 // exists in the data directory. 1242 return null; 1243 } 1244 return detectedProfiles; 1245 } 1246 } 1247 1248 /** 1249 * Opera GX migrator 1250 */ 1251 export class OperaGXProfileMigrator extends ChromeProfileMigrator { 1252 static get key() { 1253 return "opera-gx"; 1254 } 1255 1256 static get displayNameL10nID() { 1257 return "migration-wizard-migrator-display-name-opera-gx"; 1258 } 1259 1260 static get brandImage() { 1261 return "chrome://browser/content/migration/brands/operagx.png"; 1262 } 1263 1264 _chromeUserDataPathSuffix = "Opera GX"; 1265 _keychainServiceName = "Opera Safe Storage"; 1266 _keychainAccountName = "Opera"; 1267 1268 getSourceProfiles() { 1269 return null; 1270 } 1271 } 1272 1273 /** 1274 * Vivaldi migrator 1275 */ 1276 export class VivaldiProfileMigrator extends ChromeProfileMigrator { 1277 static get key() { 1278 return "vivaldi"; 1279 } 1280 1281 static get displayNameL10nID() { 1282 return "migration-wizard-migrator-display-name-vivaldi"; 1283 } 1284 1285 static get brandImage() { 1286 return "chrome://browser/content/migration/brands/vivaldi.png"; 1287 } 1288 1289 _chromeUserDataPathSuffix = "Vivaldi"; 1290 _keychainServiceName = "Vivaldi Safe Storage"; 1291 _keychainAccountName = "Vivaldi"; 1292 }