EdgeProfileMigrator.sys.mjs (15961B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs"; 6 import { MigratorBase } from "resource:///modules/MigratorBase.sys.mjs"; 7 import { MSMigrationUtils } from "resource:///modules/MSMigrationUtils.sys.mjs"; 8 9 const EDGE_COOKIE_PATH_OPTIONS = ["", "#!001\\", "#!002\\"]; 10 const EDGE_COOKIES_SUFFIX = "MicrosoftEdge\\Cookies"; 11 12 const ALLOWED_PROTOCOLS = new Set(["http:", "https:", "ftp:"]); 13 14 const lazy = {}; 15 ChromeUtils.defineESModuleGetters(lazy, { 16 ESEDBReader: "resource:///modules/ESEDBReader.sys.mjs", 17 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 18 }); 19 20 const kEdgeRegistryRoot = 21 "SOFTWARE\\Classes\\Local Settings\\Software\\" + 22 "Microsoft\\Windows\\CurrentVersion\\AppContainer\\Storage\\" + 23 "microsoft.microsoftedge_8wekyb3d8bbwe\\MicrosoftEdge"; 24 const kEdgeDatabasePath = "AC\\MicrosoftEdge\\User\\Default\\DataStore\\Data\\"; 25 26 ChromeUtils.defineLazyGetter(lazy, "gEdgeDatabase", function () { 27 let edgeDir = MSMigrationUtils.getEdgeLocalDataFolder(); 28 if (!edgeDir) { 29 return null; 30 } 31 edgeDir.appendRelativePath(kEdgeDatabasePath); 32 if (!edgeDir.exists() || !edgeDir.isReadable() || !edgeDir.isDirectory()) { 33 return null; 34 } 35 let expectedLocation = edgeDir.clone(); 36 expectedLocation.appendRelativePath( 37 "nouser1\\120712-0049\\DBStore\\spartan.edb" 38 ); 39 if ( 40 expectedLocation.exists() && 41 expectedLocation.isReadable() && 42 expectedLocation.isFile() 43 ) { 44 expectedLocation.normalize(); 45 return expectedLocation; 46 } 47 // We used to recurse into arbitrary subdirectories here, but that code 48 // went unused, so it likely isn't necessary, even if we don't understand 49 // where the magic folders above come from, they seem to be the same for 50 // everyone. Just return null if they're not there: 51 return null; 52 }); 53 54 /** 55 * Get rows from a table in the Edge DB as an array of JS objects. 56 * 57 * @param {string} tableName the name of the table to read. 58 * @param {string[]|Function} columns a list of column specifiers 59 * (see ESEDBReader.sys.mjs) or a function 60 * that generates them based on the database 61 * reference once opened. 62 * @param {nsIFile} dbFile the database file to use. Defaults to 63 * the main Edge database. 64 * @param {Function} filterFn Optional. A function that is called for each row. 65 * Only rows for which it returns a truthy 66 * value are included in the result. 67 * @returns {Array} An array of row objects. 68 */ 69 function readTableFromEdgeDB( 70 tableName, 71 columns, 72 dbFile = lazy.gEdgeDatabase, 73 filterFn = null 74 ) { 75 let database; 76 let rows = []; 77 try { 78 let logFile = dbFile.parent; 79 logFile.append("LogFiles"); 80 database = lazy.ESEDBReader.openDB(dbFile.parent, dbFile, logFile); 81 82 if (typeof columns == "function") { 83 columns = columns(database); 84 } 85 86 let tableReader = database.tableItems(tableName, columns); 87 for (let row of tableReader) { 88 if (!filterFn || filterFn(row)) { 89 rows.push(row); 90 } 91 } 92 } catch (ex) { 93 console.error( 94 "Failed to extract items from table ", 95 tableName, 96 " in Edge database at ", 97 dbFile.path, 98 " due to the following error: ", 99 ex 100 ); 101 // Deliberately make this fail so we expose failure in the UI: 102 throw ex; 103 } finally { 104 if (database) { 105 lazy.ESEDBReader.closeDB(database); 106 } 107 } 108 return rows; 109 } 110 111 function EdgeTypedURLMigrator() {} 112 113 EdgeTypedURLMigrator.prototype = { 114 type: MigrationUtils.resourceTypes.HISTORY, 115 116 get _typedURLs() { 117 if (!this.__typedURLs) { 118 this.__typedURLs = MSMigrationUtils.getTypedURLs(kEdgeRegistryRoot); 119 } 120 return this.__typedURLs; 121 }, 122 123 get exists() { 124 return this._typedURLs.size > 0; 125 }, 126 127 migrate(aCallback) { 128 let typedURLs = this._typedURLs; 129 let pageInfos = []; 130 let now = new Date(); 131 let maxDate = new Date( 132 Date.now() - MigrationUtils.HISTORY_MAX_AGE_IN_MILLISECONDS 133 ); 134 135 for (let [urlString, time] of typedURLs) { 136 let visitDate = time ? lazy.PlacesUtils.toDate(time) : now; 137 if (time && visitDate < maxDate) { 138 continue; 139 } 140 141 let url = URL.parse(urlString); 142 if (!url || !ALLOWED_PROTOCOLS.has(url.protocol)) { 143 continue; 144 } 145 146 pageInfos.push({ 147 url, 148 visits: [ 149 { 150 transition: lazy.PlacesUtils.history.TRANSITIONS.TYPED, 151 date: time ? lazy.PlacesUtils.toDate(time) : new Date(), 152 }, 153 ], 154 }); 155 } 156 157 if (!pageInfos.length) { 158 aCallback(typedURLs.size == 0); 159 return; 160 } 161 162 MigrationUtils.insertVisitsWrapper(pageInfos).then( 163 () => aCallback(true), 164 () => aCallback(false) 165 ); 166 }, 167 }; 168 169 function EdgeTypedURLDBMigrator(dbOverride) { 170 this.dbOverride = dbOverride; 171 } 172 173 EdgeTypedURLDBMigrator.prototype = { 174 type: MigrationUtils.resourceTypes.HISTORY, 175 176 get db() { 177 return this.dbOverride || lazy.gEdgeDatabase; 178 }, 179 180 get exists() { 181 return !!this.db; 182 }, 183 184 migrate(callback) { 185 this._migrateTypedURLsFromDB().then( 186 () => callback(true), 187 ex => { 188 console.error(ex); 189 callback(false); 190 } 191 ); 192 }, 193 194 async _migrateTypedURLsFromDB() { 195 if (await lazy.ESEDBReader.dbLocked(this.db)) { 196 throw new Error("Edge seems to be running - its database is locked."); 197 } 198 let columns = [ 199 { name: "URL", type: "string" }, 200 { name: "AccessDateTimeUTC", type: "date" }, 201 ]; 202 203 let typedUrls = []; 204 try { 205 typedUrls = readTableFromEdgeDB("TypedUrls", columns, this.db); 206 } catch (ex) { 207 // Maybe the table doesn't exist (older versions of Win10). 208 // Just fall through and we'll return because there's no data. 209 // The `readTableFromEdgeDB` helper will report errors to the 210 // console anyway. 211 } 212 if (!typedUrls.length) { 213 return; 214 } 215 216 let pageInfos = []; 217 218 const kDateCutOff = new Date( 219 Date.now() - MigrationUtils.HISTORY_MAX_AGE_IN_MILLISECONDS 220 ); 221 for (let typedUrlInfo of typedUrls) { 222 let date = typedUrlInfo.AccessDateTimeUTC; 223 if (!date) { 224 date = kDateCutOff; 225 } else if (date < kDateCutOff) { 226 continue; 227 } 228 229 let url = URL.parse(typedUrlInfo.URL); 230 if (!url || !ALLOWED_PROTOCOLS.has(url.protocol)) { 231 continue; 232 } 233 234 pageInfos.push({ 235 url, 236 visits: [ 237 { 238 transition: lazy.PlacesUtils.history.TRANSITIONS.TYPED, 239 date, 240 }, 241 ], 242 }); 243 } 244 await MigrationUtils.insertVisitsWrapper(pageInfos); 245 }, 246 }; 247 248 function EdgeReadingListMigrator(dbOverride) { 249 this.dbOverride = dbOverride; 250 } 251 252 EdgeReadingListMigrator.prototype = { 253 type: MigrationUtils.resourceTypes.BOOKMARKS, 254 255 get db() { 256 return this.dbOverride || lazy.gEdgeDatabase; 257 }, 258 259 get exists() { 260 return !!this.db; 261 }, 262 263 migrate(callback) { 264 this._migrateReadingList(lazy.PlacesUtils.bookmarks.menuGuid).then( 265 () => callback(true), 266 ex => { 267 console.error(ex); 268 callback(false); 269 } 270 ); 271 }, 272 273 async _migrateReadingList(parentGuid) { 274 if (await lazy.ESEDBReader.dbLocked(this.db)) { 275 throw new Error("Edge seems to be running - its database is locked."); 276 } 277 let columnFn = db => { 278 let columns = [ 279 { name: "URL", type: "string" }, 280 { name: "Title", type: "string" }, 281 { name: "AddedDate", type: "date" }, 282 ]; 283 284 // Later versions have an IsDeleted column: 285 let isDeletedColumn = db.checkForColumn("ReadingList", "IsDeleted"); 286 if ( 287 isDeletedColumn && 288 isDeletedColumn.dbType == lazy.ESEDBReader.COLUMN_TYPES.JET_coltypBit 289 ) { 290 columns.push({ name: "IsDeleted", type: "boolean" }); 291 } 292 return columns; 293 }; 294 295 let filterFn = row => { 296 return !row.IsDeleted; 297 }; 298 299 let readingListItems = readTableFromEdgeDB( 300 "ReadingList", 301 columnFn, 302 this.db, 303 filterFn 304 ); 305 if (!readingListItems.length) { 306 return; 307 } 308 309 let destFolderGuid = await this._ensureReadingListFolder(parentGuid); 310 let bookmarks = []; 311 for (let item of readingListItems) { 312 let dateAdded = item.AddedDate || new Date(); 313 // Avoid including broken URLs: 314 if (!URL.canParse(item.URL)) { 315 continue; 316 } 317 bookmarks.push({ url: item.URL, title: item.Title, dateAdded }); 318 } 319 await MigrationUtils.insertManyBookmarksWrapper(bookmarks, destFolderGuid); 320 }, 321 322 async _ensureReadingListFolder(parentGuid) { 323 if (!this.__readingListFolderGuid) { 324 let folderTitle = await MigrationUtils.getLocalizedString( 325 "migration-imported-edge-reading-list" 326 ); 327 let folderSpec = { 328 type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER, 329 parentGuid, 330 title: folderTitle, 331 }; 332 this.__readingListFolderGuid = ( 333 await MigrationUtils.insertBookmarkWrapper(folderSpec) 334 ).guid; 335 } 336 return this.__readingListFolderGuid; 337 }, 338 }; 339 340 function EdgeBookmarksMigrator(dbOverride) { 341 this.dbOverride = dbOverride; 342 } 343 344 EdgeBookmarksMigrator.prototype = { 345 type: MigrationUtils.resourceTypes.BOOKMARKS, 346 347 get db() { 348 return this.dbOverride || lazy.gEdgeDatabase; 349 }, 350 351 get TABLE_NAME() { 352 return "Favorites"; 353 }, 354 355 get exists() { 356 if (!("_exists" in this)) { 357 this._exists = !!this.db; 358 } 359 return this._exists; 360 }, 361 362 migrate(callback) { 363 this._migrateBookmarks().then( 364 () => callback(true), 365 ex => { 366 console.error(ex); 367 callback(false); 368 } 369 ); 370 }, 371 372 async _migrateBookmarks() { 373 if (await lazy.ESEDBReader.dbLocked(this.db)) { 374 throw new Error("Edge seems to be running - its database is locked."); 375 } 376 let { toplevelBMs, toolbarBMs } = this._fetchBookmarksFromDB(); 377 if (toplevelBMs.length) { 378 let parentGuid = lazy.PlacesUtils.bookmarks.menuGuid; 379 await MigrationUtils.insertManyBookmarksWrapper(toplevelBMs, parentGuid); 380 } 381 if (toolbarBMs.length) { 382 let parentGuid = lazy.PlacesUtils.bookmarks.toolbarGuid; 383 await MigrationUtils.insertManyBookmarksWrapper(toolbarBMs, parentGuid); 384 } 385 }, 386 387 _fetchBookmarksFromDB() { 388 let folderMap = new Map(); 389 let columns = [ 390 { name: "URL", type: "string" }, 391 { name: "Title", type: "string" }, 392 { name: "DateUpdated", type: "date" }, 393 { name: "IsFolder", type: "boolean" }, 394 { name: "IsDeleted", type: "boolean" }, 395 { name: "ParentId", type: "guid" }, 396 { name: "ItemId", type: "guid" }, 397 ]; 398 let filterFn = row => { 399 if (row.IsDeleted) { 400 return false; 401 } 402 if (row.IsFolder) { 403 folderMap.set(row.ItemId, row); 404 } 405 return true; 406 }; 407 let bookmarks = readTableFromEdgeDB( 408 this.TABLE_NAME, 409 columns, 410 this.db, 411 filterFn 412 ); 413 let toplevelBMs = [], 414 toolbarBMs = []; 415 for (let bookmark of bookmarks) { 416 let bmToInsert; 417 // Ignore invalid URLs: 418 if (!bookmark.IsFolder) { 419 if (!URL.canParse(bookmark.URL)) { 420 console.error( 421 `Ignoring ${bookmark.URL} when importing from Edge because it is not a valid URL.` 422 ); 423 continue; 424 } 425 bmToInsert = { 426 dateAdded: bookmark.DateUpdated || new Date(), 427 title: bookmark.Title, 428 url: bookmark.URL, 429 }; 430 } /* bookmark.IsFolder */ else { 431 // Ignore the favorites bar bookmark itself. 432 if (bookmark.Title == "_Favorites_Bar_") { 433 continue; 434 } 435 if (!bookmark._childrenRef) { 436 bookmark._childrenRef = []; 437 } 438 bmToInsert = { 439 title: bookmark.Title, 440 type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER, 441 dateAdded: bookmark.DateUpdated || new Date(), 442 children: bookmark._childrenRef, 443 }; 444 } 445 446 if (!folderMap.has(bookmark.ParentId)) { 447 toplevelBMs.push(bmToInsert); 448 } else { 449 let parent = folderMap.get(bookmark.ParentId); 450 if (parent.Title == "_Favorites_Bar_") { 451 toolbarBMs.push(bmToInsert); 452 continue; 453 } 454 if (!parent._childrenRef) { 455 parent._childrenRef = []; 456 } 457 parent._childrenRef.push(bmToInsert); 458 } 459 } 460 return { toplevelBMs, toolbarBMs }; 461 }, 462 }; 463 464 function getCookiesPaths() { 465 let folders = []; 466 let edgeDir = MSMigrationUtils.getEdgeLocalDataFolder(); 467 if (edgeDir) { 468 edgeDir.append("AC"); 469 for (let path of EDGE_COOKIE_PATH_OPTIONS) { 470 let folder = edgeDir.clone(); 471 let fullPath = path + EDGE_COOKIES_SUFFIX; 472 folder.appendRelativePath(fullPath); 473 if (folder.exists() && folder.isReadable() && folder.isDirectory()) { 474 folders.push(fullPath); 475 } 476 } 477 } 478 return folders; 479 } 480 481 /** 482 * Edge (EdgeHTML) profile migrator 483 */ 484 export class EdgeProfileMigrator extends MigratorBase { 485 static get key() { 486 return "edge"; 487 } 488 489 static get displayNameL10nID() { 490 return "migration-wizard-migrator-display-name-edge-legacy"; 491 } 492 493 static get brandImage() { 494 return "chrome://browser/content/migration/brands/edge.png"; 495 } 496 497 getBookmarksMigratorForTesting(dbOverride) { 498 return new EdgeBookmarksMigrator(dbOverride); 499 } 500 501 getReadingListMigratorForTesting(dbOverride) { 502 return new EdgeReadingListMigrator(dbOverride); 503 } 504 505 getHistoryDBMigratorForTesting(dbOverride) { 506 return new EdgeTypedURLDBMigrator(dbOverride); 507 } 508 509 getHistoryRegistryMigratorForTesting() { 510 return new EdgeTypedURLMigrator(); 511 } 512 513 getResources() { 514 let resources = [ 515 new EdgeBookmarksMigrator(), 516 new EdgeTypedURLMigrator(), 517 new EdgeTypedURLDBMigrator(), 518 new EdgeReadingListMigrator(), 519 ]; 520 let windowsVaultFormPasswordsMigrator = 521 MSMigrationUtils.getWindowsVaultFormPasswordsMigrator(); 522 windowsVaultFormPasswordsMigrator.name = "EdgeVaultFormPasswords"; 523 resources.push(windowsVaultFormPasswordsMigrator); 524 return resources.filter(r => r.exists); 525 } 526 527 async getLastUsedDate() { 528 // Don't do this if we don't have a single profile (see the comment for 529 // sourceProfiles) or if we can't find the database file: 530 let sourceProfiles = await this.getSourceProfiles(); 531 if (sourceProfiles !== null || !lazy.gEdgeDatabase) { 532 return Promise.resolve(new Date(0)); 533 } 534 let logFilePath = PathUtils.join( 535 lazy.gEdgeDatabase.parent.path, 536 "LogFiles", 537 "edb.log" 538 ); 539 let dbPath = lazy.gEdgeDatabase.path; 540 let datePromises = [logFilePath, dbPath, ...getCookiesPaths()].map(path => { 541 return IOUtils.stat(path) 542 .then(info => info.lastModified) 543 .catch(() => 0); 544 }); 545 datePromises.push( 546 new Promise(resolve => { 547 let typedURLs = new Map(); 548 try { 549 typedURLs = MSMigrationUtils.getTypedURLs(kEdgeRegistryRoot); 550 } catch (ex) {} 551 let times = [0, ...typedURLs.values()]; 552 // dates is an array of PRTimes, which are in microseconds - convert to milliseconds 553 resolve(Math.max.apply(Math, times) / 1000); 554 }) 555 ); 556 return Promise.all(datePromises).then(dates => { 557 return new Date(Math.max.apply(Math, dates)); 558 }); 559 } 560 561 /** 562 * @returns {Array|null} 563 * Somewhat counterintuitively, this returns 564 * ``null`` to indicate "There is only 1 (default) profile". 565 * See MigrationUtils.sys.mjs for slightly more info on how sourceProfiles is used. 566 */ 567 getSourceProfiles() { 568 return null; 569 } 570 }