/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- * vim: sw=2 ts=2 sts=2 et */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const AUTH_TYPE = { SCHEME_HTML: 0, SCHEME_BASIC: 1, SCHEME_DIGEST: 2, }; const { AppConstants } = ChromeUtils.import( "resource://gre/modules/AppConstants.jsm" ); const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); const { ChromeMigrationUtils } = ChromeUtils.import( "resource:///modules/ChromeMigrationUtils.jsm" ); const { MigrationUtils, MigratorPrototype } = ChromeUtils.import( "resource:///modules/MigrationUtils.jsm" ); ChromeUtils.defineModuleGetter( this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm" ); ChromeUtils.defineModuleGetter( this, "PlacesUIUtils", "resource:///modules/PlacesUIUtils.jsm" ); /** * Converts an array of chrome bookmark objects into one our own places code * understands. * * @param items * bookmark items to be inserted on this parent * @param errorAccumulator * function that gets called with any errors thrown so we don't drop them on the floor. */ function convertBookmarks(items, errorAccumulator) { let itemsToInsert = []; for (let item of items) { try { if (item.type == "url") { if (item.url.trim().startsWith("chrome:")) { // Skip invalid internal URIs. Creating an actual URI always reports // messages to the console because Gecko has its own concept of how // chrome:// URIs should be formed, so we avoid doing that. continue; } if (item.url.trim().startsWith("edge:")) { // Don't import internal Microsoft Edge URIs as they won't resolve within Firefox. continue; } itemsToInsert.push({ url: item.url, title: item.name }); } else if (item.type == "folder") { let folderItem = { type: PlacesUtils.bookmarks.TYPE_FOLDER, title: item.name, }; folderItem.children = convertBookmarks(item.children, errorAccumulator); itemsToInsert.push(folderItem); } } catch (ex) { Cu.reportError(ex); errorAccumulator(ex); } } return itemsToInsert; } function ChromeProfileMigrator() { this._chromeUserDataPathSuffix = "Chrome"; } ChromeProfileMigrator.prototype = Object.create(MigratorPrototype); ChromeProfileMigrator.prototype._keychainServiceName = "Chrome Safe Storage"; ChromeProfileMigrator.prototype._keychainAccountName = "Chrome"; ChromeProfileMigrator.prototype._getChromeUserDataPathIfExists = async function() { if (this._chromeUserDataPath) { return this._chromeUserDataPath; } let path = ChromeMigrationUtils.getDataPath(this._chromeUserDataPathSuffix); let exists = await OS.File.exists(path); if (exists) { this._chromeUserDataPath = path; } else { this._chromeUserDataPath = null; } return this._chromeUserDataPath; }; ChromeProfileMigrator.prototype.getResources = async function Chrome_getResources( aProfile ) { let chromeUserDataPath = await this._getChromeUserDataPathIfExists(); if (chromeUserDataPath) { let profileFolder = OS.Path.join(chromeUserDataPath, aProfile.id); if (await OS.File.exists(profileFolder)) { let localePropertySuffix = MigrationUtils._getLocalePropertyForBrowser( this.getBrowserKey() ).replace(/^source-name-/, ""); let possibleResourcePromises = [ GetBookmarksResource( profileFolder, localePropertySuffix, this.getBrowserKey() ), GetHistoryResource(profileFolder), GetCookiesResource(profileFolder), ]; if (ChromeMigrationUtils.supportsLoginsForPlatform) { possibleResourcePromises.push( this._GetPasswordsResource(profileFolder) ); } let possibleResources = await Promise.all(possibleResourcePromises); return possibleResources.filter(r => r != null); } } return []; }; ChromeProfileMigrator.prototype.getLastUsedDate = async function Chrome_getLastUsedDate() { let sourceProfiles = await this.getSourceProfiles(); let chromeUserDataPath = await this._getChromeUserDataPathIfExists(); if (!chromeUserDataPath) { return new Date(0); } let datePromises = sourceProfiles.map(async profile => { let basePath = OS.Path.join(chromeUserDataPath, profile.id); let fileDatePromises = ["Bookmarks", "History", "Cookies"].map( async leafName => { let path = OS.Path.join(basePath, leafName); let info = await OS.File.stat(path).catch(() => null); return info ? info.lastModificationDate : 0; } ); let dates = await Promise.all(fileDatePromises); return Math.max(...dates); }); let datesOuter = await Promise.all(datePromises); datesOuter.push(0); return new Date(Math.max(...datesOuter)); }; ChromeProfileMigrator.prototype.getSourceProfiles = async function Chrome_getSourceProfiles() { if ("__sourceProfiles" in this) { return this.__sourceProfiles; } let chromeUserDataPath = await this._getChromeUserDataPathIfExists(); if (!chromeUserDataPath) { return []; } let localState; let profiles = []; try { localState = await ChromeMigrationUtils.getLocalState( this._chromeUserDataPathSuffix ); let info_cache = localState.profile.info_cache; for (let profileFolderName in info_cache) { profiles.push({ id: profileFolderName, name: info_cache[profileFolderName].name || profileFolderName, }); } } catch (e) { // Avoid reporting NotFoundErrors from trying to get local state. if (localState || e.name != "NotFoundError") { Cu.reportError("Error detecting Chrome profiles: " + e); } // If we weren't able to detect any profiles above, fallback to the Default profile. let defaultProfilePath = PathUtils.join(chromeUserDataPath, "Default"); if (await IOUtils.exists(defaultProfilePath)) { profiles = [ { id: "Default", name: "Default", }, ]; } } let profileResources = await Promise.all( profiles.map(async profile => ({ profile, resources: await this.getResources(profile), })) ); // Only list profiles from which any data can be imported this.__sourceProfiles = profileResources .filter(({ resources }) => { return resources && !!resources.length; }, this) .map(({ profile }) => profile); return this.__sourceProfiles; }; Object.defineProperty(ChromeProfileMigrator.prototype, "sourceLocked", { get: function Chrome_sourceLocked() { // There is an exclusive lock on some SQLite databases. Assume they are locked for now. return true; }, }); async function GetBookmarksResource( aProfileFolder, aLocalePropertySuffix, aBrowserKey ) { let bookmarksPath = OS.Path.join(aProfileFolder, "Bookmarks"); if (!(await OS.File.exists(bookmarksPath))) { return null; } return { type: MigrationUtils.resourceTypes.BOOKMARKS, migrate(aCallback) { return (async function() { let gotErrors = false; let errorGatherer = function() { gotErrors = true; }; // Parse Chrome bookmark file that is JSON format let bookmarkJSON = await OS.File.read(bookmarksPath, { encoding: "UTF-8", }); let roots = JSON.parse(bookmarkJSON).roots; let histogramBookmarkRoots = 0; // Importing bookmark bar items if (roots.bookmark_bar.children && roots.bookmark_bar.children.length) { // Toolbar histogramBookmarkRoots |= MigrationUtils.SOURCE_BOOKMARK_ROOTS_BOOKMARKS_TOOLBAR; let parentGuid = PlacesUtils.bookmarks.toolbarGuid; let bookmarks = convertBookmarks( roots.bookmark_bar.children, errorGatherer ); if ( !Services.prefs.getBoolPref("browser.toolbars.bookmarks.2h2020") && !MigrationUtils.isStartupMigration && PlacesUtils.getChildCountForFolder( PlacesUtils.bookmarks.toolbarGuid ) > PlacesUIUtils.NUM_TOOLBAR_BOOKMARKS_TO_UNHIDE ) { parentGuid = await MigrationUtils.createImportedBookmarksFolder( aLocalePropertySuffix, parentGuid ); } await MigrationUtils.insertManyBookmarksWrapper( bookmarks, parentGuid ); PlacesUIUtils.maybeToggleBookmarkToolbarVisibilityAfterMigration(); } // Importing bookmark menu items if (roots.other.children && roots.other.children.length) { // Bookmark menu histogramBookmarkRoots |= MigrationUtils.SOURCE_BOOKMARK_ROOTS_BOOKMARKS_MENU; let parentGuid = PlacesUtils.bookmarks.menuGuid; let bookmarks = convertBookmarks(roots.other.children, errorGatherer); if ( !Services.prefs.getBoolPref("browser.toolbars.bookmarks.2h2020") && !MigrationUtils.isStartupMigration && PlacesUtils.getChildCountForFolder(PlacesUtils.bookmarks.menuGuid) > PlacesUIUtils.NUM_TOOLBAR_BOOKMARKS_TO_UNHIDE ) { parentGuid = await MigrationUtils.createImportedBookmarksFolder( aLocalePropertySuffix, parentGuid ); } await MigrationUtils.insertManyBookmarksWrapper( bookmarks, parentGuid ); } if (gotErrors) { throw new Error("The migration included errors."); } Services.telemetry .getKeyedHistogramById("FX_MIGRATION_BOOKMARKS_ROOTS") .add(aBrowserKey, histogramBookmarkRoots); })().then( () => aCallback(true), () => aCallback(false) ); }, }; } async function GetHistoryResource(aProfileFolder) { let historyPath = OS.Path.join(aProfileFolder, "History"); if (!(await OS.File.exists(historyPath))) { return null; } return { type: MigrationUtils.resourceTypes.HISTORY, migrate(aCallback) { (async function() { const MAX_AGE_IN_DAYS = Services.prefs.getIntPref( "browser.migrate.chrome.history.maxAgeInDays" ); const LIMIT = Services.prefs.getIntPref( "browser.migrate.chrome.history.limit" ); let query = "SELECT url, title, last_visit_time, typed_count FROM urls WHERE hidden = 0"; if (MAX_AGE_IN_DAYS) { let maxAge = ChromeMigrationUtils.dateToChromeTime( Date.now() - MAX_AGE_IN_DAYS * 24 * 60 * 60 * 1000 ); query += " AND last_visit_time > " + maxAge; } if (LIMIT) { query += " ORDER BY last_visit_time DESC LIMIT " + LIMIT; } let rows = await MigrationUtils.getRowsFromDBWithoutLocks( historyPath, "Chrome history", query ); let pageInfos = []; let fallbackVisitDate = new Date(); for (let row of rows) { try { // if having typed_count, we changes transition type to typed. let transition = PlacesUtils.history.TRANSITIONS.LINK; if (row.getResultByName("typed_count") > 0) { transition = PlacesUtils.history.TRANSITIONS.TYPED; } pageInfos.push({ title: row.getResultByName("title"), url: new URL(row.getResultByName("url")), visits: [ { transition, date: ChromeMigrationUtils.chromeTimeToDate( row.getResultByName("last_visit_time"), fallbackVisitDate ), }, ], }); } catch (e) { Cu.reportError(e); } } if (pageInfos.length) { await MigrationUtils.insertVisitsWrapper(pageInfos); } })().then( () => { aCallback(true); }, ex => { Cu.reportError(ex); aCallback(false); } ); }, }; } async function GetCookiesResource(aProfileFolder) { let cookiesPath = OS.Path.join(aProfileFolder, "Cookies"); if (!(await OS.File.exists(cookiesPath))) { return null; } return { type: MigrationUtils.resourceTypes.COOKIES, async migrate(aCallback) { // Get columns names and set is_sceure, is_httponly fields accordingly. let columns = await MigrationUtils.getRowsFromDBWithoutLocks( cookiesPath, "Chrome cookies", `PRAGMA table_info(cookies)` ).catch(ex => { Cu.reportError(ex); aCallback(false); }); // If the promise was rejected we will have already called aCallback, // so we can just return here. if (!columns) { return; } columns = columns.map(c => c.getResultByName("name")); let isHttponly = columns.includes("is_httponly") ? "is_httponly" : "httponly"; let isSecure = columns.includes("is_secure") ? "is_secure" : "secure"; let source_scheme = columns.includes("source_scheme") ? "source_scheme" : `"${Ci.nsICookie.SCHEME_UNSET}" as source_scheme`; // We don't support decrypting cookies yet so only import plaintext ones. let rows = await MigrationUtils.getRowsFromDBWithoutLocks( cookiesPath, "Chrome cookies", `SELECT host_key, name, value, path, expires_utc, ${isSecure}, ${isHttponly}, encrypted_value, ${source_scheme} FROM cookies WHERE length(encrypted_value) = 0` ).catch(ex => { Cu.reportError(ex); aCallback(false); }); // If the promise was rejected we will have already called aCallback, // so we can just return here. if (!rows) { return; } let fallbackExpiryDate = 0; for (let row of rows) { let host_key = row.getResultByName("host_key"); if (host_key.match(/^\./)) { // 1st character of host_key may be ".", so we have to remove it host_key = host_key.substr(1); } let schemeType = Ci.nsICookie.SCHEME_UNSET; switch (row.getResultByName("source_scheme")) { case 1: schemeType = Ci.nsICookie.SCHEME_HTTP; break; case 2: schemeType = Ci.nsICookie.SCHEME_HTTPS; break; } try { let expiresUtc = ChromeMigrationUtils.chromeTimeToDate( row.getResultByName("expires_utc"), fallbackExpiryDate ) / 1000; // No point adding cookies that don't have a valid expiry. if (!expiresUtc) { continue; } Services.cookies.add( host_key, row.getResultByName("path"), row.getResultByName("name"), row.getResultByName("value"), row.getResultByName(isSecure), row.getResultByName(isHttponly), false, parseInt(expiresUtc), {}, Ci.nsICookie.SAMESITE_NONE, schemeType ); } catch (e) { Cu.reportError(e); } } aCallback(true); }, }; } ChromeProfileMigrator.prototype._GetPasswordsResource = async function( aProfileFolder ) { let loginPath = OS.Path.join(aProfileFolder, "Login Data"); if (!(await OS.File.exists(loginPath))) { return null; } let { _chromeUserDataPathSuffix, _keychainServiceName, _keychainAccountName, _keychainMockPassphrase = null, } = this; return { type: MigrationUtils.resourceTypes.PASSWORDS, async migrate(aCallback) { let rows = await MigrationUtils.getRowsFromDBWithoutLocks( loginPath, "Chrome passwords", `SELECT origin_url, action_url, username_element, username_value, password_element, password_value, signon_realm, scheme, date_created, times_used FROM logins WHERE blacklisted_by_user = 0` ).catch(ex => { Cu.reportError(ex); aCallback(false); }); // If the promise was rejected we will have already called aCallback, // so we can just return here. if (!rows) { return; } // If there are no relevant rows, return before initializing crypto and // thus prompting for Keychain access on macOS. if (!rows.length) { aCallback(true); return; } let crypto; try { if (AppConstants.platform == "win") { let { ChromeWindowsLoginCrypto } = ChromeUtils.import( "resource:///modules/ChromeWindowsLoginCrypto.jsm" ); crypto = new ChromeWindowsLoginCrypto(_chromeUserDataPathSuffix); } else if (AppConstants.platform == "macosx") { let { ChromeMacOSLoginCrypto } = ChromeUtils.import( "resource:///modules/ChromeMacOSLoginCrypto.jsm" ); crypto = new ChromeMacOSLoginCrypto( _keychainServiceName, _keychainAccountName, _keychainMockPassphrase ); } else { aCallback(false); return; } } catch (ex) { // Handle the user canceling Keychain access or other OSCrypto errors. Cu.reportError(ex); aCallback(false); return; } let logins = []; let fallbackCreationDate = new Date(); for (let row of rows) { try { let origin_url = NetUtil.newURI(row.getResultByName("origin_url")); // Ignore entries for non-http(s)/ftp URLs because we likely can't // use them anyway. const kValidSchemes = new Set(["https", "http", "ftp"]); if (!kValidSchemes.has(origin_url.scheme)) { continue; } let loginInfo = { username: row.getResultByName("username_value"), password: await crypto.decryptData( row.getResultByName("password_value"), null ), origin: origin_url.prePath, formActionOrigin: null, httpRealm: null, usernameElement: row.getResultByName("username_element"), passwordElement: row.getResultByName("password_element"), timeCreated: ChromeMigrationUtils.chromeTimeToDate( row.getResultByName("date_created") + 0, fallbackCreationDate ).getTime(), timesUsed: row.getResultByName("times_used") + 0, }; switch (row.getResultByName("scheme")) { case AUTH_TYPE.SCHEME_HTML: let action_url = row.getResultByName("action_url"); if (!action_url) { // If there is no action_url, store the wildcard "" value. // See the `formActionOrigin` IDL comments. loginInfo.formActionOrigin = ""; break; } let action_uri = NetUtil.newURI(action_url); if (!kValidSchemes.has(action_uri.scheme)) { continue; // This continues the outer for loop. } loginInfo.formActionOrigin = action_uri.prePath; break; case AUTH_TYPE.SCHEME_BASIC: case AUTH_TYPE.SCHEME_DIGEST: // signon_realm format is URIrealm, so we need remove URI loginInfo.httpRealm = row .getResultByName("signon_realm") .substring(loginInfo.origin.length + 1); break; default: throw new Error( "Login data scheme type not supported: " + row.getResultByName("scheme") ); } logins.push(loginInfo); } catch (e) { Cu.reportError(e); } } try { if (logins.length) { await MigrationUtils.insertLoginsWrapper(logins); } } catch (e) { Cu.reportError(e); } if (crypto.finalize) { crypto.finalize(); } aCallback(true); }, }; }; ChromeProfileMigrator.prototype.classDescription = "Chrome Profile Migrator"; ChromeProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chrome"; ChromeProfileMigrator.prototype.classID = Components.ID( "{4cec1de4-1671-4fc3-a53e-6c539dc77a26}" ); /** * Chromium migration **/ function ChromiumProfileMigrator() { this._chromeUserDataPathSuffix = "Chromium"; this._keychainServiceName = "Chromium Safe Storage"; this._keychainAccountName = "Chromium"; } ChromiumProfileMigrator.prototype = Object.create( ChromeProfileMigrator.prototype ); ChromiumProfileMigrator.prototype.classDescription = "Chromium Profile Migrator"; ChromiumProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chromium"; ChromiumProfileMigrator.prototype.classID = Components.ID( "{8cece922-9720-42de-b7db-7cef88cb07ca}" ); var EXPORTED_SYMBOLS = ["ChromeProfileMigrator", "ChromiumProfileMigrator"]; /** * Chrome Canary * Not available on Linux **/ function CanaryProfileMigrator() { this._chromeUserDataPathSuffix = "Canary"; } CanaryProfileMigrator.prototype = Object.create( ChromeProfileMigrator.prototype ); CanaryProfileMigrator.prototype.classDescription = "Chrome Canary Profile Migrator"; CanaryProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=canary"; CanaryProfileMigrator.prototype.classID = Components.ID( "{4bf85aa5-4e21-46ca-825f-f9c51a5e8c76}" ); if (AppConstants.platform == "win" || AppConstants.platform == "macosx") { EXPORTED_SYMBOLS.push("CanaryProfileMigrator"); } /** * Chrome Dev - Linux only (not available in Mac and Windows) */ function ChromeDevMigrator() { this._chromeUserDataPathSuffix = "Chrome Dev"; } ChromeDevMigrator.prototype = Object.create(ChromeProfileMigrator.prototype); ChromeDevMigrator.prototype.classDescription = "Chrome Dev Profile Migrator"; ChromeDevMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chrome-dev"; ChromeDevMigrator.prototype.classID = Components.ID( "{7370a02a-4886-42c3-a4ec-d48c726ec30a}" ); if (AppConstants.platform != "win" && AppConstants.platform != "macosx") { EXPORTED_SYMBOLS.push("ChromeDevMigrator"); } function ChromeBetaMigrator() { this._chromeUserDataPathSuffix = "Chrome Beta"; } ChromeBetaMigrator.prototype = Object.create(ChromeProfileMigrator.prototype); ChromeBetaMigrator.prototype.classDescription = "Chrome Beta Profile Migrator"; ChromeBetaMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chrome-beta"; ChromeBetaMigrator.prototype.classID = Components.ID( "{47f75963-840b-4950-a1f0-d9c1864f8b8e}" ); if (AppConstants.platform != "macosx") { EXPORTED_SYMBOLS.push("ChromeBetaMigrator"); } function ChromiumEdgeMigrator() { this._chromeUserDataPathSuffix = "Edge"; this._keychainServiceName = "Microsoft Edge Safe Storage"; this._keychainAccountName = "Microsoft Edge"; } ChromiumEdgeMigrator.prototype = Object.create(ChromeProfileMigrator.prototype); ChromiumEdgeMigrator.prototype.classDescription = "Chromium Edge Profile Migrator"; ChromiumEdgeMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chromium-edge"; ChromiumEdgeMigrator.prototype.classID = Components.ID( "{3c7f6b7c-baa9-4338-acfa-04bf79f1dcf1}" ); function ChromiumEdgeBetaMigrator() { this._chromeUserDataPathSuffix = "Edge Beta"; this._keychainServiceName = "Microsoft Edge Safe Storage"; this._keychainAccountName = "Microsoft Edge"; } ChromiumEdgeBetaMigrator.prototype = Object.create( ChromiumEdgeMigrator.prototype ); ChromiumEdgeBetaMigrator.prototype.classDescription = "Chromium Edge Beta Profile Migrator"; ChromiumEdgeBetaMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chromium-edge-beta"; ChromiumEdgeBetaMigrator.prototype.classID = Components.ID( "{0fc3d48a-c1c3-4871-b58f-a8b47d1555fb}" ); if (AppConstants.platform == "macosx" || AppConstants.platform == "win") { EXPORTED_SYMBOLS.push("ChromiumEdgeMigrator", "ChromiumEdgeBetaMigrator"); }