diff options
Diffstat (limited to 'toolkit/components/places/nsNavBookmarks.cpp')
-rw-r--r-- | toolkit/components/places/nsNavBookmarks.cpp | 1717 |
1 files changed, 1717 insertions, 0 deletions
diff --git a/toolkit/components/places/nsNavBookmarks.cpp b/toolkit/components/places/nsNavBookmarks.cpp new file mode 100644 index 0000000000..5dcb0c1b44 --- /dev/null +++ b/toolkit/components/places/nsNavBookmarks.cpp @@ -0,0 +1,1717 @@ +/* -*- 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 "nsIProtocolHandler.h" +#include "nsIObserverService.h" +#include "nsUnicharUtils.h" +#include "nsPrintfCString.h" +#include "nsQueryObject.h" +#include "mozIStorageValueArray.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::dom; +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); + + rv = transaction.Commit(); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mCanNotify) { + return NS_OK; + } + + Sequence<OwningNonNull<PlacesEvent>> notifications; + nsAutoCString utf8spec; + aURI->GetSpec(utf8spec); + int64_t tagsRootId = mDB->GetTagsFolderId(); + + 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 == tagsRootId; + 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) { + // 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) { + // 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; +} + +//////////////////////////////////////////////////////////////////////////////// +//// 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; + } + + return NS_OK; +} |