Bug 1356567 - root icons should still create a page association if the domain differs. r=adw, kitcambridge draft
authorMarco Bonardo <mbonardo@mozilla.com>
Fri, 14 Apr 2017 20:34:27 +0200
changeset 564212 316e81af7995b601a23415a56e3407f8e0286913
parent 564098 bb38d935d699e0529f9e0bb35578d381026415c4
child 624712 23ba0eb70f5bf4e4578ce79f56fc0f21372953a1
push id54571
push usermak77@bonardo.net
push dateTue, 18 Apr 2017 13:33:44 +0000
reviewersadw, kitcambridge
bugs1356567
milestone55.0a1
Bug 1356567 - root icons should still create a page association if the domain differs. r=adw, kitcambridge Root domain icons are no more associated with their pages, BUT if the page uses a root domain icon from another domain, it should still get an association with it or we couldn't relate the two. This also fixes an overlooked problem in PlacesTestUtils where Date objects cross a boundary and fail instanceof checks. This causes failures in the same test that this patch is modifying. To protect from future similar issues some protection has been added to updatedPlaces so that it will crash in debug builds. MozReview-Commit-ID: 3MTKhGj3ehj
browser/components/newtab/tests/xpcshell/test_PlacesProvider.js
services/sync/modules/engines/history.js
services/sync/tests/unit/test_history_store.js
toolkit/components/places/FaviconHelpers.cpp
toolkit/components/places/FaviconHelpers.h
toolkit/components/places/History.cpp
toolkit/components/places/nsFaviconService.cpp
toolkit/components/places/tests/PlacesTestUtils.jsm
toolkit/components/places/tests/favicons/test_root_icons.js
toolkit/components/places/tests/unit/test_486978_sort_by_date_queries.js
--- a/browser/components/newtab/tests/xpcshell/test_PlacesProvider.js
+++ b/browser/components/newtab/tests/xpcshell/test_PlacesProvider.js
@@ -29,21 +29,21 @@ do_get_profile();
 function run_test() {
   PlacesProvider.links.init();
   run_next_test();
 }
 
 // url prefix for test history population
 const TEST_URL = "https://mozilla.com/";
 // time when the test starts execution
