summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/nsNavBookmarks.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /toolkit/components/places/nsNavBookmarks.cpp
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/places/nsNavBookmarks.cpp')
-rw-r--r--toolkit/components/places/nsNavBookmarks.cpp1788
1 files changed, 1788 insertions, 0 deletions
diff --git a/toolkit/components/places/nsNavBookmarks.cpp b/toolkit/components/places/nsNavBookmarks.cpp
new file mode 100644
index 0000000000..57943cae44
--- /dev/null
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -0,0 +1,1788 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsNavBookmarks.h"
+
+#include "nsNavHistory.h"
+#include "nsPlacesMacros.h"
+#include "Helpers.h"
+
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsITaggingService.h"
+#include "nsNetUtil.h"
+#include "nsUnicharUtils.h"
+#include "nsPrintfCString.h"
+#include "nsQueryObject.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/storage.h"
+#include "mozilla/dom/PlacesBookmarkAddition.h"
+#include "mozilla/dom/PlacesBookmarkRemoved.h"
+#include "mozilla/dom/PlacesBookmarkTags.h"
+#include "mozilla/dom/PlacesBookmarkTime.h"
+#include "mozilla/dom/PlacesBookmarkTitle.h"
+#include "mozilla/dom/PlacesObservers.h"
+#include "mozilla/dom/PlacesVisit.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;
+
+extern "C" {
+
+// Returns the total number of Sync changes recorded since Places startup for
+// all bookmarks. This function uses C linkage because it's called from the
+// Rust synced bookmarks mirror, on the storage thread. Using `get_service` to
+// access the bookmarks service from Rust trips a thread-safety assertion, so
+// we can't use `nsNavBookmarks::GetTotalSyncChanges`.
+int64_t NS_NavBookmarksTotalSyncChanges() {
+ return nsNavBookmarks::sTotalSyncChanges;
+}
+
+} // extern "C"
+
+PLACES_FACTORY_SINGLETON_IMPLEMENTATION(nsNavBookmarks, gBookmarksService)
+
+namespace {
+
+// 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_RESTORE_ON_STARTUP) {
+ 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;
+}
+
+inline nsresult GetTags(nsIURI* aURI, nsTArray<nsString>& aResult) {
+ nsresult rv;
+ nsCOMPtr<nsITaggingService> taggingService =
+ do_GetService("@mozilla.org/browser/tagging-service;1", &rv);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return taggingService->GetTagsForURI(aURI, aResult);
+}
+
+} // namespace
+
+nsNavBookmarks::nsNavBookmarks() : mCanNotify(false) {
+ NS_ASSERTION(!gBookmarksService,
+ "Attempting to create two instances of the service!");
+ gBookmarksService = this;
+}
+
+nsNavBookmarks::~nsNavBookmarks() {
+ NS_ASSERTION(gBookmarksService == this,
+ "Deleting a non-singleton instance of the service");
+ if (gBookmarksService == this) gBookmarksService = nullptr;
+}
+
+NS_IMPL_ISUPPORTS(nsNavBookmarks, nsINavBookmarksService, nsIObserver,
+ nsISupportsWeakReference)
+
+Atomic<int64_t> nsNavBookmarks::sLastInsertedItemId(0);
+
+void // static
+nsNavBookmarks::StoreLastInsertedId(const nsACString& aTable,
+ const int64_t aLastInsertedId) {
+ MOZ_ASSERT(aTable.EqualsLiteral("moz_bookmarks"));
+ sLastInsertedItemId = aLastInsertedId;
+}
+
+Atomic<int64_t> nsNavBookmarks::sTotalSyncChanges(0);
+
+void // static
+nsNavBookmarks::NoteSyncChange() {
+ sTotalSyncChanges++;
+}
+
+nsresult nsNavBookmarks::Init() {
+ mDB = Database::GetDatabase();
+ NS_ENSURE_STATE(mDB);
+
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ if (os) {
+ (void)os->AddObserver(this, TOPIC_PLACES_CONNECTION_CLOSED, true);
+ }
+
+ mCanNotify = true;
+
+ // DO NOT PUT STUFF HERE that can fail. See observer comment above.
+
+ return NS_OK;
+}
+
+nsresult nsNavBookmarks::AdjustIndices(int64_t aFolderId, int32_t aStartIndex,
+ int32_t aEndIndex, int32_t aDelta) {
+ NS_ASSERTION(
+ aStartIndex >= 0 && aEndIndex <= INT32_MAX && aStartIndex <= aEndIndex,
+ "Bad indices");
+
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "UPDATE moz_bookmarks SET position = position + :delta "
+ "WHERE parent = :parent "
+ "AND position BETWEEN :from_index AND :to_index");
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindInt32ByName("delta"_ns, aDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName("parent"_ns, aFolderId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName("from_index"_ns, aStartIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName("to_index"_ns, aEndIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult nsNavBookmarks::AdjustSeparatorsSyncCounter(int64_t aFolderId,
+ int32_t aStartIndex,
+ int64_t aSyncChangeDelta) {
+ MOZ_ASSERT(aStartIndex >= 0, "Bad start position");
+ if (!aSyncChangeDelta) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "UPDATE moz_bookmarks SET syncChangeCounter = syncChangeCounter + :delta "
+ "WHERE parent = :parent AND position >= :start_index "
+ "AND type = :item_type ");
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindInt64ByName("delta"_ns, aSyncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName("parent"_ns, aFolderId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName("start_index"_ns, aStartIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName("item_type"_ns, TYPE_SEPARATOR);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavBookmarks::GetTagsFolder(int64_t* aRoot) {
+ int64_t id = mDB->GetTagsFolderId();
+ NS_ENSURE_TRUE(id > 0, NS_ERROR_UNEXPECTED);
+ *aRoot = id;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavBookmarks::GetTotalSyncChanges(int64_t* aTotalSyncChanges) {
+ *aTotalSyncChanges = sTotalSyncChanges;
+ return NS_OK;
+}
+
+nsresult nsNavBookmarks::InsertBookmarkInDB(
+ int64_t aPlaceId, enum ItemType aItemType, int64_t aParentId,
+ int32_t aIndex, const nsACString& aTitle, PRTime aDateAdded,
+ PRTime aLastModified, const nsACString& aParentGuid, int64_t aGrandParentId,
+ nsIURI* aURI, uint16_t aSource, int64_t* _itemId, nsACString& _guid) {
+ // 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, syncStatus, syncChangeCounter) "
+ "VALUES (:item_id, :page_id, :item_type, :parent, :item_index, "
+ ":item_title, :date_added, :last_modified, "
+ ":item_guid, :sync_status, :change_counter)");
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv;
+ if (*_itemId != -1)
+ rv = stmt->BindInt64ByName("item_id"_ns, *_itemId);
+ else
+ rv = stmt->BindNullByName("item_id"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aPlaceId != -1)
+ rv = stmt->BindInt64ByName("page_id"_ns, aPlaceId);
+ else
+ rv = stmt->BindNullByName("page_id"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindInt32ByName("item_type"_ns, aItemType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName("parent"_ns, aParentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName("item_index"_ns, aIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aTitle.IsEmpty())
+ rv = stmt->BindNullByName("item_title"_ns);
+ else
+ rv = stmt->BindUTF8StringByName("item_title"_ns, aTitle);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindInt64ByName("date_added"_ns, aDateAdded);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aLastModified) {
+ rv = stmt->BindInt64ByName("last_modified"_ns, aLastModified);
+ } else {
+ rv = stmt->BindInt64ByName("last_modified"_ns, aDateAdded);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Could use IsEmpty because our callers check for GUID validity,
+ // but it doesn't hurt.
+ bool hasExistingGuid = _guid.Length() == 12;
+ if (hasExistingGuid) {
+ MOZ_ASSERT(IsValidGUID(_guid));
+ rv = stmt->BindUTF8StringByName("item_guid"_ns, _guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ nsAutoCString guid;
+ rv = GenerateGUID(guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindUTF8StringByName("item_guid"_ns, guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ _guid.Assign(guid);
+ }
+
+ int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);
+ rv = stmt->BindInt64ByName("change_counter"_ns, syncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint16_t syncStatus = DetermineInitialSyncStatus(aSource);
+ rv = stmt->BindInt32ByName("sync_status"_ns, 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, syncChangeDelta, aParentId,
+ aDateAdded);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ int64_t tagsRootId = mDB->GetTagsFolderId();
+ bool isTagging = aGrandParentId == tagsRootId;
+ 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);
+ }
+
+ // Mark all affected separators as changed
+ rv = AdjustSeparatorsSyncCounter(aParentId, aIndex + 1, 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.IsEmpty()) {
+ bookmark.title.Assign(aTitle);
+ }
+ bookmark.position = aIndex;
+ bookmark.placeId = aPlaceId;
+ bookmark.parentId = aParentId;
+ bookmark.type = aItemType;
+ bookmark.dateAdded = aDateAdded;
+ if (aLastModified)
+ bookmark.lastModified = aLastModified;
+ 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,
+ const nsACString& aTitle,
+ const nsACString& aGUID, uint16_t aSource,
+ int64_t* aNewBookmarkId) {
+ NS_ENSURE_ARG(aURI);
+ NS_ENSURE_ARG_POINTER(aNewBookmarkId);
+ NS_ENSURE_ARG_MIN(aIndex, nsINavBookmarksService::DEFAULT_INDEX);
+
+ if (!aGUID.IsEmpty() && !IsValidGUID(aGUID)) return NS_ERROR_INVALID_ARG;
+
+ mozStorageTransaction transaction(mDB->MainConn(), false);
+
+ // XXX Handle the error, bug 1696133.
+ Unused << NS_WARN_IF(NS_FAILED(transaction.Start()));
+
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+ int64_t placeId;
+ nsAutoCString placeGuid;
+ nsresult rv = history->GetOrCreateIdForPage(aURI, &placeId, placeGuid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the correct index for insertion. This also ensures the parent exists.
+ int32_t index, folderCount;
+ int64_t grandParentId;
+ nsAutoCString folderGuid;
+ rv = FetchFolderInfo(aFolder, &folderCount, folderGuid, &grandParentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (aIndex == nsINavBookmarksService::DEFAULT_INDEX ||
+ aIndex >= folderCount) {
+ index = folderCount;
+ } else {
+ index = aIndex;
+ // Create space for the insertion.
+ rv = AdjustIndices(aFolder, index, INT32_MAX, 1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ *aNewBookmarkId = -1;
+ PRTime dateAdded = RoundedPRNow();
+ nsAutoCString guid(aGUID);
+ nsCString title;
+ TruncateTitle(aTitle, title);
+
+ rv = InsertBookmarkInDB(placeId, BOOKMARK, aFolder, index, title, dateAdded,
+ 0, folderGuid, grandParentId, aURI, aSource,
+ aNewBookmarkId, guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If not a tag, recalculate frecency for this entry, since it changed.
+ int64_t tagsRootId = mDB->GetTagsFolderId();
+ if (grandParentId != tagsRootId) {
+ rv = history->UpdateFrecency(placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!mCanNotify) {
+ return NS_OK;
+ }
+
+ Sequence<OwningNonNull<PlacesEvent>> notifications;
+ nsAutoCString utf8spec;
+ aURI->GetSpec(utf8spec);
+
+ RefPtr<PlacesBookmarkAddition> bookmark = new PlacesBookmarkAddition();
+ bookmark->mItemType = TYPE_BOOKMARK;
+ bookmark->mId = *aNewBookmarkId;
+ bookmark->mParentId = aFolder;
+ bookmark->mIndex = index;
+ bookmark->mUrl.Assign(NS_ConvertUTF8toUTF16(utf8spec));
+ bookmark->mTitle.Assign(NS_ConvertUTF8toUTF16(title));
+ bookmark->mDateAdded = dateAdded / 1000;
+ bookmark->mGuid.Assign(guid);
+ bookmark->mParentGuid.Assign(folderGuid);
+ bookmark->mSource = aSource;
+ bookmark->mIsTagging = grandParentId == mDB->GetTagsFolderId();
+ bool success = !!notifications.AppendElement(bookmark.forget(), fallible);
+ MOZ_RELEASE_ASSERT(success);
+
+ // If the bookmark has been added to a tag container, notify all
+ // bookmark-folder result nodes which contain a bookmark for the new
+ // bookmark's url.
+ if (grandParentId == tagsRootId) {
+ // Notify a tags change to all bookmarks for this URI.
+ nsTArray<BookmarkData> bookmarks;
+ rv = GetBookmarksForURI(aURI, bookmarks);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<nsString> tags;
+ rv = GetTags(aURI, tags);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
+ // Check that bookmarks doesn't include the current tag itemId.
+ MOZ_ASSERT(bookmarks[i].id != *aNewBookmarkId);
+ RefPtr<PlacesBookmarkTags> tagsChanged = new PlacesBookmarkTags();
+ tagsChanged->mId = bookmarks[i].id;
+ tagsChanged->mItemType = TYPE_BOOKMARK;
+ tagsChanged->mUrl.Assign(NS_ConvertUTF8toUTF16(utf8spec));
+ tagsChanged->mGuid = bookmarks[i].guid;
+ tagsChanged->mParentGuid = bookmarks[i].parentGuid;
+ tagsChanged->mTags.Assign(tags);
+ tagsChanged->mLastModified = bookmarks[i].lastModified / 1000;
+ tagsChanged->mSource = aSource;
+ tagsChanged->mIsTagging = false;
+ success = !!notifications.AppendElement(tagsChanged.forget(), fallible);
+ MOZ_RELEASE_ASSERT(success);
+ }
+ }
+
+ PlacesObservers::NotifyListeners(notifications);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavBookmarks::RemoveItem(int64_t aItemId, uint16_t aSource) {
+ AUTO_PROFILER_LABEL("nsNavBookmarks::RemoveItem", OTHER);
+
+ BookmarkData bookmark;
+ nsresult rv = FetchItemInfo(aItemId, bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Check we're not trying to remove a root.
+ NS_ENSURE_ARG(bookmark.parentId > 0 && bookmark.grandParentId > 0);
+
+ mozStorageTransaction transaction(mDB->MainConn(), false);
+
+ // XXX Handle the error, bug 1696133.
+ Unused << NS_WARN_IF(NS_FAILED(transaction.Start()));
+
+ if (bookmark.type == TYPE_FOLDER) {
+ // Remove all of the folder's children.
+ rv = RemoveFolderChildren(bookmark.id, aSource);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<mozIStorageStatement> stmt =
+ mDB->GetStatement("DELETE FROM moz_bookmarks WHERE id = :item_id");
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->BindInt64ByName("item_id"_ns, bookmark.id);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // 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, syncChangeDelta, bookmark.parentId,
+ bookmark.lastModified);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Mark all affected separators as changed
+ rv = AdjustSeparatorsSyncCounter(bookmark.parentId, bookmark.position,
+ syncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t tagsRootId = mDB->GetTagsFolderId();
+ if (bookmark.grandParentId == tagsRootId) {
+ // 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 != tagsRootId) {
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+ rv = history->UpdateFrecency(bookmark.placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // A broken url should not interrupt the removal process.
+ (void)NS_NewURI(getter_AddRefs(uri), bookmark.url);
+ // We cannot assert since some automated tests are checking this path.
+ NS_WARNING_ASSERTION(uri, "Invalid URI in RemoveItem");
+ }
+
+ if (!mCanNotify) {
+ return NS_OK;
+ }
+
+ Sequence<OwningNonNull<PlacesEvent>> notifications;
+ RefPtr<PlacesBookmarkRemoved> bookmarkRef = new PlacesBookmarkRemoved();
+ bookmarkRef->mItemType = bookmark.type;
+ bookmarkRef->mId = bookmark.id;
+ bookmarkRef->mParentId = bookmark.parentId;
+ bookmarkRef->mIndex = bookmark.position;
+ if (bookmark.type == TYPE_BOOKMARK) {
+ bookmarkRef->mUrl.Assign(NS_ConvertUTF8toUTF16(bookmark.url));
+ }
+ bookmarkRef->mTitle.Assign(NS_ConvertUTF8toUTF16(bookmark.title));
+ bookmarkRef->mGuid.Assign(bookmark.guid);
+ bookmarkRef->mParentGuid.Assign(bookmark.parentGuid);
+ bookmarkRef->mSource = aSource;
+ bookmarkRef->mIsTagging =
+ bookmark.parentId == tagsRootId || bookmark.grandParentId == tagsRootId;
+ bookmarkRef->mIsDescendantRemoval = false;
+ bool success = !!notifications.AppendElement(bookmarkRef.forget(), fallible);
+ MOZ_RELEASE_ASSERT(success);
+
+ if (bookmark.type == TYPE_BOOKMARK && bookmark.grandParentId == tagsRootId &&
+ uri) {
+ // If the removed bookmark was child of a tag container, notify a tags
+ // change to all bookmarks for this URI.
+ nsTArray<BookmarkData> bookmarks;
+ rv = GetBookmarksForURI(uri, bookmarks);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString utf8spec;
+ uri->GetSpec(utf8spec);
+
+ nsTArray<nsString> tags;
+ rv = GetTags(uri, tags);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
+ RefPtr<PlacesBookmarkTags> tagsChanged = new PlacesBookmarkTags();
+ tagsChanged->mId = bookmarks[i].id;
+ tagsChanged->mItemType = TYPE_BOOKMARK;
+ tagsChanged->mUrl.Assign(NS_ConvertUTF8toUTF16(utf8spec));
+ tagsChanged->mGuid = bookmarks[i].guid;
+ tagsChanged->mParentGuid = bookmarks[i].parentGuid;
+ tagsChanged->mTags.Assign(tags);
+ tagsChanged->mLastModified = bookmarks[i].lastModified / 1000;
+ tagsChanged->mSource = aSource;
+ tagsChanged->mIsTagging = false;
+ success = !!notifications.AppendElement(tagsChanged.forget(), fallible);
+ MOZ_RELEASE_ASSERT(success);
+ }
+ }
+
+ PlacesObservers::NotifyListeners(notifications);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavBookmarks::CreateFolder(int64_t aParent, const nsACString& aTitle,
+ int32_t aIndex, const nsACString& aGUID,
+ uint16_t aSource, int64_t* aNewFolderId) {
+ // NOTE: aParent can be null for root creation, so not checked
+ NS_ENSURE_ARG_POINTER(aNewFolderId);
+ NS_ENSURE_ARG_MIN(aIndex, nsINavBookmarksService::DEFAULT_INDEX);
+ if (!aGUID.IsEmpty() && !IsValidGUID(aGUID)) return NS_ERROR_INVALID_ARG;
+
+ // Get the correct index for insertion. This also ensures the parent exists.
+ int32_t index = aIndex, folderCount;
+ int64_t grandParentId;
+ nsAutoCString folderGuid;
+ nsresult rv =
+ FetchFolderInfo(aParent, &folderCount, folderGuid, &grandParentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mozStorageTransaction transaction(mDB->MainConn(), false);
+
+ // XXX Handle the error, bug 1696133.
+ Unused << NS_WARN_IF(NS_FAILED(transaction.Start()));
+
+ if (aIndex == nsINavBookmarksService::DEFAULT_INDEX ||
+ aIndex >= folderCount) {
+ index = folderCount;
+ } else {
+ // Create space for the insertion.
+ rv = AdjustIndices(aParent, index, INT32_MAX, 1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ *aNewFolderId = -1;
+ PRTime dateAdded = RoundedPRNow();
+ nsAutoCString guid(aGUID);
+ nsCString title;
+ TruncateTitle(aTitle, title);
+
+ rv = InsertBookmarkInDB(-1, FOLDER, aParent, index, title, dateAdded, 0,
+ folderGuid, grandParentId, nullptr, aSource,
+ aNewFolderId, guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t tagsRootId = mDB->GetTagsFolderId();
+
+ if (mCanNotify) {
+ Sequence<OwningNonNull<PlacesEvent>> events;
+ RefPtr<PlacesBookmarkAddition> folder = new PlacesBookmarkAddition();
+ folder->mItemType = TYPE_FOLDER;
+ folder->mId = *aNewFolderId;
+ folder->mParentId = aParent;
+ folder->mIndex = index;
+ folder->mTitle.Assign(NS_ConvertUTF8toUTF16(title));
+ folder->mDateAdded = dateAdded / 1000;
+ folder->mGuid.Assign(guid);
+ folder->mParentGuid.Assign(folderGuid);
+ folder->mSource = aSource;
+ folder->mIsTagging = aParent == tagsRootId;
+ bool success = !!events.AppendElement(folder.forget(), fallible);
+ MOZ_RELEASE_ASSERT(success);
+
+ PlacesObservers::NotifyListeners(events);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsNavBookmarks::GetDescendantChildren(
+ int64_t aFolderId, const nsACString& aFolderGuid, int64_t aGrandParentId,
+ nsTArray<BookmarkData>& aFolderChildrenArray) {
+ // New children will be added from this index on.
+ uint32_t startIndex = aFolderChildrenArray.Length();
+ nsresult rv;
+ {
+ // Collect children informations.
+ // Select all children of a given folder, sorted by position.
+ // 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, b.title, h.rev_host, h.visit_count, "
+ "h.last_visit_date, null, 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.syncStatus "
+ "FROM moz_bookmarks b "
+ "LEFT JOIN moz_places h ON b.fk = h.id "
+ "WHERE b.parent = :parent "
+ "ORDER BY b.position ASC");
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->BindInt64ByName("parent"_ns, aFolderId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore;
+ while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
+ BookmarkData child;
+ rv = stmt->GetInt64(nsNavHistory::kGetInfoIndex_ItemId, &child.id);
+ NS_ENSURE_SUCCESS(rv, rv);
+ child.parentId = aFolderId;
+ child.grandParentId = aGrandParentId;
+ child.parentGuid = aFolderGuid;
+ 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);
+ }
+
+ bool isNull;
+ rv = stmt->GetIsNull(nsNavHistory::kGetInfoIndex_Title, &isNull);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isNull) {
+ rv =
+ stmt->GetUTF8String(nsNavHistory::kGetInfoIndex_Title, child.title);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Append item to children's array.
+ aFolderChildrenArray.AppendElement(child);
+ }
+ }
+
+ // Recursively call GetDescendantChildren for added folders.
+ // We start at startIndex since previous folders are checked
+ // by previous calls to this method.
+ uint32_t childCount = aFolderChildrenArray.Length();
+ for (uint32_t i = startIndex; i < childCount; ++i) {
+ if (aFolderChildrenArray[i].type == TYPE_FOLDER) {
+ // nsTarray assumes that all children can be memmove()d, thus we can't
+ // just pass aFolderChildrenArray[i].guid to a method that will change
+ // the array itself. Otherwise, since it's passed by reference, after a
+ // memmove() it could point to garbage and cause intermittent crashes.
+ nsCString guid = aFolderChildrenArray[i].guid;
+ GetDescendantChildren(aFolderChildrenArray[i].id, guid, aFolderId,
+ aFolderChildrenArray);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsNavBookmarks::RemoveFolderChildren(int64_t aFolderId,
+ uint16_t aSource) {
+ AUTO_PROFILER_LABEL("nsNavBookmarks::RemoveFolderChilder", OTHER);
+ NS_ENSURE_ARG_MIN(aFolderId, 1);
+
+ BookmarkData folder;
+ nsresult rv = FetchItemInfo(aFolderId, folder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_ARG(folder.type == TYPE_FOLDER);
+ NS_ENSURE_ARG(folder.parentId != 0);
+
+ // Fill folder children array recursively.
+ nsTArray<BookmarkData> folderChildrenArray;
+ rv = GetDescendantChildren(folder.id, folder.guid, folder.parentId,
+ folderChildrenArray);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Build a string of folders whose children will be removed.
+ nsCString foldersToRemove;
+ for (uint32_t i = 0; i < folderChildrenArray.Length(); ++i) {
+ 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);
+
+ // XXX Handle the error, bug 1696133.
+ Unused << NS_WARN_IF(NS_FAILED(transaction.Start()));
+
+ nsCOMPtr<mozIStorageStatement> deleteStatement =
+ mDB->GetStatement(nsLiteralCString("DELETE FROM moz_bookmarks "
+ "WHERE parent IN (:parent") +
+ foldersToRemove + ")"_ns);
+ NS_ENSURE_STATE(deleteStatement);
+ mozStorageStatementScoper deleteStatementScoper(deleteStatement);
+
+ rv = deleteStatement->BindInt64ByName("parent"_ns, folder.id);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = deleteStatement->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Clean up orphan items annotations.
+ nsCOMPtr<mozIStorageConnection> conn = mDB->MainConn();
+ if (!conn) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ rv = conn->ExecuteSimpleSQL(
+ nsLiteralCString("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, syncChangeDelta, folder.id,
+ RoundedPRNow());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t tagsRootId = mDB->GetTagsFolderId();
+
+ 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 == tagsRootId;
+ 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);
+
+ Sequence<OwningNonNull<PlacesEvent>> notifications;
+ // 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;
+ if (child.type == TYPE_BOOKMARK) {
+ // If not a tag, recalculate frecency for this entry, since it changed.
+ if (child.grandParentId != tagsRootId) {
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+ rv = history->UpdateFrecency(child.placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // A broken url should not interrupt the removal process.
+ (void)NS_NewURI(getter_AddRefs(uri), child.url);
+ // We cannot assert since some automated tests are checking this path.
+ NS_WARNING_ASSERTION(uri, "Invalid URI in RemoveFolderChildren");
+ }
+
+ if (!mCanNotify) {
+ return NS_OK;
+ }
+
+ RefPtr<PlacesBookmarkRemoved> bookmark = new PlacesBookmarkRemoved();
+ bookmark->mItemType = TYPE_BOOKMARK;
+ bookmark->mId = child.id;
+ bookmark->mParentId = child.parentId;
+ bookmark->mIndex = child.position;
+ bookmark->mUrl.Assign(NS_ConvertUTF8toUTF16(child.url));
+ bookmark->mTitle.Assign(NS_ConvertUTF8toUTF16(child.title));
+ bookmark->mGuid.Assign(child.guid);
+ bookmark->mParentGuid.Assign(child.parentGuid);
+ bookmark->mSource = aSource;
+ bookmark->mIsTagging = (child.grandParentId == tagsRootId);
+ bookmark->mIsDescendantRemoval = (child.grandParentId != tagsRootId);
+ bool success = !!notifications.AppendElement(bookmark.forget(), fallible);
+ MOZ_RELEASE_ASSERT(success);
+
+ if (child.type == TYPE_BOOKMARK && child.grandParentId == tagsRootId &&
+ uri) {
+ // If the removed bookmark was a child of a tag container, notify all
+ // bookmark-folder result nodes which contain a bookmark for the removed
+ // bookmark's url.
+ nsTArray<BookmarkData> bookmarks;
+ rv = GetBookmarksForURI(uri, bookmarks);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString utf8spec;
+ uri->GetSpec(utf8spec);
+
+ nsTArray<nsString> tags;
+ rv = GetTags(uri, tags);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
+ RefPtr<PlacesBookmarkTags> tagsChanged = new PlacesBookmarkTags();
+ tagsChanged->mId = bookmarks[i].id;
+ tagsChanged->mItemType = TYPE_BOOKMARK;
+ tagsChanged->mUrl.Assign(NS_ConvertUTF8toUTF16(utf8spec));
+ tagsChanged->mGuid = bookmarks[i].guid;
+ tagsChanged->mParentGuid = bookmarks[i].parentGuid;
+ tagsChanged->mTags.Assign(tags);
+ tagsChanged->mLastModified = bookmarks[i].lastModified / 1000;
+ tagsChanged->mSource = aSource;
+ tagsChanged->mIsTagging = false;
+ success = !!notifications.AppendElement(tagsChanged.forget(), fallible);
+ MOZ_RELEASE_ASSERT(success);
+ }
+ }
+ }
+
+ if (notifications.Length()) {
+ PlacesObservers::NotifyListeners(notifications);
+ }
+
+ return NS_OK;
+}
+
+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.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);
+
+ nsresult rv = stmt->BindInt64ByName("item_id"_ns, aItemId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasResult;
+ rv = stmt->ExecuteStep(&hasResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!hasResult) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ _bookmark.id = aItemId;
+ rv = stmt->GetUTF8String(1, _bookmark.url);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isNull;
+ rv = stmt->GetIsNull(2, &isNull);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isNull) {
+ rv = stmt->GetUTF8String(2, _bookmark.title);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = stmt->GetInt32(3, &_bookmark.position);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(4, &_bookmark.placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(5, &_bookmark.parentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt32(6, &_bookmark.type);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(7, reinterpret_cast<int64_t*>(&_bookmark.dateAdded));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(8, reinterpret_cast<int64_t*>(&_bookmark.lastModified));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetUTF8String(9, _bookmark.guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Getting properties of the root would show no parent.
+ rv = stmt->GetIsNull(10, &isNull);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isNull) {
+ 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::FetchItemInfo(const nsCString& aGUID,
+ 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, 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.guid = :item_guid");
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindUTF8StringByName("item_guid"_ns, aGUID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ _bookmark.guid = aGUID;
+
+ bool hasResult;
+ rv = stmt->ExecuteStep(&hasResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!hasResult) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ rv = stmt->GetInt64(0, &_bookmark.id);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->GetUTF8String(1, _bookmark.url);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isNull;
+ rv = stmt->GetIsNull(2, &isNull);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isNull) {
+ rv = stmt->GetUTF8String(2, _bookmark.title);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = stmt->GetInt32(3, &_bookmark.position);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(4, &_bookmark.placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(5, &_bookmark.parentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt32(6, &_bookmark.type);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(7, reinterpret_cast<int64_t*>(&_bookmark.dateAdded));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(8, reinterpret_cast<int64_t*>(&_bookmark.lastModified));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Getting properties of the root would show no parent.
+ rv = stmt->GetIsNull(9, &isNull);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isNull) {
+ rv = stmt->GetUTF8String(9, _bookmark.parentGuid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(10, &_bookmark.grandParentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ _bookmark.grandParentId = -1;
+ }
+ rv = stmt->GetInt32(11, &_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 = mDB->GetStatement(
+ "UPDATE moz_bookmarks SET lastModified = :date, "
+ "syncChangeCounter = syncChangeCounter + :delta "
+ "WHERE id = :item_id");
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindInt64ByName("date"_ns, aValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName("item_id"_ns, aItemId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName("delta"_ns, 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;
+}
+
+NS_IMETHODIMP
+nsNavBookmarks::SetItemLastModified(int64_t aItemId, PRTime aLastModified,
+ uint16_t aSource) {
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+
+ BookmarkData bookmark;
+ nsresult rv = FetchItemInfo(aItemId, bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t tagsRootId = mDB->GetTagsFolderId();
+ bool isTagging = bookmark.grandParentId == tagsRootId;
+ int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);
+
+ // Round here so that we notify with the right value.
+ bookmark.lastModified = RoundToMilliseconds(aLastModified);
+
+ 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);
+
+ // XXX Handle the error, bug 1696133.
+ Unused << NS_WARN_IF(NS_FAILED(transaction.Start()));
+
+ 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.
+
+ if (mCanNotify) {
+ Sequence<OwningNonNull<PlacesEvent>> events;
+ RefPtr<PlacesBookmarkTime> timeChanged = new PlacesBookmarkTime();
+ timeChanged->mId = bookmark.id;
+ timeChanged->mItemType = bookmark.type;
+ timeChanged->mUrl.Assign(NS_ConvertUTF8toUTF16(bookmark.url));
+ timeChanged->mGuid = bookmark.guid;
+ timeChanged->mParentGuid = bookmark.parentGuid;
+ timeChanged->mDateAdded = bookmark.dateAdded / 1000;
+ timeChanged->mLastModified = bookmark.lastModified / 1000;
+ timeChanged->mSource = aSource;
+ timeChanged->mIsTagging =
+ bookmark.parentId == tagsRootId || bookmark.grandParentId == tagsRootId;
+ bool success = !!events.AppendElement(timeChanged.forget(), fallible);
+ MOZ_RELEASE_ASSERT(success);
+ PlacesObservers::NotifyListeners(events);
+ }
+
+ 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("delta"_ns, aSyncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = statement->BindInt64ByName("type"_ns,
+ nsINavBookmarksService::TYPE_BOOKMARK);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = URIBinder::Bind(statement, "url"_ns, 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("delta"_ns, aSyncChangeDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = statement->BindInt64ByName("type"_ns,
+ nsINavBookmarksService::TYPE_BOOKMARK);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = statement->BindInt64ByName("parent"_ns, 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("guid"_ns, aBookmark.guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName("date_removed"_ns, 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;
+ }
+
+ nsCOMPtr<mozIStorageConnection> conn = mDB->MainConn();
+ NS_ENSURE_STATE(conn);
+
+ int32_t variableLimit = 0;
+ nsresult rv = conn->GetVariableLimit(&variableLimit);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ size_t maxRowsPerChunk = variableLimit / 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(nsLiteralCString("INSERT INTO moz_bookmarks_deleted "
+ "(guid, dateRemoved) ") +
+ tombstonesToInsert);
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ uint32_t paramIndex = 0;
+ 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("guid"_ns, aGUID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return stmt->Execute();
+}
+
+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);
+
+ int64_t tagsRootId = mDB->GetTagsFolderId();
+ bool isChangingTagFolder = bookmark.parentId == tagsRootId;
+ int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);
+
+ nsAutoCString title;
+ TruncateTitle(aTitle, 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);
+
+ // XXX Handle the error, bug 1696133.
+ Unused << NS_WARN_IF(NS_FAILED(transaction.Start()));
+
+ rv = SetItemTitleInternal(bookmark, title, 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);
+ }
+
+ if (mCanNotify) {
+ Sequence<OwningNonNull<PlacesEvent>> events;
+ RefPtr<PlacesBookmarkTitle> titleChanged = new PlacesBookmarkTitle();
+ titleChanged->mId = bookmark.id;
+ titleChanged->mItemType = bookmark.type;
+ titleChanged->mUrl.Assign(NS_ConvertUTF8toUTF16(bookmark.url));
+ titleChanged->mGuid = bookmark.guid;
+ titleChanged->mParentGuid = bookmark.parentGuid;
+ titleChanged->mTitle.Assign(NS_ConvertUTF8toUTF16(title));
+ titleChanged->mLastModified = bookmark.lastModified / 1000;
+ titleChanged->mSource = aSource;
+ titleChanged->mIsTagging =
+ bookmark.parentId == tagsRootId || bookmark.grandParentId == tagsRootId;
+ bool success = !!events.AppendElement(titleChanged.forget(), fallible);
+ MOZ_RELEASE_ASSERT(success);
+ PlacesObservers::NotifyListeners(events);
+ }
+
+ 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);
+
+ nsresult rv;
+ if (aTitle.IsEmpty()) {
+ rv = statement->BindNullByName("item_title"_ns);
+ } else {
+ rv = statement->BindUTF8StringByName("item_title"_ns, aTitle);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ aBookmark.lastModified = RoundToMilliseconds(RoundedPRNow());
+ rv = statement->BindInt64ByName("date"_ns, aBookmark.lastModified);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = statement->BindInt64ByName("item_id"_ns, aBookmark.id);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = statement->BindInt64ByName("delta"_ns, 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);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ _title = bookmark.title;
+ return NS_OK;
+}
+
+nsresult nsNavBookmarks::ResultNodeForContainer(
+ const nsCString& aGUID, nsNavHistoryQueryOptions* aOptions,
+ nsNavHistoryResultNode** aNode) {
+ BookmarkData bookmark;
+ nsresult rv = FetchItemInfo(aGUID, bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (bookmark.type == TYPE_FOLDER) { // TYPE_FOLDER
+ *aNode =
+ new nsNavHistoryFolderResultNode(bookmark.title, aOptions, bookmark.id);
+ } else {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ (*aNode)->mDateAdded = bookmark.dateAdded;
+ (*aNode)->mLastModified = bookmark.lastModified;
+ (*aNode)->mBookmarkGuid = bookmark.guid;
+ (*aNode)->GetAsFolder()->mTargetFolderGuid = bookmark.guid;
+
+ NS_ADDREF(*aNode);
+ return NS_OK;
+}
+
+nsresult nsNavBookmarks::QueryFolderChildren(
+ int64_t aFolderId, nsNavHistoryQueryOptions* aOptions,
+ nsCOMArray<nsNavHistoryResultNode>* aChildren) {
+ NS_ENSURE_ARG_POINTER(aOptions);
+ NS_ENSURE_ARG_POINTER(aChildren);
+
+ // Select all children of a given folder, sorted by position.
+ // This is a LEFT JOIN because not all bookmarks types have a place.
+ // We construct a result where the first columns exactly match those returned
+ // by mDBGetURLPageInfo, and additionally contains columns for position,
+ // item_child, and folder_child from moz_bookmarks.
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "SELECT h.id, h.url, b.title, h.rev_host, h.visit_count, "
+ "h.last_visit_date, null, 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 "
+ "FROM moz_bookmarks b "
+ "LEFT JOIN moz_places h ON b.fk = h.id "
+ "WHERE b.parent = :parent "
+ "AND (NOT :excludeItems OR "
+ "b.type = :folder OR "
+ "h.url_hash BETWEEN hash('place', 'prefix_lo') AND hash('place', "
+ "'prefix_hi')) "
+ "ORDER BY b.position ASC");
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindInt64ByName("parent"_ns, aFolderId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName("folder"_ns, TYPE_FOLDER);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName("excludeItems"_ns, aOptions->ExcludeItems());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStorageValueArray> row = do_QueryInterface(stmt, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t index = -1;
+ bool hasResult;
+ while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
+ rv = ProcessFolderNodeRow(row, aOptions, aChildren, index);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsNavBookmarks::ProcessFolderNodeRow(
+ mozIStorageValueArray* aRow, nsNavHistoryQueryOptions* aOptions,
+ nsCOMArray<nsNavHistoryResultNode>* aChildren, int32_t& aCurrentIndex) {
+ NS_ENSURE_ARG_POINTER(aRow);
+ NS_ENSURE_ARG_POINTER(aOptions);
+ NS_ENSURE_ARG_POINTER(aChildren);
+
+ // The results will be in order of aCurrentIndex. Even if we don't add a node
+ // because it was excluded, we need to count its index, so do that before
+ // doing anything else.
+ aCurrentIndex++;
+
+ int32_t itemType;
+ nsresult rv = aRow->GetInt32(kGetChildrenIndex_Type, &itemType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ int64_t id;
+ rv = aRow->GetInt64(nsNavHistory::kGetInfoIndex_ItemId, &id);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<nsNavHistoryResultNode> node;
+
+ if (itemType == TYPE_BOOKMARK) {
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+ rv = history->RowToResult(aRow, aOptions, getter_AddRefs(node));
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t nodeType;
+ node->GetType(&nodeType);
+ if (nodeType == nsINavHistoryResultNode::RESULT_TYPE_QUERY &&
+ aOptions->ExcludeQueries()) {
+ return NS_OK;
+ }
+ } else if (itemType == TYPE_FOLDER) {
+ nsAutoCString title;
+ bool isNull;
+ rv = aRow->GetIsNull(nsNavHistory::kGetInfoIndex_Title, &isNull);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isNull) {
+ rv = aRow->GetUTF8String(nsNavHistory::kGetInfoIndex_Title, title);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Don't use options from the parent to build the new folder node, it will
+ // inherit those later when it's inserted in the result.
+ node = new nsNavHistoryFolderResultNode(title,
+ new nsNavHistoryQueryOptions(), id);
+
+ rv = aRow->GetUTF8String(kGetChildrenIndex_Guid, node->mBookmarkGuid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ node->GetAsFolder()->mTargetFolderGuid = node->mBookmarkGuid;
+
+ rv = aRow->GetInt64(nsNavHistory::kGetInfoIndex_ItemDateAdded,
+ reinterpret_cast<int64_t*>(&node->mDateAdded));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aRow->GetInt64(nsNavHistory::kGetInfoIndex_ItemLastModified,
+ reinterpret_cast<int64_t*>(&node->mLastModified));
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // This is a separator.
+ node = new nsNavHistorySeparatorResultNode();
+
+ node->mItemId = id;
+ rv = aRow->GetUTF8String(kGetChildrenIndex_Guid, node->mBookmarkGuid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aRow->GetInt64(nsNavHistory::kGetInfoIndex_ItemDateAdded,
+ reinterpret_cast<int64_t*>(&node->mDateAdded));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aRow->GetInt64(nsNavHistory::kGetInfoIndex_ItemLastModified,
+ reinterpret_cast<int64_t*>(&node->mLastModified));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Store the index of the node within this container. Note that this is not
+ // moz_bookmarks.position.
+ node->mBookmarkIndex = aCurrentIndex;
+
+ NS_ENSURE_TRUE(aChildren->AppendObject(node), NS_ERROR_OUT_OF_MEMORY);
+ return NS_OK;
+}
+
+nsresult nsNavBookmarks::QueryFolderChildrenAsync(
+ nsNavHistoryFolderResultNode* aNode,
+ mozIStoragePendingStatement** _pendingStmt) {
+ NS_ENSURE_ARG_POINTER(aNode);
+ NS_ENSURE_ARG_POINTER(_pendingStmt);
+
+ // Select all children of a given folder, sorted by position.
+ // This is a LEFT JOIN because not all bookmarks types have a place.
+ // We construct a result where the first columns exactly match those returned
+ // by mDBGetURLPageInfo, and additionally contains columns for position,
+ // item_child, and folder_child from moz_bookmarks.
+ nsCOMPtr<mozIStorageAsyncStatement> stmt = mDB->GetAsyncStatement(
+ "SELECT h.id, h.url, b.title, h.rev_host, h.visit_count, "
+ "h.last_visit_date, null, 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 "
+ "FROM moz_bookmarks b "
+ "LEFT JOIN moz_places h ON b.fk = h.id "
+ "WHERE b.parent = :parent "
+ "AND (NOT :excludeItems OR "
+ "b.type = :folder OR "
+ "h.url_hash BETWEEN hash('place', 'prefix_lo') AND hash('place', "
+ "'prefix_hi')) "
+ "ORDER BY b.position ASC");
+ NS_ENSURE_STATE(stmt);
+
+ nsresult rv = stmt->BindInt64ByName("parent"_ns, aNode->mTargetFolderItemId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName("folder"_ns, TYPE_FOLDER);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv =
+ stmt->BindInt32ByName("excludeItems"_ns, aNode->mOptions->ExcludeItems());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStoragePendingStatement> pendingStmt;
+ rv = stmt->ExecuteAsync(aNode, getter_AddRefs(pendingStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*_pendingStmt = pendingStmt);
+ return NS_OK;
+}
+
+nsresult nsNavBookmarks::FetchFolderInfo(int64_t aFolderId,
+ int32_t* _folderCount,
+ nsACString& _guid,
+ int64_t* _parentId) {
+ *_folderCount = 0;
+ *_parentId = -1;
+
+ // This query has to always return results, so it can't be written as a join,
+ // though a left join of 2 subqueries would have the same cost.
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "SELECT count(*), "
+ "(SELECT guid FROM moz_bookmarks WHERE id = :parent), "
+ "(SELECT parent FROM moz_bookmarks WHERE id = :parent) "
+ "FROM moz_bookmarks "
+ "WHERE parent = :parent");
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindInt64ByName("parent"_ns, aFolderId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasResult;
+ rv = stmt->ExecuteStep(&hasResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(hasResult, NS_ERROR_UNEXPECTED);
+
+ // Ensure that the folder we are looking for exists.
+ // Can't rely only on parent, since the root has parent 0, that doesn't exist.
+ bool isNull;
+ rv = stmt->GetIsNull(2, &isNull);
+ NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && (!isNull || aFolderId == 0),
+ NS_ERROR_INVALID_ARG);
+
+ rv = stmt->GetInt32(0, _folderCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isNull) {
+ rv = stmt->GetUTF8String(1, _guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(2, _parentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsNavBookmarks::GetBookmarksForURI(
+ nsIURI* aURI, nsTArray<BookmarkData>& aBookmarks) {
+ 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, "
+ "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);
+
+ nsresult rv = URIBinder::Bind(stmt, "page_url"_ns, aURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t tagsRootId = mDB->GetTagsFolderId();
+
+ bool more;
+ nsAutoString tags;
+ while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&more))) && more) {
+ // Skip tags.
+ int64_t grandParentId;
+ nsresult rv = stmt->GetInt64(5, &grandParentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (grandParentId == tagsRootId) {
+ continue;
+ }
+
+ BookmarkData bookmark;
+ bookmark.grandParentId = grandParentId;
+ rv = stmt->GetInt64(0, &bookmark.id);
+ NS_ENSURE_SUCCESS(rv, rv);
+ 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);
+
+ aBookmarks.AppendElement(bookmark);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavBookmarks::AddObserver(nsINavBookmarkObserver* aObserver, bool aOwnsWeak) {
+ NS_ENSURE_ARG(aObserver);
+
+ if (NS_WARN_IF(!mCanNotify)) return NS_ERROR_UNEXPECTED;
+
+ return mObservers.AppendWeakElementUnlessExists(aObserver, aOwnsWeak);
+}
+
+NS_IMETHODIMP
+nsNavBookmarks::RemoveObserver(nsINavBookmarkObserver* aObserver) {
+ return mObservers.RemoveWeakElement(aObserver);
+}
+
+NS_IMETHODIMP
+nsNavBookmarks::GetObservers(
+ nsTArray<RefPtr<nsINavBookmarkObserver>>& aObservers) {
+ aObservers.Clear();
+
+ if (!mCanNotify) return NS_OK;
+
+ for (uint32_t i = 0; i < mObservers.Length(); ++i) {
+ nsCOMPtr<nsINavBookmarkObserver> observer =
+ mObservers.ElementAt(i).GetValue();
+ // Skip nullified weak observers.
+ if (observer) {
+ aObservers.AppendElement(observer.forget());
+ }
+ }
+
+ return NS_OK;
+}
+
+void nsNavBookmarks::NotifyItemChanged(const ItemChangeData& aData) {
+ // A guid must always be defined.
+ MOZ_ASSERT(!aData.bookmark.guid.IsEmpty());
+ // No more supported.
+ MOZ_ASSERT(!aData.isAnnotation, "Don't notify item annotation changes");
+ PRTime lastModified = aData.bookmark.lastModified;
+ if (aData.updateLastModified) {
+ lastModified = RoundedPRNow();
+ MOZ_ALWAYS_SUCCEEDS(SetItemDateInternal(
+ LAST_MODIFIED, DetermineSyncChangeDelta(aData.source),
+ aData.bookmark.id, lastModified));
+ }
+
+ NOTIFY_BOOKMARKS_OBSERVERS(
+ mCanNotify, mObservers,
+ OnItemChanged(aData.bookmark.id, aData.property, aData.isAnnotation,
+ aData.newValue, lastModified, aData.bookmark.type,
+ aData.bookmark.parentId, aData.bookmark.guid,
+ aData.bookmark.parentGuid, aData.oldValue, aData.source));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsIObserver
+
+NS_IMETHODIMP
+nsNavBookmarks::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+
+ if (strcmp(aTopic, TOPIC_PLACES_CONNECTION_CLOSED) == 0) {
+ // Don't even try to notify observers from this point on, the category
+ // cache would init services that could try to use our APIs.
+ mCanNotify = false;
+ mObservers.Clear();
+ }
+
+ return NS_OK;
+}