diff -r 000000000000 -r 6474c204b198 toolkit/components/places/nsNavBookmarks.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolkit/components/places/nsNavBookmarks.cpp Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,2984 @@ +/* -*- 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 "nsAnnotationService.h" +#include "nsPlacesMacros.h" +#include "Helpers.h" + +#include "nsAppDirectoryServiceDefs.h" +#include "nsNetUtil.h" +#include "nsUnicharUtils.h" +#include "nsPrintfCString.h" +#include "prprf.h" +#include "mozilla/storage.h" + +#include "GeckoProfiler.h" + +#define BOOKMARKS_TO_KEYWORDS_INITIAL_CACHE_SIZE 64 +#define RECENT_BOOKMARKS_INITIAL_CACHE_SIZE 10 +// Threashold to expire old bookmarks if the initial cache size is exceeded. +#define RECENT_BOOKMARKS_THRESHOLD PRTime((int64_t)1 * 60 * PR_USEC_PER_SEC) + +#define BEGIN_CRITICAL_BOOKMARK_CACHE_SECTION(_itemId_) \ + mUncachableBookmarks.PutEntry(_itemId_); \ + mRecentBookmarksCache.RemoveEntry(_itemId_) + +#define END_CRITICAL_BOOKMARK_CACHE_SECTION(_itemId_) \ + MOZ_ASSERT(!mRecentBookmarksCache.GetEntry(_itemId_)); \ + MOZ_ASSERT(mUncachableBookmarks.GetEntry(_itemId_)); \ + mUncachableBookmarks.RemoveEntry(_itemId_) + +#define ADD_TO_BOOKMARK_CACHE(_itemId_, _data_) \ + PR_BEGIN_MACRO \ + ExpireNonrecentBookmarks(&mRecentBookmarksCache); \ + if (!mUncachableBookmarks.GetEntry(_itemId_)) { \ + BookmarkKeyClass* key = mRecentBookmarksCache.PutEntry(_itemId_); \ + if (key) { \ + key->bookmark = _data_; \ + } \ + } \ + PR_END_MACRO + +#define TOPIC_PLACES_MAINTENANCE "places-maintenance-finished" + +using namespace mozilla; + +// These columns sit to the right of the kGetInfoIndex_* columns. +const int32_t nsNavBookmarks::kGetChildrenIndex_Guid = 15; +const int32_t nsNavBookmarks::kGetChildrenIndex_Position = 16; +const int32_t nsNavBookmarks::kGetChildrenIndex_Type = 17; +const int32_t nsNavBookmarks::kGetChildrenIndex_PlaceID = 18; + +using namespace mozilla::places; + +PLACES_FACTORY_SINGLETON_IMPLEMENTATION(nsNavBookmarks, gBookmarksService) + +#define BOOKMARKS_ANNO_PREFIX "bookmarks/" +#define BOOKMARKS_TOOLBAR_FOLDER_ANNO NS_LITERAL_CSTRING(BOOKMARKS_ANNO_PREFIX "toolbarFolder") +#define READ_ONLY_ANNO NS_LITERAL_CSTRING("placesInternal/READ_ONLY") + + +namespace { + +struct keywordSearchData +{ + int64_t itemId; + nsString keyword; +}; + +PLDHashOperator +SearchBookmarkForKeyword(nsTrimInt64HashKey::KeyType aKey, + const nsString aValue, + void* aUserArg) +{ + keywordSearchData* data = reinterpret_cast(aUserArg); + if (data->keyword.Equals(aValue)) { + data->itemId = aKey; + return PL_DHASH_STOP; + } + return PL_DHASH_NEXT; +} + +template +class AsyncGetBookmarksForURI : public AsyncStatementCallback +{ +public: + AsyncGetBookmarksForURI(nsNavBookmarks* aBookmarksSvc, + Method aCallback, + const DataType& aData) + : mBookmarksSvc(aBookmarksSvc) + , mCallback(aCallback) + , mData(aData) + { + } + + void Init() + { + nsRefPtr DB = Database::GetDatabase(); + if (DB) { + nsCOMPtr stmt = DB->GetAsyncStatement( + "SELECT b.id, b.guid, b.parent, b.lastModified, t.guid, t.parent " + "FROM moz_bookmarks b " + "JOIN moz_bookmarks t on t.id = b.parent " + "WHERE b.fk = (SELECT id FROM moz_places WHERE url = :page_url) " + "ORDER BY b.lastModified DESC, b.id DESC " + ); + if (stmt) { + (void)URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), + mData.bookmark.url); + nsCOMPtr pendingStmt; + (void)stmt->ExecuteAsync(this, getter_AddRefs(pendingStmt)); + } + } + } + + NS_IMETHOD HandleResult(mozIStorageResultSet* aResultSet) + { + nsCOMPtr row; + while (NS_SUCCEEDED(aResultSet->GetNextRow(getter_AddRefs(row))) && row) { + // Skip tags, for the use-cases of this async getter they are useless. + int64_t grandParentId, tagsFolderId; + nsresult rv = row->GetInt64(5, &grandParentId); + NS_ENSURE_SUCCESS(rv, rv); + rv = mBookmarksSvc->GetTagsFolder(&tagsFolderId); + NS_ENSURE_SUCCESS(rv, rv); + if (grandParentId == tagsFolderId) { + continue; + } + + mData.bookmark.grandParentId = grandParentId; + rv = row->GetInt64(0, &mData.bookmark.id); + NS_ENSURE_SUCCESS(rv, rv); + rv = row->GetUTF8String(1, mData.bookmark.guid); + NS_ENSURE_SUCCESS(rv, rv); + rv = row->GetInt64(2, &mData.bookmark.parentId); + NS_ENSURE_SUCCESS(rv, rv); + // lastModified (3) should not be set for the use-cases of this getter. + rv = row->GetUTF8String(4, mData.bookmark.parentGuid); + NS_ENSURE_SUCCESS(rv, rv); + + if (mCallback) { + ((*mBookmarksSvc).*mCallback)(mData); + } + } + return NS_OK; + } + +private: + nsRefPtr mBookmarksSvc; + Method mCallback; + DataType mData; +}; + +static PLDHashOperator +ExpireNonrecentBookmarksCallback(BookmarkKeyClass* aKey, + void* userArg) +{ + int64_t* threshold = reinterpret_cast(userArg); + if (aKey->creationTime < *threshold) { + return PL_DHASH_REMOVE; + } + return PL_DHASH_NEXT; +} + +static void +ExpireNonrecentBookmarks(nsTHashtable* hashTable) +{ + if (hashTable->Count() > RECENT_BOOKMARKS_INITIAL_CACHE_SIZE) { + int64_t threshold = PR_Now() - RECENT_BOOKMARKS_THRESHOLD; + (void)hashTable->EnumerateEntries(ExpireNonrecentBookmarksCallback, + reinterpret_cast(&threshold)); + } +} + +static PLDHashOperator +ExpireRecentBookmarksByParentCallback(BookmarkKeyClass* aKey, + void* userArg) +{ + int64_t* parentId = reinterpret_cast(userArg); + if (aKey->bookmark.parentId == *parentId) { + return PL_DHASH_REMOVE; + } + return PL_DHASH_NEXT; +} + +static void +ExpireRecentBookmarksByParent(nsTHashtable* hashTable, + int64_t aParentId) +{ + (void)hashTable->EnumerateEntries(ExpireRecentBookmarksByParentCallback, + reinterpret_cast(&aParentId)); +} + +} // Anonymous namespace. + + +nsNavBookmarks::nsNavBookmarks() + : mItemCount(0) + , mRoot(0) + , mMenuRoot(0) + , mTagsRoot(0) + , mUnfiledRoot(0) + , mToolbarRoot(0) + , mCanNotify(false) + , mCacheObservers("bookmark-observers") + , mBatching(false) + , mBookmarkToKeywordHash(BOOKMARKS_TO_KEYWORDS_INITIAL_CACHE_SIZE) + , mBookmarkToKeywordHashInitialized(false) + , mRecentBookmarksCache(RECENT_BOOKMARKS_INITIAL_CACHE_SIZE) + , mUncachableBookmarks(RECENT_BOOKMARKS_INITIAL_CACHE_SIZE) +{ + 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 +, nsINavHistoryObserver +, nsIAnnotationObserver +, nsIObserver +, nsISupportsWeakReference +) + + +nsresult +nsNavBookmarks::Init() +{ + mDB = Database::GetDatabase(); + NS_ENSURE_STATE(mDB); + + nsCOMPtr os = mozilla::services::GetObserverService(); + if (os) { + (void)os->AddObserver(this, TOPIC_PLACES_MAINTENANCE, true); + (void)os->AddObserver(this, TOPIC_PLACES_SHUTDOWN, true); + (void)os->AddObserver(this, TOPIC_PLACES_CONNECTION_CLOSED, true); + } + + nsresult rv = ReadRoots(); + NS_ENSURE_SUCCESS(rv, rv); + + mCanNotify = true; + + // Observe annotations. + nsAnnotationService* annosvc = nsAnnotationService::GetAnnotationService(); + NS_ENSURE_TRUE(annosvc, NS_ERROR_OUT_OF_MEMORY); + annosvc->AddObserver(this); + + // Allows us to notify on title changes. MUST BE LAST so it is impossible + // to fail after this call, or the history service will have a reference to + // us and we won't go away. + nsNavHistory* history = nsNavHistory::GetHistoryService(); + NS_ENSURE_STATE(history); + history->AddObserver(this, true); + + // DO NOT PUT STUFF HERE that can fail. See observer comment above. + + return NS_OK; +} + +nsresult +nsNavBookmarks::ReadRoots() +{ + nsCOMPtr stmt; + nsresult rv = mDB->MainConn()->CreateStatement(NS_LITERAL_CSTRING( + "SELECT root_name, folder_id FROM moz_bookmarks_roots" + ), getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasResult; + while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) { + nsAutoCString rootName; + rv = stmt->GetUTF8String(0, rootName); + NS_ENSURE_SUCCESS(rv, rv); + int64_t rootId; + rv = stmt->GetInt64(1, &rootId); + NS_ENSURE_SUCCESS(rv, rv); + NS_ABORT_IF_FALSE(rootId != 0, "Root id is 0, that is an invalid value."); + + if (rootName.EqualsLiteral("places")) { + mRoot = rootId; + } + else if (rootName.EqualsLiteral("menu")) { + mMenuRoot = rootId; + } + else if (rootName.EqualsLiteral("toolbar")) { + mToolbarRoot = rootId; + } + else if (rootName.EqualsLiteral("tags")) { + mTagsRoot = rootId; + } + else if (rootName.EqualsLiteral("unfiled")) { + mUnfiledRoot = rootId; + } + } + + if (!mRoot || !mMenuRoot || !mToolbarRoot || !mTagsRoot || !mUnfiledRoot) + return NS_ERROR_FAILURE; + + return NS_OK; +} + +// nsNavBookmarks::IsBookmarkedInDatabase +// +// This checks to see if the specified place_id is actually bookmarked. + +nsresult +nsNavBookmarks::IsBookmarkedInDatabase(int64_t aPlaceId, + bool* aIsBookmarked) +{ + nsCOMPtr stmt = mDB->GetStatement( + "SELECT 1 FROM moz_bookmarks WHERE fk = :page_id" + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), aPlaceId); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->ExecuteStep(aIsBookmarked); + NS_ENSURE_SUCCESS(rv, rv); + 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"); + + // Expire all cached items for this parent, since all positions are going to + // change. + ExpireRecentBookmarksByParent(&mRecentBookmarksCache, aFolderId); + + nsCOMPtr 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(NS_LITERAL_CSTRING("delta"), aDelta); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aFolderId); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("from_index"), aStartIndex); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("to_index"), aEndIndex); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::GetPlacesRoot(int64_t* aRoot) +{ + *aRoot = mRoot; + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::GetBookmarksMenuFolder(int64_t* aRoot) +{ + *aRoot = mMenuRoot; + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::GetToolbarFolder(int64_t* aFolderId) +{ + *aFolderId = mToolbarRoot; + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::GetTagsFolder(int64_t* aRoot) +{ + *aRoot = mTagsRoot; + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::GetUnfiledBookmarksFolder(int64_t* aRoot) +{ + *aRoot = mUnfiledRoot; + 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, + 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 stmt = mDB->GetStatement( + "INSERT INTO moz_bookmarks " + "(id, fk, type, parent, position, title, " + "dateAdded, lastModified, guid) " + "VALUES (:item_id, :page_id, :item_type, :parent, :item_index, " + ":item_title, :date_added, :last_modified, " + "IFNULL(:item_guid, GENERATE_GUID()))" + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + nsresult rv; + if (*_itemId != -1) + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), *_itemId); + else + rv = stmt->BindNullByName(NS_LITERAL_CSTRING("item_id")); + NS_ENSURE_SUCCESS(rv, rv); + + if (aPlaceId != -1) + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), aPlaceId); + else + rv = stmt->BindNullByName(NS_LITERAL_CSTRING("page_id")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_type"), aItemType); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aParentId); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_index"), aIndex); + NS_ENSURE_SUCCESS(rv, rv); + + // Support NULL titles. + if (aTitle.IsVoid()) + rv = stmt->BindNullByName(NS_LITERAL_CSTRING("item_title")); + else + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("item_title"), aTitle); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("date_added"), aDateAdded); + NS_ENSURE_SUCCESS(rv, rv); + + if (aLastModified) { + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("last_modified"), + aLastModified); + } + else { + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("last_modified"), aDateAdded); + } + NS_ENSURE_SUCCESS(rv, rv); + + // Could use IsEmpty because our callers check for GUID validity, + // but it doesn't hurt. + if (_guid.Length() == 12) { + MOZ_ASSERT(IsValidGUID(_guid)); + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("item_guid"), _guid); + } + else { + rv = stmt->BindNullByName(NS_LITERAL_CSTRING("item_guid")); + } + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + if (*_itemId == -1) { + // Get the newly inserted item id and GUID. + nsCOMPtr lastInsertIdStmt = mDB->GetStatement( + "SELECT id, guid " + "FROM moz_bookmarks " + "ORDER BY ROWID DESC " + "LIMIT 1" + ); + NS_ENSURE_STATE(lastInsertIdStmt); + mozStorageStatementScoper lastInsertIdScoper(lastInsertIdStmt); + + bool hasResult; + rv = lastInsertIdStmt->ExecuteStep(&hasResult); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(hasResult, NS_ERROR_UNEXPECTED); + rv = lastInsertIdStmt->GetInt64(0, _itemId); + NS_ENSURE_SUCCESS(rv, rv); + rv = lastInsertIdStmt->GetUTF8String(1, _guid); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (aParentId > 0) { + // Update last modified date of the ancestors. + // TODO (bug 408991): Doing this for all ancestors would be slow without a + // nested tree, so for now update only the parent. + rv = SetItemDateInternal(LAST_MODIFIED, aParentId, aDateAdded); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Add a cache entry since we know everything about this bookmark. + BookmarkData bookmark; + bookmark.id = *_itemId; + bookmark.guid.Assign(_guid); + if (aTitle.IsVoid()) { + bookmark.title.SetIsVoid(true); + } + else { + 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; + + ADD_TO_BOOKMARK_CACHE(*_itemId, bookmark); + + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::InsertBookmark(int64_t aFolder, + nsIURI* aURI, + int32_t aIndex, + const nsACString& aTitle, + const nsACString& aGUID, + 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); + + 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 = PR_Now(); + nsAutoCString guid(aGUID); + nsCString title; + TruncateTitle(aTitle, title); + + rv = InsertBookmarkInDB(placeId, BOOKMARK, aFolder, index, title, dateAdded, + 0, folderGuid, grandParentId, aURI, + aNewBookmarkId, guid); + NS_ENSURE_SUCCESS(rv, rv); + + // If not a tag, recalculate frecency for this entry, since it changed. + if (grandParentId != mTagsRoot) { + rv = history->UpdateFrecency(placeId); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = transaction.Commit(); + NS_ENSURE_SUCCESS(rv, rv); + + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemAdded(*aNewBookmarkId, aFolder, index, TYPE_BOOKMARK, + aURI, title, dateAdded, guid, folderGuid)); + + // 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 == mTagsRoot) { + // Notify a tags change to all bookmarks for this URI. + nsTArray bookmarks; + rv = GetBookmarksForURI(aURI, bookmarks); + 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); + + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemChanged(bookmarks[i].id, + NS_LITERAL_CSTRING("tags"), + false, + EmptyCString(), + bookmarks[i].lastModified, + TYPE_BOOKMARK, + bookmarks[i].parentId, + bookmarks[i].guid, + bookmarks[i].parentGuid)); + } + } + + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::RemoveItem(int64_t aItemId) +{ + PROFILER_LABEL("bookmarks", "RemoveItem"); + NS_ENSURE_ARG(!IsRoot(aItemId)); + + BookmarkData bookmark; + nsresult rv = FetchItemInfo(aItemId, bookmark); + NS_ENSURE_SUCCESS(rv, rv); + + mozStorageTransaction transaction(mDB->MainConn(), false); + + // First, if not a tag, remove item annotations. + if (bookmark.parentId != mTagsRoot && + bookmark.grandParentId != mTagsRoot) { + nsAnnotationService* annosvc = nsAnnotationService::GetAnnotationService(); + NS_ENSURE_TRUE(annosvc, NS_ERROR_OUT_OF_MEMORY); + rv = annosvc->RemoveItemAnnotations(bookmark.id); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (bookmark.type == TYPE_FOLDER) { + // Remove all of the folder's children. + rv = RemoveFolderChildren(bookmark.id); + NS_ENSURE_SUCCESS(rv, rv); + } + + BEGIN_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id); + + nsCOMPtr stmt = mDB->GetStatement( + "DELETE FROM moz_bookmarks WHERE id = :item_id" + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), 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); + } + + bookmark.lastModified = PR_Now(); + rv = SetItemDateInternal(LAST_MODIFIED, bookmark.parentId, + bookmark.lastModified); + NS_ENSURE_SUCCESS(rv, rv); + + rv = transaction.Commit(); + NS_ENSURE_SUCCESS(rv, rv); + + END_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id); + + nsCOMPtr uri; + if (bookmark.type == TYPE_BOOKMARK) { + // If not a tag, recalculate frecency for this entry, since it changed. + if (bookmark.grandParentId != mTagsRoot) { + nsNavHistory* history = nsNavHistory::GetHistoryService(); + NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY); + rv = history->UpdateFrecency(bookmark.placeId); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = UpdateKeywordsHashForRemovedBookmark(aItemId); + NS_ENSURE_SUCCESS(rv, rv); + + // A broken url should not interrupt the removal process. + (void)NS_NewURI(getter_AddRefs(uri), bookmark.url); + } + + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemRemoved(bookmark.id, + bookmark.parentId, + bookmark.position, + bookmark.type, + uri, + bookmark.guid, + bookmark.parentGuid)); + + if (bookmark.type == TYPE_BOOKMARK && bookmark.grandParentId == mTagsRoot && + uri) { + // If the removed bookmark was child of a tag container, notify a tags + // change to all bookmarks for this URI. + nsTArray bookmarks; + rv = GetBookmarksForURI(uri, bookmarks); + NS_ENSURE_SUCCESS(rv, rv); + + for (uint32_t i = 0; i < bookmarks.Length(); ++i) { + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemChanged(bookmarks[i].id, + NS_LITERAL_CSTRING("tags"), + false, + EmptyCString(), + bookmarks[i].lastModified, + TYPE_BOOKMARK, + bookmarks[i].parentId, + bookmarks[i].guid, + bookmarks[i].parentGuid)); + } + + } + + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::CreateFolder(int64_t aParent, const nsACString& aName, + int32_t aIndex, const nsACString& aGUID, + int64_t* aNewFolder) +{ + // NOTE: aParent can be null for root creation, so not checked + NS_ENSURE_ARG_POINTER(aNewFolder); + + if (!aGUID.IsEmpty() && !IsValidGUID(aGUID)) + return NS_ERROR_INVALID_ARG; + + // CreateContainerWithID returns the index of the new folder, but that's not + // used here. To avoid any risk of corrupting data should this function + // be changed, we'll use a local variable to hold it. The true argument + // will cause notifications to be sent to bookmark observers. + int32_t localIndex = aIndex; + nsresult rv = CreateContainerWithID(-1, aParent, aName, true, &localIndex, + aGUID, aNewFolder); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +NS_IMETHODIMP +nsNavBookmarks::GetFolderReadonly(int64_t aFolder, bool* aResult) +{ + NS_ENSURE_ARG_MIN(aFolder, 1); + NS_ENSURE_ARG_POINTER(aResult); + + nsAnnotationService* annosvc = nsAnnotationService::GetAnnotationService(); + NS_ENSURE_TRUE(annosvc, NS_ERROR_OUT_OF_MEMORY); + nsresult rv = annosvc->ItemHasAnnotation(aFolder, READ_ONLY_ANNO, aResult); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::SetFolderReadonly(int64_t aFolder, bool aReadOnly) +{ + NS_ENSURE_ARG_MIN(aFolder, 1); + + nsAnnotationService* annosvc = nsAnnotationService::GetAnnotationService(); + NS_ENSURE_TRUE(annosvc, NS_ERROR_OUT_OF_MEMORY); + nsresult rv; + if (aReadOnly) { + rv = annosvc->SetItemAnnotationInt32(aFolder, READ_ONLY_ANNO, 1, 0, + nsAnnotationService::EXPIRE_NEVER); + NS_ENSURE_SUCCESS(rv, rv); + } + else { + bool hasAnno; + rv = annosvc->ItemHasAnnotation(aFolder, READ_ONLY_ANNO, &hasAnno); + NS_ENSURE_SUCCESS(rv, rv); + if (hasAnno) { + rv = annosvc->RemoveItemAnnotation(aFolder, READ_ONLY_ANNO); + NS_ENSURE_SUCCESS(rv, rv); + } + } + return NS_OK; +} + + +nsresult +nsNavBookmarks::CreateContainerWithID(int64_t aItemId, + int64_t aParent, + const nsACString& aTitle, + bool aIsBookmarkFolder, + int32_t* aIndex, + const nsACString& aGUID, + int64_t* aNewFolder) +{ + NS_ENSURE_ARG_MIN(*aIndex, nsINavBookmarksService::DEFAULT_INDEX); + + // Get the correct index for insertion. This also ensures the parent exists. + int32_t index, folderCount; + int64_t grandParentId; + nsAutoCString folderGuid; + nsresult rv = FetchFolderInfo(aParent, &folderCount, folderGuid, &grandParentId); + NS_ENSURE_SUCCESS(rv, rv); + + mozStorageTransaction transaction(mDB->MainConn(), false); + + if (*aIndex == nsINavBookmarksService::DEFAULT_INDEX || + *aIndex >= folderCount) { + index = folderCount; + } else { + index = *aIndex; + // Create space for the insertion. + rv = AdjustIndices(aParent, index, INT32_MAX, 1); + NS_ENSURE_SUCCESS(rv, rv); + } + + *aNewFolder = aItemId; + PRTime dateAdded = PR_Now(); + nsAutoCString guid(aGUID); + nsCString title; + TruncateTitle(aTitle, title); + + rv = InsertBookmarkInDB(-1, FOLDER, aParent, index, + title, dateAdded, 0, folderGuid, grandParentId, + nullptr, aNewFolder, guid); + NS_ENSURE_SUCCESS(rv, rv); + + rv = transaction.Commit(); + NS_ENSURE_SUCCESS(rv, rv); + + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemAdded(*aNewFolder, aParent, index, FOLDER, + nullptr, title, dateAdded, guid, folderGuid)); + + *aIndex = index; + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::InsertSeparator(int64_t aParent, + int32_t aIndex, + const nsACString& aGUID, + int64_t* aNewItemId) +{ + NS_ENSURE_ARG_MIN(aParent, 1); + NS_ENSURE_ARG_MIN(aIndex, nsINavBookmarksService::DEFAULT_INDEX); + NS_ENSURE_ARG_POINTER(aNewItemId); + + 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, folderCount; + int64_t grandParentId; + nsAutoCString folderGuid; + nsresult rv = FetchFolderInfo(aParent, &folderCount, folderGuid, &grandParentId); + NS_ENSURE_SUCCESS(rv, rv); + + mozStorageTransaction transaction(mDB->MainConn(), false); + + if (aIndex == nsINavBookmarksService::DEFAULT_INDEX || + aIndex >= folderCount) { + index = folderCount; + } + else { + index = aIndex; + // Create space for the insertion. + rv = AdjustIndices(aParent, index, INT32_MAX, 1); + NS_ENSURE_SUCCESS(rv, rv); + } + + *aNewItemId = -1; + // Set a NULL title rather than an empty string. + nsCString voidString; + voidString.SetIsVoid(true); + nsAutoCString guid(aGUID); + PRTime dateAdded = PR_Now(); + rv = InsertBookmarkInDB(-1, SEPARATOR, aParent, index, voidString, dateAdded, + 0, folderGuid, grandParentId, nullptr, + aNewItemId, guid); + NS_ENSURE_SUCCESS(rv, rv); + + rv = transaction.Commit(); + NS_ENSURE_SUCCESS(rv, rv); + + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemAdded(*aNewItemId, aParent, index, TYPE_SEPARATOR, + nullptr, voidString, dateAdded, guid, folderGuid)); + + return NS_OK; +} + + +nsresult +nsNavBookmarks::GetLastChildId(int64_t aFolderId, int64_t* aItemId) +{ + NS_ASSERTION(aFolderId > 0, "Invalid folder id"); + *aItemId = -1; + + nsCOMPtr stmt = mDB->GetStatement( + "SELECT id FROM moz_bookmarks WHERE parent = :parent " + "ORDER BY position DESC LIMIT 1" + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aFolderId); + NS_ENSURE_SUCCESS(rv, rv); + bool found; + rv = stmt->ExecuteStep(&found); + NS_ENSURE_SUCCESS(rv, rv); + if (found) { + rv = stmt->GetInt64(0, aItemId); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::GetIdForItemAt(int64_t aFolder, + int32_t aIndex, + int64_t* aItemId) +{ + NS_ENSURE_ARG_MIN(aFolder, 1); + NS_ENSURE_ARG_POINTER(aItemId); + + *aItemId = -1; + + nsresult rv; + if (aIndex == nsINavBookmarksService::DEFAULT_INDEX) { + // Get last item within aFolder. + rv = GetLastChildId(aFolder, aItemId); + NS_ENSURE_SUCCESS(rv, rv); + } + else { + // Get the item in aFolder with position aIndex. + nsCOMPtr stmt = mDB->GetStatement( + "SELECT id, fk, type FROM moz_bookmarks " + "WHERE parent = :parent AND position = :item_index" + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aFolder); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_index"), aIndex); + NS_ENSURE_SUCCESS(rv, rv); + + bool found; + rv = stmt->ExecuteStep(&found); + NS_ENSURE_SUCCESS(rv, rv); + if (found) { + rv = stmt->GetInt64(0, aItemId); + NS_ENSURE_SUCCESS(rv, rv); + } + } + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsNavBookmarks::RemoveFolderTransaction, nsITransaction) + +NS_IMETHODIMP +nsNavBookmarks::GetRemoveFolderTransaction(int64_t aFolderId, nsITransaction** aResult) +{ + NS_ENSURE_ARG_MIN(aFolderId, 1); + NS_ENSURE_ARG_POINTER(aResult); + + // Create and initialize a RemoveFolderTransaction object that can be used to + // recreate the folder safely later. + + RemoveFolderTransaction* rft = + new RemoveFolderTransaction(aFolderId); + if (!rft) + return NS_ERROR_OUT_OF_MEMORY; + + NS_ADDREF(*aResult = rft); + return NS_OK; +} + + +nsresult +nsNavBookmarks::GetDescendantFolders(int64_t aFolderId, + nsTArray& aDescendantFoldersArray) { + nsresult rv; + // New descendant folders will be added from this index on. + uint32_t startIndex = aDescendantFoldersArray.Length(); + { + nsCOMPtr stmt = mDB->GetStatement( + "SELECT id " + "FROM moz_bookmarks " + "WHERE parent = :parent " + "AND type = :item_type " + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aFolderId); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_type"), TYPE_FOLDER); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasMore = false; + while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) { + int64_t itemId; + rv = stmt->GetInt64(0, &itemId); + NS_ENSURE_SUCCESS(rv, rv); + aDescendantFoldersArray.AppendElement(itemId); + } + } + + // Recursively call GetDescendantFolders for added folders. + // We start at startIndex since previous folders are checked + // by previous calls to this method. + uint32_t childCount = aDescendantFoldersArray.Length(); + for (uint32_t i = startIndex; i < childCount; ++i) { + GetDescendantFolders(aDescendantFoldersArray[i], aDescendantFoldersArray); + } + + return NS_OK; +} + + +nsresult +nsNavBookmarks::GetDescendantChildren(int64_t aFolderId, + const nsACString& aFolderGuid, + int64_t aGrandParentId, + nsTArray& 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 stmt = mDB->GetStatement( + "SELECT h.id, h.url, IFNULL(b.title, h.title), h.rev_host, h.visit_count, " + "h.last_visit_date, f.url, b.id, b.dateAdded, b.lastModified, " + "b.parent, null, h.frecency, h.hidden, h.guid, b.guid, " + "b.position, b.type, b.fk " + "FROM moz_bookmarks b " + "LEFT JOIN moz_places h ON b.fk = h.id " + "LEFT JOIN moz_favicons f ON h.favicon_id = f.id " + "WHERE b.parent = :parent " + "ORDER BY b.position ASC" + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), 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); + + if (child.type == TYPE_BOOKMARK) { + rv = stmt->GetUTF8String(nsNavHistory::kGetInfoIndex_URL, child.url); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Append item to children's array. + aFolderChildrenArray.AppendElement(child); + } + } + + // 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; +} + + +NS_IMETHODIMP +nsNavBookmarks::RemoveFolderChildren(int64_t aFolderId) +{ + PROFILER_LABEL("bookmarks", "RemoveFolderChilder"); + NS_ENSURE_ARG_MIN(aFolderId, 1); + NS_ENSURE_ARG(aFolderId != mRoot); + + BookmarkData folder; + nsresult rv = FetchItemInfo(aFolderId, folder); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_ARG(folder.type == TYPE_FOLDER); + + // Fill folder children array recursively. + nsTArray 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.AppendLiteral(","); + foldersToRemove.AppendInt(child.id); + } + + BEGIN_CRITICAL_BOOKMARK_CACHE_SECTION(child.id); + } + + // Delete items from the database now. + mozStorageTransaction transaction(mDB->MainConn(), false); + + nsCOMPtr deleteStatement = mDB->GetStatement( + NS_LITERAL_CSTRING( + "DELETE FROM moz_bookmarks " + "WHERE parent IN (:parent") + foldersToRemove + NS_LITERAL_CSTRING(")") + ); + NS_ENSURE_STATE(deleteStatement); + mozStorageStatementScoper deleteStatementScoper(deleteStatement); + + rv = deleteStatement->BindInt64ByName(NS_LITERAL_CSTRING("parent"), folder.id); + NS_ENSURE_SUCCESS(rv, rv); + rv = deleteStatement->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + // Clean up orphan items annotations. + rv = mDB->MainConn()->ExecuteSimpleSQL( + NS_LITERAL_CSTRING( + "DELETE FROM moz_items_annos " + "WHERE id IN (" + "SELECT a.id from moz_items_annos a " + "LEFT JOIN moz_bookmarks b ON a.item_id = b.id " + "WHERE b.id ISNULL)")); + NS_ENSURE_SUCCESS(rv, rv); + + // Set the lastModified date. + rv = SetItemDateInternal(LAST_MODIFIED, folder.id, PR_Now()); + NS_ENSURE_SUCCESS(rv, rv); + + for (uint32_t i = 0; i < folderChildrenArray.Length(); i++) { + BookmarkData& child = folderChildrenArray[i]; + if (child.type == TYPE_BOOKMARK) { + // If not a tag, recalculate frecency for this entry, since it changed. + if (child.grandParentId != mTagsRoot) { + nsNavHistory* history = nsNavHistory::GetHistoryService(); + NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY); + rv = history->UpdateFrecency(child.placeId); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = UpdateKeywordsHashForRemovedBookmark(child.id); + NS_ENSURE_SUCCESS(rv, rv); + } + END_CRITICAL_BOOKMARK_CACHE_SECTION(child.id); + } + + rv = transaction.Commit(); + NS_ENSURE_SUCCESS(rv, rv); + + // Call observers in reverse order to serve children before their parent. + for (int32_t i = folderChildrenArray.Length() - 1; i >= 0; --i) { + BookmarkData& child = folderChildrenArray[i]; + nsCOMPtr uri; + if (child.type == TYPE_BOOKMARK) { + // A broken url should not interrupt the removal process. + (void)NS_NewURI(getter_AddRefs(uri), child.url); + } + + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemRemoved(child.id, + child.parentId, + child.position, + child.type, + uri, + child.guid, + child.parentGuid)); + + if (child.type == TYPE_BOOKMARK && child.grandParentId == mTagsRoot && + 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 bookmarks; + rv = GetBookmarksForURI(uri, bookmarks); + NS_ENSURE_SUCCESS(rv, rv); + + for (uint32_t i = 0; i < bookmarks.Length(); ++i) { + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemChanged(bookmarks[i].id, + NS_LITERAL_CSTRING("tags"), + false, + EmptyCString(), + bookmarks[i].lastModified, + TYPE_BOOKMARK, + bookmarks[i].parentId, + bookmarks[i].guid, + bookmarks[i].parentGuid)); + } + } + } + + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::MoveItem(int64_t aItemId, int64_t aNewParent, int32_t aIndex) +{ + NS_ENSURE_ARG(!IsRoot(aItemId)); + NS_ENSURE_ARG_MIN(aItemId, 1); + NS_ENSURE_ARG_MIN(aNewParent, 1); + // -1 is append, but no other negative number is allowed. + NS_ENSURE_ARG_MIN(aIndex, -1); + // Disallow making an item its own parent. + NS_ENSURE_ARG(aItemId != aNewParent); + + mozStorageTransaction transaction(mDB->MainConn(), false); + + BookmarkData bookmark; + nsresult rv = FetchItemInfo(aItemId, bookmark); + NS_ENSURE_SUCCESS(rv, rv); + + // if parent and index are the same, nothing to do + if (bookmark.parentId == aNewParent && bookmark.position == aIndex) + return NS_OK; + + // Make sure aNewParent is not aFolder or a subfolder of aFolder. + // TODO: make this performant, maybe with a nested tree (bug 408991). + if (bookmark.type == TYPE_FOLDER) { + int64_t ancestorId = aNewParent; + + while (ancestorId) { + if (ancestorId == bookmark.id) { + return NS_ERROR_INVALID_ARG; + } + rv = GetFolderIdForItem(ancestorId, &ancestorId); + if (NS_FAILED(rv)) { + break; + } + } + } + + // calculate new index + int32_t newIndex, folderCount; + int64_t grandParentId; + nsAutoCString newParentGuid; + rv = FetchFolderInfo(aNewParent, &folderCount, newParentGuid, &grandParentId); + NS_ENSURE_SUCCESS(rv, rv); + if (aIndex == nsINavBookmarksService::DEFAULT_INDEX || + aIndex >= folderCount) { + newIndex = folderCount; + // If the parent remains the same, then the folder is really being moved + // to count - 1 (since it's being removed from the old position) + if (bookmark.parentId == aNewParent) { + --newIndex; + } + } else { + newIndex = aIndex; + + if (bookmark.parentId == aNewParent && newIndex > bookmark.position) { + // when an item is being moved lower in the same folder, the new index + // refers to the index before it was removed. Removal causes everything + // to shift up. + --newIndex; + } + } + + // this is like the previous check, except this covers if + // the specified index was -1 (append), and the calculated + // new index is the same as the existing index + if (aNewParent == bookmark.parentId && newIndex == bookmark.position) { + // Nothing to do! + return NS_OK; + } + + // adjust indices to account for the move + // do this before we update the parent/index fields + // or we'll re-adjust the index for the item we are moving + if (bookmark.parentId == aNewParent) { + // We can optimize the updates if moving within the same container. + // We only shift the items between the old and new positions, since the + // insertion will offset the deletion. + if (bookmark.position > newIndex) { + rv = AdjustIndices(bookmark.parentId, newIndex, bookmark.position - 1, 1); + } + else { + rv = AdjustIndices(bookmark.parentId, bookmark.position + 1, newIndex, -1); + } + NS_ENSURE_SUCCESS(rv, rv); + } + else { + // We're moving between containers, so this happens in two steps. + // First, fill the hole from the removal from the old parent. + rv = AdjustIndices(bookmark.parentId, bookmark.position + 1, INT32_MAX, -1); + NS_ENSURE_SUCCESS(rv, rv); + // Now, make room in the new parent for the insertion. + rv = AdjustIndices(aNewParent, newIndex, INT32_MAX, 1); + NS_ENSURE_SUCCESS(rv, rv); + } + + BEGIN_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id); + + { + // Update parent and position. + nsCOMPtr stmt = mDB->GetStatement( + "UPDATE moz_bookmarks SET parent = :parent, position = :item_index " + "WHERE id = :item_id " + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aNewParent); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_index"), newIndex); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), bookmark.id); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + } + + PRTime now = PR_Now(); + rv = SetItemDateInternal(LAST_MODIFIED, bookmark.parentId, now); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetItemDateInternal(LAST_MODIFIED, aNewParent, now); + NS_ENSURE_SUCCESS(rv, rv); + + rv = transaction.Commit(); + NS_ENSURE_SUCCESS(rv, rv); + + END_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id); + + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemMoved(bookmark.id, + bookmark.parentId, + bookmark.position, + aNewParent, + newIndex, + bookmark.type, + bookmark.guid, + bookmark.parentGuid, + newParentGuid)); + return NS_OK; +} + +nsresult +nsNavBookmarks::FetchItemInfo(int64_t aItemId, + BookmarkData& _bookmark) +{ + // Check if the requested id is in the recent cache and avoid the database + // lookup if so. Invalidate the cache after getting data if requested. + BookmarkKeyClass* key = mRecentBookmarksCache.GetEntry(aItemId); + if (key) { + _bookmark = key->bookmark; + return NS_OK; + } + + // LEFT JOIN since not all bookmarks have an associated place. + nsCOMPtr 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 " + "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(NS_LITERAL_CSTRING("item_id"), 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) { + _bookmark.title.SetIsVoid(true); + } + else { + 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(&_bookmark.dateAdded)); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->GetInt64(8, reinterpret_cast(&_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; + } + + ADD_TO_BOOKMARK_CACHE(aItemId, _bookmark); + + return NS_OK; +} + +nsresult +nsNavBookmarks::SetItemDateInternal(enum BookmarkDate aDateType, + int64_t aItemId, + PRTime aValue) +{ + nsCOMPtr stmt; + if (aDateType == DATE_ADDED) { + // lastModified is set to the same value as dateAdded. We do this for + // performance reasons, since it will allow us to use an index to sort items + // by date. + stmt = mDB->GetStatement( + "UPDATE moz_bookmarks SET dateAdded = :date, lastModified = :date " + "WHERE id = :item_id" + ); + } + else { + stmt = mDB->GetStatement( + "UPDATE moz_bookmarks SET lastModified = :date WHERE id = :item_id" + ); + } + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("date"), aValue); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), aItemId); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + // Update the cache entry, if needed. + BookmarkKeyClass* key = mRecentBookmarksCache.GetEntry(aItemId); + if (key) { + if (aDateType == DATE_ADDED) { + key->bookmark.dateAdded = aValue; + } + // Set lastModified in both cases. + key->bookmark.lastModified = aValue; + } + + // note, we are not notifying the observers + // that the item has changed. + + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::SetItemDateAdded(int64_t aItemId, PRTime aDateAdded) +{ + NS_ENSURE_ARG_MIN(aItemId, 1); + + BookmarkData bookmark; + nsresult rv = FetchItemInfo(aItemId, bookmark); + NS_ENSURE_SUCCESS(rv, rv); + bookmark.dateAdded = aDateAdded; + + rv = SetItemDateInternal(DATE_ADDED, bookmark.id, bookmark.dateAdded); + NS_ENSURE_SUCCESS(rv, rv); + + // Note: mDBSetItemDateAdded also sets lastModified to aDateAdded. + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemChanged(bookmark.id, + NS_LITERAL_CSTRING("dateAdded"), + false, + nsPrintfCString("%lld", bookmark.dateAdded), + bookmark.dateAdded, + bookmark.type, + bookmark.parentId, + bookmark.guid, + bookmark.parentGuid)); + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::GetItemDateAdded(int64_t aItemId, PRTime* _dateAdded) +{ + NS_ENSURE_ARG_MIN(aItemId, 1); + NS_ENSURE_ARG_POINTER(_dateAdded); + + BookmarkData bookmark; + nsresult rv = FetchItemInfo(aItemId, bookmark); + NS_ENSURE_SUCCESS(rv, rv); + + *_dateAdded = bookmark.dateAdded; + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::SetItemLastModified(int64_t aItemId, PRTime aLastModified) +{ + NS_ENSURE_ARG_MIN(aItemId, 1); + + BookmarkData bookmark; + nsresult rv = FetchItemInfo(aItemId, bookmark); + NS_ENSURE_SUCCESS(rv, rv); + bookmark.lastModified = aLastModified; + + rv = SetItemDateInternal(LAST_MODIFIED, bookmark.id, bookmark.lastModified); + NS_ENSURE_SUCCESS(rv, rv); + + // Note: mDBSetItemDateAdded also sets lastModified to aDateAdded. + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemChanged(bookmark.id, + NS_LITERAL_CSTRING("lastModified"), + false, + nsPrintfCString("%lld", bookmark.lastModified), + bookmark.lastModified, + bookmark.type, + bookmark.parentId, + bookmark.guid, + bookmark.parentGuid)); + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::GetItemLastModified(int64_t aItemId, PRTime* _lastModified) +{ + NS_ENSURE_ARG_MIN(aItemId, 1); + NS_ENSURE_ARG_POINTER(_lastModified); + + BookmarkData bookmark; + nsresult rv = FetchItemInfo(aItemId, bookmark); + NS_ENSURE_SUCCESS(rv, rv); + + *_lastModified = bookmark.lastModified; + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::SetItemTitle(int64_t aItemId, const nsACString& aTitle) +{ + NS_ENSURE_ARG_MIN(aItemId, 1); + + BookmarkData bookmark; + nsresult rv = FetchItemInfo(aItemId, bookmark); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr statement = mDB->GetStatement( + "UPDATE moz_bookmarks SET title = :item_title, lastModified = :date " + "WHERE id = :item_id " + ); + NS_ENSURE_STATE(statement); + mozStorageStatementScoper scoper(statement); + + nsCString title; + TruncateTitle(aTitle, title); + + // Support setting a null title, we support this in insertBookmark. + if (title.IsVoid()) { + rv = statement->BindNullByName(NS_LITERAL_CSTRING("item_title")); + } + else { + rv = statement->BindUTF8StringByName(NS_LITERAL_CSTRING("item_title"), + title); + } + NS_ENSURE_SUCCESS(rv, rv); + bookmark.lastModified = PR_Now(); + rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("date"), + bookmark.lastModified); + NS_ENSURE_SUCCESS(rv, rv); + rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), bookmark.id); + NS_ENSURE_SUCCESS(rv, rv); + + rv = statement->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + // Update the cache entry, if needed. + BookmarkKeyClass* key = mRecentBookmarksCache.GetEntry(aItemId); + if (key) { + if (title.IsVoid()) { + key->bookmark.title.SetIsVoid(true); + } + else { + key->bookmark.title.Assign(title); + } + key->bookmark.lastModified = bookmark.lastModified; + } + + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemChanged(bookmark.id, + NS_LITERAL_CSTRING("title"), + false, + title, + bookmark.lastModified, + bookmark.type, + bookmark.parentId, + bookmark.guid, + bookmark.parentGuid)); + 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; +} + + +NS_IMETHODIMP +nsNavBookmarks::GetBookmarkURI(int64_t aItemId, + nsIURI** _URI) +{ + NS_ENSURE_ARG_MIN(aItemId, 1); + NS_ENSURE_ARG_POINTER(_URI); + + BookmarkData bookmark; + nsresult rv = FetchItemInfo(aItemId, bookmark); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_NewURI(_URI, bookmark.url); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::GetItemType(int64_t aItemId, uint16_t* _type) +{ + NS_ENSURE_ARG_MIN(aItemId, 1); + NS_ENSURE_ARG_POINTER(_type); + + BookmarkData bookmark; + nsresult rv = FetchItemInfo(aItemId, bookmark); + NS_ENSURE_SUCCESS(rv, rv); + + *_type = static_cast(bookmark.type); + return NS_OK; +} + + +nsresult +nsNavBookmarks::ResultNodeForContainer(int64_t aItemId, + nsNavHistoryQueryOptions* aOptions, + nsNavHistoryResultNode** aNode) +{ + BookmarkData bookmark; + nsresult rv = FetchItemInfo(aItemId, 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; + + NS_ADDREF(*aNode); + return NS_OK; +} + + +nsresult +nsNavBookmarks::QueryFolderChildren( + int64_t aFolderId, + nsNavHistoryQueryOptions* aOptions, + nsCOMArray* 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 stmt = mDB->GetStatement( + "SELECT h.id, h.url, IFNULL(b.title, h.title), h.rev_host, h.visit_count, " + "h.last_visit_date, f.url, b.id, b.dateAdded, b.lastModified, " + "b.parent, null, h.frecency, h.hidden, h.guid, b.guid, " + "b.position, b.type, b.fk " + "FROM moz_bookmarks b " + "LEFT JOIN moz_places h ON b.fk = h.id " + "LEFT JOIN moz_favicons f ON h.favicon_id = f.id " + "WHERE b.parent = :parent " + "ORDER BY b.position ASC" + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aFolderId); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr 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* 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); + + nsRefPtr 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()) || + (nodeType != nsINavHistoryResultNode::RESULT_TYPE_QUERY && + nodeType != nsINavHistoryResultNode::RESULT_TYPE_FOLDER_SHORTCUT && + aOptions->ExcludeItems())) { + return NS_OK; + } + } + else if (itemType == TYPE_FOLDER) { + if (aOptions->ExcludeReadOnlyFolders()) { + // If the folder is read-only, skip it. + bool readOnly = false; + GetFolderReadonly(id, &readOnly); + if (readOnly) + return NS_OK; + } + + nsAutoCString title; + rv = aRow->GetUTF8String(nsNavHistory::kGetInfoIndex_Title, title); + NS_ENSURE_SUCCESS(rv, rv); + + node = new nsNavHistoryFolderResultNode(title, aOptions, id); + + rv = aRow->GetInt64(nsNavHistory::kGetInfoIndex_ItemDateAdded, + reinterpret_cast(&node->mDateAdded)); + NS_ENSURE_SUCCESS(rv, rv); + rv = aRow->GetInt64(nsNavHistory::kGetInfoIndex_ItemLastModified, + reinterpret_cast(&node->mLastModified)); + NS_ENSURE_SUCCESS(rv, rv); + } + else { + // This is a separator. + if (aOptions->ExcludeItems()) { + return NS_OK; + } + node = new nsNavHistorySeparatorResultNode(); + + node->mItemId = id; + rv = aRow->GetInt64(nsNavHistory::kGetInfoIndex_ItemDateAdded, + reinterpret_cast(&node->mDateAdded)); + NS_ENSURE_SUCCESS(rv, rv); + rv = aRow->GetInt64(nsNavHistory::kGetInfoIndex_ItemLastModified, + reinterpret_cast(&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; + + rv = aRow->GetUTF8String(kGetChildrenIndex_Guid, node->mBookmarkGuid); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ENSURE_TRUE(aChildren->AppendObject(node), NS_ERROR_OUT_OF_MEMORY); + + return NS_OK; +} + + +nsresult +nsNavBookmarks::QueryFolderChildrenAsync( + nsNavHistoryFolderResultNode* aNode, + int64_t aFolderId, + 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 stmt = mDB->GetAsyncStatement( + "SELECT h.id, h.url, IFNULL(b.title, h.title), h.rev_host, h.visit_count, " + "h.last_visit_date, f.url, b.id, b.dateAdded, b.lastModified, " + "b.parent, null, h.frecency, h.hidden, h.guid, b.guid, " + "b.position, b.type, b.fk " + "FROM moz_bookmarks b " + "LEFT JOIN moz_places h ON b.fk = h.id " + "LEFT JOIN moz_favicons f ON h.favicon_id = f.id " + "WHERE b.parent = :parent " + "ORDER BY b.position ASC" + ); + NS_ENSURE_STATE(stmt); + + nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aFolderId); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr 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 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(NS_LITERAL_CSTRING("parent"), 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; +} + + +NS_IMETHODIMP +nsNavBookmarks::IsBookmarked(nsIURI* aURI, bool* aBookmarked) +{ + NS_ENSURE_ARG(aURI); + NS_ENSURE_ARG_POINTER(aBookmarked); + + nsCOMPtr stmt = mDB->GetStatement( + "SELECT 1 FROM moz_bookmarks b " + "JOIN moz_places h ON b.fk = h.id " + "WHERE h.url = :page_url" + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aURI); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->ExecuteStep(aBookmarked); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::GetBookmarkedURIFor(nsIURI* aURI, nsIURI** _retval) +{ + NS_ENSURE_ARG(aURI); + NS_ENSURE_ARG_POINTER(_retval); + + *_retval = nullptr; + + nsNavHistory* history = nsNavHistory::GetHistoryService(); + NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY); + int64_t placeId; + nsAutoCString placeGuid; + nsresult rv = history->GetIdForPage(aURI, &placeId, placeGuid); + NS_ENSURE_SUCCESS(rv, rv); + if (!placeId) { + // This URI is unknown, just return null. + return NS_OK; + } + + // Check if a bookmark exists in the redirects chain for this URI. + // The query will also check if the page is directly bookmarked, and return + // the first found bookmark in case. The check is directly on moz_bookmarks + // without special filtering. + // The next query finds the bookmarked ancestors in a redirects chain. + // It won't go further than 3 levels of redirects (a->b->c->your_place_id). + // To make this path 100% correct (up to any level) we would need either: + // - A separate hash, build through recursive querying of the database. + // This solution was previously implemented, but it had a negative effect + // on startup since at each startup we have to recursively query the + // database to rebuild a hash that is always the same across sessions. + // It must be updated at each visit and bookmarks change too. The code to + // manage it is complex and prone to errors, sometimes causing incorrect + // data fetches (for example wrong favicon for a redirected bookmark). + // - A better way to track redirects for a visit. + // We would need a separate table to track redirects, in the table we would + // have visit_id, redirect_session. To get all sources for + // a visit then we could just join this table and get all visit_id that + // are in the same redirect_session as our visit. This has the drawback + // that we can't ensure data integrity in the downgrade -> upgrade path, + // since an old version would not update the table on new visits. + // + // For most cases these levels of redirects should be fine though, it's hard + // to hit a page that is 4 or 5 levels of redirects below a bookmarked page. + // + // As a bonus the query also checks first if place_id is already a bookmark, + // so you don't have to check that apart. + + nsCString query = nsPrintfCString( + "SELECT url FROM moz_places WHERE id = ( " + "SELECT :page_id FROM moz_bookmarks WHERE fk = :page_id " + "UNION ALL " + "SELECT COALESCE(grandparent.place_id, parent.place_id) AS r_place_id " + "FROM moz_historyvisits dest " + "LEFT JOIN moz_historyvisits parent ON parent.id = dest.from_visit " + "AND dest.visit_type IN (%d, %d) " + "LEFT JOIN moz_historyvisits grandparent ON parent.from_visit = grandparent.id " + "AND parent.visit_type IN (%d, %d) " + "WHERE dest.place_id = :page_id " + "AND EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = r_place_id) " + "LIMIT 1 " + ")", + nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT, + nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY, + nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT, + nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY + ); + + nsCOMPtr stmt = mDB->GetStatement(query); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), placeId); + NS_ENSURE_SUCCESS(rv, rv); + bool hasBookmarkedOrigin; + if (NS_SUCCEEDED(stmt->ExecuteStep(&hasBookmarkedOrigin)) && + hasBookmarkedOrigin) { + nsAutoCString spec; + rv = stmt->GetUTF8String(0, spec); + NS_ENSURE_SUCCESS(rv, rv); + rv = NS_NewURI(_retval, spec); + NS_ENSURE_SUCCESS(rv, rv); + } + + // If there is no bookmarked origin, we will just return null. + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::ChangeBookmarkURI(int64_t aBookmarkId, nsIURI* aNewURI) +{ + NS_ENSURE_ARG_MIN(aBookmarkId, 1); + NS_ENSURE_ARG(aNewURI); + + BookmarkData bookmark; + nsresult rv = FetchItemInfo(aBookmarkId, bookmark); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_ARG(bookmark.type == TYPE_BOOKMARK); + + mozStorageTransaction transaction(mDB->MainConn(), false); + + nsNavHistory* history = nsNavHistory::GetHistoryService(); + NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY); + int64_t newPlaceId; + nsAutoCString newPlaceGuid; + rv = history->GetOrCreateIdForPage(aNewURI, &newPlaceId, newPlaceGuid); + NS_ENSURE_SUCCESS(rv, rv); + if (!newPlaceId) + return NS_ERROR_INVALID_ARG; + + BEGIN_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id); + + nsCOMPtr statement = mDB->GetStatement( + "UPDATE moz_bookmarks SET fk = :page_id, lastModified = :date " + "WHERE id = :item_id " + ); + NS_ENSURE_STATE(statement); + mozStorageStatementScoper scoper(statement); + + rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), newPlaceId); + NS_ENSURE_SUCCESS(rv, rv); + bookmark.lastModified = PR_Now(); + rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("date"), + bookmark.lastModified); + NS_ENSURE_SUCCESS(rv, rv); + rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), bookmark.id); + NS_ENSURE_SUCCESS(rv, rv); + rv = statement->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + rv = transaction.Commit(); + NS_ENSURE_SUCCESS(rv, rv); + + END_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id); + + rv = history->UpdateFrecency(newPlaceId); + NS_ENSURE_SUCCESS(rv, rv); + + // Upon changing the URI for a bookmark, update the frecency for the old + // place as well. + rv = history->UpdateFrecency(bookmark.placeId); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString spec; + rv = aNewURI->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemChanged(bookmark.id, + NS_LITERAL_CSTRING("uri"), + false, + spec, + bookmark.lastModified, + bookmark.type, + bookmark.parentId, + bookmark.guid, + bookmark.parentGuid)); + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::GetFolderIdForItem(int64_t aItemId, int64_t* _parentId) +{ + NS_ENSURE_ARG_MIN(aItemId, 1); + NS_ENSURE_ARG_POINTER(_parentId); + + BookmarkData bookmark; + nsresult rv = FetchItemInfo(aItemId, bookmark); + NS_ENSURE_SUCCESS(rv, rv); + + // this should not happen, but see bug #400448 for details + NS_ENSURE_TRUE(bookmark.id != bookmark.parentId, NS_ERROR_UNEXPECTED); + + *_parentId = bookmark.parentId; + return NS_OK; +} + + +nsresult +nsNavBookmarks::GetBookmarkIdsForURITArray(nsIURI* aURI, + nsTArray& aResult, + bool aSkipTags) +{ + 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 stmt = mDB->GetStatement( + "SELECT b.id, b.guid, b.parent, b.lastModified, t.guid, t.parent " + "FROM moz_bookmarks b " + "JOIN moz_bookmarks t on t.id = b.parent " + "WHERE b.fk = (SELECT id FROM moz_places WHERE url = :page_url) " + "ORDER BY b.lastModified DESC, b.id DESC " + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aURI); + NS_ENSURE_SUCCESS(rv, rv); + + bool more; + while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&more))) && more) { + if (aSkipTags) { + // Skip tags, for the use-cases of this async getter they are useless. + int64_t grandParentId; + nsresult rv = stmt->GetInt64(5, &grandParentId); + NS_ENSURE_SUCCESS(rv, rv); + if (grandParentId == mTagsRoot) { + continue; + } + } + int64_t bookmarkId; + rv = stmt->GetInt64(0, &bookmarkId); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(aResult.AppendElement(bookmarkId), NS_ERROR_OUT_OF_MEMORY); + } + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +nsNavBookmarks::GetBookmarksForURI(nsIURI* aURI, + nsTArray& 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 stmt = mDB->GetStatement( + "SELECT b.id, b.guid, b.parent, b.lastModified, t.guid, t.parent " + "FROM moz_bookmarks b " + "JOIN moz_bookmarks t on t.id = b.parent " + "WHERE b.fk = (SELECT id FROM moz_places WHERE url = :page_url) " + "ORDER BY b.lastModified DESC, b.id DESC " + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aURI); + NS_ENSURE_SUCCESS(rv, rv); + + 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 == mTagsRoot) { + 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(&bookmark.lastModified)); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->GetUTF8String(4, bookmark.parentGuid); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ENSURE_TRUE(aBookmarks.AppendElement(bookmark), NS_ERROR_OUT_OF_MEMORY); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsNavBookmarks::GetBookmarkIdsForURI(nsIURI* aURI, uint32_t* aCount, + int64_t** aBookmarks) +{ + NS_ENSURE_ARG(aURI); + NS_ENSURE_ARG_POINTER(aCount); + NS_ENSURE_ARG_POINTER(aBookmarks); + + *aCount = 0; + *aBookmarks = nullptr; + nsTArray bookmarks; + + // Get the information from the DB as a TArray + // TODO (bug 653816): make this API skip tags by default. + nsresult rv = GetBookmarkIdsForURITArray(aURI, bookmarks, false); + NS_ENSURE_SUCCESS(rv, rv); + + // Copy the results into a new array for output + if (bookmarks.Length()) { + *aBookmarks = + static_cast(nsMemory::Alloc(sizeof(int64_t) * bookmarks.Length())); + if (!*aBookmarks) + return NS_ERROR_OUT_OF_MEMORY; + for (uint32_t i = 0; i < bookmarks.Length(); i ++) + (*aBookmarks)[i] = bookmarks[i]; + } + + *aCount = bookmarks.Length(); + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::GetItemIndex(int64_t aItemId, int32_t* _index) +{ + NS_ENSURE_ARG_MIN(aItemId, 1); + NS_ENSURE_ARG_POINTER(_index); + + BookmarkData bookmark; + nsresult rv = FetchItemInfo(aItemId, bookmark); + // With respect to the API. + if (NS_FAILED(rv)) { + *_index = -1; + return NS_OK; + } + + *_index = bookmark.position; + return NS_OK; +} + +NS_IMETHODIMP +nsNavBookmarks::SetItemIndex(int64_t aItemId, int32_t aNewIndex) +{ + NS_ENSURE_ARG_MIN(aItemId, 1); + NS_ENSURE_ARG_MIN(aNewIndex, 0); + + BookmarkData bookmark; + nsresult rv = FetchItemInfo(aItemId, bookmark); + NS_ENSURE_SUCCESS(rv, rv); + + // Ensure we are not going out of range. + int32_t folderCount; + int64_t grandParentId; + nsAutoCString folderGuid; + rv = FetchFolderInfo(bookmark.parentId, &folderCount, folderGuid, &grandParentId); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(aNewIndex < folderCount, NS_ERROR_INVALID_ARG); + // Check the parent's guid is the expected one. + MOZ_ASSERT(bookmark.parentGuid == folderGuid); + + BEGIN_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id); + + nsCOMPtr stmt = mDB->GetStatement( + "UPDATE moz_bookmarks SET position = :item_index WHERE id = :item_id" + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), aItemId); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_index"), aNewIndex); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + END_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id); + + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemMoved(bookmark.id, + bookmark.parentId, + bookmark.position, + bookmark.parentId, + aNewIndex, + bookmark.type, + bookmark.guid, + bookmark.parentGuid, + bookmark.parentGuid)); + + return NS_OK; +} + + +nsresult +nsNavBookmarks::UpdateKeywordsHashForRemovedBookmark(int64_t aItemId) +{ + nsAutoString keyword; + if (NS_SUCCEEDED(GetKeywordForBookmark(aItemId, keyword)) && + !keyword.IsEmpty()) { + nsresult rv = EnsureKeywordsHash(); + NS_ENSURE_SUCCESS(rv, rv); + mBookmarkToKeywordHash.Remove(aItemId); + + // If the keyword is unused, remove it from the database. + keywordSearchData searchData; + searchData.keyword.Assign(keyword); + searchData.itemId = -1; + mBookmarkToKeywordHash.EnumerateRead(SearchBookmarkForKeyword, &searchData); + if (searchData.itemId == -1) { + nsCOMPtr stmt = mDB->GetAsyncStatement( + "DELETE FROM moz_keywords " + "WHERE keyword = :keyword " + "AND NOT EXISTS ( " + "SELECT id " + "FROM moz_bookmarks " + "WHERE keyword_id = moz_keywords.id " + ")" + ); + NS_ENSURE_STATE(stmt); + + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr pendingStmt; + rv = stmt->ExecuteAsync(nullptr, getter_AddRefs(pendingStmt)); + NS_ENSURE_SUCCESS(rv, rv); + } + } + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::SetKeywordForBookmark(int64_t aBookmarkId, + const nsAString& aUserCasedKeyword) +{ + NS_ENSURE_ARG_MIN(aBookmarkId, 1); + + // This also ensures the bookmark is valid. + BookmarkData bookmark; + nsresult rv = FetchItemInfo(aBookmarkId, bookmark); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EnsureKeywordsHash(); + NS_ENSURE_SUCCESS(rv, rv); + + // Shortcuts are always lowercased internally. + nsAutoString keyword(aUserCasedKeyword); + ToLowerCase(keyword); + + // Check if bookmark was already associated to a keyword. + nsAutoString oldKeyword; + rv = GetKeywordForBookmark(bookmark.id, oldKeyword); + NS_ENSURE_SUCCESS(rv, rv); + + // Trying to set the same value or to remove a nonexistent keyword is a no-op. + if (keyword.Equals(oldKeyword) || (keyword.IsEmpty() && oldKeyword.IsEmpty())) + return NS_OK; + + mozStorageTransaction transaction(mDB->MainConn(), false); + + nsCOMPtr updateBookmarkStmt = mDB->GetStatement( + "UPDATE moz_bookmarks " + "SET keyword_id = (SELECT id FROM moz_keywords WHERE keyword = :keyword), " + "lastModified = :date " + "WHERE id = :item_id " + ); + NS_ENSURE_STATE(updateBookmarkStmt); + mozStorageStatementScoper updateBookmarkScoper(updateBookmarkStmt); + + if (keyword.IsEmpty()) { + // Remove keyword association from the hash. + mBookmarkToKeywordHash.Remove(bookmark.id); + rv = updateBookmarkStmt->BindNullByName(NS_LITERAL_CSTRING("keyword")); + } + else { + // We are associating bookmark to a new keyword. Create a new keyword + // record if needed. + nsCOMPtr newKeywordStmt = mDB->GetStatement( + "INSERT OR IGNORE INTO moz_keywords (keyword) VALUES (:keyword)" + ); + NS_ENSURE_STATE(newKeywordStmt); + mozStorageStatementScoper newKeywordScoper(newKeywordStmt); + + rv = newKeywordStmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), + keyword); + NS_ENSURE_SUCCESS(rv, rv); + rv = newKeywordStmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + // Add new keyword association to the hash, removing the old one if needed. + if (!oldKeyword.IsEmpty()) + mBookmarkToKeywordHash.Remove(bookmark.id); + mBookmarkToKeywordHash.Put(bookmark.id, keyword); + rv = updateBookmarkStmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword); + } + NS_ENSURE_SUCCESS(rv, rv); + bookmark.lastModified = PR_Now(); + rv = updateBookmarkStmt->BindInt64ByName(NS_LITERAL_CSTRING("date"), + bookmark.lastModified); + NS_ENSURE_SUCCESS(rv, rv); + rv = updateBookmarkStmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), + bookmark.id); + NS_ENSURE_SUCCESS(rv, rv); + rv = updateBookmarkStmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + rv = transaction.Commit(); + NS_ENSURE_SUCCESS(rv, rv); + + // Update the cache entry, if needed. + BookmarkKeyClass* key = mRecentBookmarksCache.GetEntry(aBookmarkId); + if (key) { + key->bookmark.lastModified = bookmark.lastModified; + } + + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemChanged(bookmark.id, + NS_LITERAL_CSTRING("keyword"), + false, + NS_ConvertUTF16toUTF8(keyword), + bookmark.lastModified, + bookmark.type, + bookmark.parentId, + bookmark.guid, + bookmark.parentGuid)); + + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::GetKeywordForURI(nsIURI* aURI, nsAString& aKeyword) +{ + NS_ENSURE_ARG(aURI); + aKeyword.Truncate(0); + + nsCOMPtr stmt = mDB->GetStatement( + "SELECT k.keyword " + "FROM moz_places h " + "JOIN moz_bookmarks b ON b.fk = h.id " + "JOIN moz_keywords k ON k.id = b.keyword_id " + "WHERE h.url = :page_url " + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aURI); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasMore = false; + rv = stmt->ExecuteStep(&hasMore); + if (NS_FAILED(rv) || !hasMore) { + aKeyword.SetIsVoid(true); + return NS_OK; // not found: return void keyword string + } + + // found, get the keyword + rv = stmt->GetString(0, aKeyword); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::GetKeywordForBookmark(int64_t aBookmarkId, nsAString& aKeyword) +{ + NS_ENSURE_ARG_MIN(aBookmarkId, 1); + aKeyword.Truncate(0); + + nsresult rv = EnsureKeywordsHash(); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString keyword; + if (!mBookmarkToKeywordHash.Get(aBookmarkId, &keyword)) { + aKeyword.SetIsVoid(true); + } + else { + aKeyword.Assign(keyword); + } + + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::GetURIForKeyword(const nsAString& aUserCasedKeyword, + nsIURI** aURI) +{ + NS_ENSURE_ARG_POINTER(aURI); + NS_ENSURE_TRUE(!aUserCasedKeyword.IsEmpty(), NS_ERROR_INVALID_ARG); + *aURI = nullptr; + + // Shortcuts are always lowercased internally. + nsAutoString keyword(aUserCasedKeyword); + ToLowerCase(keyword); + + nsresult rv = EnsureKeywordsHash(); + NS_ENSURE_SUCCESS(rv, rv); + + keywordSearchData searchData; + searchData.keyword.Assign(keyword); + searchData.itemId = -1; + mBookmarkToKeywordHash.EnumerateRead(SearchBookmarkForKeyword, &searchData); + + if (searchData.itemId == -1) { + // Not found. + return NS_OK; + } + + rv = GetBookmarkURI(searchData.itemId, aURI); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + + +nsresult +nsNavBookmarks::EnsureKeywordsHash() { + if (mBookmarkToKeywordHashInitialized) { + return NS_OK; + } + mBookmarkToKeywordHashInitialized = true; + + nsCOMPtr stmt; + nsresult rv = mDB->MainConn()->CreateStatement(NS_LITERAL_CSTRING( + "SELECT b.id, k.keyword " + "FROM moz_bookmarks b " + "JOIN moz_keywords k ON k.id = b.keyword_id " + ), getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasMore; + while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) { + int64_t itemId; + rv = stmt->GetInt64(0, &itemId); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoString keyword; + rv = stmt->GetString(1, keyword); + NS_ENSURE_SUCCESS(rv, rv); + + mBookmarkToKeywordHash.Put(itemId, keyword); + } + + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::RunInBatchMode(nsINavHistoryBatchCallback* aCallback, + nsISupports* aUserData) { + PROFILER_LABEL("bookmarks", "RunInBatchMode"); + NS_ENSURE_ARG(aCallback); + + mBatching = true; + + // Just forward the request to history. History service must exist for + // bookmarks to work and we are observing it, thus batch notifications will be + // forwarded to bookmarks observers. + nsNavHistory* history = nsNavHistory::GetHistoryService(); + NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY); + nsresult rv = history->RunInBatchMode(aCallback, aUserData); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::AddObserver(nsINavBookmarkObserver* aObserver, + bool aOwnsWeak) +{ + NS_ENSURE_ARG(aObserver); + return mObservers.AppendWeakElement(aObserver, aOwnsWeak); +} + + +NS_IMETHODIMP +nsNavBookmarks::RemoveObserver(nsINavBookmarkObserver* aObserver) +{ + return mObservers.RemoveWeakElement(aObserver); +} + +void +nsNavBookmarks::NotifyItemVisited(const ItemVisitData& aData) +{ + nsCOMPtr uri; + (void)NS_NewURI(getter_AddRefs(uri), aData.bookmark.url); + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemVisited(aData.bookmark.id, + aData.visitId, + aData.time, + aData.transitionType, + uri, + aData.bookmark.parentId, + aData.bookmark.guid, + aData.bookmark.parentGuid)); +} + +void +nsNavBookmarks::NotifyItemChanged(const ItemChangeData& aData) +{ + // A guid must always be defined. + MOZ_ASSERT(!aData.bookmark.guid.IsEmpty()); + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemChanged(aData.bookmark.id, + aData.property, + aData.isAnnotation, + aData.newValue, + aData.bookmark.lastModified, + aData.bookmark.type, + aData.bookmark.parentId, + aData.bookmark.guid, + aData.bookmark.parentGuid)); +} + +//////////////////////////////////////////////////////////////////////////////// +//// 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_MAINTENANCE) == 0) { + // Maintenance can execute direct writes to the database, thus clear all + // the cached bookmarks. + mRecentBookmarksCache.Clear(); + } + else if (strcmp(aTopic, TOPIC_PLACES_SHUTDOWN) == 0) { + // Stop Observing annotations. + nsAnnotationService* annosvc = nsAnnotationService::GetAnnotationService(); + if (annosvc) { + annosvc->RemoveObserver(this); + } + } + else 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; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsINavHistoryObserver + +NS_IMETHODIMP +nsNavBookmarks::OnBeginUpdateBatch() +{ + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, OnBeginUpdateBatch()); + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::OnEndUpdateBatch() +{ + if (mBatching) { + mBatching = false; + } + + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, OnEndUpdateBatch()); + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime, + int64_t aSessionID, int64_t aReferringID, + uint32_t aTransitionType, const nsACString& aGUID, + bool aHidden) +{ + // If the page is bookmarked, notify observers for each associated bookmark. + ItemVisitData visitData; + nsresult rv = aURI->GetSpec(visitData.bookmark.url); + NS_ENSURE_SUCCESS(rv, rv); + visitData.visitId = aVisitId; + visitData.time = aTime; + visitData.transitionType = aTransitionType; + + nsRefPtr< AsyncGetBookmarksForURI > notifier = + new AsyncGetBookmarksForURI(this, &nsNavBookmarks::NotifyItemVisited, visitData); + notifier->Init(); + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::OnDeleteURI(nsIURI* aURI, + const nsACString& aGUID, + uint16_t aReason) +{ +#ifdef DEBUG + nsNavHistory* history = nsNavHistory::GetHistoryService(); + int64_t placeId; + nsAutoCString placeGuid; + NS_ABORT_IF_FALSE( + history && NS_SUCCEEDED(history->GetIdForPage(aURI, &placeId, placeGuid)) && !placeId, + "OnDeleteURI was notified for a page that still exists?" + ); +#endif + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::OnClearHistory() +{ + // TODO(bryner): we should notify on visited-time change for all URIs + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::OnTitleChanged(nsIURI* aURI, + const nsAString& aPageTitle, + const nsACString& aGUID) +{ + // NOOP. We don't consume page titles from moz_places anymore. + // Title-change notifications are sent from SetItemTitle. + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::OnFrecencyChanged(nsIURI* aURI, + int32_t aNewFrecency, + const nsACString& aGUID, + bool aHidden, + PRTime aLastVisitDate) +{ + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::OnManyFrecenciesChanged() +{ + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::OnPageChanged(nsIURI* aURI, + uint32_t aChangedAttribute, + const nsAString& aNewValue, + const nsACString& aGUID) +{ + nsresult rv; + if (aChangedAttribute == nsINavHistoryObserver::ATTRIBUTE_FAVICON) { + ItemChangeData changeData; + rv = aURI->GetSpec(changeData.bookmark.url); + NS_ENSURE_SUCCESS(rv, rv); + changeData.property = NS_LITERAL_CSTRING("favicon"); + changeData.isAnnotation = false; + changeData.newValue = NS_ConvertUTF16toUTF8(aNewValue); + changeData.bookmark.lastModified = 0; + changeData.bookmark.type = TYPE_BOOKMARK; + + // Favicons may be set to either pure URIs or to folder URIs + bool isPlaceURI; + rv = aURI->SchemeIs("place", &isPlaceURI); + NS_ENSURE_SUCCESS(rv, rv); + if (isPlaceURI) { + nsNavHistory* history = nsNavHistory::GetHistoryService(); + NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY); + + nsCOMArray queries; + nsCOMPtr options; + rv = history->QueryStringToQueryArray(changeData.bookmark.url, + &queries, getter_AddRefs(options)); + NS_ENSURE_SUCCESS(rv, rv); + + if (queries.Count() == 1 && queries[0]->Folders().Length() == 1) { + // Fetch missing data. + rv = FetchItemInfo(queries[0]->Folders()[0], changeData.bookmark); + NS_ENSURE_SUCCESS(rv, rv); + NotifyItemChanged(changeData); + } + } + else { + nsRefPtr< AsyncGetBookmarksForURI > notifier = + new AsyncGetBookmarksForURI(this, &nsNavBookmarks::NotifyItemChanged, changeData); + notifier->Init(); + } + } + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::OnDeleteVisits(nsIURI* aURI, PRTime aVisitTime, + const nsACString& aGUID, + uint16_t aReason, uint32_t aTransitionType) +{ + // Notify "cleartime" only if all visits to the page have been removed. + if (!aVisitTime) { + // If the page is bookmarked, notify observers for each associated bookmark. + ItemChangeData changeData; + nsresult rv = aURI->GetSpec(changeData.bookmark.url); + NS_ENSURE_SUCCESS(rv, rv); + changeData.property = NS_LITERAL_CSTRING("cleartime"); + changeData.isAnnotation = false; + changeData.bookmark.lastModified = 0; + changeData.bookmark.type = TYPE_BOOKMARK; + + nsRefPtr< AsyncGetBookmarksForURI > notifier = + new AsyncGetBookmarksForURI(this, &nsNavBookmarks::NotifyItemChanged, changeData); + notifier->Init(); + } + return NS_OK; +} + + +// nsIAnnotationObserver + +NS_IMETHODIMP +nsNavBookmarks::OnPageAnnotationSet(nsIURI* aPage, const nsACString& aName) +{ + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::OnItemAnnotationSet(int64_t aItemId, const nsACString& aName) +{ + BookmarkData bookmark; + nsresult rv = FetchItemInfo(aItemId, bookmark); + NS_ENSURE_SUCCESS(rv, rv); + + bookmark.lastModified = PR_Now(); + rv = SetItemDateInternal(LAST_MODIFIED, bookmark.id, bookmark.lastModified); + NS_ENSURE_SUCCESS(rv, rv); + + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, + nsINavBookmarkObserver, + OnItemChanged(bookmark.id, + aName, + true, + EmptyCString(), + bookmark.lastModified, + bookmark.type, + bookmark.parentId, + bookmark.guid, + bookmark.parentGuid)); + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::OnPageAnnotationRemoved(nsIURI* aPage, const nsACString& aName) +{ + return NS_OK; +} + + +NS_IMETHODIMP +nsNavBookmarks::OnItemAnnotationRemoved(int64_t aItemId, const nsACString& aName) +{ + // As of now this is doing the same as OnItemAnnotationSet, so just forward + // the call. + nsresult rv = OnItemAnnotationSet(aItemId, aName); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +}