diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /browser/components/migration/ChromeProfileMigrator.sys.mjs | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/migration/ChromeProfileMigrator.sys.mjs')
-rw-r--r-- | browser/components/migration/ChromeProfileMigrator.sys.mjs | 768 |
1 files changed, 768 insertions, 0 deletions
diff --git a/browser/components/migration/ChromeProfileMigrator.sys.mjs b/browser/components/migration/ChromeProfileMigrator.sys.mjs new file mode 100644 index 0000000000..748515a71d --- /dev/null +++ b/browser/components/migration/ChromeProfileMigrator.sys.mjs @@ -0,0 +1,768 @@ +/* -*- 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/. */ + +const AUTH_TYPE = { + SCHEME_HTML: 0, + SCHEME_BASIC: 1, + SCHEME_DIGEST: 2, +}; + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs"; +import { MigratorBase } from "resource:///modules/MigratorBase.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ChromeMigrationUtils: "resource:///modules/ChromeMigrationUtils.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", + Qihoo360seMigrationUtils: "resource:///modules/360seMigrationUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + NetUtil: "resource://gre/modules/NetUtil.jsm", +}); + +/** + * Converts an array of chrome bookmark objects into one our own places code + * understands. + * + * @param {object[]} items Chrome Bookmark items to be inserted on this parent + * @param {Function} errorAccumulator function that gets called with any errors + * thrown so we don't drop them on the floor. + * @returns {object[]} + */ +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: lazy.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; +} + +/** + * Chrome profile migrator. This can also be used as a parent class for + * migrators for browsers that are variants of Chrome. + */ +export class ChromeProfileMigrator extends MigratorBase { + static get key() { + return "chrome"; + } + + get _chromeUserDataPathSuffix() { + return "Chrome"; + } + + _keychainServiceName = "Chrome Safe Storage"; + + _keychainAccountName = "Chrome"; + + async _getChromeUserDataPathIfExists() { + if (this._chromeUserDataPath) { + return this._chromeUserDataPath; + } + let path = lazy.ChromeMigrationUtils.getDataPath( + this._chromeUserDataPathSuffix + ); + let exists = await IOUtils.exists(path); + if (exists) { + this._chromeUserDataPath = path; + } else { + this._chromeUserDataPath = null; + } + return this._chromeUserDataPath; + } + + async getResources(aProfile) { + let chromeUserDataPath = await this._getChromeUserDataPathIfExists(); + if (chromeUserDataPath) { + let profileFolder = chromeUserDataPath; + if (aProfile) { + profileFolder = PathUtils.join(chromeUserDataPath, aProfile.id); + } + if (await IOUtils.exists(profileFolder)) { + let possibleResourcePromises = [ + GetBookmarksResource(profileFolder, this.constructor.key), + GetHistoryResource(profileFolder), + GetCookiesResource(profileFolder), + ]; + if (lazy.ChromeMigrationUtils.supportsLoginsForPlatform) { + possibleResourcePromises.push( + this._GetPasswordsResource(profileFolder) + ); + } + let possibleResources = await Promise.all(possibleResourcePromises); + return possibleResources.filter(r => r != null); + } + } + return []; + } + + async 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 = PathUtils.join(chromeUserDataPath, profile.id); + let fileDatePromises = ["Bookmarks", "History", "Cookies"].map( + async leafName => { + let path = PathUtils.join(basePath, leafName); + let info = await IOUtils.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)); + } + + async getSourceProfiles() { + if ("__sourceProfiles" in this) { + return this.__sourceProfiles; + } + + let chromeUserDataPath = await this._getChromeUserDataPathIfExists(); + if (!chromeUserDataPath) { + return []; + } + + let localState; + let profiles = []; + try { + localState = await lazy.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; + } + + async _GetPasswordsResource(aProfileFolder) { + let loginPath = PathUtils.join(aProfileFolder, "Login Data"); + if (!(await IOUtils.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.importESModule( + "resource:///modules/ChromeWindowsLoginCrypto.sys.mjs" + ); + crypto = new ChromeWindowsLoginCrypto(_chromeUserDataPathSuffix); + } else if (AppConstants.platform == "macosx") { + let { ChromeMacOSLoginCrypto } = ChromeUtils.importESModule( + "resource:///modules/ChromeMacOSLoginCrypto.sys.mjs" + ); + 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 = lazy.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: lazy.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 = lazy.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); + }, + }; + } +} + +async function GetBookmarksResource(aProfileFolder, aBrowserKey) { + let bookmarksPath = PathUtils.join(aProfileFolder, "Bookmarks"); + + if (aBrowserKey === "chromium-360se") { + let localState = {}; + try { + localState = await lazy.ChromeMigrationUtils.getLocalState("360 SE"); + } catch (ex) { + Cu.reportError(ex); + } + + let alternativeBookmarks = await lazy.Qihoo360seMigrationUtils.getAlternativeBookmarks( + { bookmarksPath, localState } + ); + if (alternativeBookmarks.resource) { + return alternativeBookmarks.resource; + } + + bookmarksPath = alternativeBookmarks.path; + } + + if (!(await IOUtils.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 IOUtils.readJSON(bookmarksPath); + let roots = bookmarkJSON.roots; + + // Importing bookmark bar items + if (roots.bookmark_bar.children && roots.bookmark_bar.children.length) { + // Toolbar + let parentGuid = lazy.PlacesUtils.bookmarks.toolbarGuid; + let bookmarks = convertBookmarks( + roots.bookmark_bar.children, + errorGatherer + ); + await MigrationUtils.insertManyBookmarksWrapper( + bookmarks, + parentGuid + ); + } + + // Importing Other Bookmarks items + if (roots.other.children && roots.other.children.length) { + // Other Bookmarks + let parentGuid = lazy.PlacesUtils.bookmarks.unfiledGuid; + let bookmarks = convertBookmarks(roots.other.children, errorGatherer); + await MigrationUtils.insertManyBookmarksWrapper( + bookmarks, + parentGuid + ); + } + if (gotErrors) { + throw new Error("The migration included errors."); + } + })().then( + () => aCallback(true), + () => aCallback(false) + ); + }, + }; +} + +async function GetHistoryResource(aProfileFolder) { + let historyPath = PathUtils.join(aProfileFolder, "History"); + if (!(await IOUtils.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 = lazy.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 = lazy.PlacesUtils.history.TRANSITIONS.LINK; + if (row.getResultByName("typed_count") > 0) { + transition = lazy.PlacesUtils.history.TRANSITIONS.TYPED; + } + + pageInfos.push({ + title: row.getResultByName("title"), + url: new URL(row.getResultByName("url")), + visits: [ + { + transition, + date: lazy.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 = PathUtils.join(aProfileFolder, "Cookies"); + if (!(await IOUtils.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 = + lazy.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); + }, + }; +} + +/** + * Chromium migrator + */ +export class ChromiumProfileMigrator extends ChromeProfileMigrator { + static get key() { + return "chromium"; + } + + _chromeUserDataPathSuffix = "Chromium"; + _keychainServiceName = "Chromium Safe Storage"; + _keychainAccountName = "Chromium"; +} + +/** + * Chrome Canary + * Not available on Linux + */ +export class CanaryProfileMigrator extends ChromeProfileMigrator { + static get key() { + return "canary"; + } + + get _chromeUserDataPathSuffix() { + return "Canary"; + } + + get _keychainServiceName() { + return "Chromium Safe Storage"; + } + + get _keychainAccountName() { + return "Chromium"; + } +} + +/** + * Chrome Dev - Linux only (not available in Mac and Windows) + */ +export class ChromeDevMigrator extends ChromeProfileMigrator { + static get key() { + return "chrome-dev"; + } + + _chromeUserDataPathSuffix = "Chrome Dev"; + _keychainServiceName = "Chromium Safe Storage"; + _keychainAccountName = "Chromium"; +} + +/** + * Chrome Beta migrator + */ +export class ChromeBetaMigrator extends ChromeProfileMigrator { + static get key() { + return "chrome-beta"; + } + + _chromeUserDataPathSuffix = "Chrome Beta"; + _keychainServiceName = "Chromium Safe Storage"; + _keychainAccountName = "Chromium"; +} + +/** + * Brave migrator + */ +export class BraveProfileMigrator extends ChromeProfileMigrator { + static get key() { + return "brave"; + } + + _chromeUserDataPathSuffix = "Brave"; + _keychainServiceName = "Brave Browser Safe Storage"; + _keychainAccountName = "Brave Browser"; +} + +/** + * Edge (Chromium-based) migrator + */ +export class ChromiumEdgeMigrator extends ChromeProfileMigrator { + static get key() { + return "chromium-edge"; + } + + _chromeUserDataPathSuffix = "Edge"; + _keychainServiceName = "Microsoft Edge Safe Storage"; + _keychainAccountName = "Microsoft Edge"; +} + +/** + * Edge Beta (Chromium-based) migrator + */ +export class ChromiumEdgeBetaMigrator extends ChromeProfileMigrator { + static get key() { + return "chromium-edge-beta"; + } + + _chromeUserDataPathSuffix = "Edge Beta"; + _keychainServiceName = "Microsoft Edge Safe Storage"; + _keychainAccountName = "Microsoft Edge"; +} + +/** + * Chromium 360 migrator + */ +export class Chromium360seMigrator extends ChromeProfileMigrator { + static get key() { + return "chromium-360se"; + } + + _chromeUserDataPathSuffix = "360 SE"; + _keychainServiceName = "Microsoft Edge Safe Storage"; + _keychainAccountName = "Microsoft Edge"; +} + +/** + * Opera migrator + */ +export class OperaProfileMigrator extends ChromeProfileMigrator { + static get key() { + return "opera"; + } + + _chromeUserDataPathSuffix = "Opera"; + _keychainServiceName = "Opera Safe Storage"; + _keychainAccountName = "Opera"; + + getSourceProfiles() { + return null; + } +} + +/** + * Opera GX migrator + */ +export class OperaGXProfileMigrator extends ChromeProfileMigrator { + static get key() { + return "opera-gx"; + } + + _chromeUserDataPathSuffix = "Opera GX"; + _keychainServiceName = "Opera Safe Storage"; + _keychainAccountName = "Opera"; + + getSourceProfiles() { + return null; + } +} + +/** + * Vivaldi migrator + */ +export class VivaldiProfileMigrator extends ChromeProfileMigrator { + static get key() { + return "vivaldi"; + } + + _chromeUserDataPathSuffix = "Vivaldi"; + _keychainServiceName = "Vivaldi Safe Storage"; + _keychainAccountName = "Vivaldi"; +} |