Bug 1226556 - part 2: use ESE database to import Edge bookmarks, r=MattN draft
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Sat, 30 Jan 2016 11:22:19 +0000
changeset 328539 bcc6624f825d2ac952c925f3a7ccfb78f80ff97d
parent 328538 0d78a5256fb4432b256a5f94460db7f144db8a25
child 328540 b416f9298c5b98538418918ee7263ca2e5c0e95f
push id10368
push usergijskruitbosch@gmail.com
push dateWed, 03 Feb 2016 14:49:00 +0000
reviewersMattN
bugs1226556
milestone47.0a1
Bug 1226556 - part 2: use ESE database to import Edge bookmarks, r=MattN
browser/components/migration/ESEDBReader.jsm
browser/components/migration/EdgeProfileMigrator.js
--- a/browser/components/migration/ESEDBReader.jsm
+++ b/browser/components/migration/ESEDBReader.jsm
@@ -230,17 +230,17 @@ function loadLibraries() {
   gLibs.kernel = ctypes.open("kernel32.dll");
   KERNEL.FileTimeToSystemTime = gLibs.kernel.declare("FileTimeToSystemTime",
     ctypes.default_abi, ctypes.int, KERNEL.FILETIME.ptr, KERNEL.SYSTEMTIME.ptr);
 
   declareESEFunctions();
 }
 
 function ESEDB(rootPath, dbPath, logPath) {
-  log.error("Created db");
+  log.info("Created db");
   this.rootPath = rootPath;
   this.dbPath = dbPath;
   this.logPath = logPath;
   this._references = 0;
   this._init();
 }
 
 ESEDB.prototype = {
--- a/browser/components/migration/EdgeProfileMigrator.js
+++ b/browser/components/migration/EdgeProfileMigrator.js
@@ -13,17 +13,82 @@ Cu.import("resource:///modules/MSMigrati
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ESEDBReader",
                                   "resource:///modules/ESEDBReader.jsm");
 
 const kEdgeRegistryRoot = "SOFTWARE\\Classes\\Local Settings\\Software\\" +
   "Microsoft\\Windows\\CurrentVersion\\AppContainer\\Storage\\" +
   "microsoft.microsoftedge_8wekyb3d8bbwe\\MicrosoftEdge";
-const kEdgeReadingListPath = "AC\\MicrosoftEdge\\User\\Default\\DataStore\\Data\\";
+const kEdgeDatabasePath = "AC\\MicrosoftEdge\\User\\Default\\DataStore\\Data\\";
+
+XPCOMUtils.defineLazyGetter(this, "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()) {
+    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 {function}          filterFn  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, filterFn) {
+  let database;
+  let rows = [];
+  try {
+    let logFile = gEdgeDatabase.parent;
+    logFile.append("LogFiles");
+    database = ESEDBReader.openDB(gEdgeDatabase.parent, gEdgeDatabase, logFile);
+
+    if (typeof columns == "function") {
+      columns = columns(database);
+    }
+
+    let tableReader = database.tableItems(tableName, columns);
+    for (let row of tableReader) {
+      if (filterFn(row)) {
+        rows.push(row);
+      }
+    }
+  } catch (ex) {
+    Cu.reportError("Failed to extract items from table " + tableName + " in Edge database at " +
+                   gEdgeDatabase.path + " due to the following error: " + ex);
+    // Deliberately make this fail so we expose failure in the UI:
+    throw ex;
+  } finally {
+    if (database) {
+      ESEDBReader.closeDB(database);
+    }
+  }
+  return rows;
+}
 
 function EdgeTypedURLMigrator() {
 }
 
 EdgeTypedURLMigrator.prototype = {
   type: MigrationUtils.resourceTypes.HISTORY,
 
   get _typedURLs() {
@@ -83,92 +148,56 @@ EdgeTypedURLMigrator.prototype = {
   },
 }
 
 function EdgeReadingListMigrator() {
 }
 
 EdgeReadingListMigrator.prototype = {
   type: MigrationUtils.resourceTypes.BOOKMARKS,
-  _dbFile: null,
 
   get exists() {
-    this._dbFile = this._getDBFile();
-    return !!this._dbFile;
-  },
-
-  _getDBFile() {
-    let edgeDir = MSMigrationUtils.getEdgeLocalDataFolder();
-    if (!edgeDir) {
-      return null;
-    }
-    edgeDir.appendRelativePath(kEdgeReadingListPath);
-    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()) {
-      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;
+    return !!gEdgeDatabase;
   },
 
   migrate(callback) {
     this._migrateReadingList(PlacesUtils.bookmarks.menuGuid).then(
       () => callback(true),
       ex => {
         Cu.reportError(ex);
         callback(false);
       }
     );
   },
 
   _migrateReadingList: Task.async(function*(parentGuid) {
-    let readingListItems = [];
-    let database;
-    try {
-      let logFile = this._dbFile.parent;
-      logFile.append("LogFiles");
-      database = ESEDBReader.openDB(this._dbFile.parent, this._dbFile, logFile);
+    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 = database.checkForColumn("ReadingList", "isDeleted");
+      // Later versions have an IsDeleted column:
+      let isDeletedColumn = db.checkForColumn("ReadingList", "IsDeleted");
       if (isDeletedColumn && isDeletedColumn.dbType == ESEDBReader.COLUMN_TYPES.JET_coltypBit) {
-        columns.push({name: "isDeleted", type: "boolean"});
+        columns.push({name: "IsDeleted", type: "boolean"});
       }
+      return columns;
+    };
 
-      let tableReader = database.tableItems("ReadingList", columns);
-      for (let row of tableReader) {
-        if (!row.isDeleted) {
-          readingListItems.push(row);
-        }
-      }
-    } catch (ex) {
-      Cu.reportError("Failed to extract Edge reading list information from " +
-                     "the database at " + this._dbFile.path + " due to the following error: " + ex);
-      // Deliberately make this fail so we expose failure in the UI:
-      throw ex;
-    } finally {
-      if (database) {
-        ESEDBReader.closeDB(database);
-      }
-    }
+    let filterFn = row => {
+      return !row.IsDeleted;
+    };
+
+    let readingListItems = readTableFromEdgeDB("ReadingList", columnFn, filterFn);
     if (!readingListItems.length) {
       return;
     }
+
     let destFolderGuid = yield this._ensureReadingListFolder(parentGuid);
     let exceptionThrown;
     for (let item of readingListItems) {
       let dateAdded = item.AddedDate || new Date();
       yield PlacesUtils.bookmarks.insert({
         parentGuid: destFolderGuid, url: item.URL, title: item.Title, dateAdded
       }).catch(ex => {
         if (!exceptionThrown) {
@@ -187,24 +216,164 @@ EdgeReadingListMigrator.prototype = {
       let folderTitle = MigrationUtils.getLocalizedString("importedEdgeReadingList");
       let folderSpec = {type: PlacesUtils.bookmarks.TYPE_FOLDER, parentGuid, title: folderTitle};
       this.__readingListFolderGuid = (yield PlacesUtils.bookmarks.insert(folderSpec)).guid;
     }
     return this.__readingListFolderGuid;
   }),
 };
 
+function EdgeBookmarksMigrator() {
+}
+
+EdgeBookmarksMigrator.prototype = {
+  type: MigrationUtils.resourceTypes.BOOKMARKS,
+
+  get exists() {
+    return !!gEdgeDatabase;
+  },
+
+  migrate(callback) {
+    this._migrateBookmarks(PlacesUtils.bookmarks.menuGuid).then(
+      () => callback(true),
+      ex => {
+        Cu.reportError(ex);
+        callback(false);
+      }
+    );
+  },
+
+  _migrateBookmarks: Task.async(function*(rootGuid) {
+    let {bookmarks, folderMap} = this._fetchBookmarksFromDB();
+    if (!bookmarks.length) {
+      return;
+    }
+    yield this._importBookmarks(bookmarks, folderMap, rootGuid);
+  }),
+
+  _importBookmarks: Task.async(function*(bookmarks, folderMap, rootGuid) {
+    if (!MigrationUtils.isStartupMigration) {
+      rootGuid =
+        yield MigrationUtils.createImportedBookmarksFolder("Edge", rootGuid);
+    }
+
+    let exceptionThrown;
+    for (let bookmark of bookmarks) {
+      // If this is a folder, we might have created it already to put other bookmarks in.
+      if (bookmark.IsFolder && bookmark._guid) {
+        continue;
+      }
+
+      // If this is a folder, just create folders up to and including that folder.
+      // Otherwise, create folders until we have a parent for this bookmark.
+      // This avoids duplicating logic for the bookmarks bar.
+      let folderId = bookmark.IsFolder ? bookmark.ItemId : bookmark.ParentId;
+      let parentGuid = yield this._getGuidForFolder(folderId, folderMap, rootGuid).catch(ex => {
+        if (!exceptionThrown) {
+          exceptionThrown = ex;
+        }
+        Cu.reportError(ex);
+      });
+
+      // If this was a folder, we're done with this item
+      if (bookmark.IsFolder) {
+        continue;
+      }
+
+      if (!parentGuid) {
+        // If we couldn't sort out a parent, fall back to importing on the root:
+        parentGuid = rootGuid;
+      }
+      let placesInfo = {
+        parentGuid,
+        url: bookmark.URL,
+        dateAdded: bookmark.DateUpdated || new Date(),
+        title: bookmark.Title,
+      }
+
+      yield PlacesUtils.bookmarks.insert(placesInfo).catch(ex => {
+        if (!exceptionThrown) {
+          exceptionThrown = ex;
+        }
+        Cu.reportError(ex);
+      });
+    }
+
+    if (exceptionThrown) {
+      throw exceptionThrown;
+    }
+  }),
+
+  _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("Favorites", columns, filterFn);
+    return {bookmarks, folderMap};
+  },
+
+  _getGuidForFolder: Task.async(function*(folderId, folderMap, rootGuid) {
+    // If the folderId is not known as a folder in the folder map, we assume
+    // we just need the root
+    if (!folderMap.has(folderId)) {
+      return rootGuid;
+    }
+    let folder = folderMap.get(folderId);
+    // If the folder already has a places guid, just return that.
+    if (folder._guid) {
+      return folder._guid;
+    }
+
+    // Hacks! The bookmarks bar is special:
+    if (folder.Title == "_Favorites_Bar_") {
+      let toolbarGuid = PlacesUtils.bookmarks.toolbarGuid;
+      if (!MigrationUtils.isStartupMigration) {
+        toolbarGuid =
+          yield MigrationUtils.createImportedBookmarksFolder("Edge", toolbarGuid);
+      }
+      return folder._guid = toolbarGuid;
+    }
+    // Otherwise, get the right parent guid recursively:
+    let parentGuid = yield this._getGuidForFolder(folder.ParentId, folderMap, rootGuid);
+    let folderInfo = {
+      title: folder.Title,
+      type: PlacesUtils.bookmarks.TYPE_FOLDER,
+      dateAdded: folder.DateUpdated || new Date(),
+      parentGuid,
+    };
+    // and add ourselves as a kid, and return the guid we got.
+    let parentBM = yield PlacesUtils.bookmarks.insert(folderInfo);
+    return folder._guid = parentBM.guid;
+  }),
+}
+
 function EdgeProfileMigrator() {
 }
 
 EdgeProfileMigrator.prototype = Object.create(MigratorPrototype);
 
 EdgeProfileMigrator.prototype.getResources = function() {
   let resources = [
-    MSMigrationUtils.getBookmarksMigrator(MSMigrationUtils.MIGRATION_TYPE_EDGE),
+    new EdgeBookmarksMigrator(),
     MSMigrationUtils.getCookiesMigrator(MSMigrationUtils.MIGRATION_TYPE_EDGE),
     new EdgeTypedURLMigrator(),
     new EdgeReadingListMigrator(),
   ];
   let windowsVaultFormPasswordsMigrator =
     MSMigrationUtils.getWindowsVaultFormPasswordsMigrator();
   windowsVaultFormPasswordsMigrator.name = "EdgeVaultFormPasswords";
   resources.push(windowsVaultFormPasswordsMigrator);