michael@0: /* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- michael@0: * vim: sw=2 ts=2 sts=2 et */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: const Cr = Components.results; michael@0: michael@0: const FILE_INPUT_STREAM_CID = "@mozilla.org/network/file-input-stream;1"; michael@0: michael@0: const S100NS_FROM1601TO1970 = 0x19DB1DED53E8000; michael@0: const S100NS_PER_MS = 10; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/NetUtil.jsm"); michael@0: Cu.import("resource://gre/modules/FileUtils.jsm"); michael@0: Cu.import("resource:///modules/MigrationUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", michael@0: "resource://gre/modules/PlacesUtils.jsm"); michael@0: michael@0: /** michael@0: * Convert Chrome time format to Date object michael@0: * michael@0: * @param aTime michael@0: * Chrome time michael@0: * @return converted Date object michael@0: * @note Google Chrome uses FILETIME / 10 as time. michael@0: * FILETIME is based on same structure of Windows. michael@0: */ michael@0: function chromeTimeToDate(aTime) michael@0: { michael@0: return new Date((aTime * S100NS_PER_MS - S100NS_FROM1601TO1970 ) / 10000); michael@0: } michael@0: michael@0: /** michael@0: * Insert bookmark items into specific folder. michael@0: * michael@0: * @param aFolderId michael@0: * id of folder where items will be inserted michael@0: * @param aItems michael@0: * bookmark items to be inserted michael@0: */ michael@0: function insertBookmarkItems(aFolderId, aItems) michael@0: { michael@0: for (let i = 0; i < aItems.length; i++) { michael@0: let item = aItems[i]; michael@0: michael@0: try { michael@0: if (item.type == "url") { michael@0: PlacesUtils.bookmarks.insertBookmark(aFolderId, michael@0: NetUtil.newURI(item.url), michael@0: PlacesUtils.bookmarks.DEFAULT_INDEX, michael@0: item.name); michael@0: } else if (item.type == "folder") { michael@0: let newFolderId = michael@0: PlacesUtils.bookmarks.createFolder(aFolderId, michael@0: item.name, michael@0: PlacesUtils.bookmarks.DEFAULT_INDEX); michael@0: michael@0: insertBookmarkItems(newFolderId, item.children); michael@0: } michael@0: } catch (e) { michael@0: Cu.reportError(e); michael@0: } michael@0: } michael@0: } michael@0: michael@0: michael@0: function ChromeProfileMigrator() { michael@0: let chromeUserDataFolder = FileUtils.getDir( michael@0: #ifdef XP_WIN michael@0: "LocalAppData", ["Google", "Chrome", "User Data"] michael@0: #elifdef XP_MACOSX michael@0: "ULibDir", ["Application Support", "Google", "Chrome"] michael@0: #else michael@0: "Home", [".config", "google-chrome"] michael@0: #endif michael@0: , false); michael@0: this._chromeUserDataFolder = chromeUserDataFolder.exists() ? michael@0: chromeUserDataFolder : null; michael@0: } michael@0: michael@0: ChromeProfileMigrator.prototype = Object.create(MigratorPrototype); michael@0: michael@0: ChromeProfileMigrator.prototype.getResources = michael@0: function Chrome_getResources(aProfile) { michael@0: if (this._chromeUserDataFolder) { michael@0: let profileFolder = this._chromeUserDataFolder.clone(); michael@0: profileFolder.append(aProfile); michael@0: if (profileFolder.exists()) { michael@0: let possibleResources = [GetBookmarksResource(profileFolder), michael@0: GetHistoryResource(profileFolder), michael@0: GetCookiesResource(profileFolder)]; michael@0: return [r for each (r in possibleResources) if (r != null)]; michael@0: } michael@0: } michael@0: return []; michael@0: }; michael@0: michael@0: Object.defineProperty(ChromeProfileMigrator.prototype, "sourceProfiles", { michael@0: get: function Chrome_sourceProfiles() { michael@0: if ("__sourceProfiles" in this) michael@0: return this.__sourceProfiles; michael@0: michael@0: if (!this._chromeUserDataFolder) michael@0: return []; michael@0: michael@0: let profiles; michael@0: try { michael@0: // Local State is a JSON file that contains profile info. michael@0: let localState = this._chromeUserDataFolder.clone(); michael@0: localState.append("Local State"); michael@0: if (!localState.exists()) michael@0: throw new Error("Chrome's 'Local State' file does not exist."); michael@0: if (!localState.isReadable()) michael@0: throw new Error("Chrome's 'Local State' file could not be read."); michael@0: michael@0: let fstream = Cc[FILE_INPUT_STREAM_CID].createInstance(Ci.nsIFileInputStream); michael@0: fstream.init(localState, -1, 0, 0); michael@0: let inputStream = NetUtil.readInputStreamToString(fstream, fstream.available(), michael@0: { charset: "UTF-8" }); michael@0: let info_cache = JSON.parse(inputStream).profile.info_cache; michael@0: if (info_cache) michael@0: profiles = Object.keys(info_cache); michael@0: } catch (e) { michael@0: Cu.reportError("Error detecting Chrome profiles: " + e); michael@0: // If we weren't able to detect any profiles above, fallback to the Default profile. michael@0: let defaultProfileFolder = this._chromeUserDataFolder.clone(); michael@0: defaultProfileFolder.append("Default"); michael@0: if (defaultProfileFolder.exists()) michael@0: profiles = ["Default"]; michael@0: } michael@0: michael@0: // Only list profiles from which any data can be imported michael@0: return this.__sourceProfiles = profiles.filter(function(profileName) { michael@0: let resources = this.getResources(profileName); michael@0: return resources && resources.length > 0; michael@0: }, this); michael@0: } michael@0: }); michael@0: michael@0: Object.defineProperty(ChromeProfileMigrator.prototype, "sourceHomePageURL", { michael@0: get: function Chrome_sourceHomePageURL() { michael@0: let prefsFile = this._chromeUserDataFolder.clone(); michael@0: prefsFile.append("Preferences"); michael@0: if (prefsFile.exists()) { michael@0: // XXX reading and parsing JSON is synchronous. michael@0: let fstream = Cc[FILE_INPUT_STREAM_CID]. michael@0: createInstance(Ci.nsIFileInputStream); michael@0: fstream.init(file, -1, 0, 0); michael@0: try { michael@0: return JSON.parse( michael@0: NetUtil.readInputStreamToString(fstream, fstream.available(), michael@0: { charset: "UTF-8" }) michael@0: ).homepage; michael@0: } michael@0: catch(e) { michael@0: Cu.reportError("Error parsing Chrome's preferences file: " + e); michael@0: } michael@0: } michael@0: return ""; michael@0: } michael@0: }); michael@0: michael@0: function GetBookmarksResource(aProfileFolder) { michael@0: let bookmarksFile = aProfileFolder.clone(); michael@0: bookmarksFile.append("Bookmarks"); michael@0: if (!bookmarksFile.exists()) michael@0: return null; michael@0: michael@0: return { michael@0: type: MigrationUtils.resourceTypes.BOOKMARKS, michael@0: michael@0: migrate: function(aCallback) { michael@0: NetUtil.asyncFetch(bookmarksFile, MigrationUtils.wrapMigrateFunction( michael@0: function(aInputStream, aResultCode) { michael@0: if (!Components.isSuccessCode(aResultCode)) michael@0: throw new Error("Could not read Bookmarks file"); michael@0: michael@0: // Parse Chrome bookmark file that is JSON format michael@0: let bookmarkJSON = NetUtil.readInputStreamToString( michael@0: aInputStream, aInputStream.available(), { charset : "UTF-8" }); michael@0: let roots = JSON.parse(bookmarkJSON).roots; michael@0: PlacesUtils.bookmarks.runInBatchMode({ michael@0: runBatched: function() { michael@0: // Importing bookmark bar items michael@0: if (roots.bookmark_bar.children && michael@0: roots.bookmark_bar.children.length > 0) { michael@0: // Toolbar michael@0: let parentId = PlacesUtils.toolbarFolderId; michael@0: if (!MigrationUtils.isStartupMigration) { michael@0: parentId = MigrationUtils.createImportedBookmarksFolder( michael@0: "Chrome", parentId); michael@0: } michael@0: insertBookmarkItems(parentId, roots.bookmark_bar.children); michael@0: } michael@0: michael@0: // Importing bookmark menu items michael@0: if (roots.other.children && michael@0: roots.other.children.length > 0) { michael@0: // Bookmark menu michael@0: let parentId = PlacesUtils.bookmarksMenuFolderId; michael@0: if (!MigrationUtils.isStartupMigration) { michael@0: parentId = MigrationUtils.createImportedBookmarksFolder( michael@0: "Chrome", parentId); michael@0: } michael@0: insertBookmarkItems(parentId, roots.other.children); michael@0: } michael@0: } michael@0: }, null); michael@0: }, aCallback)); michael@0: } michael@0: }; michael@0: } michael@0: michael@0: function GetHistoryResource(aProfileFolder) { michael@0: let historyFile = aProfileFolder.clone(); michael@0: historyFile.append("History"); michael@0: if (!historyFile.exists()) michael@0: return null; michael@0: michael@0: return { michael@0: type: MigrationUtils.resourceTypes.HISTORY, michael@0: michael@0: migrate: function(aCallback) { michael@0: let dbConn = Services.storage.openUnsharedDatabase(historyFile); michael@0: let stmt = dbConn.createAsyncStatement( michael@0: "SELECT url, title, last_visit_time, typed_count FROM urls WHERE hidden = 0"); michael@0: michael@0: stmt.executeAsync({ michael@0: handleResult : function(aResults) { michael@0: let places = []; michael@0: for (let row = aResults.getNextRow(); row; row = aResults.getNextRow()) { michael@0: try { michael@0: // if having typed_count, we changes transition type to typed. michael@0: let transType = PlacesUtils.history.TRANSITION_LINK; michael@0: if (row.getResultByName("typed_count") > 0) michael@0: transType = PlacesUtils.history.TRANSITION_TYPED; michael@0: michael@0: places.push({ michael@0: uri: NetUtil.newURI(row.getResultByName("url")), michael@0: title: row.getResultByName("title"), michael@0: visits: [{ michael@0: transitionType: transType, michael@0: visitDate: chromeTimeToDate( michael@0: row.getResultByName( michael@0: "last_visit_time")) * 1000, michael@0: }], michael@0: }); michael@0: } catch (e) { michael@0: Cu.reportError(e); michael@0: } michael@0: } michael@0: michael@0: try { michael@0: PlacesUtils.asyncHistory.updatePlaces(places); michael@0: } catch (e) { michael@0: Cu.reportError(e); michael@0: } michael@0: }, michael@0: michael@0: handleError : function(aError) { michael@0: Cu.reportError("Async statement execution returned with '" + michael@0: aError.result + "', '" + aError.message + "'"); michael@0: }, michael@0: michael@0: handleCompletion : function(aReason) { michael@0: dbConn.asyncClose(); michael@0: aCallback(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED); michael@0: } michael@0: }); michael@0: stmt.finalize(); michael@0: } michael@0: }; michael@0: } michael@0: michael@0: function GetCookiesResource(aProfileFolder) { michael@0: let cookiesFile = aProfileFolder.clone(); michael@0: cookiesFile.append("Cookies"); michael@0: if (!cookiesFile.exists()) michael@0: return null; michael@0: michael@0: return { michael@0: type: MigrationUtils.resourceTypes.COOKIES, michael@0: michael@0: migrate: function(aCallback) { michael@0: let dbConn = Services.storage.openUnsharedDatabase(cookiesFile); michael@0: let stmt = dbConn.createAsyncStatement( michael@0: "SELECT host_key, path, name, value, secure, httponly, expires_utc FROM cookies"); michael@0: michael@0: stmt.executeAsync({ michael@0: handleResult : function(aResults) { michael@0: for (let row = aResults.getNextRow(); row; row = aResults.getNextRow()) { michael@0: let host_key = row.getResultByName("host_key"); michael@0: if (host_key.match(/^\./)) { michael@0: // 1st character of host_key may be ".", so we have to remove it michael@0: host_key = host_key.substr(1); michael@0: } michael@0: michael@0: try { michael@0: let expiresUtc = michael@0: chromeTimeToDate(row.getResultByName("expires_utc")) / 1000; michael@0: Services.cookies.add(host_key, michael@0: row.getResultByName("path"), michael@0: row.getResultByName("name"), michael@0: row.getResultByName("value"), michael@0: row.getResultByName("secure"), michael@0: row.getResultByName("httponly"), michael@0: false, michael@0: parseInt(expiresUtc)); michael@0: } catch (e) { michael@0: Cu.reportError(e); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: handleError : function(aError) { michael@0: Cu.reportError("Async statement execution returned with '" + michael@0: aError.result + "', '" + aError.message + "'"); michael@0: }, michael@0: michael@0: handleCompletion : function(aReason) { michael@0: dbConn.asyncClose(); michael@0: aCallback(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED); michael@0: }, michael@0: }); michael@0: stmt.finalize(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: ChromeProfileMigrator.prototype.classDescription = "Chrome Profile Migrator"; michael@0: ChromeProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chrome"; michael@0: ChromeProfileMigrator.prototype.classID = Components.ID("{4cec1de4-1671-4fc3-a53e-6c539dc77a26}"); michael@0: michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ChromeProfileMigrator]);