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