1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/components/migration/src/ChromeProfileMigrator.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,343 @@ 1.4 +/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- 1.5 + * vim: sw=2 ts=2 sts=2 et */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +"use strict"; 1.11 + 1.12 +const Cc = Components.classes; 1.13 +const Ci = Components.interfaces; 1.14 +const Cu = Components.utils; 1.15 +const Cr = Components.results; 1.16 + 1.17 +const FILE_INPUT_STREAM_CID = "@mozilla.org/network/file-input-stream;1"; 1.18 + 1.19 +const S100NS_FROM1601TO1970 = 0x19DB1DED53E8000; 1.20 +const S100NS_PER_MS = 10; 1.21 + 1.22 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.23 +Cu.import("resource://gre/modules/Services.jsm"); 1.24 +Cu.import("resource://gre/modules/NetUtil.jsm"); 1.25 +Cu.import("resource://gre/modules/FileUtils.jsm"); 1.26 +Cu.import("resource:///modules/MigrationUtils.jsm"); 1.27 + 1.28 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", 1.29 + "resource://gre/modules/PlacesUtils.jsm"); 1.30 + 1.31 +/** 1.32 + * Convert Chrome time format to Date object 1.33 + * 1.34 + * @param aTime 1.35 + * Chrome time 1.36 + * @return converted Date object 1.37 + * @note Google Chrome uses FILETIME / 10 as time. 1.38 + * FILETIME is based on same structure of Windows. 1.39 + */ 1.40 +function chromeTimeToDate(aTime) 1.41 +{ 1.42 + return new Date((aTime * S100NS_PER_MS - S100NS_FROM1601TO1970 ) / 10000); 1.43 +} 1.44 + 1.45 +/** 1.46 + * Insert bookmark items into specific folder. 1.47 + * 1.48 + * @param aFolderId 1.49 + * id of folder where items will be inserted 1.50 + * @param aItems 1.51 + * bookmark items to be inserted 1.52 + */ 1.53 +function insertBookmarkItems(aFolderId, aItems) 1.54 +{ 1.55 + for (let i = 0; i < aItems.length; i++) { 1.56 + let item = aItems[i]; 1.57 + 1.58 + try { 1.59 + if (item.type == "url") { 1.60 + PlacesUtils.bookmarks.insertBookmark(aFolderId, 1.61 + NetUtil.newURI(item.url), 1.62 + PlacesUtils.bookmarks.DEFAULT_INDEX, 1.63 + item.name); 1.64 + } else if (item.type == "folder") { 1.65 + let newFolderId = 1.66 + PlacesUtils.bookmarks.createFolder(aFolderId, 1.67 + item.name, 1.68 + PlacesUtils.bookmarks.DEFAULT_INDEX); 1.69 + 1.70 + insertBookmarkItems(newFolderId, item.children); 1.71 + } 1.72 + } catch (e) { 1.73 + Cu.reportError(e); 1.74 + } 1.75 + } 1.76 +} 1.77 + 1.78 + 1.79 +function ChromeProfileMigrator() { 1.80 + let chromeUserDataFolder = FileUtils.getDir( 1.81 +#ifdef XP_WIN 1.82 + "LocalAppData", ["Google", "Chrome", "User Data"] 1.83 +#elifdef XP_MACOSX 1.84 + "ULibDir", ["Application Support", "Google", "Chrome"] 1.85 +#else 1.86 + "Home", [".config", "google-chrome"] 1.87 +#endif 1.88 + , false); 1.89 + this._chromeUserDataFolder = chromeUserDataFolder.exists() ? 1.90 + chromeUserDataFolder : null; 1.91 +} 1.92 + 1.93 +ChromeProfileMigrator.prototype = Object.create(MigratorPrototype); 1.94 + 1.95 +ChromeProfileMigrator.prototype.getResources = 1.96 + function Chrome_getResources(aProfile) { 1.97 + if (this._chromeUserDataFolder) { 1.98 + let profileFolder = this._chromeUserDataFolder.clone(); 1.99 + profileFolder.append(aProfile); 1.100 + if (profileFolder.exists()) { 1.101 + let possibleResources = [GetBookmarksResource(profileFolder), 1.102 + GetHistoryResource(profileFolder), 1.103 + GetCookiesResource(profileFolder)]; 1.104 + return [r for each (r in possibleResources) if (r != null)]; 1.105 + } 1.106 + } 1.107 + return []; 1.108 + }; 1.109 + 1.110 +Object.defineProperty(ChromeProfileMigrator.prototype, "sourceProfiles", { 1.111 + get: function Chrome_sourceProfiles() { 1.112 + if ("__sourceProfiles" in this) 1.113 + return this.__sourceProfiles; 1.114 + 1.115 + if (!this._chromeUserDataFolder) 1.116 + return []; 1.117 + 1.118 + let profiles; 1.119 + try { 1.120 + // Local State is a JSON file that contains profile info. 1.121 + let localState = this._chromeUserDataFolder.clone(); 1.122 + localState.append("Local State"); 1.123 + if (!localState.exists()) 1.124 + throw new Error("Chrome's 'Local State' file does not exist."); 1.125 + if (!localState.isReadable()) 1.126 + throw new Error("Chrome's 'Local State' file could not be read."); 1.127 + 1.128 + let fstream = Cc[FILE_INPUT_STREAM_CID].createInstance(Ci.nsIFileInputStream); 1.129 + fstream.init(localState, -1, 0, 0); 1.130 + let inputStream = NetUtil.readInputStreamToString(fstream, fstream.available(), 1.131 + { charset: "UTF-8" }); 1.132 + let info_cache = JSON.parse(inputStream).profile.info_cache; 1.133 + if (info_cache) 1.134 + profiles = Object.keys(info_cache); 1.135 + } catch (e) { 1.136 + Cu.reportError("Error detecting Chrome profiles: " + e); 1.137 + // If we weren't able to detect any profiles above, fallback to the Default profile. 1.138 + let defaultProfileFolder = this._chromeUserDataFolder.clone(); 1.139 + defaultProfileFolder.append("Default"); 1.140 + if (defaultProfileFolder.exists()) 1.141 + profiles = ["Default"]; 1.142 + } 1.143 + 1.144 + // Only list profiles from which any data can be imported 1.145 + return this.__sourceProfiles = profiles.filter(function(profileName) { 1.146 + let resources = this.getResources(profileName); 1.147 + return resources && resources.length > 0; 1.148 + }, this); 1.149 + } 1.150 +}); 1.151 + 1.152 +Object.defineProperty(ChromeProfileMigrator.prototype, "sourceHomePageURL", { 1.153 + get: function Chrome_sourceHomePageURL() { 1.154 + let prefsFile = this._chromeUserDataFolder.clone(); 1.155 + prefsFile.append("Preferences"); 1.156 + if (prefsFile.exists()) { 1.157 + // XXX reading and parsing JSON is synchronous. 1.158 + let fstream = Cc[FILE_INPUT_STREAM_CID]. 1.159 + createInstance(Ci.nsIFileInputStream); 1.160 + fstream.init(file, -1, 0, 0); 1.161 + try { 1.162 + return JSON.parse( 1.163 + NetUtil.readInputStreamToString(fstream, fstream.available(), 1.164 + { charset: "UTF-8" }) 1.165 + ).homepage; 1.166 + } 1.167 + catch(e) { 1.168 + Cu.reportError("Error parsing Chrome's preferences file: " + e); 1.169 + } 1.170 + } 1.171 + return ""; 1.172 + } 1.173 +}); 1.174 + 1.175 +function GetBookmarksResource(aProfileFolder) { 1.176 + let bookmarksFile = aProfileFolder.clone(); 1.177 + bookmarksFile.append("Bookmarks"); 1.178 + if (!bookmarksFile.exists()) 1.179 + return null; 1.180 + 1.181 + return { 1.182 + type: MigrationUtils.resourceTypes.BOOKMARKS, 1.183 + 1.184 + migrate: function(aCallback) { 1.185 + NetUtil.asyncFetch(bookmarksFile, MigrationUtils.wrapMigrateFunction( 1.186 + function(aInputStream, aResultCode) { 1.187 + if (!Components.isSuccessCode(aResultCode)) 1.188 + throw new Error("Could not read Bookmarks file"); 1.189 + 1.190 + // Parse Chrome bookmark file that is JSON format 1.191 + let bookmarkJSON = NetUtil.readInputStreamToString( 1.192 + aInputStream, aInputStream.available(), { charset : "UTF-8" }); 1.193 + let roots = JSON.parse(bookmarkJSON).roots; 1.194 + PlacesUtils.bookmarks.runInBatchMode({ 1.195 + runBatched: function() { 1.196 + // Importing bookmark bar items 1.197 + if (roots.bookmark_bar.children && 1.198 + roots.bookmark_bar.children.length > 0) { 1.199 + // Toolbar 1.200 + let parentId = PlacesUtils.toolbarFolderId; 1.201 + if (!MigrationUtils.isStartupMigration) { 1.202 + parentId = MigrationUtils.createImportedBookmarksFolder( 1.203 + "Chrome", parentId); 1.204 + } 1.205 + insertBookmarkItems(parentId, roots.bookmark_bar.children); 1.206 + } 1.207 + 1.208 + // Importing bookmark menu items 1.209 + if (roots.other.children && 1.210 + roots.other.children.length > 0) { 1.211 + // Bookmark menu 1.212 + let parentId = PlacesUtils.bookmarksMenuFolderId; 1.213 + if (!MigrationUtils.isStartupMigration) { 1.214 + parentId = MigrationUtils.createImportedBookmarksFolder( 1.215 + "Chrome", parentId); 1.216 + } 1.217 + insertBookmarkItems(parentId, roots.other.children); 1.218 + } 1.219 + } 1.220 + }, null); 1.221 + }, aCallback)); 1.222 + } 1.223 + }; 1.224 +} 1.225 + 1.226 +function GetHistoryResource(aProfileFolder) { 1.227 + let historyFile = aProfileFolder.clone(); 1.228 + historyFile.append("History"); 1.229 + if (!historyFile.exists()) 1.230 + return null; 1.231 + 1.232 + return { 1.233 + type: MigrationUtils.resourceTypes.HISTORY, 1.234 + 1.235 + migrate: function(aCallback) { 1.236 + let dbConn = Services.storage.openUnsharedDatabase(historyFile); 1.237 + let stmt = dbConn.createAsyncStatement( 1.238 + "SELECT url, title, last_visit_time, typed_count FROM urls WHERE hidden = 0"); 1.239 + 1.240 + stmt.executeAsync({ 1.241 + handleResult : function(aResults) { 1.242 + let places = []; 1.243 + for (let row = aResults.getNextRow(); row; row = aResults.getNextRow()) { 1.244 + try { 1.245 + // if having typed_count, we changes transition type to typed. 1.246 + let transType = PlacesUtils.history.TRANSITION_LINK; 1.247 + if (row.getResultByName("typed_count") > 0) 1.248 + transType = PlacesUtils.history.TRANSITION_TYPED; 1.249 + 1.250 + places.push({ 1.251 + uri: NetUtil.newURI(row.getResultByName("url")), 1.252 + title: row.getResultByName("title"), 1.253 + visits: [{ 1.254 + transitionType: transType, 1.255 + visitDate: chromeTimeToDate( 1.256 + row.getResultByName( 1.257 + "last_visit_time")) * 1000, 1.258 + }], 1.259 + }); 1.260 + } catch (e) { 1.261 + Cu.reportError(e); 1.262 + } 1.263 + } 1.264 + 1.265 + try { 1.266 + PlacesUtils.asyncHistory.updatePlaces(places); 1.267 + } catch (e) { 1.268 + Cu.reportError(e); 1.269 + } 1.270 + }, 1.271 + 1.272 + handleError : function(aError) { 1.273 + Cu.reportError("Async statement execution returned with '" + 1.274 + aError.result + "', '" + aError.message + "'"); 1.275 + }, 1.276 + 1.277 + handleCompletion : function(aReason) { 1.278 + dbConn.asyncClose(); 1.279 + aCallback(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED); 1.280 + } 1.281 + }); 1.282 + stmt.finalize(); 1.283 + } 1.284 + }; 1.285 +} 1.286 + 1.287 +function GetCookiesResource(aProfileFolder) { 1.288 + let cookiesFile = aProfileFolder.clone(); 1.289 + cookiesFile.append("Cookies"); 1.290 + if (!cookiesFile.exists()) 1.291 + return null; 1.292 + 1.293 + return { 1.294 + type: MigrationUtils.resourceTypes.COOKIES, 1.295 + 1.296 + migrate: function(aCallback) { 1.297 + let dbConn = Services.storage.openUnsharedDatabase(cookiesFile); 1.298 + let stmt = dbConn.createAsyncStatement( 1.299 + "SELECT host_key, path, name, value, secure, httponly, expires_utc FROM cookies"); 1.300 + 1.301 + stmt.executeAsync({ 1.302 + handleResult : function(aResults) { 1.303 + for (let row = aResults.getNextRow(); row; row = aResults.getNextRow()) { 1.304 + let host_key = row.getResultByName("host_key"); 1.305 + if (host_key.match(/^\./)) { 1.306 + // 1st character of host_key may be ".", so we have to remove it 1.307 + host_key = host_key.substr(1); 1.308 + } 1.309 + 1.310 + try { 1.311 + let expiresUtc = 1.312 + chromeTimeToDate(row.getResultByName("expires_utc")) / 1000; 1.313 + Services.cookies.add(host_key, 1.314 + row.getResultByName("path"), 1.315 + row.getResultByName("name"), 1.316 + row.getResultByName("value"), 1.317 + row.getResultByName("secure"), 1.318 + row.getResultByName("httponly"), 1.319 + false, 1.320 + parseInt(expiresUtc)); 1.321 + } catch (e) { 1.322 + Cu.reportError(e); 1.323 + } 1.324 + } 1.325 + }, 1.326 + 1.327 + handleError : function(aError) { 1.328 + Cu.reportError("Async statement execution returned with '" + 1.329 + aError.result + "', '" + aError.message + "'"); 1.330 + }, 1.331 + 1.332 + handleCompletion : function(aReason) { 1.333 + dbConn.asyncClose(); 1.334 + aCallback(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED); 1.335 + }, 1.336 + }); 1.337 + stmt.finalize(); 1.338 + } 1.339 + } 1.340 +} 1.341 + 1.342 +ChromeProfileMigrator.prototype.classDescription = "Chrome Profile Migrator"; 1.343 +ChromeProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chrome"; 1.344 +ChromeProfileMigrator.prototype.classID = Components.ID("{4cec1de4-1671-4fc3-a53e-6c539dc77a26}"); 1.345 + 1.346 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ChromeProfileMigrator]);