Bug 1520808 - Allow importing of history from Chrome even if it was created in the future. r=mak a=lizzard
☠☠ backed out by 1f11e71f4611 ☠ ☠
authorMark Banner <standard8@mozilla.com>
Tue, 29 Jan 2019 12:06:05 +0000
changeset 515687 8f157b0d3c52b1d8330bfe4db697ad18530cbbaa
parent 515686 6edb580f40bf764b11502c0636e5059ba8552791
child 515688 575fb73398a7f5ba784c492f1c3583fdd1b32e09
push id1953
push userffxbld-merge
push dateMon, 11 Mar 2019 12:10:20 +0000
treeherdermozilla-release@9c35dcbaa899 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak, lizzard
bugs1520808
milestone66.0
Bug 1520808 - Allow importing of history from Chrome even if it was created in the future. r=mak a=lizzard Rather than aborting adding data, we set the history items to have the current date/time. This seems mildly better than throwing away the history data. Differential Revision: https://phabricator.services.mozilla.com/D17830
browser/components/migration/ChromeMigrationUtils.jsm
browser/components/migration/ChromeProfileMigrator.js
browser/components/migration/MigrationUtils.jsm
browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/HistoryMaster
browser/components/migration/tests/unit/test_Chrome_history.js
browser/components/migration/tests/unit/xpcshell.ini
--- a/browser/components/migration/ChromeMigrationUtils.jsm
+++ b/browser/components/migration/ChromeMigrationUtils.jsm
@@ -4,16 +4,19 @@
 "use strict";
 
 var EXPORTED_SYMBOLS = ["ChromeMigrationUtils"];
 
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/osfile.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
+const S100NS_FROM1601TO1970 = 0x19DB1DED53E8000;
+const S100NS_PER_MS = 10;
+
 var ChromeMigrationUtils = {
   _extensionVersionDirectoryNames: {},
 
   // The cache for the locale strings.
   // For example, the data could be:
   // {
   //   "profile-id-1": {
   //     "extension-id-1": {
@@ -265,9 +268,34 @@ var ChromeMigrationUtils = {
     // For example, it could be "1.0_0", "1.0_1", "1.0_2", 1.1_0, 1.1_1, or 1.1_2.
     // The "1.0_1" strings mean that the "1.0_0" directory is existed and you install the version 1.0 again.
     // https://chromium.googlesource.com/chromium/src/+/0b58a813992b539a6b555ad7959adfad744b095a/chrome/common/extensions/extension_file_util_unittest.cc
     entries.sort((a, b) => Services.vc.compare(b, a));
 
     this._extensionVersionDirectoryNames[path] = entries;
     return entries;
   },
+
+  /**
+   * Convert Chrome time format to Date object
+   *
+   * @param   aTime
+   *          Chrome time
+   * @return  converted Date object
+   * @note    Google Chrome uses FILETIME / 10 as time.
+   *          FILETIME is based on same structure of Windows.
+   */
+  chromeTimeToDate(aTime) {
+    return new Date((aTime * S100NS_PER_MS - S100NS_FROM1601TO1970) / 10000);
+  },
+
+  /**
+   * Convert Date object to Chrome time format
+   *
+   * @param   aDate
+   *          Date object or integer equivalent
+   * @return  Chrome time
+   * @note    For details on Chrome time, see chromeTimeToDate.
+   */
+  dateToChromeTime(aDate) {
+    return (aDate * 10000 + S100NS_FROM1601TO1970) / S100NS_PER_MS;
+  },
 };
--- a/browser/components/migration/ChromeProfileMigrator.js
+++ b/browser/components/migration/ChromeProfileMigrator.js
@@ -1,19 +1,16 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
  * vim: sw=2 ts=2 sts=2 et */
 /* 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/. */
 
 "use strict";
 
