fune/browser/components/migration/EdgeProfileMigrator.sys.mjs
Mike Conley 828431224a Bug 1811935 - Adjust visibility of the resource types available for each migrator / profile pair in the new migration wizard. r=Gijs
This causes the MigrationWizardChild to request the full collection of available
migrators and user profiles, and then based on which resourceTypes those migrators
and user profiles have available, changes the visibility of the checkboxes in the
selection page of the new MigrationWizard component.

Differential Revision: https://phabricator.services.mozilla.com/D167617
2023-01-25 12:53:31 +00:00

551 lines
16 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/. */
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs";
import { MigratorBase } from "resource:///modules/MigratorBase.sys.mjs";
import { MSMigrationUtils } from "resource:///modules/MSMigrationUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ESEDBReader: "resource:///modules/ESEDBReader.sys.mjs",
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
});
const kEdgeRegistryRoot =
"SOFTWARE\\Classes\\Local Settings\\Software\\" +
"Microsoft\\Windows\\CurrentVersion\\AppContainer\\Storage\\" +
"microsoft.microsoftedge_8wekyb3d8bbwe\\MicrosoftEdge";
const kEdgeDatabasePath = "AC\\MicrosoftEdge\\User\\Default\\DataStore\\Data\\";
XPCOMUtils.defineLazyGetter(lazy, "gEdgeDatabase", function() {
let edgeDir = MSMigrationUtils.getEdgeLocalDataFolder();
if (!edgeDir) {
return null;
}
edgeDir.appendRelativePath(kEdgeDatabasePath);
if (!edgeDir.exists() || !edgeDir.isReadable() || !edgeDir.isDirectory()) {
return null;
}
let expectedLocation = edgeDir.clone();
expectedLocation.appendRelativePath(
"nouser1\\120712-0049\\DBStore\\spartan.edb"
);
if (
expectedLocation.exists() &&
expectedLocation.isReadable() &&
expectedLocation.isFile()
) {
expectedLocation.normalize();
return expectedLocation;
}
// We used to recurse into arbitrary subdirectories here, but that code
// went unused, so it likely isn't necessary, even if we don't understand
// where the magic folders above come from, they seem to be the same for
// everyone. Just return null if they're not there:
return null;
});
/**
* Get rows from a table in the Edge DB as an array of JS objects.
*
* @param {string} tableName the name of the table to read.
* @param {string[]|Function} columns a list of column specifiers
* (see ESEDBReader.jsm) or a function that
* generates them based on the database
* reference once opened.
* @param {nsIFile} dbFile the database file to use. Defaults to
* the main Edge database.
* @param {Function} filterFn Optional. A function that is called for each row.
* Only rows for which it returns a truthy
* value are included in the result.
* @returns {Array} An array of row objects.
*/
function readTableFromEdgeDB(
tableName,
columns,
dbFile = lazy.gEdgeDatabase,
filterFn = null
) {
let database;
let rows = [];
try {
let logFile = dbFile.parent;
logFile.append("LogFiles");
database = lazy.ESEDBReader.openDB(dbFile.parent, dbFile, logFile);
if (typeof columns == "function") {
columns = columns(database);
}
let tableReader = database.tableItems(tableName, columns);
for (let row of tableReader) {
if (!filterFn || filterFn(row)) {
rows.push(row);
}
}
} catch (ex) {
console.error(
"Failed to extract items from table ",
tableName,
" in Edge database at ",
dbFile.path,
" due to the following error: ",
ex
);
// Deliberately make this fail so we expose failure in the UI:
throw ex;
} finally {
if (database) {
lazy.ESEDBReader.closeDB(database);
}
}
return rows;
}
function EdgeTypedURLMigrator() {}
EdgeTypedURLMigrator.prototype = {
type: MigrationUtils.resourceTypes.HISTORY,
get _typedURLs() {
if (!this.__typedURLs) {
this.__typedURLs = MSMigrationUtils.getTypedURLs(kEdgeRegistryRoot);
}
return this.__typedURLs;
},
get exists() {
return this._typedURLs.size > 0;
},
migrate(aCallback) {
let typedURLs = this._typedURLs;
let pageInfos = [];
for (let [urlString, time] of typedURLs) {
let url;
try {
url = new URL(urlString);
if (!["http:", "https:", "ftp:"].includes(url.protocol)) {
continue;
}
} catch (ex) {
console.error(ex);
continue;
}
pageInfos.push({
url,
visits: [
{
transition: lazy.PlacesUtils.history.TRANSITIONS.TYPED,
date: time ? lazy.PlacesUtils.toDate(time) : new Date(),
},
],
});
}
if (!pageInfos.length) {
aCallback(typedURLs.size == 0);
return;
}
MigrationUtils.insertVisitsWrapper(pageInfos).then(
() => aCallback(true),
() => aCallback(false)
);
},
};
function EdgeTypedURLDBMigrator() {}
EdgeTypedURLDBMigrator.prototype = {
type: MigrationUtils.resourceTypes.HISTORY,
get db() {
return lazy.gEdgeDatabase;
},
get exists() {
return !!this.db;
},
migrate(callback) {
this._migrateTypedURLsFromDB().then(
() => callback(true),
ex => {
console.error(ex);
callback(false);
}
);
},
async _migrateTypedURLsFromDB() {
if (await lazy.ESEDBReader.dbLocked(this.db)) {
throw new Error("Edge seems to be running - its database is locked.");
}
let columns = [
{ name: "URL", type: "string" },
{ name: "AccessDateTimeUTC", type: "date" },
];
let typedUrls = [];
try {
typedUrls = readTableFromEdgeDB("TypedUrls", columns, this.db);
} catch (ex) {
// Maybe the table doesn't exist (older versions of Win10).
// Just fall through and we'll return because there's no data.
// The `readTableFromEdgeDB` helper will report errors to the
// console anyway.
}
if (!typedUrls.length) {
return;
}
let pageInfos = [];
// Sometimes the values are bogus (e.g. 0 becomes some date in 1600),
// and places will throw *everything* away, not just the bogus ones,
// so deal with that by having a cutoff date. Also, there's not much
// point importing really old entries. The cut-off date is related to
// Edge's launch date.
const kDateCutOff = new Date("2016", 0, 1);
for (let typedUrlInfo of typedUrls) {
try {
let url = new URL(typedUrlInfo.URL);
if (!["http:", "https:", "ftp:"].includes(url.protocol)) {
continue;
}
let date = typedUrlInfo.AccessDateTimeUTC;
if (!date) {
date = kDateCutOff;
} else if (date < kDateCutOff) {
continue;
}
pageInfos.push({
url,
visits: [
{
transition: lazy.PlacesUtils.history.TRANSITIONS.TYPED,
date,
},
],
});
} catch (ex) {
console.error(ex);
}
}
await MigrationUtils.insertVisitsWrapper(pageInfos);
},
};
function EdgeReadingListMigrator(dbOverride) {
this.dbOverride = dbOverride;
}
EdgeReadingListMigrator.prototype = {
type: MigrationUtils.resourceTypes.BOOKMARKS,
get db() {
return this.dbOverride || lazy.gEdgeDatabase;
},
get exists() {
return !!this.db;
},
migrate(callback) {
this._migrateReadingList(lazy.PlacesUtils.bookmarks.menuGuid).then(
() => callback(true),
ex => {
console.error(ex);
callback(false);
}
);
},
async _migrateReadingList(parentGuid) {
if (await lazy.ESEDBReader.dbLocked(this.db)) {
throw new Error("Edge seems to be running - its database is locked.");
}
let columnFn = db => {
let columns = [
{ name: "URL", type: "string" },
{ name: "Title", type: "string" },
{ name: "AddedDate", type: "date" },
];
// Later versions have an IsDeleted column:
let isDeletedColumn = db.checkForColumn("ReadingList", "IsDeleted");
if (
isDeletedColumn &&
isDeletedColumn.dbType == lazy.ESEDBReader.COLUMN_TYPES.JET_coltypBit
) {
columns.push({ name: "IsDeleted", type: "boolean" });
}
return columns;
};
let filterFn = row => {
return !row.IsDeleted;
};
let readingListItems = readTableFromEdgeDB(
"ReadingList",
columnFn,
this.db,
filterFn
);
if (!readingListItems.length) {
return;
}
let destFolderGuid = await this._ensureReadingListFolder(parentGuid);
let bookmarks = [];
for (let item of readingListItems) {
let dateAdded = item.AddedDate || new Date();
// Avoid including broken URLs:
try {
new URL(item.URL);
} catch (ex) {
continue;
}
bookmarks.push({ url: item.URL, title: item.Title, dateAdded });
}
await MigrationUtils.insertManyBookmarksWrapper(bookmarks, destFolderGuid);
},
async _ensureReadingListFolder(parentGuid) {
if (!this.__readingListFolderGuid) {
let folderTitle = await MigrationUtils.getLocalizedString(
"imported-edge-reading-list"
);
let folderSpec = {
type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER,
parentGuid,
title: folderTitle,
};
this.__readingListFolderGuid = (
await MigrationUtils.insertBookmarkWrapper(folderSpec)
).guid;
}
return this.__readingListFolderGuid;
},
};
function EdgeBookmarksMigrator(dbOverride) {
this.dbOverride = dbOverride;
}
EdgeBookmarksMigrator.prototype = {
type: MigrationUtils.resourceTypes.BOOKMARKS,
get db() {
return this.dbOverride || lazy.gEdgeDatabase;
},
get TABLE_NAME() {
return "Favorites";
},
get exists() {
if (!("_exists" in this)) {
this._exists = !!this.db;
}
return this._exists;
},
migrate(callback) {
this._migrateBookmarks().then(
() => callback(true),
ex => {
console.error(ex);
callback(false);
}
);
},
async _migrateBookmarks() {
if (await lazy.ESEDBReader.dbLocked(this.db)) {
throw new Error("Edge seems to be running - its database is locked.");
}
let { toplevelBMs, toolbarBMs } = this._fetchBookmarksFromDB();
if (toplevelBMs.length) {
let parentGuid = lazy.PlacesUtils.bookmarks.menuGuid;
await MigrationUtils.insertManyBookmarksWrapper(toplevelBMs, parentGuid);
}
if (toolbarBMs.length) {
let parentGuid = lazy.PlacesUtils.bookmarks.toolbarGuid;
await MigrationUtils.insertManyBookmarksWrapper(toolbarBMs, parentGuid);
}
},
_fetchBookmarksFromDB() {
let folderMap = new Map();
let columns = [
{ name: "URL", type: "string" },
{ name: "Title", type: "string" },
{ name: "DateUpdated", type: "date" },
{ name: "IsFolder", type: "boolean" },
{ name: "IsDeleted", type: "boolean" },
{ name: "ParentId", type: "guid" },
{ name: "ItemId", type: "guid" },
];
let filterFn = row => {
if (row.IsDeleted) {
return false;
}
if (row.IsFolder) {
folderMap.set(row.ItemId, row);
}
return true;
};
let bookmarks = readTableFromEdgeDB(
this.TABLE_NAME,
columns,
this.db,
filterFn
);
let toplevelBMs = [],
toolbarBMs = [];
for (let bookmark of bookmarks) {
let bmToInsert;
// Ignore invalid URLs:
if (!bookmark.IsFolder) {
try {
new URL(bookmark.URL);
} catch (ex) {
console.error(
`Ignoring ${bookmark.URL} when importing from Edge because of exception: ${ex}`
);
continue;
}
bmToInsert = {
dateAdded: bookmark.DateUpdated || new Date(),
title: bookmark.Title,
url: bookmark.URL,
};
} /* bookmark.IsFolder */ else {
// Ignore the favorites bar bookmark itself.
if (bookmark.Title == "_Favorites_Bar_") {
continue;
}
if (!bookmark._childrenRef) {
bookmark._childrenRef = [];
}
bmToInsert = {
title: bookmark.Title,
type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER,
dateAdded: bookmark.DateUpdated || new Date(),
children: bookmark._childrenRef,
};
}
if (!folderMap.has(bookmark.ParentId)) {
toplevelBMs.push(bmToInsert);
} else {
let parent = folderMap.get(bookmark.ParentId);
if (parent.Title == "_Favorites_Bar_") {
toolbarBMs.push(bmToInsert);
continue;
}
if (!parent._childrenRef) {
parent._childrenRef = [];
}
parent._childrenRef.push(bmToInsert);
}
}
return { toplevelBMs, toolbarBMs };
},
};
/**
* Edge (EdgeHTML) profile migrator
*/
export class EdgeProfileMigrator extends MigratorBase {
static get key() {
return "edge";
}
static get displayNameL10nID() {
return "migration-wizard-migrator-display-name-edge-legacy";
}
getBookmarksMigratorForTesting(dbOverride) {
return new EdgeBookmarksMigrator(dbOverride);
}
getReadingListMigratorForTesting(dbOverride) {
return new EdgeReadingListMigrator(dbOverride);
}
getResources() {
let resources = [
new EdgeBookmarksMigrator(),
MSMigrationUtils.getCookiesMigrator(MSMigrationUtils.MIGRATION_TYPE_EDGE),
new EdgeTypedURLMigrator(),
new EdgeTypedURLDBMigrator(),
new EdgeReadingListMigrator(),
];
let windowsVaultFormPasswordsMigrator = MSMigrationUtils.getWindowsVaultFormPasswordsMigrator();
windowsVaultFormPasswordsMigrator.name = "EdgeVaultFormPasswords";
resources.push(windowsVaultFormPasswordsMigrator);
return resources.filter(r => r.exists);
}
async getLastUsedDate() {
// Don't do this if we don't have a single profile (see the comment for
// sourceProfiles) or if we can't find the database file:
let sourceProfiles = await this.getSourceProfiles();
if (sourceProfiles !== null || !lazy.gEdgeDatabase) {
return Promise.resolve(new Date(0));
}
let logFilePath = PathUtils.join(
lazy.gEdgeDatabase.parent.path,
"LogFiles",
"edb.log"
);
let dbPath = lazy.gEdgeDatabase.path;
let cookieMigrator = MSMigrationUtils.getCookiesMigrator(
MSMigrationUtils.MIGRATION_TYPE_EDGE
);
let cookiePaths = cookieMigrator._cookiesFolders.map(f => f.path);
let datePromises = [logFilePath, dbPath, ...cookiePaths].map(path => {
return IOUtils.stat(path)
.then(info => info.lastModified)
.catch(() => 0);
});
datePromises.push(
new Promise(resolve => {
let typedURLs = new Map();
try {
typedURLs = MSMigrationUtils.getTypedURLs(kEdgeRegistryRoot);
} catch (ex) {}
let times = [0, ...typedURLs.values()];
// dates is an array of PRTimes, which are in microseconds - convert to milliseconds
resolve(Math.max.apply(Math, times) / 1000);
})
);
return Promise.all(datePromises).then(dates => {
return new Date(Math.max.apply(Math, dates));
});
}
/**
* @returns {Array|null}
* Somewhat counterintuitively, this returns:
* - |null| to indicate "There is only 1 (default) profile" (on win10+)
* - |[]| to indicate "There are no profiles" (on <=win8.1) which will avoid
* using this migrator.
* See MigrationUtils.sys.mjs for slightly more info on how sourceProfiles is used.
*/
getSourceProfiles() {
let isWin10OrHigher = AppConstants.isPlatformAndVersionAtLeast("win", "10");
return isWin10OrHigher ? null : [];
}
}