From 43a97878ce14b72f0981164f87f2e35e14151312 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 11:22:09 +0200 Subject: Adding upstream version 110.0.1. Signed-off-by: Daniel Baumann --- .../migration/EdgeProfileMigrator.sys.mjs | 547 +++++++++++++++++++++ 1 file changed, 547 insertions(+) create mode 100644 browser/components/migration/EdgeProfileMigrator.sys.mjs (limited to 'browser/components/migration/EdgeProfileMigrator.sys.mjs') diff --git a/browser/components/migration/EdgeProfileMigrator.sys.mjs b/browser/components/migration/EdgeProfileMigrator.sys.mjs new file mode 100644 index 0000000000..a1f10e36a2 --- /dev/null +++ b/browser/components/migration/EdgeProfileMigrator.sys.mjs @@ -0,0 +1,547 @@ +/* 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 { + static get key() { + return "edge"; + } + + 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 : []; + } +} -- cgit v1.2.3