-const TIME_NOW = (new Date()).getTime();
+const TIME_NOW = new Date();
 
 // utility function to compute past timestap
 function timeDaysAgo(numDays) {
-  return TIME_NOW - (numDays * 24 * 60 * 60 * 1000);
+  return new Date(TIME_NOW - (numDays * 24 * 60 * 60 * 1000));
 }
 
 // utility function to make a visit for insetion into places db
 function makeVisit(index, daysAgo, isTyped, domain = TEST_URL) {
   let {
     TRANSITION_TYPED,
     TRANSITION_LINK
   } = PlacesUtils.history;
--- a/services/sync/modules/engines/history.js
+++ b/services/sync/modules/engines/history.js
@@ -357,17 +357,17 @@ HistoryStore.prototype = {
       curVisits[i] = curVisits[i].date + "," + curVisits[i].type;
     }
 
     // Walk through the visits, make sure we have sound data, and eliminate
     // dupes. The latter is done by rewriting the array in-place.
     for (i = 0, k = 0; i < record.visits.length; i++) {
       let visit = record.visits[k] = record.visits[i];
 
-      if (!visit.date || typeof visit.date != "number") {
+      if (!visit.date || typeof visit.date != "number" || !Number.isInteger(visit.date)) {
         this._log.warn("Encountered record with invalid visit date: "
                        + visit.date);
         continue;
       }
 
       if (!visit.type ||
           !Object.values(PlacesUtils.history.TRANSITIONS).includes(visit.type)) {
         this._log.warn("Encountered record with invalid visit type: " +
--- a/services/sync/tests/unit/test_history_store.js
+++ b/services/sync/tests/unit/test_history_store.js
@@ -1,12 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://testing-common/PlacesTestUtils.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/engines/history.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
 
 const TIMESTAMP1 = (Date.now() - 103406528) * 1000;
 const TIMESTAMP2 = (Date.now() - 6592903) * 1000;
@@ -83,33 +84,19 @@ function run_test() {
 
 add_test(function test_store() {
   _("Verify that we've got an empty store to work with.");
   do_check_empty(store.getAllIDs());
 
   _("Let's create an entry in the database.");
   fxuri = Utils.makeURI("http://getfirefox.com/");
 
-  let place = {
-    uri: fxuri,
-    title: "Get Firefox!",
-    visits: [{
-      visitDate: TIMESTAMP1,
-      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK
-    }]
-  };
-  PlacesUtils.asyncHistory.updatePlaces(place, {
-    handleError: function handleError() {
-      do_throw("Unexpected error in adding visit.");
-    },
-    handleResult: function handleResult() {},
-    handleCompletion: onVisitAdded
-  });
-
-  function onVisitAdded() {
+  PlacesTestUtils.addVisits({ uri: fxuri, title: "Get Firefox!",
+                              visitDate: TIMESTAMP1 })
+                 .then(() => {
     _("Verify that the entry exists.");
     let ids = Object.keys(store.getAllIDs());
     do_check_eq(ids.length, 1);
     fxguid = ids[0];
     do_check_true(store.itemExists(fxguid));
 
     _("If we query a non-existent record, it's marked as deleted.");
     let record = store.createRecord("non-existent");
@@ -136,17 +123,17 @@ add_test(function test_store() {
       run_next_test();
     }));
     applyEnsureNoFailures([
       {id: fxguid,
        histUri: record.histUri,
        title: "Hol Dir Firefox!",
        visits: [record.visits[0], secondvisit]}
     ]);
-  }
+  });
 });
 
 add_test(function test_store_create() {
   _("Create a brand new record through the store.");
   tbguid = Utils.makeGUID();
   tburi = Utils.makeURI("http://getthunderbird.com");
   onNextVisit(ensureThrows(function() {
     do_check_attribute_count(store.getAllIDs(), 2);
--- a/toolkit/components/places/FaviconHelpers.cpp
+++ b/toolkit/components/places/FaviconHelpers.cpp
@@ -64,17 +64,17 @@ FetchPageInfo(const RefPtr<Database>& aD
         "LEFT JOIN moz_historyvisits parent ON parent.id = dest.from_visit "
                                           "AND dest.visit_type IN (%d, %d) "
         "LEFT JOIN moz_historyvisits grandparent ON parent.from_visit = grandparent.id "
           "AND parent.visit_type IN (%d, %d) "
         "WHERE dest.place_id = h.id "
         "AND EXISTS(SELECT 1 FROM moz_bookmarks b WHERE b.fk = r_place_id) "
         "LIMIT 1 "
       ") "
-    ") "
+    "), fixup_url(get_unreversed_host(h.rev_host)) AS host "
     "FROM moz_places h "
     "LEFT JOIN moz_pages_w_icons pi ON page_url_hash = hash(:page_url) AND page_url = :page_url "
     "WHERE h.url_hash = hash(:page_url) AND h.url = :page_url",
     nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT,
     nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY,
     nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT,
     nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY
   );
@@ -106,16 +106,21 @@ FetchPageInfo(const RefPtr<Database>& aD
   rv = stmt->GetIsNull(3, &isNull);
   NS_ENSURE_SUCCESS(rv, rv);
   // The page could not be bookmarked.
   if (!isNull) {
     rv = stmt->GetUTF8String(3, _page.bookmarkedSpec);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
+  if (_page.host.IsEmpty()) {
+    rv = stmt->GetUTF8String(4, _page.host);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
   if (!_page.canAddToHistory) {
     // Either history is disabled or the scheme is not supported.  In such a
     // case we want to update the icon only if the page is bookmarked.
 
     if (_page.bookmarkedSpec.IsEmpty()) {
       // The page is not bookmarked.  Since updating the icon with a disabled
       // history would be a privacy leak, bail out as if the page did not exist.
       return NS_ERROR_NOT_AVAILABLE;
@@ -831,17 +836,19 @@ AsyncAssociateIconToPage::Run()
     rv = transaction.Commit();
     NS_ENSURE_SUCCESS(rv, rv);
     return NS_OK;
   }
 
   // Don't associate pages to root domain icons, since those will be returned
   // regardless.  This saves a lot of work and database space since we don't
   // need to store urls and relations.
-  if (!mIcon.rootIcon) {
+  // Though, this is possible only if both the page and the icon have the same
+  // host, otherwise we couldn't relate them.
+  if (!mIcon.rootIcon || !mIcon.host.Equals(mPage.host)) {
     // The page may have associated payloads already, and those could have to be
     // expired. For example at a certain point a page could decide to stop serving
     // its usual 16px and 32px pngs, and use an svg instead.
     // On the other side, we could also be in the process of adding more payloads
     // to this page, and we should not expire the payloads we just added.
     // For this, we use the expiration field as an indicator and remove relations
     // based on it being elapsed. We don't remove orphan icons at this time since
     // it would have a cost. The privacy hit is limited since history removal
--- a/toolkit/components/places/FaviconHelpers.h
+++ b/toolkit/components/places/FaviconHelpers.h
@@ -87,16 +87,17 @@ struct IconData
   : expiration(0)
   , fetchMode(FETCH_NEVER)
   , status(ICON_STATUS_UNKNOWN)
   , rootIcon(0)
   {
   }
 
   nsCString spec;
+  nsCString host;
   PRTime expiration;
   enum AsyncFaviconFetchMode fetchMode;
   uint16_t status; // This is a bitset, see ICON_STATUS_* defines above.
   uint8_t rootIcon;
   nsTArray<IconPayload> payloads;
 };
 
 /**
@@ -110,18 +111,18 @@ struct PageData
   , canAddToHistory(true)
   {
     guid.SetIsVoid(true);
   }
 
   int64_t id; // This is the moz_pages_w_icons id.
   int64_t placeId; // This is the moz_places page id.
   nsCString spec;
+  nsCString host;
   nsCString bookmarkedSpec;
-  nsString revHost;
   bool canAddToHistory; // False for disabled history and unsupported schemas.
   nsCString guid;
 };
 
 /**
  * Info for a frame.
  */
 struct FrameData
--- a/toolkit/components/places/History.cpp
+++ b/toolkit/components/places/History.cpp
@@ -2972,16 +2972,29 @@ History::UpdatePlaces(JS::Handle<JS::Val
 
       VisitData& data = *visitData.AppendElement(VisitData(uri));
       data.title = title;
       data.guid = guid;
 
       // We must have a date and a transaction type!
       rv = GetIntFromJSObject(aCtx, visit, "visitDate", &data.visitTime);
       NS_ENSURE_SUCCESS(rv, rv);
+      // visitDate should be in microseconds. It's easy to do the wrong thing
+      // and pass milliseconds to updatePlaces, so we lazily check for that.
+      // While it's not easily distinguishable, since both are integers, we can
+      // check if the value is very far in the past, and assume it's probably
+      // a mistake.
+      if (data.visitTime < (PR_Now() / 1000)) {
+#ifdef DEBUG
+        nsCOMPtr<nsIXPConnect> xpc = do_GetService(nsIXPConnect::GetCID());
+        Unused << xpc->DebugDumpJSStack(false, false, false);
+        MOZ_CRASH("invalid time format passed to updatePlaces");
+#endif
+        return NS_ERROR_INVALID_ARG;
+      }
       uint32_t transitionType = 0;
       rv = GetIntFromJSObject(aCtx, visit, "transitionType", &transitionType);
       NS_ENSURE_SUCCESS(rv, rv);
       NS_ENSURE_ARG_RANGE(transitionType,
                           nsINavHistoryService::TRANSITION_LINK,
                           nsINavHistoryService::TRANSITION_RELOAD);
       data.SetTransitionType(transitionType);
       data.hidden = GetHiddenState(false, transitionType);
--- a/toolkit/components/places/nsFaviconService.cpp
+++ b/toolkit/components/places/nsFaviconService.cpp
@@ -334,18 +334,21 @@ nsFaviconService::SetAndFetchFaviconForP
   NS_ENSURE_TRUE(loadingPrincipal, NS_ERROR_FAILURE);
 
   bool loadPrivate = aFaviconLoadType == nsIFaviconService::FAVICON_LOAD_PRIVATE;
 
   // Build page data.
   PageData page;
   rv = aPageURI->GetSpec(page.spec);
   NS_ENSURE_SUCCESS(rv, rv);
-  // URIs can arguably miss a host.
-  Unused << GetReversedHostname(aPageURI, page.revHost);
+  // URIs can arguably lack a host.
+  Unused << aPageURI->GetHost(page.host);
+  if (StringBeginsWith(page.host, NS_LITERAL_CSTRING("www."))) {
+    page.host.Cut(0, 4);
+  }
   bool canAddToHistory;
   nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
   NS_ENSURE_TRUE(navHistory, NS_ERROR_OUT_OF_MEMORY);
   rv = navHistory->CanAddURI(aPageURI, &canAddToHistory);
   NS_ENSURE_SUCCESS(rv, rv);
   page.canAddToHistory = !!canAddToHistory && !loadPrivate;
 
   // Build icon data.
@@ -354,16 +357,21 @@ nsFaviconService::SetAndFetchFaviconForP
   UnassociatedIconHashKey* iconKey = mUnassociatedIcons.GetEntry(aFaviconURI);
   if (iconKey) {
     icon = iconKey->iconData;
     mUnassociatedIcons.RemoveEntry(iconKey);
   } else {
     icon.fetchMode = aForceReload ? FETCH_ALWAYS : FETCH_IF_MISSING;
     rv = aFaviconURI->GetSpec(icon.spec);
     NS_ENSURE_SUCCESS(rv, rv);
+    // URIs can arguably lack a host.
+    Unused << aFaviconURI->GetHost(icon.host);
+    if (StringBeginsWith(icon.host, NS_LITERAL_CSTRING("www."))) {
+      icon.host.Cut(0, 4);
+    }
     nsAutoCString path;
     rv = aFaviconURI->GetPath(path);
     if (NS_SUCCEEDED(rv) && path.EqualsLiteral("/favicon.ico")) {
       icon.rootIcon = 1;
     }
   }
 
   // If the page url points to an image, the icon's url will be the same.
@@ -432,16 +440,21 @@ nsFaviconService::ReplaceFaviconData(nsI
   iconData->fetchMode = FETCH_NEVER;
   nsresult rv = aFaviconURI->GetSpec(iconData->spec);
   NS_ENSURE_SUCCESS(rv, rv);
   nsAutoCString path;
   rv = aFaviconURI->GetPath(path);
   if (NS_SUCCEEDED(rv) && path.EqualsLiteral("/favicon.ico")) {
     iconData->rootIcon = 1;
   }
+  // URIs can arguably lack a host.
+  Unused << aFaviconURI->GetHost(iconData->host);
+  if (StringBeginsWith(iconData->host, NS_LITERAL_CSTRING("www."))) {
+    iconData->host.Cut(0, 4);
+  }
 
   IconPayload payload;
   payload.mimeType = aMimeType;
   payload.data.Assign(TO_CHARBUFFER(aData), aDataLen);
   if (payload.mimeType.EqualsLiteral(SVG_MIME_TYPE)) {
     payload.width = UINT16_MAX;
   }
   // There may already be a previous payload, so ensure to only have one.
--- a/toolkit/components/places/tests/PlacesTestUtils.jsm
+++ b/toolkit/components/places/tests/PlacesTestUtils.jsm
@@ -58,17 +58,25 @@ this.PlacesTestUtils = Object.freeze({
       info.title = (typeof place.title === "string") ? place.title : "test visit for " + info.url.spec ;
       if (typeof place.referrer == "string") {
         place.referrer = NetUtil.newURI(place.referrer);
       } else if (place.referrer && place.referrer instanceof URL) {
         place.referrer = NetUtil.newURI(place.referrer.href);
       }
       let visitDate = place.visitDate;
       if (visitDate) {
-        if (!(visitDate instanceof Date)) {
+        if (visitDate.constructor.name != "Date") {
+          // visitDate should be in microseconds. It's easy to do the wrong thing
+          // and pass milliseconds to updatePlaces, so we lazily check for that.
+          // While it's not easily distinguishable, since both are integers, we
+          // can check if the value is very far in the past, and assume it's
+          // probably a mistake.
+          if (visitDate <= Date.now()) {
+            throw new Error("AddVisits expects a Date object or _micro_seconds!");
+          }
           visitDate = PlacesUtils.toDate(visitDate);
         }
       } else {
         visitDate = new Date();
       }
       info.visits = [{
         transition: place.transition,
         date: visitDate,
--- a/toolkit/components/places/tests/favicons/test_root_icons.js
+++ b/toolkit/components/places/tests/favicons/test_root_icons.js
@@ -41,17 +41,17 @@ add_task(function* () {
   rows = yield db.execute("SELECT * FROM moz_icons");
   Assert.equal(rows.length, 0, "The icon should have been removed");
 });
 
 add_task(function* test_removePagesByTimeframe() {
   // Add a visit in the past with no directly associated icon.
   yield PlacesTestUtils.addVisits({uri: "http://www.places.test/old/", visitDate: new Date(Date.now() - 86400000)});
   let pageURI = NetUtil.newURI("http://www.places.test/page/");
-  yield PlacesTestUtils.addVisits(pageURI);
+  yield PlacesTestUtils.addVisits({uri: pageURI, visitDate: new Date(Date.now() - 7200000)});
   let faviconURI = NetUtil.newURI("http://www.places.test/page/favicon.ico");
   let rootIconURI = NetUtil.newURI("http://www.places.test/favicon.ico");
   PlacesUtils.favicons.replaceFaviconDataFromDataURL(
     faviconURI, SMALLSVG_DATA_URI.spec, 0, Services.scriptSecurityManager.getSystemPrincipal());
   yield setFaviconForPage(pageURI, faviconURI);
   PlacesUtils.favicons.replaceFaviconDataFromDataURL(
     rootIconURI, SMALLPNG_DATA_URI.spec, 0, Services.scriptSecurityManager.getSystemPrincipal());
   yield setFaviconForPage(pageURI, rootIconURI);
@@ -60,17 +60,17 @@ add_task(function* test_removePagesByTim
   Assert.equal(yield getFaviconUrlForPage(pageURI),
                faviconURI.spec, "Should get the biggest icon");
   Assert.equal(yield getFaviconUrlForPage(pageURI, 1),
                rootIconURI.spec, "Should get the smallest icon");
   Assert.equal(yield getFaviconUrlForPage("http://www.places.test/old/"),
                rootIconURI.spec, "Should get the root icon");
 
   PlacesUtils.history.removePagesByTimeframe(
-    PlacesUtils.toPRTime(Date.now() - 20000),
+    PlacesUtils.toPRTime(Date.now() - 14400000),
     PlacesUtils.toPRTime(new Date())
   );
 
   // Check database entries.
   let db = yield PlacesUtils.promiseDBConnection();
   let rows = yield db.execute("SELECT * FROM moz_icons");
   Assert.equal(rows.length, 1, "There should only be 1 icon entry");
   Assert.equal(rows[0].getResultByName("root"), 1, "It should be marked as a root icon");
@@ -78,8 +78,20 @@ add_task(function* test_removePagesByTim
   Assert.equal(rows.length, 0, "There should be no page entry");
   rows = yield db.execute("SELECT * FROM moz_icons_to_pages");
   Assert.equal(rows.length, 0, "There should be no relation entry");
 
   PlacesUtils.history.removePagesByTimeframe(0, PlacesUtils.toPRTime(new Date()));
   rows = yield db.execute("SELECT * FROM moz_icons");
   Assert.equal(rows.length, 0, "There should be no icon entry");
 });
+
+add_task(function* test_different_host() {
+  let pageURI = NetUtil.newURI("http://places.test/page/");
+  yield PlacesTestUtils.addVisits(pageURI);
+  let faviconURI = NetUtil.newURI("http://mozilla.test/favicon.ico");
+  PlacesUtils.favicons.replaceFaviconDataFromDataURL(
+    faviconURI, SMALLPNG_DATA_URI.spec, 0, Services.scriptSecurityManager.getSystemPrincipal());
+  yield setFaviconForPage(pageURI, faviconURI);
+
+  Assert.equal(yield getFaviconUrlForPage(pageURI),
+               faviconURI.spec, "Should get the png icon");
+});
--- a/toolkit/components/places/tests/unit/test_486978_sort_by_date_queries.js
+++ b/toolkit/components/places/tests/unit/test_486978_sort_by_date_queries.js
@@ -21,30 +21,24 @@ var pages = [
   "http://a.mozilla.org/3/",
   "http://a.mozilla.org/4/",
   "http://b.mozilla.org/5/",
   "http://b.mozilla.org/6/",
   "http://b.mozilla.org/7/",
   "http://b.mozilla.org/8/",
 ];
 
-function run_test() {
-  run_next_test();
-}
-
 add_task(function* test_initialize() {
-  var noon = new Date();
-  noon.setHours(12);
-
   // Add visits.
+  let now = new Date();
   for (let pageIndex = 0; pageIndex < pages.length; ++pageIndex) {
     let page = pages[pageIndex];
     yield PlacesTestUtils.addVisits({
       uri: uri(page),
-      visitDate: noon - (pages.length - pageIndex) * 1000
+      visitDate: new Date(now - (pages.length - pageIndex))
     });
   }
 });
 
 /**
  * Tests that sorting date query by none will sort by title asc.
  */
 add_task(function() {