606 lines
20 KiB
JavaScript
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]);
|