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