Bug 1405317 - When inserting trees of bookmarks, avoid updating the last modified date when marking annotations. r?mak draft
authorMark Banner <standard8@mozilla.com>
Tue, 03 Oct 2017 21:18:10 +0100
changeset 674943 a87db7837be4cfe45f1c92442b6b8437ebe80838
parent 674765 294f332a35538940469b1a2576615ff5ffe1e016
child 734464 0373c1695c9ab3d88bb75a6f4f3059e5c7942559
push id82981
push userbmo:standard8@mozilla.com
push dateWed, 04 Oct 2017 16:03:28 +0000
reviewersmak
bugs1405317
milestone58.0a1
Bug 1405317 - When inserting trees of bookmarks, avoid updating the last modified date when marking annotations. r?mak MozReview-Commit-ID: Ik8DKwtiXoM
toolkit/components/places/BookmarkJSONUtils.jsm
toolkit/components/places/Bookmarks.jsm
toolkit/components/places/PlacesTransactions.jsm
toolkit/components/places/PlacesUtils.jsm
toolkit/components/places/nsAnnotationService.cpp
toolkit/components/places/nsIAnnotationService.idl
toolkit/components/places/nsNavBookmarks.cpp
toolkit/components/places/tests/unit/bookmarks.json
toolkit/components/places/tests/unit/test_bookmarks_json.js
--- a/toolkit/components/places/BookmarkJSONUtils.jsm
+++ b/toolkit/components/places/BookmarkJSONUtils.jsm
@@ -450,17 +450,17 @@ function translateTreeTypes(node) {
   if (node.dateAdded) {
     node.dateAdded = PlacesUtils.toDate(node.dateAdded);
   }
 
   if (node.lastModified) {
     let lastModified = PlacesUtils.toDate(node.lastModified);
     // Ensure we get a last modified date that's later or equal to the dateAdded
     // so that we don't upset the Bookmarks API.
-    if (lastModified >= node.dataAdded) {
+    if (lastModified >= node.dateAdded) {
       node.lastModified = lastModified;
     } else {
       delete node.lastModified;
     }
   }
 
   if (node.tags) {
      // Separate any tags into an array, and ignore any that are too long.
--- a/toolkit/components/places/Bookmarks.jsm
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -1501,33 +1501,37 @@ async function insertLivemarkData(items)
       if (item.lastModified) {
         item.lastModified = PlacesUtils.toPRTime(item.lastModified);
       }
 
       let livemark = await PlacesUtils.livemarks.addLivemark(item);
 
       let id = livemark.id;
       if (item.annos && item.annos.length) {
+        // Note: for annotations, we intentionally skip updating the last modified
+        // value for the bookmark, to avoid a second update of the added bookmark.
         PlacesUtils.setAnnotationsForItem(id, item.annos,
-                                          item.source);
+                                          item.source, true);
       }
     }
   }
 }
 
 /**
  * Handles special data on a bookmark, e.g. annotations, keywords, tags, charsets,
  * inserting the data into the appropriate place.
  *
  * @param {Integer} itemId The ID of the item within the bookmarks database.
  * @param {Object} item The bookmark item with possible special data to be inserted.
  */
 async function handleBookmarkItemSpecialData(itemId, item) {
   if (item.annos && item.annos.length) {
-    PlacesUtils.setAnnotationsForItem(itemId, item.annos, item.source)
+    // Note: for annotations, we intentionally skip updating the last modified
+    // value for the bookmark, to avoid a second update of the added bookmark.
+    PlacesUtils.setAnnotationsForItem(itemId, item.annos, item.source, true)
   }
   if ("keyword" in item && item.keyword) {
     // POST data could be set in 2 ways:
     // 1. new backups have a postData property
     // 2. old backups have an item annotation
     let postDataAnno = item.annos &&
                        item.annos.find(anno => anno.name == PlacesUtils.POST_DATA_ANNO);
     let postData = item.postData || (postDataAnno && postDataAnno.value);
--- a/toolkit/components/places/PlacesTransactions.jsm
+++ b/toolkit/components/places/PlacesTransactions.jsm
@@ -988,18 +988,16 @@ function createItemsFromBookmarksTree(tr
 
         if ("keyword" in item) {
           let { uri: url, keyword, postData } = item;
           await PlacesUtils.keywords.insert({ url, keyword, postData });
         }
         if ("tags" in item) {
           PlacesUtils.tagging.tagURI(Services.io.newURI(item.uri),
                                      item.tags.split(","));
-          if (restoring)
-            shouldResetLastModified = true;
         }
         break;
       }
       case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER: {
         // Either a folder or a livemark
         let feedURI, siteURI;
         [feedURI, siteURI, annos] = extractLivemarkDetails(annos);
         if (!feedURI) {
@@ -1036,19 +1034,18 @@ function createItemsFromBookmarksTree(tr
     }
     if (annos.length > 0) {
       if (!restoring && excludingAnnotations.length > 0) {
         annos = annos.filter(a => !excludingAnnotations.includes(a.name));
       }
 
       if (annos.length > 0) {
         let itemId = await PlacesUtils.promiseItemId(guid);
-        PlacesUtils.setAnnotationsForItem(itemId, annos);
-        if (restoring)
-          shouldResetLastModified = true;
+        PlacesUtils.setAnnotationsForItem(itemId, annos,
+          Ci.nsINavBookmarksService.SOURCE_DEFAULT, true);
       }
     }
 
     if (shouldResetLastModified) {
       let lastModified = PlacesUtils.toDate(item.lastModified);
       await PlacesUtils.bookmarks.update({ guid, lastModified });
     }
 
@@ -1087,17 +1084,18 @@ PT.NewBookmark.prototype = Object.seal({
                                    .getTagsForURI(Services.io.newURI(url.href));
       tags = tags.filter(t => !currentTags.includes(t));
     }
 
     async function createItem() {
       info = await PlacesUtils.bookmarks.insert(info);
       if (annotations.length > 0) {
         let itemId = await PlacesUtils.promiseItemId(info.guid);
-        PlacesUtils.setAnnotationsForItem(itemId, annotations);
+        PlacesUtils.setAnnotationsForItem(itemId, annotations,
+          Ci.nsINavBookmarksService.SOURCE_DEFAULT, true);
       }
       if (tags.length > 0) {
         PlacesUtils.tagging.tagURI(Services.io.newURI(url.href), tags);
       }
     }
 
     await createItem();
 
@@ -1106,20 +1104,16 @@ PT.NewBookmark.prototype = Object.seal({
       // which could be affected by any annotation we set in createItem.
       await PlacesUtils.bookmarks.remove(info);
       if (tags.length > 0) {
         PlacesUtils.tagging.untagURI(Services.io.newURI(url.href), tags);
       }
     };
     this.redo = async function() {
       await createItem();
-      // CreateItem will update the lastModified value if tags or annotations
-      // are present, but we don't care to restore it. The likely of a user
-      // creating a bookmark, undoing and redoing that, and still caring
-      // about lastModified is basically non-existant.
     };
     return info.guid;
   }
 });
 
 /**
  * Transaction for creating a folder.
  *
@@ -1160,28 +1154,27 @@ PT.NewFolder.prototype = Object.seal({
       // therefore we update the bookmark manually afterwards.
       if (index != PlacesUtils.bookmarks.DEFAULT_INDEX) {
         bmInfo[0].index = index;
         bmInfo = await PlacesUtils.bookmarks.update(bmInfo[0]);
       }
 
       if (annotations.length > 0) {
         let itemId = await PlacesUtils.promiseItemId(folderGuid);
-        PlacesUtils.setAnnotationsForItem(itemId, annotations);
+        PlacesUtils.setAnnotationsForItem(itemId, annotations,
+          Ci.nsINavBookmarksService.SOURCE_DEFAULT, true);
       }
     }
     await createItem();
 
     this.undo = async function() {
       await PlacesUtils.bookmarks.remove(folderGuid);
     };
     this.redo = async function() {
       await createItem();
-      // See the reasoning in CreateItem for why we don't care
-      // about precisely resetting the lastModified value.
     };
     return folderGuid;
   }
 });
 
 /**
  * Transaction for creating a separator.
  *
@@ -1219,17 +1212,18 @@ PT.NewLivemark.prototype = Object.seal({
     let livemarkInfo = { title,
                          feedURI: Services.io.newURI(feedUrl.href),
                          siteURI: siteUrl ? Services.io.newURI(siteUrl.href) : null,
                          index,
                          parentGuid};
     let createItem = async function() {
       let livemark = await PlacesUtils.livemarks.addLivemark(livemarkInfo);
       if (annotations.length > 0) {
-        PlacesUtils.setAnnotationsForItem(livemark.id, annotations);
+        PlacesUtils.setAnnotationsForItem(livemark.id, annotations,
+          Ci.nsINavBookmarksService.SOURCE_DEFAULT, true);
       }
       return livemark;
     };
 
     let livemark = await createItem();
     livemarkInfo.guid = livemark.guid;
     livemarkInfo.dateAdded = livemark.dateAdded;
     livemarkInfo.lastModified = livemark.lastModified;
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -1165,28 +1165,28 @@ this.PlacesUtils = {
    * Annotate an item with a batch of annotations.
    * @param aItemId
    *        The identifier of the item for which annotations are to be set
    * @param aAnnotations
    *        Array of objects, each containing the following properties:
    *        name, flags, expires.
    *        If the value for an annotation is not set it will be removed.
    */
-  setAnnotationsForItem: function PU_setAnnotationsForItem(aItemId, aAnnos, aSource) {
+  setAnnotationsForItem: function PU_setAnnotationsForItem(aItemId, aAnnos, aSource, aDontUpdateLastModified) {
     var annosvc = this.annotations;
 
     aAnnos.forEach(function(anno) {
       if (anno.value === undefined || anno.value === null) {
         annosvc.removeItemAnnotation(aItemId, anno.name, aSource);
       } else {
         let flags = ("flags" in anno) ? anno.flags : 0;
         let expires = ("expires" in anno) ?
           anno.expires : Ci.nsIAnnotationService.EXPIRE_NEVER;
         annosvc.setItemAnnotation(aItemId, anno.name, anno.value, flags,
-                                  expires, aSource);
+                                  expires, aSource, aDontUpdateLastModified);
       }
     });
   },
 
   // Identifier getters for special folders.
   // You should use these everywhere PlacesUtils is available to avoid XPCOM
   // traversal just to get roots' ids.
   get placesRootId() {
--- a/toolkit/components/places/nsAnnotationService.cpp
+++ b/toolkit/components/places/nsAnnotationService.cpp
@@ -259,17 +259,18 @@ nsAnnotationService::SetPageAnnotation(n
 
 
 NS_IMETHODIMP
 nsAnnotationService::SetItemAnnotation(int64_t aItemId,
                                        const nsACString& aName,
                                        nsIVariant* aValue,
                                        int32_t aFlags,
                                        uint16_t aExpiration,
-                                       uint16_t aSource)
+                                       uint16_t aSource,
+                                       bool aDontUpdateLastModified)
 {
   AUTO_PROFILER_LABEL("nsAnnotationService::SetItemAnnotation", OTHER);
 
   NS_ENSURE_ARG_MIN(aItemId, 1);
   NS_ENSURE_ARG(aValue);
 
   if (aExpiration == EXPIRE_WITH_HISTORY)
     return NS_ERROR_INVALID_ARG;
@@ -285,59 +286,63 @@ nsAnnotationService::SetItemAnnotation(i
     case nsIDataType::VTYPE_UINT16:
     case nsIDataType::VTYPE_INT32:
     case nsIDataType::VTYPE_UINT32:
     case nsIDataType::VTYPE_BOOL: {
       int32_t valueInt;
       rv = aValue->GetAsInt32(&valueInt);
       if (NS_SUCCEEDED(rv)) {
         NS_ENSURE_SUCCESS(rv, rv);
-        rv = SetItemAnnotationInt32(aItemId, aName, valueInt, aFlags, aExpiration, aSource);
+        rv = SetItemAnnotationInt32(aItemId, aName, valueInt, aFlags,
+                                    aExpiration, aSource, aDontUpdateLastModified);
         NS_ENSURE_SUCCESS(rv, rv);
         return NS_OK;
       }
       // Fall through int64_t case otherwise.
       MOZ_FALLTHROUGH;
     }
     case nsIDataType::VTYPE_INT64:
     case nsIDataType::VTYPE_UINT64: {
       int64_t valueLong;
       rv = aValue->GetAsInt64(&valueLong);
       if (NS_SUCCEEDED(rv)) {
         NS_ENSURE_SUCCESS(rv, rv);
-        rv = SetItemAnnotationInt64(aItemId, aName, valueLong, aFlags, aExpiration, aSource);
+        rv = SetItemAnnotationInt64(aItemId, aName, valueLong, aFlags,
+                                    aExpiration, aSource, aDontUpdateLastModified);
         NS_ENSURE_SUCCESS(rv, rv);
         return NS_OK;
       }
       // Fall through double case otherwise.
       MOZ_FALLTHROUGH;
     }
     case nsIDataType::VTYPE_FLOAT:
     case nsIDataType::VTYPE_DOUBLE: {
       double valueDouble;
       rv = aValue->GetAsDouble(&valueDouble);
       NS_ENSURE_SUCCESS(rv, rv);
-      rv = SetItemAnnotationDouble(aItemId, aName, valueDouble, aFlags, aExpiration, aSource);
+      rv = SetItemAnnotationDouble(aItemId, aName, valueDouble, aFlags,
+                                   aExpiration, aSource, aDontUpdateLastModified);
       NS_ENSURE_SUCCESS(rv, rv);
       return NS_OK;
     }
     case nsIDataType::VTYPE_CHAR:
     case nsIDataType::VTYPE_WCHAR:
     case nsIDataType::VTYPE_DOMSTRING:
     case nsIDataType::VTYPE_CHAR_STR:
     case nsIDataType::VTYPE_WCHAR_STR:
     case nsIDataType::VTYPE_STRING_SIZE_IS:
     case nsIDataType::VTYPE_WSTRING_SIZE_IS:
     case nsIDataType::VTYPE_UTF8STRING:
     case nsIDataType::VTYPE_CSTRING:
     case nsIDataType::VTYPE_ASTRING: {
       nsAutoString stringValue;
       rv = aValue->GetAsAString(stringValue);
       NS_ENSURE_SUCCESS(rv, rv);
-      rv = SetItemAnnotationString(aItemId, aName, stringValue, aFlags, aExpiration, aSource);
+      rv = SetItemAnnotationString(aItemId, aName, stringValue, aFlags,
+                                   aExpiration, aSource, aDontUpdateLastModified);
       NS_ENSURE_SUCCESS(rv, rv);
       return NS_OK;
     }
   }
 
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
@@ -362,28 +367,29 @@ nsAnnotationService::SetPageAnnotationSt
 
 
 NS_IMETHODIMP
 nsAnnotationService::SetItemAnnotationString(int64_t aItemId,
                                              const nsACString& aName,
                                              const nsAString& aValue,
                                              int32_t aFlags,
                                              uint16_t aExpiration,
-                                             uint16_t aSource)
+                                             uint16_t aSource,
+                                             bool aDontUpdateLastModified)
 {
   NS_ENSURE_ARG_MIN(aItemId, 1);
 
   if (aExpiration == EXPIRE_WITH_HISTORY)
     return NS_ERROR_INVALID_ARG;
 
   nsresult rv = SetAnnotationStringInternal(nullptr, aItemId, aName, aValue,
                                             aFlags, aExpiration);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  NOTIFY_ANNOS_OBSERVERS(OnItemAnnotationSet(aItemId, aName, aSource));
+  NOTIFY_ANNOS_OBSERVERS(OnItemAnnotationSet(aItemId, aName, aSource, aDontUpdateLastModified));
 
   return NS_OK;
 }
 
 
 nsresult
 nsAnnotationService::SetAnnotationInt32Internal(nsIURI* aURI,
                                                 int64_t aItemId,
@@ -433,28 +439,29 @@ nsAnnotationService::SetPageAnnotationIn
 
 
 NS_IMETHODIMP
 nsAnnotationService::SetItemAnnotationInt32(int64_t aItemId,
                                             const nsACString& aName,
                                             int32_t aValue,
                                             int32_t aFlags,
                                             uint16_t aExpiration,
-                                            uint16_t aSource)
+                                            uint16_t aSource,
+                                            bool aDontUpdateLastModified)
 {
   NS_ENSURE_ARG_MIN(aItemId, 1);
 
   if (aExpiration == EXPIRE_WITH_HISTORY)
     return NS_ERROR_INVALID_ARG;
 
   nsresult rv = SetAnnotationInt32Internal(nullptr, aItemId, aName, aValue,
                                            aFlags, aExpiration);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  NOTIFY_ANNOS_OBSERVERS(OnItemAnnotationSet(aItemId, aName, aSource));
+  NOTIFY_ANNOS_OBSERVERS(OnItemAnnotationSet(aItemId, aName, aSource, aDontUpdateLastModified));
 
   return NS_OK;
 }
 
 
 nsresult
 nsAnnotationService::SetAnnotationInt64Internal(nsIURI* aURI,
                                                 int64_t aItemId,
@@ -504,28 +511,29 @@ nsAnnotationService::SetPageAnnotationIn
 
 
 NS_IMETHODIMP
 nsAnnotationService::SetItemAnnotationInt64(int64_t aItemId,
                                             const nsACString& aName,
                                             int64_t aValue,
                                             int32_t aFlags,
                                             uint16_t aExpiration,
-                                            uint16_t aSource)
+                                            uint16_t aSource,
+                                            bool aDontUpdateLastModified)
 {
   NS_ENSURE_ARG_MIN(aItemId, 1);
 
   if (aExpiration == EXPIRE_WITH_HISTORY)
     return NS_ERROR_INVALID_ARG;
 
   nsresult rv = SetAnnotationInt64Internal(nullptr, aItemId, aName, aValue,
                                            aFlags, aExpiration);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  NOTIFY_ANNOS_OBSERVERS(OnItemAnnotationSet(aItemId, aName, aSource));
+  NOTIFY_ANNOS_OBSERVERS(OnItemAnnotationSet(aItemId, aName, aSource, aDontUpdateLastModified));
 
   return NS_OK;
 }
 
 
 nsresult
 nsAnnotationService::SetAnnotationDoubleInternal(nsIURI* aURI,
                                                  int64_t aItemId,
@@ -575,28 +583,29 @@ nsAnnotationService::SetPageAnnotationDo
 
 
 NS_IMETHODIMP
 nsAnnotationService::SetItemAnnotationDouble(int64_t aItemId,
                                              const nsACString& aName,
                                              double aValue,
                                              int32_t aFlags,
                                              uint16_t aExpiration,
-                                             uint16_t aSource)
+                                             uint16_t aSource,
+                                             bool aDontUpdateLastModified)
 {
   NS_ENSURE_ARG_MIN(aItemId, 1);
 
   if (aExpiration == EXPIRE_WITH_HISTORY)
     return NS_ERROR_INVALID_ARG;
 
   nsresult rv = SetAnnotationDoubleInternal(nullptr, aItemId, aName, aValue,
                                             aFlags, aExpiration);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  NOTIFY_ANNOS_OBSERVERS(OnItemAnnotationSet(aItemId, aName, aSource));
+  NOTIFY_ANNOS_OBSERVERS(OnItemAnnotationSet(aItemId, aName, aSource, aDontUpdateLastModified));
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsAnnotationService::GetPageAnnotationString(nsIURI* aURI,
                                              const nsACString& aName,
                                              nsAString& _retval)
@@ -1634,17 +1643,17 @@ nsAnnotationService::CopyItemAnnotations
     rv = copyStmt->BindInt64ByName(NS_LITERAL_CSTRING("name_id"), annoNameID);
     NS_ENSURE_SUCCESS(rv, rv);
     rv = copyStmt->BindInt64ByName(NS_LITERAL_CSTRING("date"), PR_Now());
     NS_ENSURE_SUCCESS(rv, rv);
 
     rv = copyStmt->Execute();
     NS_ENSURE_SUCCESS(rv, rv);
 
-    NOTIFY_ANNOS_OBSERVERS(OnItemAnnotationSet(aDestItemId, annoName, aSource));
+    NOTIFY_ANNOS_OBSERVERS(OnItemAnnotationSet(aDestItemId, annoName, aSource, false));
   }
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
--- a/toolkit/components/places/nsIAnnotationService.idl
+++ b/toolkit/components/places/nsIAnnotationService.idl
@@ -15,17 +15,18 @@ interface nsIAnnotationObserver : nsISup
     /**
      * Called when an annotation value is set. It could be a new annotation,
      * or it could be a new value for an existing annotation.
      */
     void onPageAnnotationSet(in nsIURI aPage,
                              in AUTF8String aName);
     void onItemAnnotationSet(in long long aItemId,
                              in AUTF8String aName,
-                             in unsigned short aSource);
+                             in unsigned short aSource,
+                             in boolean aDontUpdateLastModified);
 
     /**
      * Called when an annotation is deleted. If aName is empty, then ALL
      * annotations for the given URI have been deleted. This is not called when
      * annotations are expired (normally happens when the app exits).
      */
     void onPageAnnotationRemoved(in nsIURI aURI,
                                  in AUTF8String aName);
@@ -35,30 +36,30 @@ interface nsIAnnotationObserver : nsISup
 };
 
 [scriptable, uuid(D4CDAAB1-8EEC-47A8-B420-AD7CB333056A)]
 interface nsIAnnotationService : nsISupports
 {
     /**
      * Valid values for aExpiration, which sets the expiration policy for your
      * annotation. The times for the days, weeks and months policies are
-     * measured since the last visit date of the page in question. These 
+     * measured since the last visit date of the page in question. These
      * will not expire so long as the user keeps visiting the page from time
      * to time.
      */
 
     // For temporary data that can be discarded when the user exits.
     // Removed at application exit.
     const unsigned short EXPIRE_SESSION = 0;
 
     // NOTE: 1 is skipped due to its temporary use as EXPIRE_NEVER in bug #319455.
 
     // For general page settings, things the user is interested in seeing
     // if they come back to this page some time in the near future.
-    // Removed at 30 days. 
+    // Removed at 30 days.
     const unsigned short EXPIRE_WEEKS = 2;
 
     // Something that the user will be interested in seeing in their
     // history like favicons. If they haven't visited a page in a couple
     // of months, they probably aren't interested in many other annotations,
     // the positions of things, or other stuff you create, so put that in
     // the weeks policy.
     // Removed at 180 days.
@@ -126,32 +127,34 @@ interface nsIAnnotationService : nsISupp
                            in nsIVariant aValue,
                            in long aFlags,
                            in unsigned short aExpiration);
     void setItemAnnotation(in long long aItemId,
                            in AUTF8String aName,
                            in nsIVariant aValue,
                            in long aFlags,
                            in unsigned short aExpiration,
-                           [optional] in unsigned short aSource);
+                           [optional] in unsigned short aSource,
+                           [optional] in bool aDontUpdateLastModified);
 
     /**
      * @throws NS_ERROR_ILLEGAL_VALUE if the page or the bookmark doesn't exist.
      */
     [noscript] void setPageAnnotationString(in nsIURI aURI,
                                             in AUTF8String aName,
                                             in AString aValue,
                                             in long aFlags,
                                             in unsigned short aExpiration);
     [noscript] void setItemAnnotationString(in long long aItemId,
                                             in AUTF8String aName,
                                             in AString aValue,
                                             in long aFlags,
                                             in unsigned short aExpiration,
-                                            [optional] in unsigned short aSource);
+                                            [optional] in unsigned short aSource,
+                                            [optional] in bool aDontUpdateLastModified);
 
     /**
      * Sets an annotation just like setAnnotationString, but takes an Int32 as
      * input.
      *
      * @throws NS_ERROR_ILLEGAL_VALUE if the page or the bookmark doesn't exist.
      */
     [noscript] void setPageAnnotationInt32(in nsIURI aURI,
@@ -159,17 +162,18 @@ interface nsIAnnotationService : nsISupp
                                            in long aValue,
                                            in long aFlags,
                                            in unsigned short aExpiration);
     [noscript] void setItemAnnotationInt32(in long long aItemId,
                                            in AUTF8String aName,
                                            in long aValue,
                                            in long aFlags,
                                            in unsigned short aExpiration,
-                                           [optional] in unsigned short aSource);
+                                           [optional] in unsigned short aSource,
+                                           [optional] in bool aDontUpdateLastModified);
 
     /**
      * Sets an annotation just like setAnnotationString, but takes an Int64 as
      * input.
      *
      * @throws NS_ERROR_ILLEGAL_VALUE if the page or the bookmark doesn't exist.
      */
     [noscript] void setPageAnnotationInt64(in nsIURI aURI,
@@ -177,17 +181,18 @@ interface nsIAnnotationService : nsISupp
                                            in long long aValue,
                                            in long aFlags,
                                            in unsigned short aExpiration);
     [noscript] void setItemAnnotationInt64(in long long aItemId,
                                            in AUTF8String aName,
                                            in long long aValue,
                                            in long aFlags,
                                            in unsigned short aExpiration,
-                                           [optional] in unsigned short aSource);
+                                           [optional] in unsigned short aSource,
+                                           [optional] in bool aDontUpdateLastModified);
 
     /**
      * Sets an annotation just like setAnnotationString, but takes a double as
      * input.
      *
      * @throws NS_ERROR_ILLEGAL_VALUE if the page or the bookmark doesn't exist.
      */
     [noscript] void setPageAnnotationDouble(in nsIURI aURI,
@@ -195,17 +200,18 @@ interface nsIAnnotationService : nsISupp
                                             in double aValue,
                                             in long aFlags,
                                             in unsigned short aExpiration);
     [noscript] void setItemAnnotationDouble(in long long aItemId,
                                             in AUTF8String aName,
                                             in double aValue,
                                             in long aFlags,
                                             in unsigned short aExpiration,
-                                            [optional] in unsigned short aSource);
+                                            [optional] in unsigned short aSource,
+                                            [optional] in boolean aDontUpdateLastModified);
 
     /**
      * Retrieves the value of a given annotation. Throws an error if the
      * annotation does not exist. C++ consumers may use the type-specific
      * methods.
      *
      * The type-specific methods throw if the given annotation is set in
      * a different type.
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -3314,26 +3314,28 @@ NS_IMETHODIMP
 nsNavBookmarks::OnPageAnnotationSet(nsIURI* aPage, const nsACString& aName)
 {
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::OnItemAnnotationSet(int64_t aItemId, const nsACString& aName,
-                                    uint16_t aSource)
+                                    uint16_t aSource, bool aDontUpdateLastModified)
 {
   BookmarkData bookmark;
   nsresult rv = FetchItemInfo(aItemId, bookmark);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  bookmark.lastModified = RoundedPRNow();
-  rv = SetItemDateInternal(LAST_MODIFIED, DetermineSyncChangeDelta(aSource),
-                           bookmark.id, bookmark.lastModified);
-  NS_ENSURE_SUCCESS(rv, rv);
+  if (!aDontUpdateLastModified) {
+    bookmark.lastModified = RoundedPRNow();
+    rv = SetItemDateInternal(LAST_MODIFIED, DetermineSyncChangeDelta(aSource),
+                             bookmark.id, bookmark.lastModified);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
 
   NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
                    nsINavBookmarkObserver,
                    OnItemChanged(bookmark.id,
                                  aName,
                                  true,
                                  EmptyCString(),
                                  bookmark.lastModified,
@@ -3355,12 +3357,12 @@ nsNavBookmarks::OnPageAnnotationRemoved(
 
 
 NS_IMETHODIMP
 nsNavBookmarks::OnItemAnnotationRemoved(int64_t aItemId, const nsACString& aName,
                                         uint16_t aSource)
 {
   // As of now this is doing the same as OnItemAnnotationSet, so just forward
   // the call.
-  nsresult rv = OnItemAnnotationSet(aItemId, aName, aSource);
+  nsresult rv = OnItemAnnotationSet(aItemId, aName, aSource, false);
   NS_ENSURE_SUCCESS(rv, rv);
   return NS_OK;
 }
--- a/toolkit/components/places/tests/unit/bookmarks.json
+++ b/toolkit/components/places/tests/unit/bookmarks.json
@@ -263,13 +263,24 @@
           "guid": "OCyeUO5uu9FW",
           "title": "Example.tld",
           "id": 14,
           "parent": 5,
           "dateAdded": 1361551979401846,
           "lastModified": 1361551979402952,
           "type": "text/x-moz-place",
           "uri": "http://example.tld/"
+        },
+        {
+          "guid": "Cfkety492Afk",
+          "title": "test tagged bookmark",
+          "id": 15,
+          "parent": 5,
+          "dateAdded": 1507025843703345,
+          "lastModified": 1507025844703124,
+          "type": "text/x-moz-place",
+          "uri": "http://example.tld/tagged",
+          "tags": "foo"
         }
       ]
     }
   ]
 }
--- a/toolkit/components/places/tests/unit/test_bookmarks_json.js
+++ b/toolkit/components/places/tests/unit/test_bookmarks_json.js
@@ -43,23 +43,23 @@ var test_bookmarks = {
       guid: "OCyeUO5uu9FK",
       type: Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR
     },
     {
       guid: "OCyeUO5uu9FL",
       title: "test",
       description: "folder test comment",
       dateAdded: 1177541020000000,
-      // lastModified: 1177541050000000,
+      lastModified: 1177541050000000,
       children: [
         { guid: "OCyeUO5uu9GX",
           title: "test post keyword",
           description: "item description",
           dateAdded: 1177375336000000,
-          // lastModified: 1177375423000000,
+          lastModified: 1177375423000000,
           keyword: "test",
           sidebar: true,
           postData: "hidden1%3Dbar&text1%3D%25s",
           charset: "ISO-8859-1"
         }
       ]
     }
   ],
@@ -71,22 +71,30 @@ var test_bookmarks = {
     },
     { guid: "OCyeUO5uu9FR",
       title: "Latest Headlines",
       url: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/",
       feedUrl: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml",
       // Note: date gets truncated to milliseconds, whereas the value in bookmarks.json
       // has full microseconds.
       dateAdded: 1361551979451000,
+      lastModified: 1361551979457000,
     }
   ],
   unfiled: [
     { guid: "OCyeUO5uu9FW",
       title: "Example.tld",
       url: "http://example.tld/"
+    },
+    { guid: "Cfkety492Afk",
+      title: "test tagged bookmark",
+      dateAdded: 1507025843703000,
+      lastModified: 1507025844703000,
+      url: "http://example.tld/tagged",
+      tags: ["foo"],
     }
   ]
 };
 
 // Exported bookmarks file pointer.
 var bookmarksExportedFile;
 
 add_task(async function test_import_bookmarks() {
@@ -204,17 +212,17 @@ async function checkItem(aExpected, aNod
                     id, LOAD_IN_SIDEBAR_ANNO), aExpected.sidebar);
         break;
       case "postData": {
         let entry = await PlacesUtils.keywords.fetch({ url: aNode.uri });
         Assert.equal(entry.postData, aExpected.postData);
         break;
       }
       case "charset":
-        let testURI = NetUtil.newURI(aNode.uri);
+        let testURI = Services.io.newURI(aNode.uri);
         do_check_eq((await PlacesUtils.getCharsetForURI(testURI)), aExpected.charset);
         break;
       case "feedUrl":
         let livemark = await PlacesUtils.livemarks.getLivemark({ id });
         do_check_eq(livemark.siteURI.spec, aExpected.url);
         do_check_eq(livemark.feedURI.spec, aExpected.feedUrl);
         break;
       case "children":
@@ -224,13 +232,18 @@ async function checkItem(aExpected, aNod
         do_check_eq(folder.childCount, aExpected.children.length);
 
         for (let index = 0; index < aExpected.children.length; index++) {
           await checkItem(aExpected.children[index], folder.getChild(index));
         }
 
         folder.containerOpen = false;
         break;
+      case "tags":
+        let uri = Services.io.newURI(aNode.uri);
+        Assert.deepEqual(PlacesUtils.tagging.getTagsForURI(uri), aExpected.tags,
+          "should have the expected tags");
+        break;
       default:
         throw new Error("Unknown property");
     }
   }
 }