--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -10,35 +10,39 @@
#include "nsPlacesMacros.h"
#include "Helpers.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsNetUtil.h"
#include "nsUnicharUtils.h"
#include "nsPrintfCString.h"
#include "prprf.h"
+#include "mozilla/Preferences.h"
#include "mozilla/storage.h"
#include "GeckoProfiler.h"
using namespace mozilla;
// These columns sit to the right of the kGetInfoIndex_* columns.
const int32_t nsNavBookmarks::kGetChildrenIndex_Guid = 18;
const int32_t nsNavBookmarks::kGetChildrenIndex_Position = 19;
const int32_t nsNavBookmarks::kGetChildrenIndex_Type = 20;
const int32_t nsNavBookmarks::kGetChildrenIndex_PlaceID = 21;
+const int32_t nsNavBookmarks::kGetChildrenIndex_SyncStatus = 22;
using namespace mozilla::places;
PLACES_FACTORY_SINGLETON_IMPLEMENTATION(nsNavBookmarks, gBookmarksService)
#define BOOKMARKS_ANNO_PREFIX "bookmarks/"
#define BOOKMARKS_TOOLBAR_FOLDER_ANNO NS_LITERAL_CSTRING(BOOKMARKS_ANNO_PREFIX "toolbarFolder")
#define FEED_URI_ANNO NS_LITERAL_CSTRING("livemark/feedURI")
+#define SYNC_PARENT_ANNO "sync/parent"
+#define SQLITE_MAX_VARIABLE_NUMBER 999
namespace {
#define SKIP_TAGS(condition) ((condition) ? SkipTags : DontSkip)
bool DontSkip(nsCOMPtr<nsINavBookmarkObserver> obs) { return false; }
bool SkipTags(nsCOMPtr<nsINavBookmarkObserver> obs) {
@@ -119,16 +123,41 @@ public:
}
private:
RefPtr<nsNavBookmarks> mBookmarksSvc;
Method mCallback;
DataType mData;
};
+// Returns the sync change counter increment for a change source constant.
+inline int64_t
+DetermineSyncChangeDelta(uint16_t aSource) {
+ return aSource == nsINavBookmarksService::SOURCE_SYNC ? 0 : 1;
+}
+
+// Returns the sync status for a new item inserted by a change source.
+inline int32_t
+DetermineInitialSyncStatus(uint16_t aSource) {
+ if (aSource == nsINavBookmarksService::SOURCE_SYNC) {
+ return nsINavBookmarksService::SYNC_STATUS_NORMAL;
+ }
+ if (aSource == nsINavBookmarksService::SOURCE_IMPORT_REPLACE) {
+ return nsINavBookmarksService::SYNC_STATUS_UNKNOWN;
+ }
+ return nsINavBookmarksService::SYNC_STATUS_NEW;
+}
+
+// Indicates whether an item has been uploaded to the server and
+// needs a tombstone on deletion.
+inline bool
+NeedsTombstone(const BookmarkData& aBookmark) {
+ return aBookmark.syncStatus == nsINavBookmarksService::SYNC_STATUS_NORMAL;
+}
+
} // namespace
nsNavBookmarks::nsNavBookmarks()
: mItemCount(0)
, mRoot(0)
, mMenuRoot(0)
, mTagsRoot(0)
@@ -376,20 +405,20 @@ nsNavBookmarks::InsertBookmarkInDB(int64
// Check for a valid itemId.
MOZ_ASSERT(_itemId && (*_itemId == -1 || *_itemId > 0));
// Check for a valid placeId.
MOZ_ASSERT(aPlaceId && (aPlaceId == -1 || aPlaceId > 0));
nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
"INSERT INTO moz_bookmarks "
"(id, fk, type, parent, position, title, "
- "dateAdded, lastModified, guid) "
+ "dateAdded, lastModified, guid, syncStatus, syncChangeCounter) "
"VALUES (:item_id, :page_id, :item_type, :parent, :item_index, "
":item_title, :date_added, :last_modified, "
- ":item_guid)"
+ ":item_guid, :sync_status, :change_counter)"
);
NS_ENSURE_STATE(stmt);
mozStorageStatementScoper scoper(stmt);
nsresult rv;
if (*_itemId != -1)
rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), *_itemId);
else
@@ -425,42 +454,68 @@ nsNavBookmarks::InsertBookmarkInDB(int64
}
else {
rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("last_modified"), aDateAdded);
}
NS_ENSURE_SUCCESS(rv, rv);
// Could use IsEmpty because our callers check for GUID validity,
// but it doesn't hurt.
- if (_guid.Length() == 12) {
+ bool hasExistingGuid = _guid.Length() == 12;
+ if (hasExistingGuid) {
MOZ_ASSERT(IsValidGUID(_guid));
rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("item_guid"), _guid);
NS_ENSURE_SUCCESS(rv, rv);
}
else {
nsAutoCString guid;
rv = GenerateGUID(guid);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("item_guid"), guid);
NS_ENSURE_SUCCESS(rv, rv);
_guid.Assign(guid);
}
+ int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("change_counter"),
+ syncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint16_t syncStatus = DetermineInitialSyncStatus(aSource);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("sync_status"),
+ syncStatus);
+ NS_ENSURE_SUCCESS(rv, rv);
+
rv = stmt->Execute();
NS_ENSURE_SUCCESS(rv, rv);
+ // Remove stale tombstones if we're reinserting an item.
+ if (hasExistingGuid) {
+ rv = RemoveTombstone(_guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
if (*_itemId == -1) {
*_itemId = sLastInsertedItemId;
}
if (aParentId > 0) {
// Update last modified date of the ancestors.
// TODO (bug 408991): Doing this for all ancestors would be slow without a
// nested tree, so for now update only the parent.
- rv = SetItemDateInternal(LAST_MODIFIED, aParentId, aDateAdded);
+ rv = SetItemDateInternal(LAST_MODIFIED, syncChangeDelta, aParentId,
+ aDateAdded);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ bool isTagging = aGrandParentId == mTagsRoot;
+ if (isTagging) {
+ // If we're tagging a bookmark, increment the change counter for all
+ // bookmarks with the URI.
+ rv = AddSyncChangesForBookmarksWithURI(aURI, syncChangeDelta);
NS_ENSURE_SUCCESS(rv, rv);
}
// Add a cache entry since we know everything about this bookmark.
BookmarkData bookmark;
bookmark.id = *_itemId;
bookmark.guid.Assign(_guid);
if (aTitle.IsVoid()) {
@@ -479,16 +534,17 @@ nsNavBookmarks::InsertBookmarkInDB(int64
else
bookmark.lastModified = aDateAdded;
if (aURI) {
rv = aURI->GetSpec(bookmark.url);
NS_ENSURE_SUCCESS(rv, rv);
}
bookmark.parentGuid = aParentGuid;
bookmark.grandParentId = aGrandParentId;
+ bookmark.syncStatus = syncStatus;
return NS_OK;
}
NS_IMETHODIMP
nsNavBookmarks::InsertBookmark(int64_t aFolder,
nsIURI* aURI,
int32_t aIndex,
@@ -599,18 +655,18 @@ nsNavBookmarks::RemoveItem(int64_t aItem
BookmarkData bookmark;
nsresult rv = FetchItemInfo(aItemId, bookmark);
NS_ENSURE_SUCCESS(rv, rv);
mozStorageTransaction transaction(mDB->MainConn(), false);
// First, if not a tag, remove item annotations.
- if (bookmark.parentId != mTagsRoot &&
- bookmark.grandParentId != mTagsRoot) {
+ bool isUntagging = bookmark.grandParentId == mTagsRoot;
+ if (bookmark.parentId != mTagsRoot && !isUntagging) {
nsAnnotationService* annosvc = nsAnnotationService::GetAnnotationService();
NS_ENSURE_TRUE(annosvc, NS_ERROR_OUT_OF_MEMORY);
rv = annosvc->RemoveItemAnnotations(bookmark.id, aSource);
NS_ENSURE_SUCCESS(rv, rv);
}
if (bookmark.type == TYPE_FOLDER) {
// Remove all of the folder's children.
@@ -631,21 +687,36 @@ nsNavBookmarks::RemoveItem(int64_t aItem
// Fix indices in the parent.
if (bookmark.position != DEFAULT_INDEX) {
rv = AdjustIndices(bookmark.parentId,
bookmark.position + 1, INT32_MAX, -1);
NS_ENSURE_SUCCESS(rv, rv);
}
+ int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);
+
+ // Add a tombstone for synced items.
+ if (syncChangeDelta) {
+ rv = InsertTombstone(bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
bookmark.lastModified = RoundedPRNow();
- rv = SetItemDateInternal(LAST_MODIFIED, bookmark.parentId,
- bookmark.lastModified);
+ rv = SetItemDateInternal(LAST_MODIFIED, syncChangeDelta,
+ bookmark.parentId, bookmark.lastModified);
NS_ENSURE_SUCCESS(rv, rv);
+ if (isUntagging) {
+ // If we're removing a tag, increment the change counter for all bookmarks
+ // with the URI.
+ rv = AddSyncChangesForBookmarksWithURL(bookmark.url, syncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
rv = transaction.Commit();
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIURI> uri;
if (bookmark.type == TYPE_BOOKMARK) {
// If not a tag, recalculate frecency for this entry, since it changed.
if (bookmark.grandParentId != mTagsRoot) {
nsNavHistory* history = nsNavHistory::GetHistoryService();
@@ -993,17 +1064,17 @@ nsNavBookmarks::GetDescendantChildren(in
// This is a LEFT JOIN because not all bookmarks types have a place.
// We construct a result where the first columns exactly match
// kGetInfoIndex_* order, and additionally contains columns for position,
// item_child, and folder_child from moz_bookmarks.
nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
"SELECT h.id, h.url, IFNULL(b.title, h.title), h.rev_host, h.visit_count, "
"h.last_visit_date, f.url, b.id, b.dateAdded, b.lastModified, "
"b.parent, null, h.frecency, h.hidden, h.guid, null, null, null, "
- "b.guid, b.position, b.type, b.fk "
+ "b.guid, b.position, b.type, b.fk, b.syncStatus "
"FROM moz_bookmarks b "
"LEFT JOIN moz_places h ON b.fk = h.id "
"LEFT JOIN moz_favicons f ON h.favicon_id = f.id "
"WHERE b.parent = :parent "
"ORDER BY b.position ASC"
);
NS_ENSURE_STATE(stmt);
mozStorageStatementScoper scoper(stmt);
@@ -1022,16 +1093,18 @@ nsNavBookmarks::GetDescendantChildren(in
rv = stmt->GetInt32(kGetChildrenIndex_Type, &child.type);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->GetInt64(kGetChildrenIndex_PlaceID, &child.placeId);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->GetInt32(kGetChildrenIndex_Position, &child.position);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->GetUTF8String(kGetChildrenIndex_Guid, child.guid);
NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt32(kGetChildrenIndex_SyncStatus, &child.syncStatus);
+ NS_ENSURE_SUCCESS(rv, rv);
if (child.type == TYPE_BOOKMARK) {
rv = stmt->GetUTF8String(nsNavHistory::kGetInfoIndex_URL, child.url);
NS_ENSURE_SUCCESS(rv, rv);
}
// Append item to children's array.
aFolderChildrenArray.AppendElement(child);
@@ -1086,16 +1159,18 @@ nsNavBookmarks::RemoveFolderChildren(int
BookmarkData& child = folderChildrenArray[i];
if (child.type == TYPE_FOLDER) {
foldersToRemove.Append(',');
foldersToRemove.AppendInt(child.id);
}
}
+ int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);
+
// Delete items from the database now.
mozStorageTransaction transaction(mDB->MainConn(), false);
nsCOMPtr<mozIStorageStatement> deleteStatement = mDB->GetStatement(
NS_LITERAL_CSTRING(
"DELETE FROM moz_bookmarks "
"WHERE parent IN (:parent") + foldersToRemove + NS_LITERAL_CSTRING(")")
);
@@ -1113,19 +1188,44 @@ nsNavBookmarks::RemoveFolderChildren(int
"DELETE FROM moz_items_annos "
"WHERE id IN ("
"SELECT a.id from moz_items_annos a "
"LEFT JOIN moz_bookmarks b ON a.item_id = b.id "
"WHERE b.id ISNULL)"));
NS_ENSURE_SUCCESS(rv, rv);
// Set the lastModified date.
- rv = SetItemDateInternal(LAST_MODIFIED, folder.id, RoundedPRNow());
+ rv = SetItemDateInternal(LAST_MODIFIED, syncChangeDelta, folder.id,
+ RoundedPRNow());
NS_ENSURE_SUCCESS(rv, rv);
+ if (syncChangeDelta) {
+ nsTArray<TombstoneData> tombstones(folderChildrenArray.Length());
+ PRTime dateRemoved = RoundedPRNow();
+
+ for (uint32_t i = 0; i < folderChildrenArray.Length(); ++i) {
+ BookmarkData& child = folderChildrenArray[i];
+ if (NeedsTombstone(child)) {
+ // Write tombstones for synced children.
+ TombstoneData childTombstone = {child.guid, dateRemoved};
+ tombstones.AppendElement(childTombstone);
+ }
+ bool isUntagging = child.grandParentId == mTagsRoot;
+ if (isUntagging) {
+ // Bump the change counter for all tagged bookmarks when removing a tag
+ // folder.
+ rv = AddSyncChangesForBookmarksWithURL(child.url, syncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ rv = InsertTombstones(tombstones);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
rv = transaction.Commit();
NS_ENSURE_SUCCESS(rv, rv);
// Call observers in reverse order to serve children before their parent.
for (int32_t i = folderChildrenArray.Length() - 1; i >= 0; --i) {
BookmarkData& child = folderChildrenArray[i];
nsCOMPtr<nsIURI> uri;
@@ -1256,17 +1356,18 @@ nsNavBookmarks::MoveItem(int64_t aItemId
if (aNewParent == bookmark.parentId && newIndex == bookmark.position) {
// Nothing to do!
return NS_OK;
}
// adjust indices to account for the move
// do this before we update the parent/index fields
// or we'll re-adjust the index for the item we are moving
- if (bookmark.parentId == aNewParent) {
+ bool sameParent = bookmark.parentId == aNewParent;
+ if (sameParent) {
// We can optimize the updates if moving within the same container.
// We only shift the items between the old and new positions, since the
// insertion will offset the deletion.
if (bookmark.position > newIndex) {
rv = AdjustIndices(bookmark.parentId, newIndex, bookmark.position - 1, 1);
}
else {
rv = AdjustIndices(bookmark.parentId, bookmark.position + 1, newIndex, -1);
@@ -1278,16 +1379,18 @@ nsNavBookmarks::MoveItem(int64_t aItemId
// First, fill the hole from the removal from the old parent.
rv = AdjustIndices(bookmark.parentId, bookmark.position + 1, INT32_MAX, -1);
NS_ENSURE_SUCCESS(rv, rv);
// Now, make room in the new parent for the insertion.
rv = AdjustIndices(aNewParent, newIndex, INT32_MAX, 1);
NS_ENSURE_SUCCESS(rv, rv);
}
+ int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);
+
{
// Update parent and position.
nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
"UPDATE moz_bookmarks SET parent = :parent, position = :item_index "
"WHERE id = :item_id "
);
NS_ENSURE_STATE(stmt);
mozStorageStatementScoper scoper(stmt);
@@ -1298,19 +1401,43 @@ nsNavBookmarks::MoveItem(int64_t aItemId
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), bookmark.id);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->Execute();
NS_ENSURE_SUCCESS(rv, rv);
}
PRTime now = RoundedPRNow();
- rv = SetItemDateInternal(LAST_MODIFIED, bookmark.parentId, now);
+ rv = SetItemDateInternal(LAST_MODIFIED, syncChangeDelta,
+ bookmark.parentId, now);
NS_ENSURE_SUCCESS(rv, rv);
- rv = SetItemDateInternal(LAST_MODIFIED, aNewParent, now);
+ if (sameParent) {
+ // If we're moving within the same container, only the parent needs a sync
+ // change. Update the item's last modified date without bumping its counter.
+ rv = SetItemDateInternal(LAST_MODIFIED, 0, bookmark.id, now);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // Otherwise, if we're moving between containers, both parents and the child
+ // need sync changes.
+ rv = SetItemDateInternal(LAST_MODIFIED, syncChangeDelta, aNewParent, now);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetItemDateInternal(LAST_MODIFIED, syncChangeDelta, bookmark.id, now);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ bool isChangingTagFolder = bookmark.parentId == mTagsRoot;
+ if (isChangingTagFolder) {
+ // Moving a tag folder out of the tags root untags all its bookmarks. This
+ // is an odd case, but the tagging service adds an observer to handle it,
+ // so we bump the change counter for each untagged item for consistency.
+ rv = AddSyncChangesForBookmarksInFolder(bookmark.id, syncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = PreventSyncReparenting(bookmark);
NS_ENSURE_SUCCESS(rv, rv);
rv = transaction.Commit();
NS_ENSURE_SUCCESS(rv, rv);
NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
nsINavBookmarkObserver,
OnItemMoved(bookmark.id,
@@ -1328,17 +1455,18 @@ nsNavBookmarks::MoveItem(int64_t aItemId
nsresult
nsNavBookmarks::FetchItemInfo(int64_t aItemId,
BookmarkData& _bookmark)
{
// LEFT JOIN since not all bookmarks have an associated place.
nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
"SELECT b.id, h.url, b.title, b.position, b.fk, b.parent, b.type, "
- "b.dateAdded, b.lastModified, b.guid, t.guid, t.parent "
+ "b.dateAdded, b.lastModified, b.guid, t.guid, t.parent, "
+ "b.syncStatus "
"FROM moz_bookmarks b "
"LEFT JOIN moz_bookmarks t ON t.id = b.parent "
"LEFT JOIN moz_places h ON h.id = b.fk "
"WHERE b.id = :item_id"
);
NS_ENSURE_STATE(stmt);
mozStorageStatementScoper scoper(stmt);
@@ -1386,49 +1514,57 @@ nsNavBookmarks::FetchItemInfo(int64_t aI
rv = stmt->GetUTF8String(10, _bookmark.parentGuid);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->GetInt64(11, &_bookmark.grandParentId);
NS_ENSURE_SUCCESS(rv, rv);
}
else {
_bookmark.grandParentId = -1;
}
+ rv = stmt->GetInt32(12, &_bookmark.syncStatus);
+ NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
nsresult
nsNavBookmarks::SetItemDateInternal(enum BookmarkDate aDateType,
+ int64_t aSyncChangeDelta,
int64_t aItemId,
PRTime aValue)
{
aValue = RoundToMilliseconds(aValue);
nsCOMPtr<mozIStorageStatement> stmt;
if (aDateType == DATE_ADDED) {
// lastModified is set to the same value as dateAdded. We do this for
// performance reasons, since it will allow us to use an index to sort items
// by date.
stmt = mDB->GetStatement(
- "UPDATE moz_bookmarks SET dateAdded = :date, lastModified = :date "
+ "UPDATE moz_bookmarks SET dateAdded = :date, lastModified = :date, "
+ "syncChangeCounter = syncChangeCounter + :delta "
"WHERE id = :item_id"
);
}
else {
stmt = mDB->GetStatement(
- "UPDATE moz_bookmarks SET lastModified = :date WHERE id = :item_id"
+ "UPDATE moz_bookmarks SET lastModified = :date, "
+ "syncChangeCounter = syncChangeCounter + :delta "
+ "WHERE id = :item_id"
);
}
NS_ENSURE_STATE(stmt);
mozStorageStatementScoper scoper(stmt);
nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("date"), aValue);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), aItemId);
NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("delta"), aSyncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->Execute();
NS_ENSURE_SUCCESS(rv, rv);
// note, we are not notifying the observers
// that the item has changed.
return NS_OK;
@@ -1440,21 +1576,42 @@ nsNavBookmarks::SetItemDateAdded(int64_t
uint16_t aSource)
{
NS_ENSURE_ARG_MIN(aItemId, 1);
BookmarkData bookmark;
nsresult rv = FetchItemInfo(aItemId, bookmark);
NS_ENSURE_SUCCESS(rv, rv);
+ bool isTagging = bookmark.grandParentId == mTagsRoot;
+ int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);
+
// Round here so that we notify with the right value.
bookmark.dateAdded = RoundToMilliseconds(aDateAdded);
- rv = SetItemDateInternal(DATE_ADDED, bookmark.id, bookmark.dateAdded);
- NS_ENSURE_SUCCESS(rv, rv);
+ if (isTagging) {
+ // If we're changing a tag, bump the change counter for all tagged
+ // bookmarks. We use a separate code path to avoid a transaction for
+ // non-tags.
+ mozStorageTransaction transaction(mDB->MainConn(), false);
+
+ rv = SetItemDateInternal(DATE_ADDED, syncChangeDelta, bookmark.id,
+ bookmark.dateAdded);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = AddSyncChangesForBookmarksWithURL(bookmark.url, syncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ rv = SetItemDateInternal(DATE_ADDED, syncChangeDelta, bookmark.id,
+ bookmark.dateAdded);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
// Note: mDBSetItemDateAdded also sets lastModified to aDateAdded.
NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
nsINavBookmarkObserver,
OnItemChanged(bookmark.id,
NS_LITERAL_CSTRING("dateAdded"),
false,
nsPrintfCString("%lld", bookmark.dateAdded),
@@ -1489,21 +1646,42 @@ nsNavBookmarks::SetItemLastModified(int6
uint16_t aSource)
{
NS_ENSURE_ARG_MIN(aItemId, 1);
BookmarkData bookmark;
nsresult rv = FetchItemInfo(aItemId, bookmark);
NS_ENSURE_SUCCESS(rv, rv);
+ bool isTagging = bookmark.grandParentId == mTagsRoot;
+ int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);
+
// Round here so that we notify with the right value.
bookmark.lastModified = RoundToMilliseconds(aLastModified);
- rv = SetItemDateInternal(LAST_MODIFIED, bookmark.id, bookmark.lastModified);
- NS_ENSURE_SUCCESS(rv, rv);
+ if (isTagging) {
+ // If we're changing a tag, bump the change counter for all tagged
+ // bookmarks. We use a separate code path to avoid a transaction for
+ // non-tags.
+ mozStorageTransaction transaction(mDB->MainConn(), false);
+
+ rv = SetItemDateInternal(LAST_MODIFIED, syncChangeDelta, bookmark.id,
+ bookmark.lastModified);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = AddSyncChangesForBookmarksWithURL(bookmark.url, syncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ rv = SetItemDateInternal(LAST_MODIFIED, syncChangeDelta, bookmark.id,
+ bookmark.lastModified);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
// Note: mDBSetItemDateAdded also sets lastModified to aDateAdded.
NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
nsINavBookmarkObserver,
OnItemChanged(bookmark.id,
NS_LITERAL_CSTRING("lastModified"),
false,
nsPrintfCString("%lld", bookmark.lastModified),
@@ -1528,54 +1706,249 @@ nsNavBookmarks::GetItemLastModified(int6
nsresult rv = FetchItemInfo(aItemId, bookmark);
NS_ENSURE_SUCCESS(rv, rv);
*_lastModified = bookmark.lastModified;
return NS_OK;
}
+nsresult
+nsNavBookmarks::AddSyncChangesForBookmarksWithURL(const nsACString& aURL,
+ int64_t aSyncChangeDelta)
+{
+ if (!aSyncChangeDelta) {
+ return NS_OK;
+ }
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // Ignore sync changes for invalid URLs.
+ return NS_OK;
+ }
+ return AddSyncChangesForBookmarksWithURI(uri, aSyncChangeDelta);
+}
+
+
+nsresult
+nsNavBookmarks::AddSyncChangesForBookmarksWithURI(nsIURI* aURI,
+ int64_t aSyncChangeDelta)
+{
+ if (NS_WARN_IF(!aURI) || !aSyncChangeDelta) {
+ // Ignore sync changes for invalid URIs.
+ return NS_OK;
+ }
+
+ nsCOMPtr<mozIStorageStatement> statement = mDB->GetStatement(
+ "UPDATE moz_bookmarks SET "
+ "syncChangeCounter = syncChangeCounter + :delta "
+ "WHERE type = :type AND "
+ "fk = (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND "
+ "url = :url)"
+ );
+ NS_ENSURE_STATE(statement);
+ mozStorageStatementScoper scoper(statement);
+
+ nsresult rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("delta"),
+ aSyncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("type"),
+ nsINavBookmarksService::TYPE_BOOKMARK);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = URIBinder::Bind(statement, NS_LITERAL_CSTRING("url"), aURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return statement->Execute();
+}
+
+
+nsresult
+nsNavBookmarks::AddSyncChangesForBookmarksInFolder(int64_t aFolderId,
+ int64_t aSyncChangeDelta)
+{
+ if (!aSyncChangeDelta) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<mozIStorageStatement> statement = mDB->GetStatement(
+ "UPDATE moz_bookmarks SET "
+ "syncChangeCounter = syncChangeCounter + :delta "
+ "WHERE type = :type AND "
+ "fk = (SELECT fk FROM moz_bookmarks WHERE parent = :parent)"
+ );
+ NS_ENSURE_STATE(statement);
+ mozStorageStatementScoper scoper(statement);
+
+ nsresult rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("delta"),
+ aSyncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("type"),
+ nsINavBookmarksService::TYPE_BOOKMARK);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aFolderId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = statement->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+nsresult
+nsNavBookmarks::InsertTombstone(const BookmarkData& aBookmark)
+{
+ if (!NeedsTombstone(aBookmark)) {
+ return NS_OK;
+ }
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "INSERT INTO moz_bookmarks_deleted (guid, dateRemoved) "
+ "VALUES (:guid, :date_removed)"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"),
+ aBookmark.guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("date_removed"),
+ RoundedPRNow());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+nsresult
+nsNavBookmarks::InsertTombstones(const nsTArray<TombstoneData>& aTombstones)
+{
+ if (aTombstones.IsEmpty()) {
+ return NS_OK;
+ }
+
+ size_t maxRowsPerChunk = SQLITE_MAX_VARIABLE_NUMBER / 2;
+ for (uint32_t startIndex = 0; startIndex < aTombstones.Length(); startIndex += maxRowsPerChunk) {
+ size_t rowsPerChunk = std::min(maxRowsPerChunk, aTombstones.Length() - startIndex);
+
+ // Build a query to insert all tombstones in a single statement, chunking to
+ // avoid the SQLite bound parameter limit.
+ nsAutoCString tombstonesToInsert;
+ tombstonesToInsert.AppendLiteral("VALUES (?, ?)");
+ for (uint32_t i = 1; i < rowsPerChunk; ++i) {
+ tombstonesToInsert.AppendLiteral(", (?, ?)");
+ }
+#ifdef DEBUG
+ MOZ_ASSERT(tombstonesToInsert.CountChar('?') == rowsPerChunk * 2,
+ "Expected one binding param per column for each tombstone");
+#endif
+
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ NS_LITERAL_CSTRING("INSERT INTO moz_bookmarks_deleted "
+ "(guid, dateRemoved) ") +
+ tombstonesToInsert
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ uint32_t paramIndex = 0;
+ nsresult rv;
+ for (uint32_t i = 0; i < rowsPerChunk; ++i) {
+ const TombstoneData& tombstone = aTombstones[startIndex + i];
+ rv = stmt->BindUTF8StringByIndex(paramIndex++, tombstone.guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByIndex(paramIndex++, tombstone.dateRemoved);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+
+nsresult
+nsNavBookmarks::RemoveTombstone(const nsACString& aGUID)
+{
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "DELETE FROM moz_bookmarks_deleted WHERE guid = :guid"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), aGUID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return stmt->Execute();
+}
+
+
+nsresult
+nsNavBookmarks::PreventSyncReparenting(const BookmarkData& aBookmark)
+{
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "DELETE FROM moz_items_annos WHERE "
+ "item_id = :item_id AND "
+ "anno_attribute_id = (SELECT id FROM moz_anno_attributes "
+ "WHERE name = :orphan_anno)"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), aBookmark.id);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("orphan_anno"),
+ NS_LITERAL_CSTRING(SYNC_PARENT_ANNO));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
NS_IMETHODIMP
nsNavBookmarks::SetItemTitle(int64_t aItemId, const nsACString& aTitle,
uint16_t aSource)
{
NS_ENSURE_ARG_MIN(aItemId, 1);
BookmarkData bookmark;
nsresult rv = FetchItemInfo(aItemId, bookmark);
NS_ENSURE_SUCCESS(rv, rv);
- nsCOMPtr<mozIStorageStatement> statement = mDB->GetStatement(
- "UPDATE moz_bookmarks SET title = :item_title, lastModified = :date "
- "WHERE id = :item_id "
- );
- NS_ENSURE_STATE(statement);
- mozStorageStatementScoper scoper(statement);
-
- nsCString title;
+ bool isChangingTagFolder = bookmark.parentId == mTagsRoot;
+ int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);
+
+ nsAutoCString title;
TruncateTitle(aTitle, title);
- // Support setting a null title, we support this in insertBookmark.
- if (title.IsVoid()) {
- rv = statement->BindNullByName(NS_LITERAL_CSTRING("item_title"));
+ if (isChangingTagFolder) {
+ // If we're changing the title of a tag folder, bump the change counter
+ // for all tagged bookmarks. We use a separate code path to avoid a
+ // transaction for non-tags.
+ mozStorageTransaction transaction(mDB->MainConn(), false);
+
+ rv = SetItemTitleInternal(bookmark, aTitle, syncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = AddSyncChangesForBookmarksInFolder(bookmark.id, syncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ rv = SetItemTitleInternal(bookmark, title, syncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
}
- else {
- rv = statement->BindUTF8StringByName(NS_LITERAL_CSTRING("item_title"),
- title);
- }
- NS_ENSURE_SUCCESS(rv, rv);
- bookmark.lastModified = RoundToMilliseconds(RoundedPRNow());
- rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("date"),
- bookmark.lastModified);
- NS_ENSURE_SUCCESS(rv, rv);
- rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), bookmark.id);
- NS_ENSURE_SUCCESS(rv, rv);
-
- rv = statement->Execute();
- NS_ENSURE_SUCCESS(rv, rv);
NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
nsINavBookmarkObserver,
OnItemChanged(bookmark.id,
NS_LITERAL_CSTRING("title"),
false,
title,
bookmark.lastModified,
@@ -1584,16 +1957,57 @@ nsNavBookmarks::SetItemTitle(int64_t aIt
bookmark.guid,
bookmark.parentGuid,
EmptyCString(),
aSource));
return NS_OK;
}
+nsresult
+nsNavBookmarks::SetItemTitleInternal(BookmarkData& aBookmark,
+ const nsACString& aTitle,
+ int64_t aSyncChangeDelta)
+{
+ nsCOMPtr<mozIStorageStatement> statement = mDB->GetStatement(
+ "UPDATE moz_bookmarks SET "
+ "title = :item_title, lastModified = :date, "
+ "syncChangeCounter = syncChangeCounter + :delta "
+ "WHERE id = :item_id"
+ );
+ NS_ENSURE_STATE(statement);
+ mozStorageStatementScoper scoper(statement);
+
+ // Support setting a null title, we support this in insertBookmark.
+ nsresult rv;
+ if (aTitle.IsVoid()) {
+ rv = statement->BindNullByName(NS_LITERAL_CSTRING("item_title"));
+ }
+ else {
+ rv = statement->BindUTF8StringByName(NS_LITERAL_CSTRING("item_title"),
+ aTitle);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ aBookmark.lastModified = RoundToMilliseconds(RoundedPRNow());
+ rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("date"),
+ aBookmark.lastModified);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), aBookmark.id);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("delta"),
+ aSyncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = statement->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
NS_IMETHODIMP
nsNavBookmarks::GetItemTitle(int64_t aItemId,
nsACString& _title)
{
NS_ENSURE_ARG_MIN(aItemId, 1);
BookmarkData bookmark;
nsresult rv = FetchItemInfo(aItemId, bookmark);
@@ -2011,43 +2425,59 @@ nsNavBookmarks::ChangeBookmarkURI(int64_
BookmarkData bookmark;
nsresult rv = FetchItemInfo(aBookmarkId, bookmark);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_ARG(bookmark.type == TYPE_BOOKMARK);
mozStorageTransaction transaction(mDB->MainConn(), false);
+ bool isTagging = bookmark.grandParentId == mTagsRoot;
+ int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);
+
nsNavHistory* history = nsNavHistory::GetHistoryService();
NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
int64_t newPlaceId;
nsAutoCString newPlaceGuid;
rv = history->GetOrCreateIdForPage(aNewURI, &newPlaceId, newPlaceGuid);
NS_ENSURE_SUCCESS(rv, rv);
if (!newPlaceId)
return NS_ERROR_INVALID_ARG;
nsCOMPtr<mozIStorageStatement> statement = mDB->GetStatement(
- "UPDATE moz_bookmarks SET fk = :page_id, lastModified = :date "
+ "UPDATE moz_bookmarks SET "
+ "fk = :page_id, lastModified = :date, "
+ "syncChangeCounter = syncChangeCounter + :delta "
"WHERE id = :item_id "
);
NS_ENSURE_STATE(statement);
mozStorageStatementScoper scoper(statement);
rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), newPlaceId);
NS_ENSURE_SUCCESS(rv, rv);
bookmark.lastModified = RoundedPRNow();
rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("date"),
bookmark.lastModified);
NS_ENSURE_SUCCESS(rv, rv);
rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), bookmark.id);
NS_ENSURE_SUCCESS(rv, rv);
+ rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("delta"), syncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
rv = statement->Execute();
NS_ENSURE_SUCCESS(rv, rv);
+ if (isTagging) {
+ // For consistency with the tagging service behavior, changing a tag entry's
+ // URL bumps the change counter for bookmarks with the old and new URIs.
+ rv = AddSyncChangesForBookmarksWithURL(bookmark.url, syncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = AddSyncChangesForBookmarksWithURI(aNewURI, syncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
rv = transaction.Commit();
NS_ENSURE_SUCCESS(rv, rv);
rv = history->UpdateFrecency(newPlaceId);
NS_ENSURE_SUCCESS(rv, rv);
// Upon changing the URI for a bookmark, update the frecency for the old
// place as well.
@@ -2144,17 +2574,17 @@ nsNavBookmarks::GetBookmarksForURI(nsIUR
{
NS_ENSURE_ARG(aURI);
// Double ordering covers possible lastModified ties, that could happen when
// importing, syncing or due to extensions.
// Note: not using a JOIN is cheaper in this case.
nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
"/* do not warn (bug 1175249) */ "
- "SELECT b.id, b.guid, b.parent, b.lastModified, t.guid, t.parent "
+ "SELECT b.id, b.guid, b.parent, b.lastModified, t.guid, t.parent, b.syncStatus "
"FROM moz_bookmarks b "
"JOIN moz_bookmarks t on t.id = b.parent "
"WHERE b.fk = (SELECT id FROM moz_places WHERE url_hash = hash(:page_url) AND url = :page_url) "
"ORDER BY b.lastModified DESC, b.id DESC "
);
NS_ENSURE_STATE(stmt);
mozStorageStatementScoper scoper(stmt);
@@ -2179,16 +2609,18 @@ nsNavBookmarks::GetBookmarksForURI(nsIUR
rv = stmt->GetUTF8String(1, bookmark.guid);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->GetInt64(2, &bookmark.parentId);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->GetInt64(3, reinterpret_cast<int64_t*>(&bookmark.lastModified));
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->GetUTF8String(4, bookmark.parentGuid);
NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt32(6, &bookmark.syncStatus);
+ NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_TRUE(aBookmarks.AppendElement(bookmark), NS_ERROR_OUT_OF_MEMORY);
}
return NS_OK;
}
NS_IMETHODIMP
@@ -2258,28 +2690,64 @@ nsNavBookmarks::SetItemIndex(int64_t aIt
int64_t grandParentId;
nsAutoCString folderGuid;
rv = FetchFolderInfo(bookmark.parentId, &folderCount, folderGuid, &grandParentId);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_TRUE(aNewIndex < folderCount, NS_ERROR_INVALID_ARG);
// Check the parent's guid is the expected one.
MOZ_ASSERT(bookmark.parentGuid == folderGuid);
- nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
- "UPDATE moz_bookmarks SET position = :item_index WHERE id = :item_id"
- );
- NS_ENSURE_STATE(stmt);
- mozStorageStatementScoper scoper(stmt);
-
- rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), aItemId);
+ mozStorageTransaction transaction(mDB->MainConn(), false);
+
+ {
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "UPDATE moz_bookmarks SET "
+ "position = :item_index "
+ "WHERE id = :item_id"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), aItemId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_index"), aNewIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);
+
+ {
+ // Sync stores child indices in the parent's record, so we only need to
+ // bump the parent's change counter.
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "UPDATE moz_bookmarks SET "
+ "syncChangeCounter = syncChangeCounter + :delta "
+ "WHERE id = :parent_id"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent_id"),
+ bookmark.parentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("delta"),
+ syncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = PreventSyncReparenting(bookmark);
NS_ENSURE_SUCCESS(rv, rv);
- rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_index"), aNewIndex);
- NS_ENSURE_SUCCESS(rv, rv);
-
- rv = stmt->Execute();
+
+ rv = transaction.Commit();
NS_ENSURE_SUCCESS(rv, rv);
NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
nsINavBookmarkObserver,
OnItemMoved(bookmark.id,
bookmark.parentId,
bookmark.position,
bookmark.parentId,
@@ -2334,17 +2802,21 @@ nsNavBookmarks::SetKeywordForBookmark(in
}
}
// Trying to remove a non-existent keyword is a no-op.
if (keyword.IsEmpty() && oldKeywords.Length() == 0) {
return NS_OK;
}
+ int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);
+
if (keyword.IsEmpty()) {
+ mozStorageTransaction removeTxn(mDB->MainConn(), false);
+
// We are removing the existing keywords.
for (uint32_t i = 0; i < oldKeywords.Length(); ++i) {
nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
"DELETE FROM moz_keywords WHERE keyword = :old_keyword"
);
NS_ENSURE_STATE(stmt);
mozStorageStatementScoper scoper(stmt);
rv = stmt->BindStringByName(NS_LITERAL_CSTRING("old_keyword"),
@@ -2352,16 +2824,47 @@ nsNavBookmarks::SetKeywordForBookmark(in
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->Execute();
NS_ENSURE_SUCCESS(rv, rv);
}
nsTArray<BookmarkData> bookmarks;
rv = GetBookmarksForURI(uri, bookmarks);
NS_ENSURE_SUCCESS(rv, rv);
+
+ if (syncChangeDelta && !bookmarks.IsEmpty()) {
+ // Build a query to update all bookmarks in a single statement.
+ nsAutoCString changedIds;
+ changedIds.AppendInt(bookmarks[0].id);
+ for (uint32_t i = 1; i < bookmarks.Length(); ++i) {
+ changedIds.Append(',');
+ changedIds.AppendInt(bookmarks[i].id);
+ }
+ // Update the sync change counter for all bookmarks with the removed
+ // keyword.
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ NS_LITERAL_CSTRING(
+ "UPDATE moz_bookmarks SET "
+ "syncChangeCounter = syncChangeCounter + :delta "
+ "WHERE id IN (") + changedIds + NS_LITERAL_CSTRING(")")
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("delta"),
+ syncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = removeTxn.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
nsINavBookmarkObserver,
OnItemChanged(bookmarks[i].id,
NS_LITERAL_CSTRING("keyword"),
false,
EmptyCString(),
bookmarks[i].lastModified,
@@ -2400,16 +2903,18 @@ nsNavBookmarks::SetKeywordForBookmark(in
rv = NS_NewURI(getter_AddRefs(oldUri), spec);
NS_ENSURE_SUCCESS(rv, rv);
}
}
// If another uri is using the new keyword, we must update the keyword entry.
// Note we cannot use INSERT OR REPLACE cause it wouldn't invoke the delete
// trigger.
+ mozStorageTransaction updateTxn(mDB->MainConn(), false);
+
nsCOMPtr<mozIStorageStatement> stmt;
if (oldUri) {
// In both cases, notify about the change.
nsTArray<BookmarkData> bookmarks;
rv = GetBookmarksForURI(oldUri, bookmarks);
NS_ENSURE_SUCCESS(rv, rv);
for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
@@ -2443,20 +2948,49 @@ nsNavBookmarks::SetKeywordForBookmark(in
rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("place_id"), bookmark.placeId);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->Execute();
NS_ENSURE_SUCCESS(rv, rv);
- // In both cases, notify about the change.
nsTArray<BookmarkData> bookmarks;
rv = GetBookmarksForURI(uri, bookmarks);
NS_ENSURE_SUCCESS(rv, rv);
+
+ if (syncChangeDelta && !bookmarks.IsEmpty()) {
+ // Build a query to update all bookmarks in a single statement.
+ nsAutoCString changedIds;
+ changedIds.AppendInt(bookmarks[0].id);
+ for (uint32_t i = 1; i < bookmarks.Length(); ++i) {
+ changedIds.Append(',');
+ changedIds.AppendInt(bookmarks[i].id);
+ }
+ // Update the sync change counter for all bookmarks with the new keyword.
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ NS_LITERAL_CSTRING(
+ "UPDATE moz_bookmarks SET "
+ "syncChangeCounter = syncChangeCounter + :delta "
+ "WHERE id IN (") + changedIds + NS_LITERAL_CSTRING(")")
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("delta"), syncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = updateTxn.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // In both cases, notify about the change.
for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
nsINavBookmarkObserver,
OnItemChanged(bookmarks[i].id,
NS_LITERAL_CSTRING("keyword"),
false,
NS_ConvertUTF16toUTF8(keyword),
bookmarks[i].lastModified,
@@ -2893,17 +3427,18 @@ NS_IMETHODIMP
nsNavBookmarks::OnItemAnnotationSet(int64_t aItemId, const nsACString& aName,
uint16_t aSource)
{
BookmarkData bookmark;
nsresult rv = FetchItemInfo(aItemId, bookmark);
NS_ENSURE_SUCCESS(rv, rv);
bookmark.lastModified = RoundedPRNow();
- rv = SetItemDateInternal(LAST_MODIFIED, bookmark.id, bookmark.lastModified);
+ 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(),
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_sync_fields.js
@@ -0,0 +1,384 @@
+// Tracks a set of bookmark guids and their syncChangeCounter field and
+// provides a simple way for the test to check the correct fields had the
+// counter incremented.
+class CounterTracker {
+ constructor() {
+ this.tracked = new Map();
+ }
+
+ async _getCounter(guid) {
+ let fields = await PlacesTestUtils.fetchBookmarkSyncFields(guid);
+ if (!fields.length) {
+ throw new Error(`Item ${guid} does not exist`);
+ }
+ return fields[0].syncChangeCounter;
+ }
+
+ // Call this after creating a new bookmark.
+ async track(guid, name, expectedInitial = 1) {
+ if (this.tracked.has(guid)) {
+ throw new Error(`Already tracking item ${guid}`);
+ }
+ let initial = await this._getCounter(guid);
+ Assert.equal(initial, expectedInitial, `Initial value of item '${name}' is correct`);
+ this.tracked.set(guid, { name, value: expectedInitial });
+ }
+
+ // Call this to check *only* the specified IDs had a change increment, and
+ // that none of the other "tracked" ones did.
+ async check(...expectedToIncrement) {
+ do_print(`Checking counter for items ${JSON.stringify(expectedToIncrement)}`);
+ for (let [guid, entry] of this.tracked) {
+ let { name, value } = entry;
+ let newValue = await this._getCounter(guid);
+ let desc = `record '${name}' (guid=${guid})`;
+ if (expectedToIncrement.includes(guid)) {
+ // Note we don't check specifically for +1, as some changes will
+ // increment the counter by more than 1 (which is OK).
+ Assert.ok(newValue > value,
+ `${desc} was expected to increment - was ${value}, now ${newValue}`);
+ this.tracked.set(guid, { name, value: newValue });
+ } else {
+ Assert.equal(newValue, value, `${desc} was NOT expected to increment`);
+ }
+ }
+ }
+}
+
+async function checkSyncFields(guid, expected) {
+ let results = await PlacesTestUtils.fetchBookmarkSyncFields(guid);
+ if (!results.length) {
+ throw new Error(`Missing sync fields for ${guid}`);
+ }
+ for (let name in expected) {
+ let expectedValue = expected[name];
+ Assert.equal(results[0][name], expectedValue, `field ${name} matches item ${guid}`);
+ }
+}
+
+// Common test cases for sync field changes.
+class TestCases {
+ async run() {
+ do_print("Test 1: inserts, updates, tags, and keywords");
+ try {
+ await this.testChanges();
+ } finally {
+ do_print("Reset sync fields after test 1");
+ await PlacesTestUtils.markBookmarksAsSynced();
+ }
+
+ do_print("Test 2: reparenting");
+ try {
+ await this.testReparenting();
+ } finally {
+ do_print("Reset sync fields after test 2");
+ await PlacesTestUtils.markBookmarksAsSynced();
+ }
+ }
+
+ async testChanges() {
+ let testUri = NetUtil.newURI("http://test.mozilla.org");
+
+ let guid = await this.insertBookmark(PlacesUtils.bookmarks.unfiledGuid,
+ testUri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark title");
+ do_print(`Inserted bookmark ${guid}`);
+ await checkSyncFields(guid, { syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NEW,
+ syncChangeCounter: 1 });
+
+ // Pretend Sync just did whatever it does
+ await PlacesTestUtils.setBookmarkSyncFields({ guid,
+ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL });
+ do_print(`Updated sync status of ${guid}`);
+ await checkSyncFields(guid, { syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ syncChangeCounter: 1 });
+
+ // update it - it should increment the change counter
+ await this.setTitle(guid, "new title");
+ do_print(`Changed title of ${guid}`);
+ await checkSyncFields(guid, { syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ syncChangeCounter: 2 });
+
+ await this.setAnno(guid, "random-anno", "random-value");
+ do_print(`Set anno on ${guid}`);
+ await checkSyncFields(guid, { syncChangeCounter: 3 });
+
+ // Tagging a bookmark should update its change counter.
+ await this.tagURI(testUri, ["test-tag"]);
+ do_print(`Tagged bookmark ${guid}`);
+ await checkSyncFields(guid, { syncChangeCounter: 4 });
+
+ await this.setKeyword(guid, "keyword");
+ do_print(`Set keyword for bookmark ${guid}`);
+ await checkSyncFields(guid, { syncChangeCounter: 5 });
+
+ await this.removeKeyword(guid, "keyword");
+ do_print(`Removed keyword from bookmark ${guid}`);
+ await checkSyncFields(guid, { syncChangeCounter: 6 });
+ }
+
+ async testReparenting() {
+ let counterTracker = new CounterTracker();
+
+ let folder1 = await this.createFolder(PlacesUtils.bookmarks.unfiledGuid,
+ "folder1",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ do_print(`Created the first folder, guid is ${folder1}`);
+
+ // New folder should have a change recorded.
+ await counterTracker.track(folder1, "folder 1");
+
+ // Put a new bookmark in the folder.
+ let testUri = NetUtil.newURI("http://test2.mozilla.org");
+ let child1 = await this.insertBookmark(folder1,
+ testUri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark 1");
+ do_print(`Created a new bookmark into ${folder1}, guid is ${child1}`);
+ // both the folder and the child should have a change recorded.
+ await counterTracker.track(child1, "child 1");
+ await counterTracker.check(folder1);
+
+ // A new child in the folder at index 0 - even though the existing child
+ // was bumped down the list, it should *not* have a change recorded.
+ let child2 = await this.insertBookmark(folder1,
+ testUri,
+ 0,
+ "bookmark 2");
+ do_print(`Created a second new bookmark into folder ${folder1}, guid is ${child2}`);
+
+ await counterTracker.track(child2, "child 2");
+ await counterTracker.check(folder1);
+
+ // Move the items within the same folder - this should result in just a
+ // change for the parent, but for neither of the children.
+ // child0 is currently at index 0, so move child1 there.
+ await this.moveItem(child1, folder1, 0);
+ await counterTracker.check(folder1);
+
+ // Another folder to play with.
+ let folder2 = await this.createFolder(PlacesUtils.bookmarks.unfiledGuid,
+ "folder2",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ do_print(`Created a second new folder, guid is ${folder2}`);
+ await counterTracker.track(folder2, "folder 2");
+ // nothing else has changed.
+ await counterTracker.check();
+
+ // Move one of the children to the new folder.
+ do_print(`Moving bookmark ${child2} from folder ${folder1} to folder ${folder2}`);
+ await this.moveItem(child2, folder2, PlacesUtils.bookmarks.DEFAULT_INDEX);
+ // child1 should have no change, everything should have a new change.
+ await counterTracker.check(folder1, folder2, child2);
+
+ // Move the new folder to another root.
+ await this.moveItem(folder2, PlacesUtils.bookmarks.toolbarGuid,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ do_print(`Moving folder ${folder2} to toolbar`);
+ await counterTracker.check(folder2, PlacesUtils.bookmarks.toolbarGuid,
+ PlacesUtils.bookmarks.unfiledGuid);
+
+ let child3 = await this.insertBookmark(folder2,
+ testUri,
+ 0,
+ "bookmark 3");
+ do_print(`Prepended child ${child3} to folder ${folder2}`);
+ await counterTracker.check(folder2, child3);
+
+ // Reordering should only track the parent.
+ await this.reorder(folder2, [child2, child3]);
+ do_print(`Reorder children of ${folder2}`);
+ await counterTracker.check(folder2);
+
+ // All fields still have a syncStatus of SYNC_STATUS_NEW - so deleting them
+ // should *not* cause any deleted items to be written.
+ await this.removeItem(folder1);
+ Assert.equal((await PlacesTestUtils.fetchSyncTombstones()).length, 0);
+
+ // Set folder2 and child2 to have a state of SYNC_STATUS_NORMAL and deleting
+ // them will cause both GUIDs to be written to moz_bookmarks_deleted.
+ await PlacesTestUtils.setBookmarkSyncFields({ guid: folder2,
+ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL });
+ await PlacesTestUtils.setBookmarkSyncFields({ guid: child2,
+ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL });
+ await this.removeItem(folder2);
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ let tombstoneGuids = sortBy(tombstones, "guid").map(({ guid }) => guid);
+ Assert.equal(tombstoneGuids.length, 2);
+ Assert.deepEqual(tombstoneGuids, [folder2, child2].sort(compareAscending));
+ }
+
+ // Annos don't have an async API, so we use the sync API for both tests.
+ async setAnno(guid, name, value) {
+ let id = await PlacesUtils.promiseItemId(guid);
+ PlacesUtils.annotations.setItemAnnotation(id, name, value, 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ }
+}
+
+// Exercises the legacy, synchronous `nsINavBookmarksService` calls implemented
+// in C++.
+class SyncTestCases extends TestCases {
+ async createFolder(parentGuid, title, index) {
+ let parentId = await PlacesUtils.promiseItemId(parentGuid);
+ let id = PlacesUtils.bookmarks.createFolder(parentId, title, index);
+ return await PlacesUtils.promiseItemGuid(id);
+ }
+
+ async insertBookmark(parentGuid, uri, index, title) {
+ let parentId = await PlacesUtils.promiseItemId(parentGuid);
+ let id = PlacesUtils.bookmarks.insertBookmark(parentId, uri, index, title);
+ return await PlacesUtils.promiseItemGuid(id);
+ }
+
+ async moveItem(guid, newParentGuid, index) {
+ let id = await PlacesUtils.promiseItemId(guid);
+ let newParentId = await PlacesUtils.promiseItemId(newParentGuid);
+ PlacesUtils.bookmarks.moveItem(id, newParentId, index);
+ }
+
+ async removeItem(guid) {
+ let id = await PlacesUtils.promiseItemId(guid);
+ PlacesUtils.bookmarks.removeItem(id);
+ }
+
+ async setTitle(guid, title) {
+ let id = await PlacesUtils.promiseItemId(guid);
+ PlacesUtils.bookmarks.setItemTitle(id, title);
+ }
+
+ async setKeyword(guid, keyword) {
+ let id = await PlacesUtils.promiseItemId(guid);
+ PlacesUtils.bookmarks.setKeywordForBookmark(id, keyword);
+ }
+
+ async removeKeyword(guid, keyword) {
+ let id = await PlacesUtils.promiseItemId(guid);
+ if (PlacesUtils.bookmarks.getKeywordForBookmark(id) != keyword) {
+ throw new Error(`Keyword ${keyword} not set for bookmark ${guid}`);
+ }
+ PlacesUtils.bookmarks.setKeywordForBookmark(id, "");
+ }
+
+ async tagURI(uri, tags) {
+ PlacesUtils.tagging.tagURI(uri, tags);
+ }
+
+ async reorder(parentGuid, childGuids) {
+ let parentId = await PlacesUtils.promiseItemId(parentGuid);
+ for (let index = 0; index < childGuids.length; ++index) {
+ let id = await PlacesUtils.promiseItemId(childGuids[index]);
+ PlacesUtils.bookmarks.moveItem(id, parentId, index);
+ }
+ }
+}
+
+async function findTagFolder(tag) {
+ let db = await PlacesUtils.promiseDBConnection()
+ let results = await db.executeCached(`
+ SELECT guid
+ FROM moz_bookmarks
+ WHERE type = :type AND
+ parent = :tagsFolderId AND
+ title = :tag`,
+ { type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ tagsFolderId: PlacesUtils.tagsFolderId, tag });
+ return results.length ? results[0].getResultByName("guid") : null;
+}
+
+// Exercises the new, async calls implemented in `Bookmarks.jsm`.
+class AsyncTestCases extends TestCases {
+ async createFolder(parentGuid, title, index) {
+ let item = await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid,
+ title,
+ index,
+ });
+ return item.guid;
+ }
+
+ async insertBookmark(parentGuid, uri, index, title) {
+ let item = await PlacesUtils.bookmarks.insert({
+ parentGuid,
+ url: uri,
+ index,
+ title,
+ });
+ return item.guid;
+ }
+
+ async moveItem(guid, newParentGuid, index) {
+ await PlacesUtils.bookmarks.update({
+ guid,
+ parentGuid: newParentGuid,
+ index,
+ });
+ }
+
+ async removeItem(guid) {
+ await PlacesUtils.bookmarks.remove(guid);
+ }
+
+ async setTitle(guid, title) {
+ await PlacesUtils.bookmarks.update({ guid, title });
+ }
+
+ async setKeyword(guid, keyword) {
+ let item = await PlacesUtils.bookmarks.fetch(guid);
+ if (!item) {
+ throw new Error(`Cannot set keyword ${
+ keyword} on nonexistent bookmark ${guid}`);
+ }
+ await PlacesUtils.keywords.insert({ keyword, url: item.url });
+ }
+
+ async removeKeyword(guid, keyword) {
+ let item = await PlacesUtils.bookmarks.fetch(guid);
+ if (!item) {
+ throw new Error(`Cannot remove keyword ${
+ keyword} from nonexistent bookmark ${guid}`);
+ }
+ let entry = await PlacesUtils.keywords.fetch({ keyword, url: item.url });
+ if (!entry) {
+ throw new Error(`Keyword ${keyword} not set on bookmark ${guid}`);
+ }
+ await PlacesUtils.keywords.remove(entry);
+ }
+
+ // There's no async API for tags, but the `PlacesUtils.bookmarks` methods are
+ // tag-aware, and should bump the change counters for tagged bookmarks when
+ // called directly.
+ async tagURI(uri, tags) {
+ for (let tag of tags) {
+ let tagFolderGuid = await findTagFolder(tag);
+ if (!tagFolderGuid) {
+ let tagFolder = await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.tagsGuid,
+ title: tag,
+ });
+ tagFolderGuid = tagFolder.guid;
+ }
+ await PlacesUtils.bookmarks.insert({
+ url: uri,
+ parentGuid: tagFolderGuid,
+ });
+ }
+ }
+
+ async reorder(parentGuid, childGuids) {
+ await PlacesUtils.bookmarks.reorder(parentGuid, childGuids);
+ }
+}
+
+add_task(async function test_sync_api() {
+ let tests = new SyncTestCases();
+ await tests.run();
+});
+
+add_task(async function test_async_api() {
+ let tests = new AsyncTestCases();
+ await tests.run();
+});