Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 * vim: sw=2 ts=2 sts=2 et */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 "use strict";
9 const Cc = Components.classes;
10 const Ci = Components.interfaces;
11 const Cu = Components.utils;
12 const Cr = Components.results;
14 const FILE_INPUT_STREAM_CID = "@mozilla.org/network/file-input-stream;1";
16 const S100NS_FROM1601TO1970 = 0x19DB1DED53E8000;
17 const S100NS_PER_MS = 10;
19 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
20 Cu.import("resource://gre/modules/Services.jsm");
21 Cu.import("resource://gre/modules/NetUtil.jsm");
22 Cu.import("resource://gre/modules/FileUtils.jsm");
23 Cu.import("resource:///modules/MigrationUtils.jsm");
25 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
26 "resource://gre/modules/PlacesUtils.jsm");
28 /**
29 * Convert Chrome time format to Date object
30 *
31 * @param aTime
32 * Chrome time
33 * @return converted Date object
34 * @note Google Chrome uses FILETIME / 10 as time.
35 * FILETIME is based on same structure of Windows.
36 */
37 function chromeTimeToDate(aTime)
38 {
39 return new Date((aTime * S100NS_PER_MS - S100NS_FROM1601TO1970 ) / 10000);
40 }
42 /**
43 * Insert bookmark items into specific folder.
44 *
45 * @param aFolderId
46 * id of folder where items will be inserted
47 * @param aItems
48 * bookmark items to be inserted
49 */
50 function insertBookmarkItems(aFolderId, aItems)
51 {
52 for (let i = 0; i < aItems.length; i++) {
53 let item = aItems[i];
55 try {
56 if (item.type == "url") {
57 PlacesUtils.bookmarks.insertBookmark(aFolderId,
58 NetUtil.newURI(item.url),
59 PlacesUtils.bookmarks.DEFAULT_INDEX,
60 item.name);
61 } else if (item.type == "folder") {
62 let newFolderId =
63 PlacesUtils.bookmarks.createFolder(aFolderId,
64 item.name,
65 PlacesUtils.bookmarks.DEFAULT_INDEX);
67 insertBookmarkItems(newFolderId, item.children);
68 }
69 } catch (e) {
70 Cu.reportError(e);
71 }
72 }
73 }
76 function ChromeProfileMigrator() {
77 let chromeUserDataFolder = FileUtils.getDir(
78 #ifdef XP_WIN
79 "LocalAppData", ["Google", "Chrome", "User Data"]
80 #elifdef XP_MACOSX
81 "ULibDir", ["Application Support", "Google", "Chrome"]
82 #else
83 "Home", [".config", "google-chrome"]
84 #endif
85 , false);
86 this._chromeUserDataFolder = chromeUserDataFolder.exists() ?
87 chromeUserDataFolder : null;
88 }
90 ChromeProfileMigrator.prototype = Object.create(MigratorPrototype);
92 ChromeProfileMigrator.prototype.getResources =
93 function Chrome_getResources(aProfile) {
94 if (this._chromeUserDataFolder) {
95 let profileFolder = this._chromeUserDataFolder.clone();
96 profileFolder.append(aProfile);
97 if (profileFolder.exists()) {
98 let possibleResources = [GetBookmarksResource(profileFolder),
99 GetHistoryResource(profileFolder),
100 GetCookiesResource(profileFolder)];
101 return [r for each (r in possibleResources) if (r != null)];
102 }
103 }
104 return [];
105 };
107 Object.defineProperty(ChromeProfileMigrator.prototype, "sourceProfiles", {
108 get: function Chrome_sourceProfiles() {
109 if ("__sourceProfiles" in this)
110 return this.__sourceProfiles;
112 if (!this._chromeUserDataFolder)
113 return [];
115 let profiles;
116 try {
117 // Local State is a JSON file that contains profile info.
118 let localState = this._chromeUserDataFolder.clone();
119 localState.append("Local State");
120 if (!localState.exists())
121 throw new Error("Chrome's 'Local State' file does not exist.");
122 if (!localState.isReadable())
123 throw new Error("Chrome's 'Local State' file could not be read.");
125 let fstream = Cc[FILE_INPUT_STREAM_CID].createInstance(Ci.nsIFileInputStream);
126 fstream.init(localState, -1, 0, 0);
127 let inputStream = NetUtil.readInputStreamToString(fstream, fstream.available(),
128 { charset: "UTF-8" });
129 let info_cache = JSON.parse(inputStream).profile.info_cache;
130 if (info_cache)
131 profiles = Object.keys(info_cache);
132 } catch (e) {
133 Cu.reportError("Error detecting Chrome profiles: " + e);
134 // If we weren't able to detect any profiles above, fallback to the Default profile.
135 let defaultProfileFolder = this._chromeUserDataFolder.clone();
136 defaultProfileFolder.append("Default");
137 if (defaultProfileFolder.exists())
138 profiles = ["Default"];
139 }
141 // Only list profiles from which any data can be imported
142 return this.__sourceProfiles = profiles.filter(function(profileName) {
143 let resources = this.getResources(profileName);
144 return resources && resources.length > 0;
145 }, this);
146 }
147 });
149 Object.defineProperty(ChromeProfileMigrator.prototype, "sourceHomePageURL", {
150 get: function Chrome_sourceHomePageURL() {
151 let prefsFile = this._chromeUserDataFolder.clone();
152 prefsFile.append("Preferences");
153 if (prefsFile.exists()) {
154 // XXX reading and parsing JSON is synchronous.
155 let fstream = Cc[FILE_INPUT_STREAM_CID].
156 createInstance(Ci.nsIFileInputStream);
157 fstream.init(file, -1, 0, 0);
158 try {
159 return JSON.parse(
160 NetUtil.readInputStreamToString(fstream, fstream.available(),
161 { charset: "UTF-8" })
162 ).homepage;
163 }
164 catch(e) {
165 Cu.reportError("Error parsing Chrome's preferences file: " + e);
166 }
167 }
168 return "";
169 }
170 });
172 function GetBookmarksResource(aProfileFolder) {
173 let bookmarksFile = aProfileFolder.clone();
174 bookmarksFile.append("Bookmarks");
175 if (!bookmarksFile.exists())
176 return null;
178 return {
179 type: MigrationUtils.resourceTypes.BOOKMARKS,
181 migrate: function(aCallback) {
182 NetUtil.asyncFetch(bookmarksFile, MigrationUtils.wrapMigrateFunction(
183 function(aInputStream, aResultCode) {
184 if (!Components.isSuccessCode(aResultCode))
185 throw new Error("Could not read Bookmarks file");
187 // Parse Chrome bookmark file that is JSON format
188 let bookmarkJSON = NetUtil.readInputStreamToString(
189 aInputStream, aInputStream.available(), { charset : "UTF-8" });
190 let roots = JSON.parse(bookmarkJSON).roots;
191 PlacesUtils.bookmarks.runInBatchMode({
192 runBatched: function() {
193 // Importing bookmark bar items
194 if (roots.bookmark_bar.children &&
195 roots.bookmark_bar.children.length > 0) {
196 // Toolbar
197 let parentId = PlacesUtils.toolbarFolderId;
198 if (!MigrationUtils.isStartupMigration) {
199 parentId = MigrationUtils.createImportedBookmarksFolder(
200 "Chrome", parentId);
201 }
202 insertBookmarkItems(parentId, roots.bookmark_bar.children);
203 }
205 // Importing bookmark menu items
206 if (roots.other.children &&
207 roots.other.children.length > 0) {
208 // Bookmark menu
209 let parentId = PlacesUtils.bookmarksMenuFolderId;
210 if (!MigrationUtils.isStartupMigration) {
211 parentId = MigrationUtils.createImportedBookmarksFolder(
212 "Chrome", parentId);
213 }
214 insertBookmarkItems(parentId, roots.other.children);
215 }
216 }
217 }, null);
218 }, aCallback));
219 }
220 };
221 }
223 function GetHistoryResource(aProfileFolder) {
224 let historyFile = aProfileFolder.clone();
225 historyFile.append("History");
226 if (!historyFile.exists())
227 return null;
229 return {
230 type: MigrationUtils.resourceTypes.HISTORY,
232 migrate: function(aCallback) {
233 let dbConn = Services.storage.openUnsharedDatabase(historyFile);
234 let stmt = dbConn.createAsyncStatement(
235 "SELECT url, title, last_visit_time, typed_count FROM urls WHERE hidden = 0");
237 stmt.executeAsync({
238 handleResult : function(aResults) {
239 let places = [];
240 for (let row = aResults.getNextRow(); row; row = aResults.getNextRow()) {
241 try {
242 // if having typed_count, we changes transition type to typed.
243 let transType = PlacesUtils.history.TRANSITION_LINK;
244 if (row.getResultByName("typed_count") > 0)
245 transType = PlacesUtils.history.TRANSITION_TYPED;
247 places.push({
248 uri: NetUtil.newURI(row.getResultByName("url")),
249 title: row.getResultByName("title"),
250 visits: [{
251 transitionType: transType,
252 visitDate: chromeTimeToDate(
253 row.getResultByName(
254 "last_visit_time")) * 1000,
255 }],
256 });
257 } catch (e) {
258 Cu.reportError(e);
259 }
260 }
262 try {
263 PlacesUtils.asyncHistory.updatePlaces(places);
264 } catch (e) {
265 Cu.reportError(e);
266 }
267 },
269 handleError : function(aError) {
270 Cu.reportError("Async statement execution returned with '" +
271 aError.result + "', '" + aError.message + "'");
272 },
274 handleCompletion : function(aReason) {
275 dbConn.asyncClose();
276 aCallback(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED);
277 }
278 });
279 stmt.finalize();
280 }
281 };
282 }
284 function GetCookiesResource(aProfileFolder) {
285 let cookiesFile = aProfileFolder.clone();
286 cookiesFile.append("Cookies");
287 if (!cookiesFile.exists())
288 return null;
290 return {
291 type: MigrationUtils.resourceTypes.COOKIES,
293 migrate: function(aCallback) {
294 let dbConn = Services.storage.openUnsharedDatabase(cookiesFile);
295 let stmt = dbConn.createAsyncStatement(
296 "SELECT host_key, path, name, value, secure, httponly, expires_utc FROM cookies");
298 stmt.executeAsync({
299 handleResult : function(aResults) {
300 for (let row = aResults.getNextRow(); row; row = aResults.getNextRow()) {
301 let host_key = row.getResultByName("host_key");
302 if (host_key.match(/^\./)) {
303 // 1st character of host_key may be ".", so we have to remove it
304 host_key = host_key.substr(1);
305 }
307 try {
308 let expiresUtc =
309 chromeTimeToDate(row.getResultByName("expires_utc")) / 1000;
310 Services.cookies.add(host_key,
311 row.getResultByName("path"),
312 row.getResultByName("name"),
313 row.getResultByName("value"),
314 row.getResultByName("secure"),
315 row.getResultByName("httponly"),
316 false,
317 parseInt(expiresUtc));
318 } catch (e) {
319 Cu.reportError(e);
320 }
321 }
322 },
324 handleError : function(aError) {
325 Cu.reportError("Async statement execution returned with '" +
326 aError.result + "', '" + aError.message + "'");
327 },
329 handleCompletion : function(aReason) {
330 dbConn.asyncClose();
331 aCallback(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED);
332 },
333 });
334 stmt.finalize();
335 }
336 }
337 }
339 ChromeProfileMigrator.prototype.classDescription = "Chrome Profile Migrator";
340 ChromeProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chrome";
341 ChromeProfileMigrator.prototype.classID = Components.ID("{4cec1de4-1671-4fc3-a53e-6c539dc77a26}");
343 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ChromeProfileMigrator]);