Files
tubestation/browser/components/migration/EdgeProfileMigrator.sys.mjs
Sandor Molnar c46a05c875 Backed out 9 changesets (bug 1800917, bug 718280) for causing xpc failures in browser/components/migration/tests/unit/test_Edge_db_migration.js CLOSED TREE
Backed out changeset d55b95d9d14f (bug 1800917)
Backed out changeset 8427f1e65df4 (bug 1800917)
Backed out changeset 2d0f77d324f2 (bug 718280)
Backed out changeset eba58fd1ed1c (bug 718280)
Backed out changeset 33027a0f9ce9 (bug 718280)
Backed out changeset 237be1f62d01 (bug 718280)
Backed out changeset 007afe158ae5 (bug 718280)
Backed out changeset f1dea7abdfb7 (bug 718280)
Backed out changeset 47e2cccd33c7 (bug 718280)
2022-12-13 00:50:16 +02:00

561 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) {
Cu.reportError(
"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) {
Cu.reportError(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 => {
Cu.reportError(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) {
Cu.reportError(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 => {
Cu.reportError(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 => {
Cu.reportError(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) {
Cu.reportError(
`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 {
constructor() {
super();
this.wrappedJSObject = this;
}
get classDescription() {
return "Edge Profile Migrator";
}
get contractID() {
return "@mozilla.org/profile/migrator;1?app=browser&type=edge";
}
get classID() {
return Components.ID("{62e8834b-2d17-49f5-96ff-56344903a2ae}");
}
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 : [];
}
}