diff options
Diffstat (limited to 'browser/components/migration/ChromeProfileMigrator.js')
-rw-r--r-- | browser/components/migration/ChromeProfileMigrator.js | 557 |
1 files changed, 557 insertions, 0 deletions
diff --git a/browser/components/migration/ChromeProfileMigrator.js b/browser/components/migration/ChromeProfileMigrator.js new file mode 100644 index 000000000..ec0c8d444 --- /dev/null +++ b/browser/components/migration/ChromeProfileMigrator.js @@ -0,0 +1,557 @@ +/* -*- 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 { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; + +const FILE_INPUT_STREAM_CID = "@mozilla.org/network/file-input-stream;1"; + +const S100NS_FROM1601TO1970 = 0x19DB1DED53E8000; +const S100NS_PER_MS = 10; + +const AUTH_TYPE = { + SCHEME_HTML: 0, + SCHEME_BASIC: 1, + SCHEME_DIGEST: 2 +}; + +Cu.import("resource://gre/modules/AppConstants.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/osfile.jsm"); /* globals OS */ +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource:///modules/MigrationUtils.jsm"); /* globals MigratorPrototype */ + +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OSCrypto", + "resource://gre/modules/OSCrypto.jsm"); +/** + * Get an nsIFile instance representing the expected location of user data + * for this copy of Chrome/Chromium/Canary on different OSes. + * @param subfoldersWin {Array} an array of subfolders to use for Windows + * @param subfoldersOSX {Array} an array of subfolders to use for OS X + * @param subfoldersUnix {Array} an array of subfolders to use for *nix systems + * @returns {nsIFile} the place we expect data to live. Might not actually exist! + */ +function getDataFolder(subfoldersWin, subfoldersOSX, subfoldersUnix) { + let dirServiceID, subfolders; + if (AppConstants.platform == "win") { + dirServiceID = "LocalAppData"; + subfolders = subfoldersWin.concat(["User Data"]); + } else if (AppConstants.platform == "macosx") { + dirServiceID = "ULibDir"; + subfolders = ["Application Support"].concat(subfoldersOSX); + } else { + dirServiceID = "Home"; + subfolders = [".config"].concat(subfoldersUnix); + } + return FileUtils.getDir(dirServiceID, subfolders, false); +} + +/** + * Convert Chrome time format to Date object + * + * @param aTime + * Chrome time + * @return converted Date object + * @note Google Chrome uses FILETIME / 10 as time. + * FILETIME is based on same structure of Windows. + */ +function chromeTimeToDate(aTime) +{ + return new Date((aTime * S100NS_PER_MS - S100NS_FROM1601TO1970) / 10000); +} + +/** + * Convert Date object to Chrome time format + * + * @param aDate + * Date object or integer equivalent + * @return Chrome time + * @note For details on Chrome time, see chromeTimeToDate. + */ +function dateToChromeTime(aDate) { + return (aDate * 10000 + S100NS_FROM1601TO1970) / S100NS_PER_MS; +} + +/** + * Insert bookmark items into specific folder. + * + * @param parentGuid + * GUID of the folder where items will be inserted + * @param items + * bookmark items to be inserted + * @param errorAccumulator + * function that gets called with any errors thrown so we don't drop them on the floor. + */ +function* insertBookmarkItems(parentGuid, items, errorAccumulator) { + for (let item of items) { + try { + if (item.type == "url") { + if (item.url.trim().startsWith("chrome:")) { + // Skip invalid chrome URIs. Creating an actual URI always reports + // messages to the console, so we avoid doing that. + continue; + } + yield MigrationUtils.insertBookmarkWrapper({ + parentGuid, url: item.url, title: item.name + }); + } else if (item.type == "folder") { + let newFolderGuid = (yield MigrationUtils.insertBookmarkWrapper({ + parentGuid, type: PlacesUtils.bookmarks.TYPE_FOLDER, title: item.name + })).guid; + + yield insertBookmarkItems(newFolderGuid, item.children, errorAccumulator); + } + } catch (e) { + Cu.reportError(e); + errorAccumulator(e); + } + } +} + +function ChromeProfileMigrator() { + let chromeUserDataFolder = + getDataFolder(["Google", "Chrome"], ["Google", "Chrome"], ["google-chrome"]); + this._chromeUserDataFolder = chromeUserDataFolder.exists() ? + chromeUserDataFolder : null; +} + +ChromeProfileMigrator.prototype = Object.create(MigratorPrototype); + +ChromeProfileMigrator.prototype.getResources = + function Chrome_getResources(aProfile) { + if (this._chromeUserDataFolder) { + let profileFolder = this._chromeUserDataFolder.clone(); + profileFolder.append(aProfile.id); + if (profileFolder.exists()) { + let possibleResources = [ + GetBookmarksResource(profileFolder), + GetHistoryResource(profileFolder), + GetCookiesResource(profileFolder), + ]; + if (AppConstants.platform == "win") { + possibleResources.push(GetWindowsPasswordsResource(profileFolder)); + } + return possibleResources.filter(r => r != null); + } + } + return []; + }; + +ChromeProfileMigrator.prototype.getLastUsedDate = + function Chrome_getLastUsedDate() { + let datePromises = this.sourceProfiles.map(profile => { + let basePath = OS.Path.join(this._chromeUserDataFolder.path, profile.id); + let fileDatePromises = ["Bookmarks", "History", "Cookies"].map(leafName => { + let path = OS.Path.join(basePath, leafName); + return OS.File.stat(path).catch(() => null).then(info => { + return info ? info.lastModificationDate : 0; + }); + }); + return Promise.all(fileDatePromises).then(dates => { + return Math.max.apply(Math, dates); + }); + }); + return Promise.all(datePromises).then(dates => { + dates.push(0); + return new Date(Math.max.apply(Math, dates)); + }); + }; + +Object.defineProperty(ChromeProfileMigrator.prototype, "sourceProfiles", { + get: function Chrome_sourceProfiles() { + if ("__sourceProfiles" in this) + return this.__sourceProfiles; + + if (!this._chromeUserDataFolder) + return []; + + let profiles = []; + try { + // Local State is a JSON file that contains profile info. + let localState = this._chromeUserDataFolder.clone(); + localState.append("Local State"); + if (!localState.exists()) + throw new Error("Chrome's 'Local State' file does not exist."); + if (!localState.isReadable()) + throw new Error("Chrome's 'Local State' file could not be read."); + + let fstream = Cc[FILE_INPUT_STREAM_CID].createInstance(Ci.nsIFileInputStream); + fstream.init(localState, -1, 0, 0); + let inputStream = NetUtil.readInputStreamToString(fstream, fstream.available(), + { charset: "UTF-8" }); + let info_cache = JSON.parse(inputStream).profile.info_cache; + for (let profileFolderName in info_cache) { + let profileFolder = this._chromeUserDataFolder.clone(); + profileFolder.append(profileFolderName); + profiles.push({ + id: profileFolderName, + name: info_cache[profileFolderName].name || profileFolderName, + }); + } + } catch (e) { + Cu.reportError("Error detecting Chrome profiles: " + e); + // If we weren't able to detect any profiles above, fallback to the Default profile. + let defaultProfileFolder = this._chromeUserDataFolder.clone(); + defaultProfileFolder.append("Default"); + if (defaultProfileFolder.exists()) { + profiles = [{ + id: "Default", + name: "Default", + }]; + } + } + + // Only list profiles from which any data can be imported + this.__sourceProfiles = profiles.filter(function(profile) { + let resources = this.getResources(profile); + return resources && resources.length > 0; + }, this); + return this.__sourceProfiles; + } +}); + +Object.defineProperty(ChromeProfileMigrator.prototype, "sourceHomePageURL", { + get: function Chrome_sourceHomePageURL() { + let prefsFile = this._chromeUserDataFolder.clone(); + prefsFile.append("Preferences"); + if (prefsFile.exists()) { + // XXX reading and parsing JSON is synchronous. + let fstream = Cc[FILE_INPUT_STREAM_CID]. + createInstance(Ci.nsIFileInputStream); + fstream.init(prefsFile, -1, 0, 0); + try { + return JSON.parse( + NetUtil.readInputStreamToString(fstream, fstream.available(), + { charset: "UTF-8" }) + ).homepage; + } + catch (e) { + Cu.reportError("Error parsing Chrome's preferences file: " + e); + } + } + return ""; + } +}); + +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; + }, +}); + +function GetBookmarksResource(aProfileFolder) { + let bookmarksFile = aProfileFolder.clone(); + bookmarksFile.append("Bookmarks"); + if (!bookmarksFile.exists()) + return null; + + return { + type: MigrationUtils.resourceTypes.BOOKMARKS, + + migrate: function(aCallback) { + return Task.spawn(function* () { + let gotErrors = false; + let errorGatherer = function() { gotErrors = true }; + let jsonStream = yield new Promise((resolve, reject) => { + let options = { + uri: NetUtil.newURI(bookmarksFile), + loadUsingSystemPrincipal: true + }; + NetUtil.asyncFetch(options, (inputStream, resultCode) => { + if (Components.isSuccessCode(resultCode)) { + resolve(inputStream); + } else { + reject(new Error("Could not read Bookmarks file")); + } + }); + }); + + // Parse Chrome bookmark file that is JSON format + let bookmarkJSON = NetUtil.readInputStreamToString( + jsonStream, jsonStream.available(), { charset : "UTF-8" }); + let roots = JSON.parse(bookmarkJSON).roots; + + // Importing bookmark bar items + if (roots.bookmark_bar.children && + roots.bookmark_bar.children.length > 0) { + // Toolbar + let parentGuid = PlacesUtils.bookmarks.toolbarGuid; + if (!MigrationUtils.isStartupMigration) { + parentGuid = + yield MigrationUtils.createImportedBookmarksFolder("Chrome", parentGuid); + } + yield insertBookmarkItems(parentGuid, roots.bookmark_bar.children, errorGatherer); + } + + // Importing bookmark menu items + if (roots.other.children && + roots.other.children.length > 0) { + // Bookmark menu + let parentGuid = PlacesUtils.bookmarks.menuGuid; + if (!MigrationUtils.isStartupMigration) { + parentGuid = + yield MigrationUtils.createImportedBookmarksFolder("Chrome", parentGuid); + } + yield insertBookmarkItems(parentGuid, roots.other.children, errorGatherer); + } + if (gotErrors) { + throw new Error("The migration included errors."); + } + }.bind(this)).then(() => aCallback(true), + () => aCallback(false)); + } + }; +} + +function GetHistoryResource(aProfileFolder) { + let historyFile = aProfileFolder.clone(); + historyFile.append("History"); + if (!historyFile.exists()) + return null; + + return { + type: MigrationUtils.resourceTypes.HISTORY, + + migrate(aCallback) { + Task.spawn(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 = 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 = + yield MigrationUtils.getRowsFromDBWithoutLocks(historyFile.path, "Chrome history", query); + let places = []; + for (let row of rows) { + try { + // if having typed_count, we changes transition type to typed. + let transType = PlacesUtils.history.TRANSITION_LINK; + if (row.getResultByName("typed_count") > 0) + transType = PlacesUtils.history.TRANSITION_TYPED; + + places.push({ + uri: NetUtil.newURI(row.getResultByName("url")), + title: row.getResultByName("title"), + visits: [{ + transitionType: transType, + visitDate: chromeTimeToDate( + row.getResultByName( + "last_visit_time")) * 1000, + }], + }); + } catch (e) { + Cu.reportError(e); + } + } + + if (places.length > 0) { + yield new Promise((resolve, reject) => { + MigrationUtils.insertVisitsWrapper(places, { + _success: false, + handleResult: function() { + // Importing any entry is considered a successful import. + this._success = true; + }, + handleError: function() {}, + handleCompletion: function() { + if (this._success) { + resolve(); + } else { + reject(new Error("Couldn't add visits")); + } + } + }); + }); + } + }).then(() => { aCallback(true) }, + ex => { + Cu.reportError(ex); + aCallback(false); + }); + } + }; +} + +function GetCookiesResource(aProfileFolder) { + let cookiesFile = aProfileFolder.clone(); + cookiesFile.append("Cookies"); + if (!cookiesFile.exists()) + return null; + + return { + type: MigrationUtils.resourceTypes.COOKIES, + + migrate: Task.async(function* (aCallback) { + // We don't support decrypting cookies yet so only import plaintext ones. + let rows = yield MigrationUtils.getRowsFromDBWithoutLocks(cookiesFile.path, "Chrome cookies", + `SELECT host_key, name, value, path, expires_utc, secure, httponly, encrypted_value + 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; + } + + 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); + } + + try { + let expiresUtc = + chromeTimeToDate(row.getResultByName("expires_utc")) / 1000; + Services.cookies.add(host_key, + row.getResultByName("path"), + row.getResultByName("name"), + row.getResultByName("value"), + row.getResultByName("secure"), + row.getResultByName("httponly"), + false, + parseInt(expiresUtc), + {}); + } catch (e) { + Cu.reportError(e); + } + } + aCallback(true); + }), + }; +} + +function GetWindowsPasswordsResource(aProfileFolder) { + let loginFile = aProfileFolder.clone(); + loginFile.append("Login Data"); + if (!loginFile.exists()) + return null; + + return { + type: MigrationUtils.resourceTypes.PASSWORDS, + + migrate: Task.async(function* (aCallback) { + let rows = yield MigrationUtils.getRowsFromDBWithoutLocks(loginFile.path, "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; + } + let crypto = new OSCrypto(); + + 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: crypto. + decryptData(crypto.arrayToString(row.getResultByName("password_value")), + null), + hostname: origin_url.prePath, + formSubmitURL: null, + httpRealm: null, + usernameElement: row.getResultByName("username_element"), + passwordElement: row.getResultByName("password_element"), + timeCreated: chromeTimeToDate(row.getResultByName("date_created") + 0).getTime(), + timesUsed: row.getResultByName("times_used") + 0, + }; + + switch (row.getResultByName("scheme")) { + case AUTH_TYPE.SCHEME_HTML: + let action_url = NetUtil.newURI(row.getResultByName("action_url")); + if (!kValidSchemes.has(action_url.scheme)) { + continue; // This continues the outer for loop. + } + loginInfo.formSubmitURL = action_url.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.hostname.length + 1); + break; + default: + throw new Error("Login data scheme type not supported: " + + row.getResultByName("scheme")); + } + MigrationUtils.insertLoginWrapper(loginInfo); + } catch (e) { + Cu.reportError(e); + } + } + 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() { + let chromiumUserDataFolder = getDataFolder(["Chromium"], ["Chromium"], ["chromium"]); + this._chromeUserDataFolder = chromiumUserDataFolder.exists() ? chromiumUserDataFolder : null; +} + +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 componentsArray = [ChromeProfileMigrator, ChromiumProfileMigrator]; + +/** + * Chrome Canary + * Not available on Linux + **/ +function CanaryProfileMigrator() { + let chromeUserDataFolder = getDataFolder(["Google", "Chrome SxS"], ["Google", "Chrome Canary"]); + this._chromeUserDataFolder = chromeUserDataFolder.exists() ? chromeUserDataFolder : null; +} +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") { + componentsArray.push(CanaryProfileMigrator); +} + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(componentsArray); |