562 lines
20 KiB
JavaScript
562 lines
20 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
this.EXPORTED_SYMBOLS = ["MSMigrationUtils"];
|
|
|
|
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
Cu.import("resource://gre/modules/Task.jsm");
|
|
Cu.import("resource:///modules/MigrationUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
|
|
"resource://gre/modules/PlacesUtils.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
|
|
"resource://gre/modules/WindowsRegistry.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
|
|
"resource://gre/modules/ctypes.jsm");
|
|
|
|
const EDGE_COOKIE_PATH_OPTIONS = ["", "#!001\\", "#!002\\"];
|
|
const EDGE_COOKIES_SUFFIX = "MicrosoftEdge\\Cookies";
|
|
const EDGE_FAVORITES = "AC\\MicrosoftEdge\\User\\Default\\Favorites";
|
|
const EDGE_READINGLIST = "AC\\MicrosoftEdge\\User\\Default\\DataStore\\Data\\";
|
|
|
|
Cu.importGlobalProperties(["File"]);
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
//// Helpers.
|
|
|
|
function CtypesHelpers() {
|
|
this._structs = {};
|
|
this._functions = {};
|
|
this._libs = {};
|
|
|
|
const WORD = ctypes.uint16_t;
|
|
const DWORD = ctypes.uint32_t;
|
|
const BOOL = ctypes.int;
|
|
|
|
this._structs.SYSTEMTIME = new ctypes.StructType('SYSTEMTIME', [
|
|
{wYear: WORD},
|
|
{wMonth: WORD},
|
|
{wDayOfWeek: WORD},
|
|
{wDay: WORD},
|
|
{wHour: WORD},
|
|
{wMinute: WORD},
|
|
{wSecond: WORD},
|
|
{wMilliseconds: WORD}
|
|
]);
|
|
|
|
this._structs.FILETIME = new ctypes.StructType('FILETIME', [
|
|
{dwLowDateTime: DWORD},
|
|
{dwHighDateTime: DWORD}
|
|
]);
|
|
|
|
try {
|
|
this._libs.kernel32 = ctypes.open("Kernel32");
|
|
this._functions.FileTimeToSystemTime =
|
|
this._libs.kernel32.declare("FileTimeToSystemTime",
|
|
ctypes.default_abi,
|
|
BOOL,
|
|
this._structs.FILETIME.ptr,
|
|
this._structs.SYSTEMTIME.ptr);
|
|
} catch (ex) {
|
|
this.finalize();
|
|
}
|
|
}
|
|
|
|
CtypesHelpers.prototype = {
|
|
/**
|
|
* Must be invoked once after last use of any of the provided helpers.
|
|
*/
|
|
finalize() {
|
|
this._structs = {};
|
|
this._functions = {};
|
|
for each (let lib in this._libs) {
|
|
try {
|
|
lib.close();
|
|
} catch (ex) {}
|
|
}
|
|
this._libs = {};
|
|
},
|
|
|
|
/**
|
|
* Converts a FILETIME struct (2 DWORDS), to a SYSTEMTIME struct,
|
|
* and then deduces the number of seconds since the epoch (which
|
|
* is the data we want for the cookie expiry date).
|
|
*
|
|
* @param aTimeHi
|
|
* Least significant DWORD.
|
|
* @param aTimeLo
|
|
* Most significant DWORD.
|
|
* @return the number of seconds since the epoch
|
|
*/
|
|
fileTimeToSecondsSinceEpoch(aTimeHi, aTimeLo) {
|
|
let fileTime = this._structs.FILETIME();
|
|
fileTime.dwLowDateTime = aTimeLo;
|
|
fileTime.dwHighDateTime = aTimeHi;
|
|
let systemTime = this._structs.SYSTEMTIME();
|
|
let result = this._functions.FileTimeToSystemTime(fileTime.address(),
|
|
systemTime.address());
|
|
if (result == 0)
|
|
throw new Error(ctypes.winLastError);
|
|
|
|
// System time is in UTC, so we use Date.UTC to get milliseconds from epoch,
|
|
// then divide by 1000 to get seconds, and round down:
|
|
return Math.floor(Date.UTC(systemTime.wYear,
|
|
systemTime.wMonth - 1,
|
|
systemTime.wDay,
|
|
systemTime.wHour,
|
|
systemTime.wMinute,
|
|
systemTime.wSecond,
|
|
systemTime.wMilliseconds) / 1000);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Checks whether an host is an IP (v4 or v6) address.
|
|
*
|
|
* @param aHost
|
|
* The host to check.
|
|
* @return whether aHost is an IP address.
|
|
*/
|
|
function hostIsIPAddress(aHost) {
|
|
try {
|
|
Services.eTLD.getBaseDomainFromHost(aHost);
|
|
} catch (e if e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS) {
|
|
return true;
|
|
} catch (e) {}
|
|
return false;
|
|
}
|
|
|
|
var gEdgeDir;
|
|
function getEdgeLocalDataFolder() {
|
|
if (gEdgeDir) {
|
|
return gEdgeDir.clone();
|
|
}
|
|
let packages = Services.dirsvc.get("LocalAppData", Ci.nsIFile);
|
|
packages.append("Packages");
|
|
let edgeDir = packages.clone();
|
|
edgeDir.append("Microsoft.MicrosoftEdge_8wekyb3d8bbwe");
|
|
try {
|
|
if (edgeDir.exists() && edgeDir.isReadable() && edgeDir.isDirectory()) {
|
|
gEdgeDir = edgeDir;
|
|
return edgeDir.clone();
|
|
}
|
|
|
|
// Let's try the long way:
|
|
let dirEntries = packages.directoryEntries;
|
|
while (dirEntries.hasMoreElements()) {
|
|
let subDir = dirEntries.getNext();
|
|
subDir.QueryInterface(Ci.nsIFile);
|
|
if (subDir.leafName.startsWith("Microsoft.MicrosoftEdge") && subDir.isReadable() &&
|
|
subDir.isDirectory()) {
|
|
gEdgeDir = subDir;
|
|
return subDir.clone();
|
|
}
|
|
}
|
|
} catch (ex) {
|
|
Cu.reportError("Exception trying to find the Edge favorites directory: " + ex);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
function Bookmarks(migrationType) {
|
|
this._migrationType = migrationType;
|
|
}
|
|
|
|
Bookmarks.prototype = {
|
|
type: MigrationUtils.resourceTypes.BOOKMARKS,
|
|
|
|
get exists() !!this._favoritesFolder,
|
|
|
|
get importedAppLabel() this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE ? "IE" : "Edge",
|
|
|
|
__favoritesFolder: null,
|
|
get _favoritesFolder() {
|
|
if (!this.__favoritesFolder) {
|
|
if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE) {
|
|
let favoritesFolder = Services.dirsvc.get("Favs", Ci.nsIFile);
|
|
if (favoritesFolder.exists() && favoritesFolder.isReadable())
|
|
return this.__favoritesFolder = favoritesFolder;
|
|
}
|
|
if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_EDGE) {
|
|
let edgeDir = getEdgeLocalDataFolder();
|
|
if (edgeDir) {
|
|
edgeDir.appendRelativePath(EDGE_FAVORITES);
|
|
if (edgeDir.exists() && edgeDir.isReadable() && edgeDir.isDirectory()) {
|
|
return this.__favoritesFolder = edgeDir;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return this.__favoritesFolder;
|
|
},
|
|
|
|
__toolbarFolderName: null,
|
|
get _toolbarFolderName() {
|
|
if (!this.__toolbarFolderName) {
|
|
if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE) {
|
|
// Retrieve the name of IE's favorites subfolder that holds the bookmarks
|
|
// in the toolbar. This was previously stored in the registry and changed
|
|
// in IE7 to always be called "Links".
|
|
let folderName = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
|
|
"Software\\Microsoft\\Internet Explorer\\Toolbar",
|
|
"LinksFolderName");
|
|
this.__toolbarFolderName = folderName || "Links";
|
|
} else {
|
|
this.__toolbarFolderName = "Links";
|
|
}
|
|
}
|
|
return this.__toolbarFolderName;
|
|
},
|
|
|
|
migrate: function B_migrate(aCallback) {
|
|
return Task.spawn(function* () {
|
|
// Import to the bookmarks menu.
|
|
let folderGuid = PlacesUtils.bookmarks.menuGuid;
|
|
if (!MigrationUtils.isStartupMigration) {
|
|
folderGuid =
|
|
yield MigrationUtils.createImportedBookmarksFolder(this.importedAppLabel, folderGuid);
|
|
}
|
|
yield this._migrateFolder(this._favoritesFolder, folderGuid);
|
|
|
|
if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_EDGE) {
|
|
yield this._migrateEdgeReadingList(PlacesUtils.bookmarks.menuGuid);
|
|
}
|
|
}.bind(this)).then(() => aCallback(true),
|
|
e => { Cu.reportError(e); aCallback(false) });
|
|
},
|
|
|
|
_migrateFolder: Task.async(function* (aSourceFolder, aDestFolderGuid) {
|
|
// TODO (bug 741993): the favorites order is stored in the Registry, at
|
|
// HCU\Software\Microsoft\Windows\CurrentVersion\Explorer\MenuOrder\Favorites
|
|
// for IE, and in a similar location for Edge.
|
|
// Until we support it, bookmarks are imported in alphabetical order.
|
|
let entries = aSourceFolder.directoryEntries;
|
|
while (entries.hasMoreElements()) {
|
|
let entry = entries.getNext().QueryInterface(Ci.nsIFile);
|
|
try {
|
|
// Make sure that entry.path == entry.target to not follow .lnk folder
|
|
// shortcuts which could lead to infinite cycles.
|
|
// Don't use isSymlink(), since it would throw for invalid
|
|
// lnk files pointing to URLs or to unresolvable paths.
|
|
if (entry.path == entry.target && entry.isDirectory()) {
|
|
let folderGuid;
|
|
if (entry.leafName == this._toolbarFolderName &&
|
|
entry.parent.equals(this._favoritesFolder)) {
|
|
// Import to the bookmarks toolbar.
|
|
folderGuid = PlacesUtils.bookmarks.toolbarGuid;
|
|
if (!MigrationUtils.isStartupMigration) {
|
|
folderGuid =
|
|
yield MigrationUtils.createImportedBookmarksFolder(this.importedAppLabel, folderGuid);
|
|
}
|
|
}
|
|
else {
|
|
// Import to a new folder.
|
|
folderGuid = (yield PlacesUtils.bookmarks.insert({
|
|
type: PlacesUtils.bookmarks.TYPE_FOLDER,
|
|
parentGuid: aDestFolderGuid,
|
|
title: entry.leafName
|
|
})).guid;
|
|
}
|
|
|
|
if (entry.isReadable()) {
|
|
// Recursively import the folder.
|
|
yield this._migrateFolder(entry, folderGuid);
|
|
}
|
|
}
|
|
else {
|
|
// Strip the .url extension, to both check this is a valid link file,
|
|
// and get the associated title.
|
|
let matches = entry.leafName.match(/(.+)\.url$/i);
|
|
if (matches) {
|
|
let fileHandler = Cc["@mozilla.org/network/protocol;1?name=file"].
|
|
getService(Ci.nsIFileProtocolHandler);
|
|
let uri = fileHandler.readURLFile(entry);
|
|
let title = matches[1];
|
|
|
|
yield PlacesUtils.bookmarks.insert({
|
|
parentGuid: aDestFolderGuid, url: uri, title
|
|
});
|
|
}
|
|
}
|
|
} catch (ex) {
|
|
Components.utils.reportError("Unable to import " + this.importedAppLabel + " favorite (" + entry.leafName + "): " + ex);
|
|
}
|
|
}
|
|
}),
|
|
|
|
_migrateEdgeReadingList: Task.async(function*(parentGuid) {
|
|
let edgeDir = getEdgeLocalDataFolder();
|
|
if (!edgeDir) {
|
|
return;
|
|
}
|
|
|
|
this._readingListExtractor = Cc["@mozilla.org/profile/migrator/edgereadinglistextractor;1"].
|
|
createInstance(Ci.nsIEdgeReadingListExtractor);
|
|
edgeDir.appendRelativePath(EDGE_READINGLIST);
|
|
if (edgeDir.exists() && edgeDir.isReadable() && edgeDir.isDirectory()) {
|
|
let expectedDir = edgeDir.clone();
|
|
expectedDir.appendRelativePath("nouser1\\120712-0049");
|
|
if (expectedDir.exists() && expectedDir.isReadable() && expectedDir.isDirectory()) {
|
|
yield this._migrateEdgeReadingListDB(expectedDir, parentGuid);
|
|
} else {
|
|
let getSubdirs = someDir => {
|
|
let subdirs = someDir.directoryEntries;
|
|
let rv = [];
|
|
while (subdirs.hasMoreElements()) {
|
|
let subdir = subdirs.getNext().QueryInterface(Ci.nsIFile);
|
|
if (subdir.isDirectory() && subdir.isReadable()) {
|
|
rv.push(subdir);
|
|
}
|
|
}
|
|
return rv;
|
|
};
|
|
let dirs = getSubdirs(edgeDir).map(getSubdirs);
|
|
for (let dir of dirs) {
|
|
yield this._migrateEdgeReadingListDB(dir, parentGuid);
|
|
}
|
|
}
|
|
}
|
|
}),
|
|
_migrateEdgeReadingListDB: Task.async(function*(dbFile, parentGuid) {
|
|
dbFile.appendRelativePath("DBStore\\spartan.edb");
|
|
if (!dbFile.exists() || !dbFile.isReadable() || !dbFile.isFile()) {
|
|
return;
|
|
}
|
|
let readingListItems;
|
|
try {
|
|
readingListItems = this._readingListExtractor.extract(dbFile.path);
|
|
} catch (ex) {
|
|
Cu.reportError("Failed to extract Edge reading list information from " +
|
|
"the database at " + dbPath + " due to the following error: " + ex);
|
|
return;
|
|
}
|
|
if (!readingListItems.length) {
|
|
return;
|
|
}
|
|
let destFolderGuid = yield this._ensureEdgeReadingListFolder(parentGuid);
|
|
for (let i = 0; i < readingListItems.length; i++) {
|
|
let readingListItem = readingListItems.queryElementAt(i, Ci.nsIPropertyBag2);
|
|
let url = readingListItem.get("uri");
|
|
let title = readingListItem.get("title");
|
|
let time = readingListItem.get("time");
|
|
// time is a PRTime, which is microseconds (since unix epoch), or null.
|
|
// We need milliseconds for the date constructor, so divide by 1000:
|
|
let dateAdded = time ? new Date(time / 1000) : new Date();
|
|
yield PlacesUtils.bookmarks.insert({
|
|
parentGuid: destFolderGuid, url: url, title, dateAdded
|
|
});
|
|
}
|
|
}),
|
|
|
|
_ensureEdgeReadingListFolder: Task.async(function*(parentGuid) {
|
|
if (!this.__edgeReadingListFolderGuid) {
|
|
let folderTitle = MigrationUtils.getLocalizedString("importedEdgeReadingList");
|
|
let folderSpec = {type: PlacesUtils.bookmarks.TYPE_FOLDER, parentGuid, title: folderTitle};
|
|
this.__edgeReadingListFolderGuid = (yield PlacesUtils.bookmarks.insert(folderSpec)).guid;
|
|
}
|
|
return this.__edgeReadingListFolderGuid;
|
|
}),
|
|
};
|
|
|
|
function Cookies(migrationType) {
|
|
this._migrationType = migrationType;
|
|
}
|
|
|
|
Cookies.prototype = {
|
|
type: MigrationUtils.resourceTypes.COOKIES,
|
|
|
|
get exists() {
|
|
if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE) {
|
|
return !!this._cookiesFolder;
|
|
}
|
|
return !!this._cookiesFolders;
|
|
},
|
|
|
|
__cookiesFolder: null,
|
|
get _cookiesFolder() {
|
|
// Edge stores cookies in a number of places, and this shouldn't get called:
|
|
if (this._migrationType != MSMigrationUtils.MIGRATION_TYPE_IE) {
|
|
throw new Error("Shouldn't be looking for a single cookie folder unless we're migrating IE");
|
|
}
|
|
|
|
// Cookies are stored in txt files, in a Cookies folder whose path varies
|
|
// across the different OS versions. CookD takes care of most of these
|
|
// cases, though, in Windows Vista/7, UAC makes a difference.
|
|
// If UAC is enabled, the most common destination is CookD/Low. Though,
|
|
// if the user runs the application in administrator mode or disables UAC,
|
|
// cookies are stored in the original CookD destination. Cause running the
|
|
// browser in administrator mode is unsafe and discouraged, we just care
|
|
// about the UAC state.
|
|
if (!this.__cookiesFolder) {
|
|
let cookiesFolder = Services.dirsvc.get("CookD", Ci.nsIFile);
|
|
if (cookiesFolder.exists() && cookiesFolder.isReadable()) {
|
|
// Check if UAC is enabled.
|
|
if (Services.appinfo.QueryInterface(Ci.nsIWinAppHelper).userCanElevate) {
|
|
cookiesFolder.append("Low");
|
|
}
|
|
this.__cookiesFolder = cookiesFolder;
|
|
}
|
|
}
|
|
return this.__cookiesFolder;
|
|
},
|
|
|
|
__cookiesFolders: null,
|
|
get _cookiesFolders() {
|
|
if (this._migrationType != MSMigrationUtils.MIGRATION_TYPE_EDGE) {
|
|
throw new Error("Shouldn't be looking for multiple cookie folders unless we're migrating Edge");
|
|
}
|
|
|
|
let folders = [];
|
|
let edgeDir = getEdgeLocalDataFolder();
|
|
if (edgeDir) {
|
|
edgeDir.append("AC");
|
|
for (let path of EDGE_COOKIE_PATH_OPTIONS) {
|
|
let folder = edgeDir.clone();
|
|
let fullPath = path + EDGE_COOKIES_SUFFIX;
|
|
folder.appendRelativePath(fullPath);
|
|
if (folder.exists() && folder.isReadable() && folder.isDirectory()) {
|
|
folders.push(folder);
|
|
}
|
|
}
|
|
}
|
|
return this.__cookiesFolders = folders.length ? folders : null;
|
|
},
|
|
|
|
migrate(aCallback) {
|
|
this.ctypesHelpers = new CtypesHelpers();
|
|
|
|
let cookiesGenerator = (function genCookie() {
|
|
let success = false;
|
|
let folders = this._migrationType == MSMigrationUtils.MIGRATION_TYPE_EDGE ?
|
|
this.__cookiesFolders : [this.__cookiesFolder];
|
|
for (let folder of folders) {
|
|
let entries = folder.directoryEntries;
|
|
while (entries.hasMoreElements()) {
|
|
let entry = entries.getNext().QueryInterface(Ci.nsIFile);
|
|
// Skip eventual bogus entries.
|
|
if (!entry.isFile() || !/\.txt$/.test(entry.leafName))
|
|
continue;
|
|
|
|
this._readCookieFile(entry, function(aSuccess) {
|
|
// Importing even a single cookie file is considered a success.
|
|
if (aSuccess)
|
|
success = true;
|
|
try {
|
|
cookiesGenerator.next();
|
|
} catch (ex) {}
|
|
});
|
|
|
|
yield undefined;
|
|
}
|
|
}
|
|
|
|
this.ctypesHelpers.finalize();
|
|
|
|
aCallback(success);
|
|
}).apply(this);
|
|
cookiesGenerator.next();
|
|
},
|
|
|
|
_readCookieFile(aFile, aCallback) {
|
|
let fileReader = Cc["@mozilla.org/files/filereader;1"].
|
|
createInstance(Ci.nsIDOMFileReader);
|
|
let onLoadEnd = () => {
|
|
fileReader.removeEventListener("loadend", onLoadEnd, false);
|
|
|
|
if (fileReader.readyState != fileReader.DONE) {
|
|
Cu.reportError("Could not read cookie contents: " + fileReader.error);
|
|
aCallback(false);
|
|
return;
|
|
}
|
|
|
|
let success = true;
|
|
try {
|
|
this._parseCookieBuffer(fileReader.result);
|
|
} catch (ex) {
|
|
Components.utils.reportError("Unable to migrate cookie: " + ex);
|
|
success = false;
|
|
} finally {
|
|
aCallback(success);
|
|
}
|
|
};
|
|
fileReader.addEventListener("loadend", onLoadEnd, false);
|
|
fileReader.readAsText(new File(aFile));
|
|
},
|
|
|
|
/**
|
|
* Parses a cookie file buffer and returns an array of the contained cookies.
|
|
*
|
|
* The cookie file format is a newline-separated-values with a "*" used as
|
|
* delimeter between multiple records.
|
|
* Each cookie has the following fields:
|
|
* - name
|
|
* - value
|
|
* - host/path
|
|
* - flags
|
|
* - Expiration time most significant integer
|
|
* - Expiration time least significant integer
|
|
* - Creation time most significant integer
|
|
* - Creation time least significant integer
|
|
* - Record delimiter "*"
|
|
*
|
|
* @note All the times are in FILETIME format.
|
|
*/
|
|
_parseCookieBuffer(aTextBuffer) {
|
|
// Note the last record is an empty string.
|
|
let records = [r for each (r in aTextBuffer.split("*\n")) if (r)];
|
|
for (let record of records) {
|
|
let [name, value, hostpath, flags,
|
|
expireTimeLo, expireTimeHi] = record.split("\n");
|
|
|
|
// IE stores deleted cookies with a zero-length value, skip them.
|
|
if (value.length == 0)
|
|
continue;
|
|
|
|
let hostLen = hostpath.indexOf("/");
|
|
let host = hostpath.substr(0, hostLen);
|
|
let path = hostpath.substr(hostLen);
|
|
|
|
// For a non-null domain, assume it's what Mozilla considers
|
|
// a domain cookie. See bug 222343.
|
|
if (host.length > 0) {
|
|
// Fist delete any possible extant matching host cookie.
|
|
Services.cookies.remove(host, name, path, false);
|
|
// Now make it a domain cookie.
|
|
if (host[0] != "." && !hostIsIPAddress(host))
|
|
host = "." + host;
|
|
}
|
|
|
|
let expireTime = this.ctypesHelpers.fileTimeToSecondsSinceEpoch(Number(expireTimeHi),
|
|
Number(expireTimeLo));
|
|
Services.cookies.add(host,
|
|
path,
|
|
name,
|
|
value,
|
|
Number(flags) & 0x1, // secure
|
|
false, // httpOnly
|
|
false, // session
|
|
expireTime);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
var MSMigrationUtils = {
|
|
MIGRATION_TYPE_IE: 1,
|
|
MIGRATION_TYPE_EDGE: 2,
|
|
CtypesHelpers: CtypesHelpers,
|
|
getBookmarksMigrator(migrationType = this.MIGRATION_TYPE_IE) {
|
|
return new Bookmarks(migrationType);
|
|
},
|
|
getCookiesMigrator(migrationType = this.MIGRATION_TYPE_IE) {
|
|
return new Cookies(migrationType);
|
|
},
|
|
};
|