-const S100NS_FROM1601TO1970 = 0x19DB1DED53E8000;
-const S100NS_PER_MS = 10;
-
 const AUTH_TYPE = {
   SCHEME_HTML: 0,
   SCHEME_BASIC: 1,
   SCHEME_DIGEST: 2,
 };
 
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
@@ -24,41 +21,16 @@ ChromeUtils.import("resource:///modules/
 ChromeUtils.import("resource:///modules/MigrationUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "PlacesUtils",
                                "resource://gre/modules/PlacesUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "OSCrypto",
                                "resource://gre/modules/OSCrypto.jsm");
 
 /**
- * Convert Chrome time format to Date object
- *
- * @param   aTime
- *          Chrome time
- * @return  converted Date object
- * @note    Google Chrome uses FILETIME / 10 as time.
- *          FILETIME is based on same structure of Windows.
- */
-function chromeTimeToDate(aTime) {
-  return new Date((aTime * S100NS_PER_MS - S100NS_FROM1601TO1970) / 10000);
-}
-
-/**
- * Convert Date object to Chrome time format
- *
- * @param   aDate
- *          Date object or integer equivalent
- * @return  Chrome time
- * @note    For details on Chrome time, see chromeTimeToDate.
- */
-function dateToChromeTime(aDate) {
-  return (aDate * 10000 + S100NS_FROM1601TO1970) / S100NS_PER_MS;
-}
-
-/**
  * Converts an array of chrome bookmark objects into one our own places code
  * understands.
  *
  * @param   items
  *          bookmark items to be inserted on this parent
  * @param   errorAccumulator
  *          function that gets called with any errors thrown so we don't drop them on the floor.
  */
@@ -259,17 +231,18 @@ async function GetHistoryResource(aProfi
 
     migrate(aCallback) {
       (async function() {
         const MAX_AGE_IN_DAYS = Services.prefs.getIntPref("browser.migrate.chrome.history.maxAgeInDays");
         const LIMIT = Services.prefs.getIntPref("browser.migrate.chrome.history.limit");
 
         let query = "SELECT url, title, last_visit_time, typed_count FROM urls WHERE hidden = 0";
         if (MAX_AGE_IN_DAYS) {
-          let maxAge = dateToChromeTime(Date.now() - MAX_AGE_IN_DAYS * 24 * 60 * 60 * 1000);
+          let maxAge = ChromeMigrationUtils.dateToChromeTime(
+            Date.now() - MAX_AGE_IN_DAYS * 24 * 60 * 60 * 1000);
           query += " AND last_visit_time > " + maxAge;
         }
         if (LIMIT) {
           query += " ORDER BY last_visit_time DESC LIMIT " + LIMIT;
         }
 
         let rows =
           await MigrationUtils.getRowsFromDBWithoutLocks(historyPath, "Chrome history", query);
@@ -281,17 +254,18 @@ async function GetHistoryResource(aProfi
             if (row.getResultByName("typed_count") > 0)
               transition = PlacesUtils.history.TRANSITIONS.TYPED;
 
             pageInfos.push({
               title: row.getResultByName("title"),
               url: new URL(row.getResultByName("url")),
               visits: [{
                 transition,
-                date: chromeTimeToDate(row.getResultByName("last_visit_time")),
+                date: ChromeMigrationUtils.chromeTimeToDate(
+                  row.getResultByName("last_visit_time")),
               }],
             });
           } catch (e) {
             Cu.reportError(e);
           }
         }
 
         if (pageInfos.length > 0) {
@@ -332,18 +306,18 @@ async function GetCookiesResource(aProfi
       for (let row of rows) {
         let host_key = row.getResultByName("host_key");
         if (host_key.match(/^\./)) {
           // 1st character of host_key may be ".", so we have to remove it
           host_key = host_key.substr(1);
         }
 
         try {
-          let expiresUtc =
-            chromeTimeToDate(row.getResultByName("expires_utc")) / 1000;
+          let expiresUtc = ChromeMigrationUtils.chromeTimeToDate(
+            row.getResultByName("expires_utc")) / 1000;
           Services.cookies.add(host_key,
                                row.getResultByName("path"),
                                row.getResultByName("name"),
                                row.getResultByName("value"),
                                row.getResultByName("secure"),
                                row.getResultByName("httponly"),
                                false,
                                parseInt(expiresUtc),
@@ -395,17 +369,18 @@ async function GetWindowsPasswordsResour
             password: crypto.
                       decryptData(crypto.arrayToString(row.getResultByName("password_value")),
                                                        null),
             hostname: origin_url.prePath,
             formSubmitURL: null,
             httpRealm: null,
             usernameElement: row.getResultByName("username_element"),
             passwordElement: row.getResultByName("password_element"),
-            timeCreated: chromeTimeToDate(row.getResultByName("date_created") + 0).getTime(),
+            timeCreated: ChromeMigrationUtils.chromeTimeToDate(
+              row.getResultByName("date_created") + 0).getTime(),
             timesUsed: row.getResultByName("times_used") + 0,
           };
 
           switch (row.getResultByName("scheme")) {
             case AUTH_TYPE.SCHEME_HTML:
               let action_url = NetUtil.newURI(row.getResultByName("action_url"));
               if (!kValidSchemes.has(action_url.scheme)) {
                 continue; // This continues the outer for loop.
--- a/browser/components/migration/MigrationUtils.jsm
+++ b/browser/components/migration/MigrationUtils.jsm
@@ -1018,16 +1018,27 @@ var MigrationUtils = Object.freeze({
           let {parentGuid, guid, lastModified, type} = bm;
           bmData.push({parentGuid, guid, lastModified, type});
         }
       }
     }, ex => Cu.reportError(ex));
   },
 
   insertVisitsWrapper(pageInfos) {
+    let now = new Date();
+    // Ensure that none of the dates are in the future. If they are, rewrite
+    // them to be now. This means we don't loose history entries, but they will
+    // be valid for the history store.
+    for (let pageInfo of pageInfos) {
+      for (let visit of pageInfo.visits) {
+        if (visit.date && visit.date > now) {
+          visit.date = now;
+        }
+      }
+    }
     this._importQuantities.history += pageInfos.length;
     if (gKeepUndoData) {
       this._updateHistoryUndo(pageInfos);
     }
     return PlacesUtils.history.insertMany(pageInfos);
   },
 
   async insertLoginsWrapper(logins) {
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7fb19903b0879caa87505ae2089715fbd4b42b94
GIT binary patch
literal 118784
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
new file mode 100644
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_history.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {ChromeMigrationUtils} =
+  ChromeUtils.import("resource:///modules/ChromeMigrationUtils.jsm");
+
+const SOURCE_PROFILE_DIR = "Library/Application Support/Google/Chrome/Default/";
+
+const PROFILE = {
+  id: "Default",
+  name: "Person 1",
+};
+
+/**
+ * TEST_URLS reflects the data stored in '${SOURCE_PROFILE_DIR}HistoryMaster'.
+ * The main object reflects the data in the 'urls' table. The visits property
+ * reflects the associated data in the 'visits' table.
+ */
+const TEST_URLS = [{
+  id: 1,
+  url: "http://example.com/",
+  title: "test",
+  visit_count: 1,
+  typed_count: 0,
+  last_visit_time: 13193151310368000,
+  hidden: 0,
+  visits: [{
+    id: 1,
+    url: 1,
+    visit_time: 13193151310368000,
+    from_visit: 0,
+    transition: 805306370,
+    segment_id: 0,
+    visit_duration: 10745006,
+    incremented_omnibox_typed_score: 0,
+  }],
+}, {
+  id: 2,
+  url: "http://invalid.com/",
+  title: "test2",
+  visit_count: 1,
+  typed_count: 0,
+  last_visit_time: 13193154948901000,
+  hidden: 0,
+  visits: [{
+    id: 2,
+    url: 2,
+    visit_time: 13193154948901000,
+    from_visit: 0,
+    transition: 805306376,
+    segment_id: 0,
+    visit_duration: 6568270,
+    incremented_omnibox_typed_score: 0,
+  }],
+}];
+
+async function setVisitTimes(time) {
+  let loginDataFile = do_get_file(`${SOURCE_PROFILE_DIR}History`);
+  let dbConn = await Sqlite.openConnection({ path: loginDataFile.path });
+
+  await dbConn.execute(`UPDATE urls SET last_visit_time = :last_visit_time`, {
+    last_visit_time: time,
+  });
+  await dbConn.execute(`UPDATE visits SET visit_time = :visit_time`, {
+    visit_time: time,
+  });
+
+  await dbConn.close();
+}
+
+function assertEntryMatches(entry, urlInfo, dateWasInFuture = false) {
+  info(`Checking url: ${urlInfo.url}`);
+  Assert.ok(entry, `Should have stored an entry`);
+
+  Assert.equal(entry.url, urlInfo.url, "Should have the correct URL");
+  Assert.equal(entry.title, urlInfo.title, "Should have the correct title");
+  Assert.equal(entry.visits.length, urlInfo.visits.length,
+    "Should have the correct number of visits");
+
+  for (let index in urlInfo.visits) {
+    Assert.equal(entry.visits[index].transition,
+      PlacesUtils.history.TRANSITIONS.LINK,
+      "Should have Link type transition");
+
+    if (dateWasInFuture) {
+      Assert.lessOrEqual(entry.visits[index].date.getTime(), new Date().getTime(),
+        "Should have moved the date to no later than the current date.");
+    } else {
+      Assert.equal(
+        entry.visits[index].date.getTime(),
+        ChromeMigrationUtils.chromeTimeToDate(urlInfo.visits[index].visit_time).getTime(),
+        "Should have the correct date");
+    }
+  }
+}
+
+function setupHistoryFile() {
+  removeHistoryFile();
+  let file = do_get_file(`${SOURCE_PROFILE_DIR}HistoryMaster`);
+  file.copyTo(file.parent, "History");
+}
+
+function removeHistoryFile() {
+  let file = do_get_file(`${SOURCE_PROFILE_DIR}History`, true);
+  try {
+    file.remove(false);
+  } catch (ex) {
+    // It is ok if this doesn't exist.
+    if (ex.result != Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
+      throw ex;
+    }
+  }
+}
+
+add_task(async function setup() {
+  registerFakePath("ULibDir", do_get_file("Library/"));
+
+  registerCleanupFunction(async () => {
+    await PlacesUtils.history.clear();
+    removeHistoryFile();
+  });
+});
+
+add_task(async function test_import() {
+  setupHistoryFile();
+  await PlacesUtils.history.clear();
+
+  let migrator = await MigrationUtils.getMigrator("chrome");
+  Assert.ok(await migrator.isSourceAvailable(), "Sanity check the source exists");
+
+  await promiseMigration(migrator, MigrationUtils.resourceTypes.HISTORY, PROFILE);
+
+  for (let urlInfo of TEST_URLS) {
+    let entry = await PlacesUtils.history.fetch(urlInfo.url, {includeVisits: true});
+    assertEntryMatches(entry, urlInfo);
+  }
+});
+
+add_task(async function test_import_future_date() {
+  setupHistoryFile();
+  await PlacesUtils.history.clear();
+  const futureDate = new Date().getTime() + 6000 * 60 * 24;
+  await setVisitTimes(ChromeMigrationUtils.dateToChromeTime(futureDate));
+
+  let migrator = await MigrationUtils.getMigrator("chrome");
+  Assert.ok(await migrator.isSourceAvailable(), "Sanity check the source exists");
+
+  await promiseMigration(migrator, MigrationUtils.resourceTypes.HISTORY, PROFILE);
+
+  for (let urlInfo of TEST_URLS) {
+    let entry = await PlacesUtils.history.fetch(urlInfo.url, {includeVisits: true});
+    assertEntryMatches(entry, urlInfo, true);
+  }
+});
--- a/browser/components/migration/tests/unit/xpcshell.ini
+++ b/browser/components/migration/tests/unit/xpcshell.ini
@@ -6,16 +6,18 @@ support-files =
   Library/**
   AppData/**
 
 [test_360se_bookmarks.js]
 skip-if = os != "win"
 [test_Chrome_bookmarks.js]
 [test_Chrome_cookies.js]
 skip-if = os != "mac" # Relies on ULibDir
+[test_Chrome_history.js]
+skip-if = os != "mac" # Relies on ULibDir
 [test_Chrome_passwords.js]
 skip-if = os != "win"
 [test_ChromeMigrationUtils.js]
 [test_ChromeMigrationUtils_path.js]
 [test_Edge_db_migration.js]
 skip-if = os != "win"
 [test_fx_telemetry.js]
 [test_IE_bookmarks.js]