tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }