Files
tubestation/browser/components/migration/src/ChromeProfileMigrator.js

606 lines
20 KiB
JavaScript

/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: sw=2 ts=2 sts=2 et
* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License
* Version 1.1 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License
* at http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
* the License for the specific language governing rights and
* limitations under the License.
*
* The Original Code is the Browser Profile Migrator.
*
* The Initial Developer of the Original Code is the Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Makoto Kato <m_kato@ga2.so-net.ne.jp> (Original Author)
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
const MIGRATOR = Ci.nsIBrowserProfileMigrator;
const LOCAL_FILE_CID = "@mozilla.org/file/local;1";
const FILE_INPUT_STREAM_CID = "@mozilla.org/network/file-input-stream;1";
const BUNDLE_MIGRATION = "chrome://browser/locale/migration/migration.properties";
const S100NS_FROM1601TO1970 = 0x19DB1DED53E8000;
const S100NS_PER_MS = 10;
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
Components.utils.import("resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyGetter(this, "bookmarksSubfolderTitle", function () {
// get "import from google chrome" string for folder
let strbundle =
Services.strings.createBundle(BUNDLE_MIGRATION);
let sourceNameChrome = strbundle.GetStringFromName("sourceNameChrome");
return strbundle.formatStringFromName("importedBookmarksFolder",
[sourceNameChrome],
1);
});
/**
* 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);
}
/**
* Insert bookmark items into specific folder.
*
* @param aFolderId
* id of folder where items will be inserted
* @param aItems
* bookmark items to be inserted
*/
function insertBookmarkItems(aFolderId, aItems)
{
for (let i = 0; i < aItems.length; i++) {
let item = aItems[i];
try {
if (item.type == "url") {
PlacesUtils.bookmarks.insertBookmark(aFolderId,
NetUtil.newURI(item.url),
PlacesUtils.bookmarks.DEFAULT_INDEX,
item.name);
} else if (item.type == "folder") {
let newFolderId =
PlacesUtils.bookmarks.createFolder(aFolderId,
item.name,
PlacesUtils.bookmarks.DEFAULT_INDEX);
insertBookmarkItems(newFolderId, item.children);
}
} catch (e) {
Cu.reportError(e);
}
}
}
function ChromeProfileMigrator()
{
}
ChromeProfileMigrator.prototype = {
_paths: {
bookmarks : null,
cookies : null,
history : null,
prefs : null,
userData : null,
},
_homepageURL : null,
_replaceBookmarks : false,
_sourceProfile: null,
_profilesCache: null,
/**
* Notify to observers to start migration
*
* @param aType
* notification type such as MIGRATOR.BOOKMARKS
*/
_notifyStart : function Chrome_notifyStart(aType)
{
Services.obs.notifyObservers(null, "Migration:ItemBeforeMigrate", aType);
this._pendingCount++;
},
/**
* Notify observers that a migration error occured with an item
*
* @param aType
* notification type such as MIGRATOR.BOOKMARKS
*/
_notifyError : function Chrome_notifyError(aType)
{
Services.obs.notifyObservers(null, "Migration:ItemError", aType);
},
/**
* Notify to observers to finish migration for item
* If all items are finished, it sends migration end notification.
*
* @param aType
* notification type such as MIGRATOR.BOOKMARKS
*/
_notifyCompleted : function Chrome_notifyIfCompleted(aType)
{
Services.obs.notifyObservers(null, "Migration:ItemAfterMigrate", aType);
if (--this._pendingCount == 0) {
// All items are migrated, so we have to send end notification.
Services.obs.notifyObservers(null, "Migration:Ended", null);
}
},
/**
* Migrating bookmark items
*/
_migrateBookmarks : function Chrome_migrateBookmarks()
{
this._notifyStart(MIGRATOR.BOOKMARKS);
try {
PlacesUtils.bookmarks.runInBatchMode({
_self : this,
runBatched : function (aUserData) {
let migrator = this._self;
let file = Cc[LOCAL_FILE_CID].createInstance(Ci.nsILocalFile);
file.initWithPath(migrator._paths.bookmarks);
NetUtil.asyncFetch(file, function(aInputStream, aResultCode) {
if (!Components.isSuccessCode(aResultCode)) {
migrator._notifyCompleted(MIGRATOR.BOOKMARKS);
return;
}
// Parse Chrome bookmark file that is JSON format
let bookmarkJSON = NetUtil.readInputStreamToString(aInputStream,
aInputStream.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 parentId = PlacesUtils.toolbarFolderId;
if (!migrator._replaceBookmarks) {
parentId =
PlacesUtils.bookmarks.createFolder(parentId,
bookmarksSubfolderTitle,
PlacesUtils.bookmarks.DEFAULT_INDEX);
}
insertBookmarkItems(parentId, roots.bookmark_bar.children);
}
// Importing bookmark menu items
if (roots.other.children &&
roots.other.children.length > 0) {
// Bookmark menu
let parentId = PlacesUtils.bookmarksMenuFolderId;
if (!migrator._replaceBookmarks) {
parentId =
PlacesUtils.bookmarks.createFolder(parentId,
bookmarksSubfolderTitle,
PlacesUtils.bookmarks.DEFAULT_INDEX);
}
insertBookmarkItems(parentId, roots.other.children);
}
migrator._notifyCompleted(MIGRATOR.BOOKMARKS);
});
}
}, null);
} catch (e) {
Cu.reportError(e);
this._notifyError(MIGRATOR.BOOKMARKS);
this._notifyCompleted(MIGRATOR.BOOKMARKS);
}
},
/**
* Migrating history
*/
_migrateHistory : function Chrome_migrateHistory()
{
this._notifyStart(MIGRATOR.HISTORY);
try {
PlacesUtils.history.runInBatchMode({
_self : this,
runBatched : function (aUserData) {
// access sqlite3 database of Chrome's history
let file = Cc[LOCAL_FILE_CID].createInstance(Ci.nsILocalFile);
file.initWithPath(this._self._paths.history);
let dbConn = Services.storage.openUnsharedDatabase(file);
let stmt = dbConn.createAsyncStatement(
"SELECT url, title, last_visit_time, typed_count FROM urls WHERE hidden = 0");
stmt.executeAsync({
_asyncHistory : Cc["@mozilla.org/browser/history;1"]
.getService(Ci.mozIAsyncHistory),
_db : dbConn,
_self : this._self,
handleResult : function(aResults) {
let places = [];
for (let row = aResults.getNextRow(); row; row = aResults.getNextRow()) {
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);
}
}
try {
this._asyncHistory.updatePlaces(places);
} catch (e) {
Cu.reportError(e);
}
},
handleError : function(aError) {
Cu.reportError("Async statement execution returned with '" +
aError.result + "', '" + aError.message + "'");
},
handleCompletion : function(aReason) {
this._db.asyncClose();
this._self._notifyCompleted(MIGRATOR.HISTORY);
}
});
stmt.finalize();
}
}, null);
} catch (e) {
Cu.reportError(e);
this._notifyError(MIGRATOR.HISTORY);
this._notifyCompleted(MIGRATOR.HISTORY);
}
},
/**
* Migrating cookies
*/
_migrateCookies : function Chrome_migrateCookies()
{
this._notifyStart(MIGRATOR.COOKIES);
try {
// Access sqlite3 database of Chrome's cookie
let file = Cc[LOCAL_FILE_CID].createInstance(Ci.nsILocalFile);
file.initWithPath(this._paths.cookies);
let dbConn = Services.storage.openUnsharedDatabase(file);
let stmt = dbConn.createAsyncStatement(
"SELECT host_key, path, name, value, secure, httponly, expires_utc FROM cookies");
stmt.executeAsync({
_db : dbConn,
_self : this,
handleResult : function(aResults) {
for (let row = aResults.getNextRow(); row; row = aResults.getNextRow()) {
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);
}
}
},
handleError : function(aError) {
Cu.reportError("Async statement execution returned with '" +
aError.result + "', '" + aError.message + "'");
},
handleCompletion : function(aReason) {
this._db.asyncClose();
this._self._notifyCompleted(MIGRATOR.COOKIES);
},
});
stmt.finalize();
} catch (e) {
Cu.reportError(e);
this._notifyError(MIGRATOR.COOKIES);
this._notifyCompleted(MIGRATOR.COOKIES);
}
},
/**
* nsIBrowserProfileMigrator interface implementation
*/
/**
* Let's migrate all items
*
* @param aItems
* list of data items to migrate.
* @param aStartup
* non-null if called during startup.
* @param aProfile
* profile directory name to migrate
*/
migrate : function Chrome_migrate(aItems, aStartup, aProfile)
{
if (aStartup) {
aStartup.doStartup();
this._replaceBookmarks = true;
}
this._sourceProfile = aProfile;
Services.obs.notifyObservers(null, "Migration:Started", null);
// Reset panding count. If this count becomes 0, "Migration:Ended"
// notification is sent
this._pendingCount = 1;
if (aItems & MIGRATOR.HISTORY)
this._migrateHistory();
if (aItems & MIGRATOR.COOKIES)
this._migrateCookies();
if (aItems & MIGRATOR.BOOKMARKS)
this._migrateBookmarks();
if (--this._pendingCount == 0) {
// When async imports are immeditelly completed unfortunately,
// this will be called.
// Usually, this notification is sent by _notifyCompleted()
Services.obs.notifyObservers(null, "Migration:Ended", null);
}
},
/**
* return supported migration types
*
* @param aProfile
* directory name of the profile
* @param aDoingStartup
* non-null if called during startup.
* @return supported migration types
*/
getMigrateData: function Chrome_getMigrateData(aProfile, aDoingStartup)
{
this._sourceProfile = aProfile;
let chromeProfileDir = Cc[LOCAL_FILE_CID].createInstance(Ci.nsILocalFile);
chromeProfileDir.initWithPath(this._paths.userData + aProfile);
let result = 0;
if (!chromeProfileDir.exists() || !chromeProfileDir.isReadable())
return result;
// bookmark and preference are JSON format
try {
let file = chromeProfileDir.clone();
file.append("Bookmarks");
if (file.exists()) {
this._paths.bookmarks = file.path;
result += MIGRATOR.BOOKMARKS;
}
} catch (e) {
Cu.reportError(e);
}
if (!this._paths.prefs) {
let file = chromeProfileDir.clone();
file.append("Preferences");
this._paths.prefs = file.path;
}
// history and cookies are SQLite database
try {
let file = chromeProfileDir.clone();
file.append("History");
if (file.exists()) {
this._paths.history = file.path;
result += MIGRATOR.HISTORY;
}
} catch (e) {
Cu.reportError(e);
}
try {
let file = chromeProfileDir.clone();
file.append("Cookies");
if (file.exists()) {
this._paths.cookies = file.path;
result += MIGRATOR.COOKIES;
}
} catch (e) {
Cu.reportError(e);
}
return result;
},
/**
* Whether we support migration of Chrome
*
* @return true if supported
*/
get sourceExists()
{
#ifdef XP_WIN
this._paths.userData = Services.dirsvc.get("LocalAppData", Ci.nsIFile).path +
"\\Google\\Chrome\\User Data\\";
#elifdef XP_MACOSX
this._paths.userData = Services.dirsvc.get("Home", Ci.nsIFile).path +
"/Library/Application Support/Google/Chrome/";
#else
this._paths.userData = Services.dirsvc.get("Home", Ci.nsIFile).path +
"/.config/google-chrome/";
#endif
let result = 0;
try {
let userDataDir = Cc[LOCAL_FILE_CID].createInstance(Ci.nsILocalFile);
userDataDir.initWithPath(this._paths.userData);
if (!userDataDir.exists() || !userDataDir.isReadable())
return false;
let profiles = this.sourceProfiles;
if (profiles.length < 1)
return false;
// check that we can actually get data from the first profile
result = this.getMigrateData(profiles.queryElementAt(0, Ci.nsISupportsString), false);
} catch (e) {
Cu.reportError(e);
}
return result > 0;
},
get sourceHasMultipleProfiles()
{
return this.sourceProfiles.length > 1;
},
get sourceProfiles()
{
let profiles = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
try {
if (!this._profilesCache) {
let localState = Cc[LOCAL_FILE_CID].createInstance(Ci.nsILocalFile);
// Local State is a JSON file that contains profile info.
localState.initWithPath(this._paths.userData + "Local State");
if (!localState.exists())
throw new Components.Exception("Chrome's 'Local State' file does not exist.",
Cr.NS_ERROR_FILE_NOT_FOUND);
if (!localState.isReadable())
throw new Components.Exception("Chrome's 'Local State' file could not be read.",
Cr.NS_ERROR_FILE_ACCESS_DENIED);
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" });
this._profilesCache = JSON.parse(inputStream).profile.info_cache;
}
for (let index in this._profilesCache) {
let str = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
str.data = index;
profiles.appendElement(str, false);
}
} catch (e) {
Cu.reportError("Error detecting Chrome profiles: " + e);
// if we weren't able to detect any profiles above, fallback to the Default profile.
if (profiles.length < 1) {
let str = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
// the default profile name is "Default"
str.data = "Default";
profiles.appendElement(str, false);
}
}
return profiles;
},
/**
* Return home page URL
*
* @return home page URL
*/
get sourceHomePageURL()
{
try {
if (this._homepageURL)
return this._homepageURL;
if (!this._paths.prefs)
this.getMigrateData(this._sourceProfile, false);
// XXX reading and parsing JSON is synchronous.
let file = Cc[LOCAL_FILE_CID].createInstance(Ci.nsILocalFile);
file.initWithPath(this._paths.prefs);
let fstream = Cc[FILE_INPUT_STREAM_CID].
createInstance(Ci.nsIFileInputStream);
fstream.init(file, -1, 0, 0);
this._homepageURL = JSON.parse(
NetUtil.readInputStreamToString(fstream, fstream.available(),
{ charset: "UTF-8" })).homepage;
return this._homepageURL;
} catch (e) {
Cu.reportError(e);
}
return "";
},
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIBrowserProfileMigrator
]),
classDescription: "Chrome Profile Migrator",
contractID: "@mozilla.org/profile/migrator;1?app=browser&type=chrome",
classID: Components.ID("{4cec1de4-1671-4fc3-a53e-6c539dc77a26}")
};
const NSGetFactory = XPCOMUtils.generateNSGetFactory([ChromeProfileMigrator]);