/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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 #include "nsNavHistory.h" #include "nsNavBookmarks.h" #include "nsFaviconService.h" #include "Helpers.h" #include "mozilla/DebugOnly.h" #include "nsDebug.h" #include "nsNetUtil.h" #include "nsString.h" #include "nsReadableUtils.h" #include "nsUnicharUtils.h" #include "prtime.h" #include "nsQueryObject.h" #include "mozilla/dom/PlacesObservers.h" #include "mozilla/dom/PlacesVisit.h" #include "mozilla/dom/PlacesVisitRemoved.h" #include "mozilla/dom/PlacesVisitTitle.h" #include "mozilla/dom/PlacesBookmarkMoved.h" #include "mozilla/dom/PlacesBookmarkTags.h" #include "mozilla/dom/PlacesBookmarkTime.h" #include "mozilla/dom/PlacesBookmarkTitle.h" #include "mozilla/dom/PlacesBookmarkUrl.h" #include "nsCycleCollectionParticipant.h" // Thanks, Windows.h :( #undef CompareString #define TO_ICONTAINER(_node) \ static_cast(_node) #define TO_CONTAINER(_node) static_cast(_node) #define NOTIFY_RESULT_OBSERVERS_RET(_result, _method, _ret) \ PR_BEGIN_MACRO \ NS_ENSURE_TRUE(_result, _ret); \ if (!_result->mSuppressNotifications) { \ ENUMERATE_WEAKARRAY(_result->mObservers, nsINavHistoryResultObserver, \ _method) \ } \ PR_END_MACRO #define NOTIFY_RESULT_OBSERVERS(_result, _method) \ NOTIFY_RESULT_OBSERVERS_RET(_result, _method, NS_ERROR_UNEXPECTED) // What we want is: NS_INTERFACE_MAP_ENTRY(self) for static IID accessors, // but some of our classes (like nsNavHistoryResult) have an ambiguous base // class of nsISupports which prevents this from working (the default macro // converts it to nsISupports, then addrefs it, then returns it). Therefore, we // expand the macro here and change it so that it works. Yuck. #define NS_INTERFACE_MAP_STATIC_AMBIGUOUS(_class) \ if (aIID.Equals(NS_GET_IID(_class))) { \ NS_ADDREF(this); \ *aInstancePtr = this; \ return NS_OK; \ } else // Number of changes to handle separately in a batch. If more changes are // requested the node will switch to full refresh mode. #define MAX_BATCH_CHANGES_BEFORE_REFRESH 5 // Number of Page_removed events to handle by simply refreshing all containers, // because doing so is much faster than incremental updates. #define MAX_PAGE_REMOVES_BEFORE_REFRESH 10 using namespace mozilla; using namespace mozilla::places; namespace { /** * Returns conditions for query update. * QUERYUPDATE_TIME: * This query is only limited by an inclusive time range on the first * query object. The caller can quickly evaluate the time itself if it * chooses. This is even simpler than "simple" below. * QUERYUPDATE_SIMPLE: * This query is evaluatable using evaluateQueryForNode to do live * updating. * QUERYUPDATE_COMPLEX: * This query is not evaluatable using evaluateQueryForNode. When something * happens that this query updates, you will need to re-run the query. * QUERYUPDATE_COMPLEX_WITH_BOOKMARKS: * A complex query that additionally has dependence on bookmarks. All * bookmark-dependent queries fall under this category. * QUERYUPDATE_MOBILEPREF: * A complex query but only updates when the mobile preference changes. * QUERYUPDATE_NONE: * A query that never updates, e.g. the left-pane root query. * * aHasSearchTerms will be set to true if the query has any dependence on * keywords. When there is no dependence on keywords, we can handle title * change operations as simple instead of complex. */ uint32_t getUpdateRequirements(const RefPtr& aQuery, const RefPtr& aOptions, bool* aHasSearchTerms) { // first check if there are search terms bool hasSearchTerms = *aHasSearchTerms = !aQuery->SearchTerms().IsEmpty(); bool nonTimeBasedItems = false; bool domainBasedItems = false; if (aQuery->Parents().Length() > 0 || aQuery->OnlyBookmarked() || aQuery->Tags().Length() > 0 || (aOptions->QueryType() == nsINavHistoryQueryOptions::QUERY_TYPE_BOOKMARKS && hasSearchTerms)) { return QUERYUPDATE_COMPLEX_WITH_BOOKMARKS; } // Note: we don't currently have any complex non-bookmarked items, but these // are expected to be added. Put detection of these items here. if (hasSearchTerms || !aQuery->Domain().IsVoid() || aQuery->Uri() != nullptr) nonTimeBasedItems = true; if (!aQuery->Domain().IsVoid()) domainBasedItems = true; if (aOptions->ResultType() == nsINavHistoryQueryOptions::RESULTS_AS_TAGS_ROOT) return QUERYUPDATE_COMPLEX_WITH_BOOKMARKS; if (aOptions->ResultType() == nsINavHistoryQueryOptions::RESULTS_AS_ROOTS_QUERY) return QUERYUPDATE_MOBILEPREF; if (aOptions->ResultType() == nsINavHistoryQueryOptions::RESULTS_AS_LEFT_PANE_QUERY) return QUERYUPDATE_NONE; // Whenever there is a maximum number of results, // and we are not a bookmark query we must requery. This // is because we can't generally know if any given addition/change causes // the item to be in the top N items in the database. uint16_t sortingMode = aOptions->SortingMode(); if (aOptions->MaxResults() > 0 && sortingMode != nsINavHistoryQueryOptions::SORT_BY_DATE_ASCENDING && sortingMode != nsINavHistoryQueryOptions::SORT_BY_DATE_DESCENDING) return QUERYUPDATE_COMPLEX; if (domainBasedItems) return QUERYUPDATE_HOST; if (!nonTimeBasedItems) return QUERYUPDATE_TIME; return QUERYUPDATE_SIMPLE; } /** * We might have interesting encodings and different case in the host name. * This will convert that host name into an ASCII host name by sending it * through the URI canonicalization. The result can be used for comparison * with other ASCII host name strings. */ nsresult asciiHostNameFromHostString(const nsACString& aHostName, nsACString& aAscii) { aAscii.Truncate(); if (aHostName.IsEmpty()) { return NS_OK; } // To properly generate a uri we must provide a protocol. nsAutoCString fakeURL("http://"); fakeURL.Append(aHostName); nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), fakeURL); NS_ENSURE_SUCCESS(rv, rv); rv = uri->GetAsciiHost(aAscii); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } /** * This runs the node through the given query to see if satisfies the * query conditions. Not every query parameters are handled by this code, * but we handle the most common ones so that performance is better. * We assume that the time on the node is the time that we want to compare. * This is not necessarily true because URL nodes have the last access time, * which is not necessarily the same. However, since this is being called * to update the list, we assume that the last access time is the current * access time that we are being asked to compare so it works out. * Returns true if node matches the query, false if not. */ bool evaluateQueryForNode(const RefPtr& aQuery, const RefPtr& aOptions, const RefPtr& aNode) { // Hidden if (aNode->mHidden && !aOptions->IncludeHidden()) return false; bool hasIt; // Begin time aQuery->GetHasBeginTime(&hasIt); if (hasIt) { PRTime beginTime = nsNavHistory::NormalizeTime(aQuery->BeginTimeReference(), aQuery->BeginTime()); if (aNode->mTime < beginTime) return false; } // End time aQuery->GetHasEndTime(&hasIt); if (hasIt) { PRTime endTime = nsNavHistory::NormalizeTime(aQuery->EndTimeReference(), aQuery->EndTime()); if (aNode->mTime > endTime) return false; } // Search terms if (!aQuery->SearchTerms().IsEmpty()) { // we can use the existing filtering code, just give it our one object in // an array. nsCOMArray inputSet; inputSet.AppendObject(aNode); nsCOMArray filteredSet; nsresult rv = nsNavHistory::FilterResultSet(nullptr, inputSet, &filteredSet, aQuery, aOptions); if (NS_FAILED(rv)) return false; if (!filteredSet.Count()) return false; } // Domain/host if (!aQuery->Domain().IsVoid()) { nsCOMPtr nodeUri; if (NS_FAILED(NS_NewURI(getter_AddRefs(nodeUri), aNode->mURI))) return false; nsAutoCString asciiRequest; if (NS_FAILED(asciiHostNameFromHostString(aQuery->Domain(), asciiRequest))) return false; if (aQuery->DomainIsHost()) { nsAutoCString host; if (NS_FAILED(nodeUri->GetAsciiHost(host))) return false; if (!asciiRequest.Equals(host)) return false; } // check domain names. nsNavHistory* history = nsNavHistory::GetHistoryService(); if (!history) return false; nsAutoCString domain; history->DomainNameFromURI(nodeUri, domain); if (!asciiRequest.Equals(domain)) return false; } // URI if (aQuery->Uri()) { nsCOMPtr nodeUri; if (NS_FAILED(NS_NewURI(getter_AddRefs(nodeUri), aNode->mURI))) return false; bool equals; nsresult rv = aQuery->Uri()->Equals(nodeUri, &equals); NS_ENSURE_SUCCESS(rv, false); if (!equals) return false; } // Transitions const nsTArray& transitions = aQuery->Transitions(); if (aNode->mTransitionType > 0 && transitions.Length() && !transitions.Contains(aNode->mTransitionType)) { return false; } // If we ever make it to the bottom, that means it passed all the tests for // the given query. return true; } // Emulate string comparison (used for sorting) for PRTime and int. inline int32_t ComparePRTime(PRTime a, PRTime b) { if (a < b) return -1; else if (a > b) return 1; return 0; } inline int32_t CompareIntegers(uint32_t a, uint32_t b) { return a - b; } } // anonymous namespace NS_IMPL_CYCLE_COLLECTION(nsNavHistoryResultNode, mParent) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsNavHistoryResultNode) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsINavHistoryResultNode) NS_INTERFACE_MAP_ENTRY(nsINavHistoryResultNode) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(nsNavHistoryResultNode) NS_IMPL_CYCLE_COLLECTING_RELEASE(nsNavHistoryResultNode) nsNavHistoryResultNode::nsNavHistoryResultNode(const nsACString& aURI, const nsACString& aTitle, uint32_t aAccessCount, PRTime aTime) : mParent(nullptr), mURI(aURI), mTitle(aTitle), mAreTagsSorted(false), mAccessCount(aAccessCount), mTime(aTime), mBookmarkIndex(-1), mItemId(-1), mVisitId(-1), mFromVisitId(-1), mDateAdded(0), mLastModified(0), mIndentLevel(-1), mFrecency(0), mHidden(false), mTransitionType(0) { mTags.SetIsVoid(true); } NS_IMETHODIMP nsNavHistoryResultNode::GetIcon(nsACString& aIcon) { if (this->IsContainer() || mURI.IsEmpty()) { return NS_OK; } aIcon.AppendLiteral("page-icon:"); aIcon.Append(mURI); return NS_OK; } NS_IMETHODIMP nsNavHistoryResultNode::GetParent(nsINavHistoryContainerResultNode** aParent) { NS_IF_ADDREF(*aParent = mParent); return NS_OK; } NS_IMETHODIMP nsNavHistoryResultNode::GetParentResult(nsINavHistoryResult** aResult) { *aResult = nullptr; if (IsContainer()) NS_IF_ADDREF(*aResult = GetAsContainer()->mResult); else if (mParent) NS_IF_ADDREF(*aResult = mParent->mResult); NS_ENSURE_STATE(*aResult); return NS_OK; } NS_IMETHODIMP nsNavHistoryResultNode::GetTags(nsAString& aTags) { // Only URI-nodes may be associated with tags if (!IsURI()) { aTags.Truncate(); return NS_OK; } // Initially, the tags string is set to a void string (see constructor). We // then build it the first time this method called is called (and by that, // implicitly unset the void flag). Result observers may re-set the void flag // in order to force rebuilding of the tags string. if (!mTags.IsVoid()) { // If mTags is assigned by a history query it is unsorted for performance // reasons, it must be sorted by name on first read access. if (!mAreTagsSorted) { nsTArray tags; ParseString(NS_ConvertUTF16toUTF8(mTags), ',', tags); tags.Sort(); mTags.SetIsVoid(true); for (nsTArray::index_type i = 0; i < tags.Length(); ++i) { AppendUTF8toUTF16(tags[i], mTags); if (i < tags.Length() - 1) mTags.AppendLiteral(", "); } mAreTagsSorted = true; } aTags.Assign(mTags); return NS_OK; } // Fetch the tags RefPtr DB = Database::GetDatabase(); NS_ENSURE_STATE(DB); nsCOMPtr stmt = DB->GetStatement( "/* do not warn (bug 487594) */ " "SELECT GROUP_CONCAT(tag_title, ', ') " "FROM ( " "SELECT t.title AS tag_title " "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) " "AND t.parent = :tags_folder " "ORDER BY t.title COLLATE NOCASE ASC " ") "); NS_ENSURE_STATE(stmt); mozStorageStatementScoper scoper(stmt); nsNavHistory* history = nsNavHistory::GetHistoryService(); NS_ENSURE_STATE(history); nsresult rv = stmt->BindInt64ByName("tags_folder"_ns, history->GetTagsFolder()); NS_ENSURE_SUCCESS(rv, rv); rv = URIBinder::Bind(stmt, "page_url"_ns, mURI); NS_ENSURE_SUCCESS(rv, rv); bool hasTags = false; if (NS_SUCCEEDED(stmt->ExecuteStep(&hasTags)) && hasTags) { rv = stmt->GetString(0, mTags); NS_ENSURE_SUCCESS(rv, rv); aTags.Assign(mTags); mAreTagsSorted = true; } // If this node is a child of a history query, we need to make sure changes // to tags are properly live-updated. if (mParent && mParent->IsQuery() && mParent->mOptions->QueryType() == nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY) { nsNavHistoryQueryResultNode* query = mParent->GetAsQuery(); nsNavHistoryResult* result = query->GetResult(); NS_ENSURE_STATE(result); result->AddAllBookmarksObserver(query); } return NS_OK; } NS_IMETHODIMP nsNavHistoryResultNode::GetPageGuid(nsACString& aPageGuid) { aPageGuid = mPageGuid; return NS_OK; } NS_IMETHODIMP nsNavHistoryResultNode::GetBookmarkGuid(nsACString& aBookmarkGuid) { aBookmarkGuid = mBookmarkGuid; return NS_OK; } NS_IMETHODIMP nsNavHistoryResultNode::GetVisitId(int64_t* aVisitId) { *aVisitId = mVisitId; return NS_OK; } NS_IMETHODIMP nsNavHistoryResultNode::GetFromVisitId(int64_t* aFromVisitId) { *aFromVisitId = mFromVisitId; return NS_OK; } NS_IMETHODIMP nsNavHistoryResultNode::GetVisitType(uint32_t* aVisitType) { *aVisitType = mTransitionType; return NS_OK; } void nsNavHistoryResultNode::OnRemoving() { mParent = nullptr; } /** * This will find the result for this node. We can ask the nearest container * for this value (either ourselves or our parents should be a container, * and all containers have result pointers). * * @note The result may be null, if the container is detached from the result * who owns it. */ nsNavHistoryResult* nsNavHistoryResultNode::GetResult() { nsNavHistoryResultNode* node = this; do { if (node->IsContainer()) { nsNavHistoryContainerResultNode* container = TO_CONTAINER(node); return container->mResult; } node = node->mParent; } while (node); MOZ_ASSERT(false, "No container node found in hierarchy!"); return nullptr; } NS_IMPL_CYCLE_COLLECTION_INHERITED(nsNavHistoryContainerResultNode, nsNavHistoryResultNode, mResult, mChildren) NS_IMPL_ADDREF_INHERITED(nsNavHistoryContainerResultNode, nsNavHistoryResultNode) NS_IMPL_RELEASE_INHERITED(nsNavHistoryContainerResultNode, nsNavHistoryResultNode) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsNavHistoryContainerResultNode) NS_INTERFACE_MAP_STATIC_AMBIGUOUS(nsNavHistoryContainerResultNode) NS_INTERFACE_MAP_ENTRY(nsINavHistoryContainerResultNode) NS_INTERFACE_MAP_END_INHERITING(nsNavHistoryResultNode) nsNavHistoryContainerResultNode::nsNavHistoryContainerResultNode( const nsACString& aURI, const nsACString& aTitle, PRTime aTime, uint32_t aContainerType, nsNavHistoryQueryOptions* aOptions) : nsNavHistoryResultNode(aURI, aTitle, 0, aTime), mResult(nullptr), mContainerType(aContainerType), mExpanded(false), mOptions(aOptions), mAsyncCanceledState(NOT_CANCELED) { MOZ_ASSERT(mOptions); MOZ_ALWAYS_SUCCEEDS(mOptions->Clone(getter_AddRefs(mOriginalOptions))); } nsNavHistoryContainerResultNode::~nsNavHistoryContainerResultNode() { // Explicitly clean up array of children of this container. We must ensure // all references are gone and all of their destructors are called. mChildren.Clear(); } /** * Containers should notify their children that they are being removed when the * container is being removed. */ void nsNavHistoryContainerResultNode::OnRemoving() { nsNavHistoryResultNode::OnRemoving(); for (int32_t i = 0; i < mChildren.Count(); ++i) mChildren[i]->OnRemoving(); mChildren.Clear(); mResult = nullptr; } bool nsNavHistoryContainerResultNode::AreChildrenVisible() { nsNavHistoryResult* result = GetResult(); if (!result) { MOZ_ASSERT_UNREACHABLE("Invalid result"); return false; } if (!mExpanded) return false; // Now check if any ancestor is closed. nsNavHistoryContainerResultNode* ancestor = mParent; while (ancestor) { if (!ancestor->mExpanded) return false; ancestor = ancestor->mParent; } return true; } NS_IMETHODIMP nsNavHistoryContainerResultNode::GetContainerOpen(bool* aContainerOpen) { *aContainerOpen = mExpanded; return NS_OK; } NS_IMETHODIMP nsNavHistoryContainerResultNode::SetContainerOpen(bool aContainerOpen) { if (aContainerOpen) { if (!mExpanded) { if (mOptions->AsyncEnabled()) OpenContainerAsync(); else OpenContainer(); } } else { if (mExpanded) CloseContainer(); else if (mAsyncPendingStmt) CancelAsyncOpen(false); } return NS_OK; } /** * Notifies the result's observers of a change in the container's state. The * notification includes both the old and new states: The old is aOldState, and * the new is the container's current state. * * @param aOldState * The state being transitioned out of. */ nsresult nsNavHistoryContainerResultNode::NotifyOnStateChange( uint16_t aOldState) { nsNavHistoryResult* result = GetResult(); NS_ENSURE_STATE(result); nsresult rv; uint16_t currState; rv = GetState(&currState); NS_ENSURE_SUCCESS(rv, rv); // Notify via the new ContainerStateChanged observer method. NOTIFY_RESULT_OBSERVERS(result, ContainerStateChanged(this, aOldState, currState)); return NS_OK; } NS_IMETHODIMP nsNavHistoryContainerResultNode::GetState(uint16_t* _state) { NS_ENSURE_ARG_POINTER(_state); *_state = mExpanded ? (uint16_t)STATE_OPENED : mAsyncPendingStmt ? (uint16_t)STATE_LOADING : (uint16_t)STATE_CLOSED; return NS_OK; } /** * This handles the generic container case. Other container types should * override this to do their own handling. */ nsresult nsNavHistoryContainerResultNode::OpenContainer() { NS_ASSERTION(!mExpanded, "Container must not be expanded to open it"); mExpanded = true; nsresult rv = NotifyOnStateChange(STATE_CLOSED); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } /** * Unset aSuppressNotifications to notify observers on this change. That is * the normal operation. This is set to false for the recursive calls since the * root container that is being closed will handle recomputation of the visible * elements for its entire subtree. */ nsresult nsNavHistoryContainerResultNode::CloseContainer( bool aSuppressNotifications) { NS_ASSERTION( (mExpanded && !mAsyncPendingStmt) || (!mExpanded && mAsyncPendingStmt), "Container must be expanded or loading to close it"); nsresult rv; uint16_t oldState; rv = GetState(&oldState); NS_ENSURE_SUCCESS(rv, rv); if (mExpanded) { // Recursively close all child containers. for (int32_t i = 0; i < mChildren.Count(); ++i) { if (mChildren[i]->IsContainer() && mChildren[i]->GetAsContainer()->mExpanded) mChildren[i]->GetAsContainer()->CloseContainer(true); } mExpanded = false; } // Be sure to set this to null before notifying observers. It signifies that // the container is no longer loading (if it was in the first place). mAsyncPendingStmt = nullptr; if (!aSuppressNotifications) { rv = NotifyOnStateChange(oldState); NS_ENSURE_SUCCESS(rv, rv); } // If this is the root container of a result, we can tell the result to stop // observing changes, otherwise the result will stay in memory and updates // itself till it is cycle collected. nsNavHistoryResult* result = GetResult(); NS_ENSURE_STATE(result); if (result->mRootNode == this) { result->StopObserving(); // When reopening this node its result will be out of sync. // We must clear our children to ensure we will call FillChildren // again in such a case. if (this->IsQuery()) this->GetAsQuery()->ClearChildren(true); else if (this->IsFolder()) this->GetAsFolder()->ClearChildren(true); } return NS_OK; } /** * The async version of OpenContainer. */ nsresult nsNavHistoryContainerResultNode::OpenContainerAsync() { return NS_ERROR_NOT_IMPLEMENTED; } /** * Cancels the pending asynchronous Storage execution triggered by * FillChildrenAsync, if it exists. This method doesn't do much, because after * cancelation Storage will call this node's HandleCompletion callback, where * the real work is done. * * @param aRestart * If true, async execution will be restarted by HandleCompletion. */ void nsNavHistoryContainerResultNode::CancelAsyncOpen(bool aRestart) { NS_ASSERTION(mAsyncPendingStmt, "Async execution canceled but not pending"); mAsyncCanceledState = aRestart ? CANCELED_RESTART_NEEDED : CANCELED; // Cancel will fail if the pending statement has already been canceled. // That's OK since this method may be called multiple times, and multiple // cancels don't harm anything. (void)mAsyncPendingStmt->Cancel(); } /** * This builds up tree statistics from the bottom up. Call with a container * and the indent level of that container. To init the full tree, call with * the root container. The default indent level is -1, which is appropriate * for the root level. * * CALL THIS AFTER FILLING ANY CONTAINER to update the parent and result node * pointers, even if you don't care about visit counts and last visit dates. */ void nsNavHistoryContainerResultNode::FillStats() { uint32_t accessCount = 0; PRTime newTime = 0; for (int32_t i = 0; i < mChildren.Count(); ++i) { nsNavHistoryResultNode* node = mChildren[i]; SetAsParentOfNode(node); accessCount += node->mAccessCount; // this is how container nodes get sorted by date // The container gets the most recent time of the child nodes. if (node->mTime > newTime) newTime = node->mTime; } if (mExpanded) { mAccessCount = accessCount; if (!IsQuery() || newTime > mTime) mTime = newTime; } } void nsNavHistoryContainerResultNode::SetAsParentOfNode( nsNavHistoryResultNode* aNode) { aNode->mParent = this; aNode->mIndentLevel = mIndentLevel + 1; if (aNode->IsContainer()) { nsNavHistoryContainerResultNode* container = aNode->GetAsContainer(); // Propagate some of the parent's options to this container. if (mOptions->ExcludeItems()) { container->mOptions->SetExcludeItems(true); } if (mOptions->ExcludeQueries()) { container->mOptions->SetExcludeQueries(true); } if (aNode->IsFolder() && mOptions->AsyncEnabled()) { container->mOptions->SetAsyncEnabled(true); } if (!mOptions->ExpandQueries()) { container->mOptions->SetExpandQueries(false); } container->mResult = mResult; container->FillStats(); } } /** * This is used when one container changes to do a minimal update of the tree * structure. When something changes, you want to call FillStats if necessary * and update this container completely. Then call this function which will * walk up the tree and fill in the previous containers. * * Note that you have to tell us by how much our access count changed. Our * access count should already be set to the new value; this is used tochange * the parents without having to re-count all their children. * * This does NOT update the last visit date downward. Therefore, if you are * deleting a node that has the most recent last visit date, the parents will * not get their last visit dates downshifted accordingly. This is a rather * unusual case: we don't often delete things, and we usually don't even show * the last visit date for folders. Updating would be slower because we would * have to recompute it from scratch. */ nsresult nsNavHistoryContainerResultNode::ReverseUpdateStats( int32_t aAccessCountChange) { if (mParent) { nsNavHistoryResult* result = GetResult(); bool shouldNotify = result && mParent->mParent && mParent->mParent->AreChildrenVisible(); uint32_t oldAccessCount = mParent->mAccessCount; PRTime oldTime = mParent->mTime; mParent->mAccessCount += aAccessCountChange; bool timeChanged = false; if (mTime > mParent->mTime) { timeChanged = true; mParent->mTime = mTime; } if (shouldNotify && !result->CanSkipHistoryDetailsNotifications()) { NOTIFY_RESULT_OBSERVERS( result, NodeHistoryDetailsChanged(TO_ICONTAINER(mParent), oldTime, oldAccessCount)); } // check sorting, the stats may have caused this node to move if the // sorting depended on something we are changing. uint16_t sortMode = mParent->GetSortType(); bool sortingByVisitCount = sortMode == nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_ASCENDING || sortMode == nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_DESCENDING; bool sortingByTime = sortMode == nsINavHistoryQueryOptions::SORT_BY_DATE_ASCENDING || sortMode == nsINavHistoryQueryOptions::SORT_BY_DATE_DESCENDING; if ((sortingByVisitCount && aAccessCountChange != 0) || (sortingByTime && timeChanged)) { int32_t ourIndex = mParent->FindChild(this); NS_ASSERTION(ourIndex >= 0, "Could not find self in parent"); if (ourIndex >= 0) EnsureItemPosition(static_cast(ourIndex)); } nsresult rv = mParent->ReverseUpdateStats(aAccessCountChange); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } /** * This walks up the tree until we find a query result node or the root to get * the sorting type. */ uint16_t nsNavHistoryContainerResultNode::GetSortType() { if (mParent) return mParent->GetSortType(); if (mResult) return mResult->mSortingMode; // This is a detached container, just use natural order. return nsINavHistoryQueryOptions::SORT_BY_NONE; } nsresult nsNavHistoryContainerResultNode::Refresh() { NS_WARNING( "Refresh() is supported by queries or folders, not generic containers."); return NS_OK; } /** * @return the sorting comparator function for the give sort type, or null if * there is no comparator. */ nsNavHistoryContainerResultNode::SortComparator nsNavHistoryContainerResultNode::GetSortingComparator(uint16_t aSortType) { switch (aSortType) { case nsINavHistoryQueryOptions::SORT_BY_NONE: return &SortComparison_Bookmark; case nsINavHistoryQueryOptions::SORT_BY_TITLE_ASCENDING: return &SortComparison_TitleLess; case nsINavHistoryQueryOptions::SORT_BY_TITLE_DESCENDING: return &SortComparison_TitleGreater; case nsINavHistoryQueryOptions::SORT_BY_DATE_ASCENDING: return &SortComparison_DateLess; case nsINavHistoryQueryOptions::SORT_BY_DATE_DESCENDING: return &SortComparison_DateGreater; case nsINavHistoryQueryOptions::SORT_BY_URI_ASCENDING: return &SortComparison_URILess; case nsINavHistoryQueryOptions::SORT_BY_URI_DESCENDING: return &SortComparison_URIGreater; case nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_ASCENDING: return &SortComparison_VisitCountLess; case nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_DESCENDING: return &SortComparison_VisitCountGreater; case nsINavHistoryQueryOptions::SORT_BY_DATEADDED_ASCENDING: return &SortComparison_DateAddedLess; case nsINavHistoryQueryOptions::SORT_BY_DATEADDED_DESCENDING: return &SortComparison_DateAddedGreater; case nsINavHistoryQueryOptions::SORT_BY_LASTMODIFIED_ASCENDING: return &SortComparison_LastModifiedLess; case nsINavHistoryQueryOptions::SORT_BY_LASTMODIFIED_DESCENDING: return &SortComparison_LastModifiedGreater; case nsINavHistoryQueryOptions::SORT_BY_TAGS_ASCENDING: return &SortComparison_TagsLess; case nsINavHistoryQueryOptions::SORT_BY_TAGS_DESCENDING: return &SortComparison_TagsGreater; case nsINavHistoryQueryOptions::SORT_BY_FRECENCY_ASCENDING: return &SortComparison_FrecencyLess; case nsINavHistoryQueryOptions::SORT_BY_FRECENCY_DESCENDING: return &SortComparison_FrecencyGreater; default: MOZ_ASSERT_UNREACHABLE("Bad sorting type"); return nullptr; } } /** * This is used by Result::SetSortingMode and QueryResultNode::FillChildren to * sort the child list. * * This does NOT update any visibility or tree information. The caller will * have to completely rebuild the visible list after this. */ void nsNavHistoryContainerResultNode::RecursiveSort( SortComparator aComparator) { mChildren.Sort(aComparator, nullptr); for (int32_t i = 0; i < mChildren.Count(); ++i) { if (mChildren[i]->IsContainer()) mChildren[i]->GetAsContainer()->RecursiveSort(aComparator); } } /** * @return the index that the given item would fall on if it were to be * inserted using the given sorting. */ uint32_t nsNavHistoryContainerResultNode::FindInsertionPoint( nsNavHistoryResultNode* aNode, SortComparator aComparator, bool* aItemExists) { if (aItemExists) (*aItemExists) = false; if (mChildren.Count() == 0) return 0; // The common case is the beginning or the end because this is used to insert // new items that are added to history, which is usually sorted by date. int32_t res; res = aComparator(aNode, mChildren[0], nullptr); if (res <= 0) { if (aItemExists && res == 0) (*aItemExists) = true; return 0; } res = aComparator(aNode, mChildren[mChildren.Count() - 1], nullptr); if (res >= 0) { if (aItemExists && res == 0) (*aItemExists) = true; return mChildren.Count(); } uint32_t beginRange = 0; // inclusive uint32_t endRange = mChildren.Count(); // exclusive while (1) { if (beginRange == endRange) return endRange; uint32_t center = beginRange + (endRange - beginRange) / 2; int32_t res = aComparator(aNode, mChildren[center], nullptr); if (res <= 0) { endRange = center; // left side if (aItemExists && res == 0) (*aItemExists) = true; } else { beginRange = center + 1; // right site } } } /** * This checks the child node at the given index to see if its sorting is * correct. This is called when nodes are updated and we need to see whether * we need to move it. * * @returns true if not and it should be resorted. */ bool nsNavHistoryContainerResultNode::DoesChildNeedResorting( uint32_t aIndex, SortComparator aComparator) { NS_ASSERTION(aIndex < uint32_t(mChildren.Count()), "Input index out of range"); if (mChildren.Count() == 1) return false; if (aIndex > 0) { // compare to previous item if (aComparator(mChildren[aIndex - 1], mChildren[aIndex], nullptr) > 0) return true; } if (aIndex < uint32_t(mChildren.Count()) - 1) { // compare to next item if (aComparator(mChildren[aIndex], mChildren[aIndex + 1], nullptr) > 0) return true; } return false; } /* static */ int32_t nsNavHistoryContainerResultNode::SortComparison_StringLess( const nsAString& a, const nsAString& b) { nsNavHistory* history = nsNavHistory::GetHistoryService(); NS_ENSURE_TRUE(history, 0); const mozilla::intl::Collator* collator = history->GetCollator(); NS_ENSURE_TRUE(collator, 0); int32_t res = collator->CompareStrings(a, b); return res; } /** * When there are bookmark indices, we should never have ties, so we don't * need to worry about tiebreaking. When there are no bookmark indices, * everything will be -1 and we don't worry about sorting. */ int32_t nsNavHistoryContainerResultNode::SortComparison_Bookmark( nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure) { return a->mBookmarkIndex - b->mBookmarkIndex; } /** * These are a little more complicated because they do a localization * conversion. If this is too slow, we can compute the sort keys once in * advance, sort that array, and then reorder the real array accordingly. * This would save some key generations. * * The collation object must be allocated before sorting on title! */ int32_t nsNavHistoryContainerResultNode::SortComparison_TitleLess( nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure) { uint32_t aType; a->GetType(&aType); int32_t value = SortComparison_StringLess(NS_ConvertUTF8toUTF16(a->mTitle), NS_ConvertUTF8toUTF16(b->mTitle)); if (value == 0) { // resolve by URI if (a->IsURI()) { value = Compare(a->mURI, b->mURI); } if (value == 0) { // resolve by date value = ComparePRTime(a->mTime, b->mTime); if (value == 0) value = nsNavHistoryContainerResultNode::SortComparison_Bookmark( a, b, closure); } } return value; } int32_t nsNavHistoryContainerResultNode::SortComparison_TitleGreater( nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure) { return -SortComparison_TitleLess(a, b, closure); } /** * Equal times will be very unusual, but it is important that there is some * deterministic ordering of the results so they don't move around. */ int32_t nsNavHistoryContainerResultNode::SortComparison_DateLess( nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure) { int32_t value = ComparePRTime(a->mTime, b->mTime); if (value == 0) { value = SortComparison_StringLess(NS_ConvertUTF8toUTF16(a->mTitle), NS_ConvertUTF8toUTF16(b->mTitle)); if (value == 0) value = nsNavHistoryContainerResultNode::SortComparison_Bookmark(a, b, closure); } return value; } int32_t nsNavHistoryContainerResultNode::SortComparison_DateGreater( nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure) { return -nsNavHistoryContainerResultNode::SortComparison_DateLess(a, b, closure); } int32_t nsNavHistoryContainerResultNode::SortComparison_DateAddedLess( nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure) { int32_t value = ComparePRTime(a->mDateAdded, b->mDateAdded); if (value == 0) { value = SortComparison_StringLess(NS_ConvertUTF8toUTF16(a->mTitle), NS_ConvertUTF8toUTF16(b->mTitle)); if (value == 0) value = nsNavHistoryContainerResultNode::SortComparison_Bookmark(a, b, closure); } return value; } int32_t nsNavHistoryContainerResultNode::SortComparison_DateAddedGreater( nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure) { return -nsNavHistoryContainerResultNode::SortComparison_DateAddedLess( a, b, closure); } int32_t nsNavHistoryContainerResultNode::SortComparison_LastModifiedLess( nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure) { int32_t value = ComparePRTime(a->mLastModified, b->mLastModified); if (value == 0) { value = SortComparison_StringLess(NS_ConvertUTF8toUTF16(a->mTitle), NS_ConvertUTF8toUTF16(b->mTitle)); if (value == 0) value = nsNavHistoryContainerResultNode::SortComparison_Bookmark(a, b, closure); } return value; } int32_t nsNavHistoryContainerResultNode::SortComparison_LastModifiedGreater( nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure) { return -nsNavHistoryContainerResultNode::SortComparison_LastModifiedLess( a, b, closure); } /** * Certain types of parent nodes are treated specially because URIs are not * valid (like days or hosts). */ int32_t nsNavHistoryContainerResultNode::SortComparison_URILess( nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure) { int32_t value; if (a->IsURI() && b->IsURI()) { // normal URI or visit value = Compare(a->mURI, b->mURI); } else if (a->IsContainer() && !b->IsContainer()) { // Containers appear before entries with a uri. return -1; } else if (b->IsContainer() && !a->IsContainer()) { return 1; } else { // For everything else, use title sorting. value = SortComparison_StringLess(NS_ConvertUTF8toUTF16(a->mTitle), NS_ConvertUTF8toUTF16(b->mTitle)); } if (value == 0) { value = ComparePRTime(a->mTime, b->mTime); if (value == 0) value = nsNavHistoryContainerResultNode::SortComparison_Bookmark(a, b, closure); } return value; } int32_t nsNavHistoryContainerResultNode::SortComparison_URIGreater( nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure) { return -SortComparison_URILess(a, b, closure); } /** * Fall back on dates for conflict resolution */ int32_t nsNavHistoryContainerResultNode::SortComparison_VisitCountLess( nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure) { int32_t value = CompareIntegers(a->mAccessCount, b->mAccessCount); if (value == 0) { value = ComparePRTime(a->mTime, b->mTime); if (value == 0) value = nsNavHistoryContainerResultNode::SortComparison_Bookmark(a, b, closure); } return value; } int32_t nsNavHistoryContainerResultNode::SortComparison_VisitCountGreater( nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure) { return -nsNavHistoryContainerResultNode::SortComparison_VisitCountLess( a, b, closure); } int32_t nsNavHistoryContainerResultNode::SortComparison_TagsLess( nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure) { int32_t value = 0; nsAutoString aTags, bTags; nsresult rv = a->GetTags(aTags); NS_ENSURE_SUCCESS(rv, 0); rv = b->GetTags(bTags); NS_ENSURE_SUCCESS(rv, 0); value = SortComparison_StringLess(aTags, bTags); // fall back to title sorting if (value == 0) value = SortComparison_TitleLess(a, b, closure); return value; } int32_t nsNavHistoryContainerResultNode::SortComparison_TagsGreater( nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure) { return -SortComparison_TagsLess(a, b, closure); } /** * Fall back on date and bookmarked status, for conflict resolution. */ int32_t nsNavHistoryContainerResultNode::SortComparison_FrecencyLess( nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure) { int32_t value = CompareIntegers(a->mFrecency, b->mFrecency); if (value == 0) { value = ComparePRTime(a->mTime, b->mTime); if (value == 0) { value = nsNavHistoryContainerResultNode::SortComparison_Bookmark(a, b, closure); } } return value; } int32_t nsNavHistoryContainerResultNode::SortComparison_FrecencyGreater( nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure) { return -nsNavHistoryContainerResultNode::SortComparison_FrecencyLess(a, b, closure); } /** * Searches this folder for a node with the given URI. Returns null if not * found. * * @note Does not addref the node! */ nsNavHistoryResultNode* nsNavHistoryContainerResultNode::FindChildByURI( const nsACString& aSpec, uint32_t* aNodeIndex) { for (int32_t i = 0; i < mChildren.Count(); ++i) { if (mChildren[i]->IsURI()) { if (aSpec.Equals(mChildren[i]->mURI)) { *aNodeIndex = i; return mChildren[i]; } } } return nullptr; } /** * Searches for matches for the given URI. */ void nsNavHistoryContainerResultNode::FindChildrenByURI( const nsCString& aSpec, nsCOMArray* aMatches) { for (int32_t i = 0; i < mChildren.Count(); ++i) { if (mChildren[i]->IsURI()) { if (aSpec.Equals(mChildren[i]->mURI)) { aMatches->AppendObject(mChildren[i]); } } } } /** * Searches this folder for a node with the given guid/target-folder-guid. * * @return the node if found, null otherwise. * @note Does not addref the node! */ nsNavHistoryResultNode* nsNavHistoryContainerResultNode::FindChildByGuid( const nsACString& guid, int32_t* nodeIndex) { *nodeIndex = -1; for (int32_t i = 0; i < mChildren.Count(); ++i) { if (mChildren[i]->mBookmarkGuid == guid || mChildren[i]->mPageGuid == guid || (mChildren[i]->IsFolder() && mChildren[i]->GetAsFolder()->mTargetFolderGuid == guid)) { *nodeIndex = i; return mChildren[i]; } } return nullptr; } /** * This does the work of adding a child to the container. The child can be * either a container or or a single item that may even be collapsed with the * adjacent ones. */ nsresult nsNavHistoryContainerResultNode::InsertChildAt( nsNavHistoryResultNode* aNode, int32_t aIndex) { nsNavHistoryResult* result = GetResult(); NS_ENSURE_STATE(result); SetAsParentOfNode(aNode); if (!mChildren.InsertObjectAt(aNode, aIndex)) return NS_ERROR_OUT_OF_MEMORY; // Update our stats and notify the result's observers. uint32_t oldAccessCount = mAccessCount; PRTime oldTime = mTime; mAccessCount += aNode->mAccessCount; if (mTime < aNode->mTime) mTime = aNode->mTime; if ((!mParent || mParent->AreChildrenVisible()) && !result->CanSkipHistoryDetailsNotifications()) { NOTIFY_RESULT_OBSERVERS( result, NodeHistoryDetailsChanged(TO_ICONTAINER(this), oldTime, oldAccessCount)); } nsresult rv = ReverseUpdateStats(aNode->mAccessCount); NS_ENSURE_SUCCESS(rv, rv); // Update tree if we are visible. Note that we could be here and not // expanded, like when there is a bookmark folder being updated because its // parent is visible. if (AreChildrenVisible()) NOTIFY_RESULT_OBSERVERS(result, NodeInserted(this, aNode, aIndex)); return NS_OK; } /** * This locates the proper place for insertion according to the current sort * and calls InsertChildAt */ nsresult nsNavHistoryContainerResultNode::InsertSortedChild( nsNavHistoryResultNode* aNode, bool aIgnoreDuplicates) { if (mChildren.Count() == 0) return InsertChildAt(aNode, 0); SortComparator comparator = GetSortingComparator(GetSortType()); if (comparator) { // When inserting a new node, it must have proper statistics because we use // them to find the correct insertion point. The insert function will then // recompute these statistics and fill in the proper parents and hierarchy // level. Doing this twice shouldn't be a large performance penalty because // when we are inserting new containers, they typically contain only one // item (because we've browsed a new page). if (aNode->IsContainer()) { // need to update all the new item's children nsNavHistoryContainerResultNode* container = aNode->GetAsContainer(); container->mResult = mResult; container->FillStats(); } bool itemExists; uint32_t position = FindInsertionPoint(aNode, comparator, &itemExists); if (aIgnoreDuplicates && itemExists) return NS_OK; return InsertChildAt(aNode, position); } return InsertChildAt(aNode, mChildren.Count()); } /** * This checks if the item at aIndex is located correctly given the sorting * move. If it's not, the item is moved, and the result's observers are * notified. * * @return true if the item position has been changed, false otherwise. */ bool nsNavHistoryContainerResultNode::EnsureItemPosition(uint32_t aIndex) { NS_ASSERTION(aIndex < (uint32_t)mChildren.Count(), "Invalid index"); if (aIndex >= (uint32_t)mChildren.Count()) return false; SortComparator comparator = GetSortingComparator(GetSortType()); if (!comparator) return false; if (!DoesChildNeedResorting(aIndex, comparator)) return false; RefPtr node(mChildren[aIndex]); mChildren.RemoveObjectAt(aIndex); uint32_t newIndex = FindInsertionPoint(node, comparator, nullptr); mChildren.InsertObjectAt(node.get(), newIndex); if (AreChildrenVisible()) { nsNavHistoryResult* result = GetResult(); NOTIFY_RESULT_OBSERVERS_RET( result, NodeMoved(node, this, aIndex, this, newIndex), false); } return true; } /** * This does all the work of removing a child from this container, including * updating the tree if necessary. Note that we do not need to be open for * this to work. */ nsresult nsNavHistoryContainerResultNode::RemoveChildAt(int32_t aIndex) { NS_ASSERTION(aIndex >= 0 && aIndex < mChildren.Count(), "Invalid index"); // Hold an owning reference to keep from expiring while we work with it. RefPtr oldNode = mChildren[aIndex]; // Update stats. // XXX This assertion does not reliably pass -- investigate!! (bug 1049797) // MOZ_ASSERT(mAccessCount >= mChildren[aIndex]->mAccessCount, // "Invalid access count while updating!"); uint32_t oldAccessCount = mAccessCount; mAccessCount -= mChildren[aIndex]->mAccessCount; // Remove it from our list and notify the result's observers. mChildren.RemoveObjectAt(aIndex); if (AreChildrenVisible()) { nsNavHistoryResult* result = GetResult(); NOTIFY_RESULT_OBSERVERS(result, NodeRemoved(this, oldNode, aIndex)); } nsresult rv = ReverseUpdateStats(mAccessCount - oldAccessCount); NS_ENSURE_SUCCESS(rv, rv); oldNode->OnRemoving(); return NS_OK; } /** * Searches for matches for the given URI. If aOnlyOne is set, it will * terminate as soon as it finds a single match. This would be used when there * are URI results so there will only ever be one copy of any URI. * * When aOnlyOne is false, it will check all elements. This is for non-history * or visit style results that may have multiple copies of any given URI. */ void nsNavHistoryContainerResultNode::RecursiveFindURIs( bool aOnlyOne, nsNavHistoryContainerResultNode* aContainer, const nsCString& aSpec, nsCOMArray* aMatches) { for (int32_t i = 0; i < aContainer->mChildren.Count(); ++i) { auto* node = aContainer->mChildren[i]; if (node->IsURI()) { if (node->mURI.Equals(aSpec)) { aMatches->AppendObject(node); if (aOnlyOne) { return; } } } else if (node->IsContainer() && node->GetAsContainer()->mExpanded) { RecursiveFindURIs(aOnlyOne, node->GetAsContainer(), aSpec, aMatches); } } } /** * If aUpdateSort is true, we will also update the sorting of this item. * Normally you want this to be true, but it can be false if the thing you are * changing can not affect sorting (like favicons). * * You should NOT change any child lists as part of the callback function. */ bool nsNavHistoryContainerResultNode::UpdateURIs( bool aRecursive, bool aOnlyOne, bool aUpdateSort, const nsCString& aSpec, nsresult (*aCallback)(nsNavHistoryResultNode*, const void*, const nsNavHistoryResult*), const void* aClosure) { const nsNavHistoryResult* result = GetResult(); if (!result) { MOZ_ASSERT(false, "Should have a result"); return false; } // this needs to be owning since sometimes we remove and re-insert nodes // in their parents and we don't want them to go away. nsCOMArray matches; if (aRecursive) { RecursiveFindURIs(aOnlyOne, this, aSpec, &matches); } else if (aOnlyOne) { uint32_t nodeIndex; nsNavHistoryResultNode* node = FindChildByURI(aSpec, &nodeIndex); if (node) matches.AppendObject(node); } else { MOZ_ASSERT( false, "UpdateURIs does not handle nonrecursive updates of multiple items."); // this case easy to add if you need it, just find all the matching URIs // at this level. However, this isn't currently used. History uses // recursive, Bookmarks uses one level and knows that the match is unique. return false; } if (matches.Count() == 0) return false; // PERFORMANCE: This updates each container for each child in it that // changes. In some cases, many elements have changed inside the same // container. It would be better to compose a list of containers, and // update each one only once for all the items that have changed in it. for (int32_t i = 0; i < matches.Count(); ++i) { nsNavHistoryResultNode* node = matches[i]; nsNavHistoryContainerResultNode* parent = node->mParent; if (!parent) { MOZ_ASSERT(false, "All URI nodes being updated must have parents"); continue; } uint32_t oldAccessCount = node->mAccessCount; PRTime oldTime = node->mTime; uint32_t parentOldAccessCount = parent->mAccessCount; PRTime parentOldTime = parent->mTime; aCallback(node, aClosure, result); if (oldAccessCount != node->mAccessCount || oldTime != node->mTime) { parent->mAccessCount += node->mAccessCount - oldAccessCount; if (node->mTime > parent->mTime) parent->mTime = node->mTime; if (parent->AreChildrenVisible() && !result->CanSkipHistoryDetailsNotifications()) { NOTIFY_RESULT_OBSERVERS_RET( result, NodeHistoryDetailsChanged(TO_ICONTAINER(parent), parentOldTime, parentOldAccessCount), true); } DebugOnly rv = parent->ReverseUpdateStats(node->mAccessCount - oldAccessCount); MOZ_ASSERT(NS_SUCCEEDED(rv), "should be able to ReverseUpdateStats"); } if (aUpdateSort) { int32_t childIndex = parent->FindChild(node); MOZ_ASSERT(childIndex >= 0, "Could not find child we just got a reference to"); if (childIndex >= 0) parent->EnsureItemPosition(childIndex); } } return true; } /** * This is used to update the titles in the tree. This is called from both * query and bookmark folder containers to update the tree. Bookmark folders * should be sure to set recursive to false, since child folders will have * their own callbacks registered. */ static nsresult setTitleCallback(nsNavHistoryResultNode* aNode, const void* aClosure, const nsNavHistoryResult* aResult) { const nsACString* newTitle = static_cast(aClosure); aNode->mTitle = *newTitle; if (aResult && (!aNode->mParent || aNode->mParent->AreChildrenVisible())) NOTIFY_RESULT_OBSERVERS(aResult, NodeTitleChanged(aNode, *newTitle)); return NS_OK; } nsresult nsNavHistoryContainerResultNode::ChangeTitles( nsIURI* aURI, const nsACString& aNewTitle, bool aRecursive, bool aOnlyOne) { // uri string nsAutoCString uriString; nsresult rv = aURI->GetSpec(uriString); NS_ENSURE_SUCCESS(rv, rv); // The recursive function will update the result's tree nodes, but only if we // give it a non-null pointer. So if there isn't a tree, just pass nullptr // so it doesn't bother trying to call the result. nsNavHistoryResult* result = GetResult(); NS_ENSURE_STATE(result); uint16_t sortType = GetSortType(); bool updateSorting = (sortType == nsINavHistoryQueryOptions::SORT_BY_TITLE_ASCENDING || sortType == nsINavHistoryQueryOptions::SORT_BY_TITLE_DESCENDING); UpdateURIs(aRecursive, aOnlyOne, updateSorting, uriString, setTitleCallback, static_cast(&aNewTitle)); return NS_OK; } /** * Complex containers (folders and queries) will override this. Here, we * handle the case of simple containers (like host groups) where the children * are always stored. */ NS_IMETHODIMP nsNavHistoryContainerResultNode::GetHasChildren(bool* aHasChildren) { *aHasChildren = (mChildren.Count() > 0); return NS_OK; } /** * @throws if this node is closed. */ NS_IMETHODIMP nsNavHistoryContainerResultNode::GetChildCount(uint32_t* aChildCount) { if (!mExpanded) return NS_ERROR_NOT_AVAILABLE; *aChildCount = mChildren.Count(); return NS_OK; } NS_IMETHODIMP nsNavHistoryContainerResultNode::GetChild(uint32_t aIndex, nsINavHistoryResultNode** _child) { if (!mExpanded) return NS_ERROR_NOT_AVAILABLE; if (aIndex >= uint32_t(mChildren.Count())) return NS_ERROR_INVALID_ARG; nsCOMPtr child = mChildren[aIndex]; child.forget(_child); return NS_OK; } NS_IMETHODIMP nsNavHistoryContainerResultNode::GetChildIndex(nsINavHistoryResultNode* aNode, uint32_t* _retval) { if (!mExpanded) return NS_ERROR_NOT_AVAILABLE; int32_t nodeIndex = FindChild(static_cast(aNode)); if (nodeIndex == -1) return NS_ERROR_INVALID_ARG; *_retval = nodeIndex; return NS_OK; } /** * HOW QUERY UPDATING WORKS * * Queries are different than bookmark folders in that we can not always do * dynamic updates (easily) and updates are more expensive. Therefore, we do * NOT query if we are not open and want to see if we have any children (for * drawing a twisty) and always assume we will. * * When the container is opened, we execute the query and register the * listeners. Like bookmark folders, we stay registered even when closed, and * clear ourselves as soon as a message comes in. This lets us respond quickly * if the user closes and reopens the container. * * We try to handle the most common notifications for the most common query * types dynamically, that is, figuring out what should happen in response to * a message without doing a requery. For complex changes or complex queries, * we give up and requery. */ NS_IMPL_ISUPPORTS_INHERITED(nsNavHistoryQueryResultNode, nsNavHistoryContainerResultNode, nsINavHistoryQueryResultNode) nsNavHistoryQueryResultNode::nsNavHistoryQueryResultNode( const nsACString& aTitle, PRTime aTime, const nsACString& aQueryURI, const RefPtr& aQuery, const RefPtr& aOptions) : nsNavHistoryContainerResultNode(aQueryURI, aTitle, aTime, nsNavHistoryResultNode::RESULT_TYPE_QUERY, aOptions), mQuery(aQuery), mLiveUpdate(getUpdateRequirements(aQuery, aOptions, &mHasSearchTerms)), mContentsValid(false), mBatchChanges(0), mTransitions(aQuery->Transitions().Clone()) {} nsNavHistoryQueryResultNode::~nsNavHistoryQueryResultNode() { // Remove this node from result's observers. We don't need to be notified // anymore. if (mResult && mResult->mAllBookmarksObservers.Contains(this)) mResult->RemoveAllBookmarksObserver(this); if (mResult && mResult->mHistoryObservers.Contains(this)) mResult->RemoveHistoryObserver(this); if (mResult && mResult->mMobilePrefObservers.Contains(this)) mResult->RemoveMobilePrefsObserver(this); } /** * Whoever made us may want non-expanding queries. However, we always expand * when we are the root node, or else asking for non-expanding queries would be * useless. A query node is not expandable if excludeItems is set or if * expandQueries is unset. */ bool nsNavHistoryQueryResultNode::CanExpand() { // The root node and containersQueries can always expand; if ((mResult && mResult->mRootNode == this) || IsContainersQuery()) { return true; } if (mOptions->ExcludeItems()) { return false; } if (mOptions->ExpandQueries()) { return true; } return false; } /** * Some query with a particular result type can contain other queries. They * must be always expandable */ bool nsNavHistoryQueryResultNode::IsContainersQuery() { uint16_t resultType = Options()->ResultType(); return resultType == nsINavHistoryQueryOptions::RESULTS_AS_DATE_QUERY || resultType == nsINavHistoryQueryOptions::RESULTS_AS_DATE_SITE_QUERY || resultType == nsINavHistoryQueryOptions::RESULTS_AS_TAGS_ROOT || resultType == nsINavHistoryQueryOptions::RESULTS_AS_SITE_QUERY || resultType == nsINavHistoryQueryOptions::RESULTS_AS_ROOTS_QUERY || resultType == nsINavHistoryQueryOptions::RESULTS_AS_LEFT_PANE_QUERY; } /** * Here we do not want to call ContainerResultNode::OnRemoving since our own * ClearChildren will do the same thing and more (unregister the observers). * The base ResultNode::OnRemoving will clear some regular node stats, so it * is OK. */ void nsNavHistoryQueryResultNode::OnRemoving() { nsNavHistoryResultNode::OnRemoving(); ClearChildren(true); mResult = nullptr; } /** * Marks the container as open, rebuilding results if they are invalid. We * may still have valid results if the container was previously open and * nothing happened since closing it. * * We do not handle CloseContainer specially. The default one just marks the * container as closed, but doesn't actually mark the results as invalid. * The results will be invalidated by the next history or bookmark * notification that comes in. This means if you open and close the item * without anything happening in between, it will be fast (this actually * happens when results are used as menus). */ nsresult nsNavHistoryQueryResultNode::OpenContainer() { NS_ASSERTION(!mExpanded, "Container must be closed to open it"); mExpanded = true; nsresult rv; if (!CanExpand()) return NS_OK; if (!mContentsValid) { rv = FillChildren(); NS_ENSURE_SUCCESS(rv, rv); } rv = NotifyOnStateChange(STATE_CLOSED); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } /** * When we have valid results we can always give an exact answer. When we * don't we just assume we'll have results, since actually doing the query * might be hard. This is used to draw twisties on the tree, so precise results * don't matter. */ NS_IMETHODIMP nsNavHistoryQueryResultNode::GetHasChildren(bool* aHasChildren) { *aHasChildren = false; if (!CanExpand()) { return NS_OK; } uint16_t resultType = mOptions->ResultType(); // Tags are always populated, otherwise they are removed. if (mQuery->Tags().Length() == 1 && mParent && mParent->mOptions->ResultType() == nsINavHistoryQueryOptions::RESULTS_AS_TAGS_ROOT) { *aHasChildren = true; return NS_OK; } // AllBookmarks and the left pane folder also always have children. if (resultType == nsINavHistoryQueryOptions::RESULTS_AS_ROOTS_QUERY || resultType == nsINavHistoryQueryOptions::RESULTS_AS_LEFT_PANE_QUERY) { *aHasChildren = true; return NS_OK; } // For history containers query we must check if we have any history if (resultType == nsINavHistoryQueryOptions::RESULTS_AS_DATE_QUERY || resultType == nsINavHistoryQueryOptions::RESULTS_AS_DATE_SITE_QUERY || resultType == nsINavHistoryQueryOptions::RESULTS_AS_SITE_QUERY) { nsNavHistory* history = nsNavHistory::GetHistoryService(); NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY); *aHasChildren = history->hasHistoryEntries(); return NS_OK; } // TODO (Bug 1477934): We don't have a good synchronous way to fetch whether // we have tags or not, to properly reply to the hasChildren request on the // tags root. Potentially we could pass this information when we create the // container. // If the container is open and populated, this is trivial. if (mContentsValid) { *aHasChildren = (mChildren.Count() > 0); return NS_OK; } // Fallback to assume we have children. *aHasChildren = true; return NS_OK; } /** * This doesn't just return mURI because in the case of queries that may * be lazily constructed from the query objects. */ NS_IMETHODIMP nsNavHistoryQueryResultNode::GetUri(nsACString& aURI) { aURI = mURI; return NS_OK; } NS_IMETHODIMP nsNavHistoryQueryResultNode::GetFolderItemId(int64_t* aItemId) { *aItemId = -1; return NS_OK; } NS_IMETHODIMP nsNavHistoryQueryResultNode::GetTargetFolderGuid(nsACString& aGuid) { aGuid.Truncate(); return NS_OK; } NS_IMETHODIMP nsNavHistoryQueryResultNode::GetQuery(nsINavHistoryQuery** _query) { RefPtr query = mQuery; query.forget(_query); return NS_OK; } NS_IMETHODIMP nsNavHistoryQueryResultNode::GetQueryOptions( nsINavHistoryQueryOptions** _options) { MOZ_ASSERT(mOptions, "Options should be valid"); RefPtr options = mOptions; options.forget(_options); return NS_OK; } /** * Safe options getter, ensures query is parsed first. */ nsNavHistoryQueryOptions* nsNavHistoryQueryResultNode::Options() { MOZ_ASSERT(mOptions, "Options invalid, cannot generate from URI"); return mOptions; } nsresult nsNavHistoryQueryResultNode::FillChildren() { MOZ_ASSERT(!mContentsValid, "Don't call FillChildren when contents are valid"); MOZ_ASSERT(mChildren.Count() == 0, "We are trying to fill children when there already are some"); NS_ENSURE_STATE(mQuery && mOptions); // get the results from the history service nsNavHistory* history = nsNavHistory::GetHistoryService(); NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY); nsresult rv = history->GetQueryResults(this, mQuery, mOptions, &mChildren); NS_ENSURE_SUCCESS(rv, rv); // it is important to call FillStats to fill in the parents on all // nodes and the result node pointers on the containers FillStats(); uint16_t sortType = GetSortType(); if (mResult && mResult->mNeedsToApplySortingMode) { // We should repopulate container and then apply sortingMode. To avoid // sorting 2 times we simply do that here. mResult->SetSortingMode(mResult->mSortingMode); } else if (mOptions->QueryType() != nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY || sortType != nsINavHistoryQueryOptions::SORT_BY_NONE) { // The default SORT_BY_NONE sorts by the bookmark index (position), // which we do not have for history queries. // Once we've computed all tree stats, we can sort, because containers will // then have proper visit counts and dates. SortComparator comparator = GetSortingComparator(GetSortType()); if (comparator) { // Usually containers queries results comes already sorted from the // database, but some locales could have special rules to sort by title. // RecursiveSort won't apply these rules to containers in containers // queries because when setting sortingMode on the result we want to sort // contained items (bug 473157). // Base container RecursiveSort will sort both our children and all // descendants, and is used in this case because we have to do manual // title sorting. // Query RecursiveSort will instead only sort descendants if we are a // constinaersQuery, e.g. a grouped query that will return other queries. // For other type of queries it will act as the base one. if (IsContainersQuery() && sortType == mOptions->SortingMode() && (sortType == nsINavHistoryQueryOptions::SORT_BY_TITLE_ASCENDING || sortType == nsINavHistoryQueryOptions::SORT_BY_TITLE_DESCENDING)) { nsNavHistoryContainerResultNode::RecursiveSort(comparator); } else { RecursiveSort(comparator); } } } // if we are limiting our results remove items from the end of the // mChildren array after sorting. This is done for root node only. // note, if count < max results, we won't do anything. if (!mParent && mOptions->MaxResults()) { while ((uint32_t)mChildren.Count() > mOptions->MaxResults()) mChildren.RemoveObjectAt(mChildren.Count() - 1); } // If we're not updating the query, we don't need to add listeners, so bail // out early. if (mLiveUpdate == QUERYUPDATE_NONE) { mContentsValid = true; return NS_OK; } nsNavHistoryResult* result = GetResult(); NS_ENSURE_STATE(result); // Ensure to add history observer before bookmarks observer, because the // latter wants to know if an history observer was added. if (mOptions->QueryType() == nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY || mOptions->QueryType() == nsINavHistoryQueryOptions::QUERY_TYPE_UNIFIED) { // Date containers that contain site containers have no reason to observe // history, if the inside site container is expanded it will update, // otherwise we are going to refresh the parent query. if (!mParent || mParent->mOptions->ResultType() != nsINavHistoryQueryOptions::RESULTS_AS_DATE_SITE_QUERY) { // register with the result for history updates result->AddHistoryObserver(this); } } if (mOptions->QueryType() == nsINavHistoryQueryOptions::QUERY_TYPE_BOOKMARKS || mOptions->QueryType() == nsINavHistoryQueryOptions::QUERY_TYPE_UNIFIED || mLiveUpdate == QUERYUPDATE_COMPLEX_WITH_BOOKMARKS || mHasSearchTerms) { // register with the result for bookmark updates result->AddAllBookmarksObserver(this); } if (mLiveUpdate == QUERYUPDATE_MOBILEPREF) { result->AddMobilePrefsObserver(this); } mContentsValid = true; return NS_OK; } /** * Call with unregister = false when we are going to update the children (for * example, when the container is open). This will clear the list and notify * all the children that they are going away. * * When the results are becoming invalid and we are not going to refresh them, * set unregister = true, which will unregister the listener from the * result if any. We use unregister = false when we are refreshing the list * immediately so want to stay a notifier. */ void nsNavHistoryQueryResultNode::ClearChildren(bool aUnregister) { for (int32_t i = 0; i < mChildren.Count(); ++i) mChildren[i]->OnRemoving(); mChildren.Clear(); if (aUnregister && mContentsValid) { nsNavHistoryResult* result = GetResult(); if (result) { result->RemoveHistoryObserver(this); result->RemoveAllBookmarksObserver(this); result->RemoveMobilePrefsObserver(this); } } mContentsValid = false; } /** * This is called to update the result when something has changed that we * can not incrementally update. */ nsresult nsNavHistoryQueryResultNode::Refresh() { nsNavHistoryResult* result = GetResult(); NS_ENSURE_STATE(result); if (result->IsBatching()) { result->requestRefresh(this); return NS_OK; } // This is not a root node but it does not have a parent - this means that // the node has already been cleared and it is now called, because it was // left in a local copy of the observers array. if (mIndentLevel > -1 && !mParent) return NS_OK; // Do not refresh if we are not expanded or if we are child of a query // containing other queries. In this case calling Refresh for each child // query could cause a major slowdown. We should not refresh nested // queries, since we will already refresh the parent one. // The only exception to this, is if the parent query is of QUERYUPDATE_NONE, // this can be the case for the RESULTS_AS_TAGS_ROOT // under RESULTS_AS_LEFT_PANE_QUERY. if (!mExpanded) { ClearChildren(true); return NS_OK; } if (mParent && mParent->IsQuery()) { nsNavHistoryQueryResultNode* parent = mParent->GetAsQuery(); if (parent->IsContainersQuery() && parent->mLiveUpdate != QUERYUPDATE_NONE) { // Don't update, just invalidate and unhook ClearChildren(true); return NS_OK; // no updates in tree state } } if (mLiveUpdate == QUERYUPDATE_COMPLEX_WITH_BOOKMARKS) ClearChildren(true); else ClearChildren(false); // Ignore errors from FillChildren, since we will still want to refresh // the tree (there just might not be anything in it on error). (void)FillChildren(); NOTIFY_RESULT_OBSERVERS(result, InvalidateContainer(TO_CONTAINER(this))); return NS_OK; } /** * Here, we override GetSortType to return the current sorting for this * query. GetSortType is used when dynamically inserting query results so we * can see which comparator we should use to find the proper insertion point * (it shouldn't be called from folder containers which maintain their own * sorting). * * Normally, the container just forwards it up the chain. This is what we want * for host groups, for example. For queries, we often want to use the query's * sorting mode. * * However, we only use this query node's sorting when it is not the root. * When it is the root, we use the result's sorting mode. This is because * there are two cases: * - You are looking at a bookmark hierarchy that contains an embedded * result. We should always use the query's sort ordering since the result * node's headers have nothing to do with us (and are disabled). * - You are looking at a query in the tree. In this case, we want the * result sorting to override ours (it should be initialized to the same * sorting mode). */ uint16_t nsNavHistoryQueryResultNode::GetSortType() { if (mParent) return mOptions->SortingMode(); if (mResult) return mResult->mSortingMode; // This is a detached container, just use natural order. return nsINavHistoryQueryOptions::SORT_BY_NONE; } void nsNavHistoryQueryResultNode::RecursiveSort(SortComparator aComparator) { if (!IsContainersQuery()) mChildren.Sort(aComparator, nullptr); for (int32_t i = 0; i < mChildren.Count(); ++i) { if (mChildren[i]->IsContainer()) mChildren[i]->GetAsContainer()->RecursiveSort(aComparator); } } NS_IMETHODIMP nsNavHistoryQueryResultNode::GetSkipTags(bool* aSkipTags) { *aSkipTags = false; return NS_OK; } nsresult nsNavHistoryQueryResultNode::OnBeginUpdateBatch() { return NS_OK; } nsresult nsNavHistoryQueryResultNode::OnEndUpdateBatch() { // If the query has no children it's possible it's not yet listening to // bookmarks changes, in such a case it's safer to force a refresh to gather // eventual new nodes matching query options. if (mChildren.Count() == 0) { nsresult rv = Refresh(); NS_ENSURE_SUCCESS(rv, rv); } mBatchChanges = 0; return NS_OK; } static nsresult setHistoryDetailsCallback(nsNavHistoryResultNode* aNode, const void* aClosure, const nsNavHistoryResult* aResult) { const nsNavHistoryResultNode* updatedNode = static_cast(aClosure); aNode->mAccessCount = updatedNode->mAccessCount; aNode->mTime = updatedNode->mTime; aNode->mFrecency = updatedNode->mFrecency; aNode->mHidden = updatedNode->mHidden; return NS_OK; } /** * Here we need to update all copies of the URI we have with the new visit * count, and potentially add a new entry in our query. This is the most * common update operation and it is important that it be as efficient as * possible. */ nsresult nsNavHistoryQueryResultNode::OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime, uint32_t aTransitionType, bool aHidden, uint32_t* aAdded) { if (aHidden && !mOptions->IncludeHidden()) return NS_OK; // Skip the notification if the query is filtered by specific transition types // and this visit has a different one. if (mTransitions.Length() > 0 && !mTransitions.Contains(aTransitionType)) return NS_OK; nsNavHistoryResult* result = GetResult(); NS_ENSURE_STATE(result); if (result->IsBatching() && ++mBatchChanges > MAX_BATCH_CHANGES_BEFORE_REFRESH) { nsresult rv = Refresh(); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsNavHistory* history = nsNavHistory::GetHistoryService(); NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY); switch (mLiveUpdate) { case QUERYUPDATE_MOBILEPREF: { return NS_OK; } case QUERYUPDATE_HOST: { // For these simple yet common cases we can check the host ourselves // before doing the overhead of creating a new result node. if (mQuery->Domain().IsVoid()) return NS_OK; nsAutoCString host; if (NS_FAILED(aURI->GetAsciiHost(host))) return NS_OK; if (!mQuery->Domain().Equals(host)) return NS_OK; // Fall through to check the time, if the time is not present it will // still match. [[fallthrough]]; } case QUERYUPDATE_TIME: { // For these simple yet common cases we can check the time ourselves // before doing the overhead of creating a new result node. bool hasIt; mQuery->GetHasBeginTime(&hasIt); if (hasIt) { PRTime beginTime = history->NormalizeTime(mQuery->BeginTimeReference(), mQuery->BeginTime()); if (aTime < beginTime) return NS_OK; // before our time range } mQuery->GetHasEndTime(&hasIt); if (hasIt) { PRTime endTime = history->NormalizeTime(mQuery->EndTimeReference(), mQuery->EndTime()); if (aTime > endTime) return NS_OK; // after our time range } // Now we know that our visit satisfies the time range, fall through to // the QUERYUPDATE_SIMPLE case below. [[fallthrough]]; } case QUERYUPDATE_SIMPLE: { // The history service can tell us whether the new item should appear // in the result. We first have to construct a node for it to check. RefPtr addition; nsresult rv = history->VisitIdToResultNode(aVisitId, mOptions, getter_AddRefs(addition)); NS_ENSURE_SUCCESS(rv, rv); if (!addition) { // Certain result types manage the nodes by themselves. return NS_OK; } addition->mTransitionType = aTransitionType; if (!evaluateQueryForNode(mQuery, mOptions, addition)) return NS_OK; // don't need to include in our query // Optimization for a common case: if the query has maxResults and is // sorted by date, get the current boundaries and check if the added visit // would fit. // Later, we may have to remove the last child to respect maxResults. if (mOptions->MaxResults() && static_cast(mChildren.Count()) >= mOptions->MaxResults()) { uint16_t sortType = GetSortType(); if (sortType == nsINavHistoryQueryOptions::SORT_BY_DATE_ASCENDING && aTime > std::max(mChildren[0]->mTime, mChildren[mChildren.Count() - 1]->mTime)) { return NS_OK; } if (sortType == nsINavHistoryQueryOptions::SORT_BY_DATE_DESCENDING && aTime < std::min(mChildren[0]->mTime, mChildren[mChildren.Count() - 1]->mTime)) { return NS_OK; } } if (mOptions->ResultType() == nsNavHistoryQueryOptions::RESULTS_AS_VISIT) { // If this is a visit type query, just insert the new visit. We never // update visits, only add or remove them. rv = InsertSortedChild(addition); NS_ENSURE_SUCCESS(rv, rv); } else { uint16_t sortType = GetSortType(); bool updateSorting = sortType == nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_ASCENDING || sortType == nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_DESCENDING || sortType == nsINavHistoryQueryOptions::SORT_BY_DATE_ASCENDING || sortType == nsINavHistoryQueryOptions::SORT_BY_DATE_DESCENDING || sortType == nsINavHistoryQueryOptions::SORT_BY_FRECENCY_ASCENDING || sortType == nsINavHistoryQueryOptions::SORT_BY_FRECENCY_DESCENDING; if (!UpdateURIs( false, true, updateSorting, addition->mURI, setHistoryDetailsCallback, const_cast(static_cast(addition.get())))) { // Couldn't find a node to update. rv = InsertSortedChild(addition); NS_ENSURE_SUCCESS(rv, rv); } } // Trim the children if necessary. if (mOptions->MaxResults() && static_cast(mChildren.Count()) > mOptions->MaxResults()) { mChildren.RemoveObjectAt(mChildren.Count() - 1); } if (aAdded) ++(*aAdded); break; } case QUERYUPDATE_COMPLEX: case QUERYUPDATE_COMPLEX_WITH_BOOKMARKS: // need to requery in complex cases return Refresh(); default: MOZ_ASSERT(false, "Invalid value for mLiveUpdate"); return Refresh(); } return NS_OK; } /** * Find every node that matches this URI and rename it. We try to do * incremental updates here, even when we are closed, because changing titles * is easier than requerying if we are invalid. * * This actually gets called a lot. Typically, we will get an AddURI message * when the user visits the page, and then the title will be set asynchronously * when the title element of the page is parsed. */ nsresult nsNavHistoryQueryResultNode::OnTitleChanged( nsIURI* aURI, const nsAString& aPageTitle, const nsACString& aGUID) { if (!mExpanded) { // When we are not expanded, we don't update, just invalidate and unhook. // It would still be pretty easy to traverse the results and update the // titles, but when a title changes, its unlikely that it will be the only // thing. Therefore, we just give up. ClearChildren(true); return NS_OK; // no updates in tree state } nsNavHistoryResult* result = GetResult(); NS_ENSURE_STATE(result); if (result->IsBatching() && ++mBatchChanges > MAX_BATCH_CHANGES_BEFORE_REFRESH) { nsresult rv = Refresh(); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } // compute what the new title should be NS_ConvertUTF16toUTF8 newTitle(aPageTitle); bool onlyOneEntry = mOptions->ResultType() == nsINavHistoryQueryOptions::RESULTS_AS_URI; // See if our query has any search term matching. if (mHasSearchTerms) { // Find all matching URI nodes. nsCOMArray matches; nsAutoCString spec; nsresult rv = aURI->GetSpec(spec); NS_ENSURE_SUCCESS(rv, rv); RecursiveFindURIs(onlyOneEntry, this, spec, &matches); if (matches.Count() == 0) { // This could be a new node matching the query, thus we could need // to add it to the result. RefPtr node; nsNavHistory* history = nsNavHistory::GetHistoryService(); NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY); rv = history->URIToResultNode(aURI, mOptions, getter_AddRefs(node)); NS_ENSURE_SUCCESS(rv, rv); if (evaluateQueryForNode(mQuery, mOptions, node)) { rv = InsertSortedChild(node); NS_ENSURE_SUCCESS(rv, rv); } } for (int32_t i = 0; i < matches.Count(); ++i) { // For each matched node we check if it passes the query filter, if not // we remove the node from the result, otherwise we'll update the title // later. nsNavHistoryResultNode* node = matches[i]; // We must check the node with the new title. node->mTitle = newTitle; nsNavHistory* history = nsNavHistory::GetHistoryService(); NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY); if (!evaluateQueryForNode(mQuery, mOptions, node)) { nsNavHistoryContainerResultNode* parent = node->mParent; // URI nodes should always have parents NS_ENSURE_TRUE(parent, NS_ERROR_UNEXPECTED); int32_t childIndex = parent->FindChild(node); NS_ASSERTION(childIndex >= 0, "Child not found in parent"); parent->RemoveChildAt(childIndex); } } } return ChangeTitles(aURI, newTitle, true, onlyOneEntry); } /** * Here, we can always live update by just deleting all occurrences of * the given URI. */ nsresult nsNavHistoryQueryResultNode::OnPageRemovedFromStore( nsIURI* aURI, const nsACString& aGUID, uint16_t aReason) { nsNavHistoryResult* result = GetResult(); NS_ENSURE_STATE(result); if (result->IsBatching() && ++mBatchChanges > MAX_BATCH_CHANGES_BEFORE_REFRESH) { nsresult rv = Refresh(); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } if (IsContainersQuery()) { // Incremental updates of query returning queries are pretty much // complicated. In this case it's possible one of the child queries has // no more children and it should be removed. Unfortunately there is no // way to know that without executing the child query and counting results. nsresult rv = Refresh(); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } bool onlyOneEntry = mOptions->ResultType() == nsINavHistoryQueryOptions::RESULTS_AS_URI; nsAutoCString spec; nsresult rv = aURI->GetSpec(spec); NS_ENSURE_SUCCESS(rv, rv); nsCOMArray matches; RecursiveFindURIs(onlyOneEntry, this, spec, &matches); for (int32_t i = 0; i < matches.Count(); ++i) { nsNavHistoryResultNode* node = matches[i]; nsNavHistoryContainerResultNode* parent = node->mParent; // URI nodes should always have parents NS_ENSURE_TRUE(parent, NS_ERROR_UNEXPECTED); int32_t childIndex = parent->FindChild(node); NS_ASSERTION(childIndex >= 0, "Child not found in parent"); parent->RemoveChildAt(childIndex); if (parent->mChildren.Count() == 0 && parent->IsQuery() && parent->mIndentLevel > -1) { // When query subcontainers (like hosts) get empty we should remove them // as well. If the parent is not the root node, append it to our list // and it will get evaluated later in the loop. matches.AppendObject(parent); } } return NS_OK; } nsresult nsNavHistoryQueryResultNode::OnClearHistory() { nsresult rv = Refresh(); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } static nsresult setFaviconCallback(nsNavHistoryResultNode* aNode, const void* aClosure, const nsNavHistoryResult* aResult) { if (aResult && (!aNode->mParent || aNode->mParent->AreChildrenVisible())) NOTIFY_RESULT_OBSERVERS(aResult, NodeIconChanged(aNode)); return NS_OK; } nsresult nsNavHistoryQueryResultNode::OnPageRemovedVisits( nsIURI* aURI, bool aPartialRemoval, const nsACString& aGUID, uint16_t aReason, uint32_t aTransitionType) { MOZ_ASSERT( mOptions->QueryType() == nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY, "Bookmarks queries should not get a OnDeleteVisits notification"); if (!aPartialRemoval) { // All visits for this uri have been removed, but the uri won't be removed // from the databse, most likely because it's a bookmark. For a history // query this is equivalent to a OnPageRemovedFromStore notification. nsresult rv = OnPageRemovedFromStore(aURI, aGUID, aReason); NS_ENSURE_SUCCESS(rv, rv); } if (aTransitionType > 0) { // All visits for aTransitionType have been removed, if the query is // filtering on such transition type, this is equivalent to an // OnPageRemovedFromStore notification. if (mTransitions.Length() > 0 && mTransitions.Contains(aTransitionType)) { nsresult rv = OnPageRemovedFromStore(aURI, aGUID, aReason); NS_ENSURE_SUCCESS(rv, rv); } } return NS_OK; } nsresult nsNavHistoryQueryResultNode::NotifyIfTagsChanged(nsIURI* aURI) { nsNavHistoryResult* result = GetResult(); NS_ENSURE_STATE(result); nsAutoCString spec; nsresult rv = aURI->GetSpec(spec); NS_ENSURE_SUCCESS(rv, rv); bool onlyOneEntry = mOptions->ResultType() == nsINavHistoryQueryOptions::RESULTS_AS_URI; // Find matching URI nodes. RefPtr node; nsNavHistory* history = nsNavHistory::GetHistoryService(); nsCOMArray matches; RecursiveFindURIs(onlyOneEntry, this, spec, &matches); if (matches.Count() == 0 && mHasSearchTerms) { // A new tag has been added, it's possible it matches our query. NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY); rv = history->URIToResultNode(aURI, mOptions, getter_AddRefs(node)); NS_ENSURE_SUCCESS(rv, rv); if (evaluateQueryForNode(mQuery, mOptions, node)) { rv = InsertSortedChild(node); NS_ENSURE_SUCCESS(rv, rv); } } for (int32_t i = 0; i < matches.Count(); ++i) { nsNavHistoryResultNode* node = matches[i]; // Force a tags update before checking the node. node->mTags.SetIsVoid(true); nsAutoString tags; rv = node->GetTags(tags); NS_ENSURE_SUCCESS(rv, rv); // It's possible now this node does not respect anymore the conditions. // In such a case it should be removed. if (mHasSearchTerms && !evaluateQueryForNode(mQuery, mOptions, node)) { nsNavHistoryContainerResultNode* parent = node->mParent; // URI nodes should always have parents NS_ENSURE_TRUE(parent, NS_ERROR_UNEXPECTED); int32_t childIndex = parent->FindChild(node); NS_ASSERTION(childIndex >= 0, "Child not found in parent"); parent->RemoveChildAt(childIndex); } else { NOTIFY_RESULT_OBSERVERS(result, NodeTagsChanged(node)); } } return NS_OK; } /** * These are the bookmark observer functions for query nodes. They listen * for bookmark events and refresh the results if we have any dependence on * the bookmark system. */ nsresult nsNavHistoryQueryResultNode::OnItemAdded( int64_t aItemId, int64_t aParentId, int32_t aIndex, uint16_t aItemType, nsIURI* aURI, PRTime aDateAdded, const nsACString& aGUID, const nsACString& aParentGUID, uint16_t aSource) { if (aItemType == nsINavBookmarksService::TYPE_BOOKMARK && mLiveUpdate != QUERYUPDATE_SIMPLE && mLiveUpdate != QUERYUPDATE_TIME && mLiveUpdate != QUERYUPDATE_MOBILEPREF) { nsresult rv = Refresh(); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } nsresult nsNavHistoryQueryResultNode::OnItemRemoved( int64_t aItemId, int64_t aParentFolder, int32_t aIndex, uint16_t aItemType, nsIURI* aURI, const nsACString& aGUID, const nsACString& aParentGUID, uint16_t aSource) { if ((aItemType == nsINavBookmarksService::TYPE_BOOKMARK || (aItemType == nsINavBookmarksService::TYPE_FOLDER && mOptions->ResultType() == nsINavHistoryQueryOptions::RESULTS_AS_TAGS_ROOT && aParentGUID.EqualsLiteral(TAGS_ROOT_GUID))) && mLiveUpdate != QUERYUPDATE_SIMPLE && mLiveUpdate != QUERYUPDATE_TIME && mLiveUpdate != QUERYUPDATE_MOBILEPREF) { nsresult rv = Refresh(); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } NS_IMETHODIMP nsNavHistoryQueryResultNode::OnItemChanged( int64_t aItemId, const nsACString& aProperty, bool aIsAnnotationProperty, const nsACString& aNewValue, PRTime aLastModified, uint16_t aItemType, int64_t aParentId, const nsACString& aGUID, const nsACString& aParentGUID, const nsACString& aOldValue, uint16_t aSource) { if (aItemType != nsINavBookmarksService::TYPE_BOOKMARK) { // No separators or folders in queries. return NS_OK; } // Update ourselves first. nsresult rv = nsNavHistoryResultNode::OnItemChanged( aItemId, aProperty, aIsAnnotationProperty, aNewValue, aLastModified, aItemType, aParentId, aGUID, aParentGUID, aOldValue, aSource); NS_ENSURE_SUCCESS(rv, rv); // Did our uri change? if (aItemId == mItemId && aProperty.EqualsLiteral("uri")) { nsNavHistory* history = nsNavHistory::GetHistoryService(); NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY); nsCOMPtr query; nsCOMPtr options; rv = history->QueryStringToQuery(mURI, getter_AddRefs(query), getter_AddRefs(options)); NS_ENSURE_SUCCESS(rv, rv); mQuery = do_QueryObject(query); NS_ENSURE_STATE(mQuery); mOptions = do_QueryObject(options); NS_ENSURE_STATE(mOptions); rv = mOptions->Clone(getter_AddRefs(mOriginalOptions)); NS_ENSURE_SUCCESS(rv, rv); } // History observers should not get OnItemChanged but should get the // corresponding history notifications instead. // For bookmark queries, "all bookmark" observers should get OnItemChanged. // For example, when a title of a bookmark changes, we want that to refresh. if (mLiveUpdate == QUERYUPDATE_COMPLEX_WITH_BOOKMARKS) { return Refresh(); } return NS_OK; } nsresult nsNavHistoryQueryResultNode::OnItemMoved( int64_t aFolder, int32_t aOldIndex, int32_t aNewIndex, uint16_t aItemType, const nsACString& aGUID, const nsACString& aOldParentGUID, const nsACString& aNewParentGUID, uint16_t aSource, const nsACString& aURI) { // 1. The query cannot be affected by the item's position // 2. For the time being, we cannot optimize this not to update // queries which are not restricted to some folders, due to way // sub-queries are updated (see Refresh) if (mLiveUpdate == QUERYUPDATE_COMPLEX_WITH_BOOKMARKS && aItemType != nsINavBookmarksService::TYPE_SEPARATOR && !aNewParentGUID.Equals(aOldParentGUID)) { return Refresh(); } return NS_OK; } nsresult nsNavHistoryQueryResultNode::OnItemTagsChanged(int64_t aItemId, const nsAString& aURL) { nsresult rv = nsNavHistoryResultNode::OnItemTagsChanged(aItemId, aURL); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr uri; rv = NS_NewURI(getter_AddRefs(uri), aURL); NS_ENSURE_SUCCESS(rv, rv); rv = NotifyIfTagsChanged(uri); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult nsNavHistoryQueryResultNode::OnItemTimeChanged(int64_t aItemId, const nsACString& aGUID, PRTime aDateAdded, PRTime aLastModified) { // Update ourselves first. nsresult rv = nsNavHistoryResultNode::OnItemTimeChanged( aItemId, aGUID, aDateAdded, aLastModified); NS_ENSURE_SUCCESS(rv, rv); if (mLiveUpdate == QUERYUPDATE_COMPLEX_WITH_BOOKMARKS) { return Refresh(); } return NS_OK; } nsresult nsNavHistoryQueryResultNode::OnItemTitleChanged( int64_t aItemId, const nsACString& aGUID, const nsACString& aTitle, PRTime aLastModified) { // Update ourselves first. nsresult rv = nsNavHistoryResultNode::OnItemTitleChanged( aItemId, aGUID, aTitle, aLastModified); NS_ENSURE_SUCCESS(rv, rv); if (mLiveUpdate == QUERYUPDATE_COMPLEX_WITH_BOOKMARKS) { return Refresh(); } return NS_OK; } nsresult nsNavHistoryQueryResultNode::OnItemUrlChanged(int64_t aItemId, const nsACString& aGUID, const nsACString& aURL, PRTime aLastModified) { if (aItemId != mItemId) { return NS_OK; } nsresult rv = nsNavHistoryResultNode::OnItemUrlChanged(aItemId, aGUID, aURL, aLastModified); NS_ENSURE_SUCCESS(rv, rv); nsNavHistory* history = nsNavHistory::GetHistoryService(); NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY); nsCOMPtr query; nsCOMPtr options; rv = history->QueryStringToQuery(mURI, getter_AddRefs(query), getter_AddRefs(options)); NS_ENSURE_SUCCESS(rv, rv); mQuery = do_QueryObject(query); NS_ENSURE_STATE(mQuery); mOptions = do_QueryObject(options); NS_ENSURE_STATE(mOptions); rv = mOptions->Clone(getter_AddRefs(mOriginalOptions)); NS_ENSURE_SUCCESS(rv, rv); if (mLiveUpdate == QUERYUPDATE_COMPLEX_WITH_BOOKMARKS) { return Refresh(); } return NS_OK; } /** * HOW DYNAMIC FOLDER UPDATING WORKS * * When you create a result, it will automatically keep itself in sync with * stuff that happens in the system. For folder nodes, this means changes to * bookmarks. * * A folder will fill its children "when necessary." This means it is being * opened or whether we need to see if it is empty for twisty drawing. It will * then register its ID with the main result object that owns it. This result * object will listen for all bookmark notifications and pass those * notifications to folder nodes that have registered for that specific folder * ID. * * When a bookmark folder is closed, it will not clear its children. Instead, * it will keep them and also stay registered as a listener. This means that * you can more quickly re-open the same folder without doing any work. This * happens a lot for menus, and bookmarks don't change very often. * * When a message comes in and the folder is open, we will do the correct * operations to keep ourselves in sync with the bookmark service. If the * folder is closed, we just clear our list to mark it as invalid and * unregister as a listener. This means we do not have to keep maintaining * an up-to-date list for the entire bookmark menu structure in every place * it is used. */ NS_IMPL_ISUPPORTS_INHERITED(nsNavHistoryFolderResultNode, nsNavHistoryContainerResultNode, nsINavHistoryQueryResultNode, mozIStorageStatementCallback) nsNavHistoryFolderResultNode::nsNavHistoryFolderResultNode( const nsACString& aTitle, nsNavHistoryQueryOptions* aOptions, int64_t aFolderId) : nsNavHistoryContainerResultNode( ""_ns, aTitle, 0, nsNavHistoryResultNode::RESULT_TYPE_FOLDER, aOptions), mContentsValid(false), mTargetFolderItemId(aFolderId), mIsRegisteredFolderObserver(false) { mItemId = aFolderId; } nsNavHistoryFolderResultNode::~nsNavHistoryFolderResultNode() { if (mIsRegisteredFolderObserver && mResult) mResult->RemoveBookmarkFolderObserver(this, mTargetFolderGuid); } /** * Here we do not want to call ContainerResultNode::OnRemoving since our own * ClearChildren will do the same thing and more (unregister the observers). * The base ResultNode::OnRemoving will clear some regular node stats, so it is * OK. */ void nsNavHistoryFolderResultNode::OnRemoving() { nsNavHistoryResultNode::OnRemoving(); ClearChildren(true); mResult = nullptr; } nsresult nsNavHistoryFolderResultNode::OpenContainer() { NS_ASSERTION(!mExpanded, "Container must be expanded to close it"); nsresult rv; if (!mContentsValid) { rv = FillChildren(); NS_ENSURE_SUCCESS(rv, rv); } mExpanded = true; rv = NotifyOnStateChange(STATE_CLOSED); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } /** * The async version of OpenContainer. */ nsresult nsNavHistoryFolderResultNode::OpenContainerAsync() { NS_ASSERTION(!mExpanded, "Container already expanded when opening it"); // If the children are valid, open the container synchronously. This will be // the case when the container has already been opened and any other time // FillChildren or FillChildrenAsync has previously been called. if (mContentsValid) return OpenContainer(); nsresult rv = FillChildrenAsync(); NS_ENSURE_SUCCESS(rv, rv); rv = NotifyOnStateChange(STATE_CLOSED); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } /** * @see nsNavHistoryQueryResultNode::HasChildren. The semantics here are a * little different. Querying the contents of a bookmark folder is relatively * fast and it is common to have empty folders. Therefore, we always want to * return the correct result so that twisties are drawn properly. */ NS_IMETHODIMP nsNavHistoryFolderResultNode::GetHasChildren(bool* aHasChildren) { if (!mContentsValid) { nsresult rv = FillChildren(); NS_ENSURE_SUCCESS(rv, rv); } *aHasChildren = (mChildren.Count() > 0); return NS_OK; } NS_IMETHODIMP nsNavHistoryFolderResultNode::GetFolderItemId(int64_t* aItemId) { *aItemId = mTargetFolderItemId; return NS_OK; } NS_IMETHODIMP nsNavHistoryFolderResultNode::GetTargetFolderGuid(nsACString& aGuid) { aGuid = mTargetFolderGuid; return NS_OK; } /** * Lazily computes the URI for this specific folder query with the current * options. */ NS_IMETHODIMP nsNavHistoryFolderResultNode::GetUri(nsACString& aURI) { if (!mURI.IsEmpty()) { aURI = mURI; return NS_OK; } nsCOMPtr query; nsresult rv = GetQuery(getter_AddRefs(query)); NS_ENSURE_SUCCESS(rv, rv); nsNavHistory* history = nsNavHistory::GetHistoryService(); NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY); rv = history->QueryToQueryString(query, mOriginalOptions, mURI); NS_ENSURE_SUCCESS(rv, rv); aURI = mURI; return NS_OK; } /** * @return the queries that give you this bookmarks folder */ NS_IMETHODIMP nsNavHistoryFolderResultNode::GetQuery(nsINavHistoryQuery** _query) { // get the query object RefPtr query = new nsNavHistoryQuery(); nsTArray parents; // query just has the folder ID set and nothing else // XXX(Bug 1631371) Check if this should use a fallible operation as it // pretended earlier, or change the return type to void. parents.AppendElement(mTargetFolderGuid); nsresult rv = query->SetParents(parents); NS_ENSURE_SUCCESS(rv, rv); query.forget(_query); return NS_OK; } /** * Options for the query that gives you this bookmarks folder. This is just * the options for the folder with the current folder ID set. */ NS_IMETHODIMP nsNavHistoryFolderResultNode::GetQueryOptions( nsINavHistoryQueryOptions** _options) { MOZ_ASSERT(mOptions, "Options should be valid"); RefPtr options = mOptions; options.forget(_options); return NS_OK; } nsresult nsNavHistoryFolderResultNode::FillChildren() { NS_ASSERTION(!mContentsValid, "Don't call FillChildren when contents are valid"); NS_ASSERTION(mChildren.Count() == 0, "We are trying to fill children when there already are some"); nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService(); NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY); // Actually get the folder children from the bookmark service. nsresult rv = bookmarks->QueryFolderChildren(mTargetFolderItemId, mOptions, &mChildren); NS_ENSURE_SUCCESS(rv, rv); // PERFORMANCE: it may be better to also fill any child folders at this point // so that we can draw tree twisties without doing a separate query later. // If we don't end up drawing twisties a lot, it doesn't matter. If we do // this, we should wrap everything in a transaction here on the bookmark // service's connection. return OnChildrenFilled(); } /** * Performs some tasks after all the children of the container have been added. * The container's contents are not valid until this method has been called. */ nsresult nsNavHistoryFolderResultNode::OnChildrenFilled() { // It is important to call FillStats to fill in the parents on all // nodes and the result node pointers on the containers. FillStats(); if (mResult && mResult->mNeedsToApplySortingMode) { // We should repopulate container and then apply sortingMode. To avoid // sorting 2 times we simply do that here. mResult->SetSortingMode(mResult->mSortingMode); } else { // Once we've computed all tree stats, we can sort, because containers will // then have proper visit counts and dates. SortComparator comparator = GetSortingComparator(GetSortType()); if (comparator) { RecursiveSort(comparator); } } // If we are limiting our results remove items from the end of the // mChildren array after sorting. This is done for root node only. // Note, if count < max results, we won't do anything. if (!mParent && mOptions->MaxResults()) { while ((uint32_t)mChildren.Count() > mOptions->MaxResults()) mChildren.RemoveObjectAt(mChildren.Count() - 1); } // Register with the result for updates. EnsureRegisteredAsFolderObserver(); mContentsValid = true; return NS_OK; } /** * Registers the node with its result as a folder observer if it is not already * registered. */ void nsNavHistoryFolderResultNode::EnsureRegisteredAsFolderObserver() { if (!mIsRegisteredFolderObserver && mResult) { mResult->AddBookmarkFolderObserver(this, mTargetFolderGuid); mIsRegisteredFolderObserver = true; } } /** * The async version of FillChildren. This begins asynchronous execution by * calling nsNavBookmarks::QueryFolderChildrenAsync. During execution, this * node's async Storage callbacks, HandleResult and HandleCompletion, will be * called. */ nsresult nsNavHistoryFolderResultNode::FillChildrenAsync() { NS_ASSERTION(!mContentsValid, "FillChildrenAsync when contents are valid"); NS_ASSERTION(mChildren.Count() == 0, "FillChildrenAsync when children exist"); // ProcessFolderNodeChild, called in HandleResult, increments this for every // result row it processes. Initialize it here as we begin async execution. mAsyncBookmarkIndex = -1; nsNavBookmarks* bmSvc = nsNavBookmarks::GetBookmarksService(); NS_ENSURE_TRUE(bmSvc, NS_ERROR_OUT_OF_MEMORY); nsresult rv = bmSvc->QueryFolderChildrenAsync(this, getter_AddRefs(mAsyncPendingStmt)); NS_ENSURE_SUCCESS(rv, rv); // Register with the result for updates. All updates during async execution // will cause it to be restarted. EnsureRegisteredAsFolderObserver(); return NS_OK; } /** * A mozIStorageStatementCallback method. Called during the async execution * begun by FillChildrenAsync. * * @param aResultSet * The result set containing the data from the database. */ NS_IMETHODIMP nsNavHistoryFolderResultNode::HandleResult(mozIStorageResultSet* aResultSet) { NS_ENSURE_ARG_POINTER(aResultSet); nsNavBookmarks* bmSvc = nsNavBookmarks::GetBookmarksService(); if (!bmSvc) { CancelAsyncOpen(false); return NS_ERROR_OUT_OF_MEMORY; } // Consume all the currently available rows of the result set. nsCOMPtr row; while (NS_SUCCEEDED(aResultSet->GetNextRow(getter_AddRefs(row))) && row) { nsresult rv = bmSvc->ProcessFolderNodeRow(row, mOptions, &mChildren, mAsyncBookmarkIndex); if (NS_FAILED(rv)) { CancelAsyncOpen(false); return rv; } } return NS_OK; } /** * A mozIStorageStatementCallback method. Called during the async execution * begun by FillChildrenAsync. * * @param aReason * Indicates the final state of execution. */ NS_IMETHODIMP nsNavHistoryFolderResultNode::HandleCompletion(uint16_t aReason) { if (aReason == mozIStorageStatementCallback::REASON_FINISHED && mAsyncCanceledState == NOT_CANCELED) { // Async execution successfully completed. The container is ready to open. nsresult rv = OnChildrenFilled(); NS_ENSURE_SUCCESS(rv, rv); mExpanded = true; mAsyncPendingStmt = nullptr; // Notify observers only after mExpanded and mAsyncPendingStmt are set. rv = NotifyOnStateChange(STATE_LOADING); NS_ENSURE_SUCCESS(rv, rv); } else if (mAsyncCanceledState == CANCELED_RESTART_NEEDED) { // Async execution was canceled and needs to be restarted. mAsyncCanceledState = NOT_CANCELED; ClearChildren(false); FillChildrenAsync(); } else { // Async execution failed or was canceled without restart. Remove all // children and close the container, notifying observers. mAsyncCanceledState = NOT_CANCELED; ClearChildren(true); CloseContainer(); } return NS_OK; } void nsNavHistoryFolderResultNode::ClearChildren(bool unregister) { for (int32_t i = 0; i < mChildren.Count(); ++i) mChildren[i]->OnRemoving(); mChildren.Clear(); bool needsUnregister = unregister && (mContentsValid || mAsyncPendingStmt); if (needsUnregister && mResult && mIsRegisteredFolderObserver) { mResult->RemoveBookmarkFolderObserver(this, mTargetFolderGuid); mIsRegisteredFolderObserver = false; } mContentsValid = false; } /** * This is called to update the result when something has changed that we * can not incrementally update. */ nsresult nsNavHistoryFolderResultNode::Refresh() { nsNavHistoryResult* result = GetResult(); NS_ENSURE_STATE(result); if (result->IsBatching()) { result->requestRefresh(this); return NS_OK; } ClearChildren(true); if (!mExpanded) { // When we are not expanded, we don't update, just invalidate and unhook. return NS_OK; } // Ignore errors from FillChildren, since we will still want to refresh // the tree (there just might not be anything in it on error). ClearChildren // has unregistered us as an observer since FillChildren will try to // re-register us. (void)FillChildren(); NOTIFY_RESULT_OBSERVERS(result, InvalidateContainer(TO_CONTAINER(this))); return NS_OK; } /** * Implements the logic described above the constructor. This sees if we * should do an incremental update and returns true if so. If not, it * invalidates our children, unregisters us an observer, and returns false. */ bool nsNavHistoryFolderResultNode::StartIncrementalUpdate() { // if any items are excluded, we can not do incremental updates since the // indices from the bookmark service will not be valid if (!mOptions->ExcludeItems() && !mOptions->ExcludeQueries()) { // easy case: we are visible, always do incremental update if (mExpanded || AreChildrenVisible()) return true; nsNavHistoryResult* result = GetResult(); NS_ENSURE_TRUE(result, false); // When any observers are attached also do incremental updates if our // parent is visible, so that twisties are drawn correctly. if (mParent) return result->mObservers.Length() > 0; } // otherwise, we don't do incremental updates, invalidate and unregister (void)Refresh(); return false; } /** * This function adds aDelta to all bookmark indices between the two endpoints, * inclusive. It is used when items are added or removed from the bookmark * folder. */ void nsNavHistoryFolderResultNode::ReindexRange(int32_t aStartIndex, int32_t aEndIndex, int32_t aDelta) { for (int32_t i = 0; i < mChildren.Count(); ++i) { nsNavHistoryResultNode* node = mChildren[i]; if (node->mBookmarkIndex >= aStartIndex && node->mBookmarkIndex <= aEndIndex) node->mBookmarkIndex += aDelta; } } /** * Searches this folder for a node with the given id/target-folder-id. * * @return the node if found, null otherwise. * @note Does not addref the node! */ nsNavHistoryResultNode* nsNavHistoryFolderResultNode::FindChildById( int64_t aItemId, uint32_t* aNodeIndex) { for (int32_t i = 0; i < mChildren.Count(); ++i) { if (mChildren[i]->mItemId == aItemId || (mChildren[i]->IsFolder() && mChildren[i]->GetAsFolder()->mTargetFolderItemId == aItemId)) { *aNodeIndex = i; return mChildren[i]; } } return nullptr; } // Used by nsNavHistoryFolderResultNode's nsINavBookmarkObserver methods below. // If the container is notified of a bookmark event while asynchronous execution // is pending, this restarts it and returns. #define RESTART_AND_RETURN_IF_ASYNC_PENDING() \ if (mAsyncPendingStmt) { \ CancelAsyncOpen(true); \ return NS_OK; \ } NS_IMETHODIMP nsNavHistoryFolderResultNode::GetSkipTags(bool* aSkipTags) { *aSkipTags = false; return NS_OK; } nsresult nsNavHistoryFolderResultNode::OnBeginUpdateBatch() { return NS_OK; } nsresult nsNavHistoryFolderResultNode::OnEndUpdateBatch() { return NS_OK; } nsresult nsNavHistoryFolderResultNode::OnItemAdded( int64_t aItemId, int64_t aParentFolder, int32_t aIndex, uint16_t aItemType, nsIURI* aURI, PRTime aDateAdded, const nsACString& aGUID, const nsACString& aParentGUID, uint16_t aSource) { MOZ_ASSERT(aParentFolder == mTargetFolderItemId, "Got wrong bookmark update"); RESTART_AND_RETURN_IF_ASYNC_PENDING(); { uint32_t index; nsNavHistoryResultNode* node = FindChildById(aItemId, &index); // Bug 1097528. // It's possible our result registered due to a previous notification, for // example the Library left pane could have refreshed and replaced the // right pane as a consequence. In such a case our contents are already // up-to-date. That's OK. if (node) return NS_OK; } bool excludeItems = mOptions->ExcludeItems(); // here, try to do something reasonable if the bookmark service gives us // a bogus index. if (aIndex < 0) { MOZ_ASSERT_UNREACHABLE("Invalid index for item adding: <0"); aIndex = 0; } else if (aIndex > mChildren.Count()) { if (!excludeItems) { // Something wrong happened while updating indexes. MOZ_ASSERT_UNREACHABLE( "Invalid index for item adding: greater than " "count"); } aIndex = mChildren.Count(); } nsresult rv; // Check for query URIs, which are bookmarks, but treated as containers // in results and views. bool isQuery = false; if (aItemType == nsINavBookmarksService::TYPE_BOOKMARK) { NS_ASSERTION(aURI, "Got a null URI when we are a bookmark?!"); nsAutoCString itemURISpec; rv = aURI->GetSpec(itemURISpec); NS_ENSURE_SUCCESS(rv, rv); isQuery = IsQueryURI(itemURISpec); } if (aItemType != nsINavBookmarksService::TYPE_FOLDER && !isQuery && excludeItems) { // don't update items when we aren't displaying them, but we still need // to adjust bookmark indices to account for the insertion ReindexRange(aIndex, INT32_MAX, 1); return NS_OK; } if (!StartIncrementalUpdate()) return NS_OK; // folder was completely refreshed for us // adjust indices to account for insertion ReindexRange(aIndex, INT32_MAX, 1); RefPtr node; if (aItemType == nsINavBookmarksService::TYPE_BOOKMARK) { nsNavHistory* history = nsNavHistory::GetHistoryService(); NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY); rv = history->BookmarkIdToResultNode(aItemId, mOptions, getter_AddRefs(node)); NS_ENSURE_SUCCESS(rv, rv); } else if (aItemType == nsINavBookmarksService::TYPE_FOLDER) { nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService(); NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY); rv = bookmarks->ResultNodeForContainer(PromiseFlatCString(aGUID), new nsNavHistoryQueryOptions(), getter_AddRefs(node)); NS_ENSURE_SUCCESS(rv, rv); } else if (aItemType == nsINavBookmarksService::TYPE_SEPARATOR) { node = new nsNavHistorySeparatorResultNode(); node->mItemId = aItemId; node->mBookmarkGuid = aGUID; node->mDateAdded = aDateAdded; node->mLastModified = aDateAdded; } node->mBookmarkIndex = aIndex; if (aItemType == nsINavBookmarksService::TYPE_SEPARATOR || GetSortType() == nsINavHistoryQueryOptions::SORT_BY_NONE) { // insert at natural bookmarks position return InsertChildAt(node, aIndex); } // insert at sorted position return InsertSortedChild(node); } nsresult nsNavHistoryQueryResultNode::OnMobilePrefChanged(bool newValue) { RESTART_AND_RETURN_IF_ASYNC_PENDING(); if (newValue) { // If the folder is being added, simply refresh the query as that is // simpler than re-inserting just the mobile folder. return Refresh(); } // We're removing the mobile folder, so find it. int32_t existingIndex; FindChildByGuid(nsLiteralCString(MOBILE_BOOKMARKS_VIRTUAL_GUID), &existingIndex); if (existingIndex == -1) { return NS_OK; } return RemoveChildAt(existingIndex); } nsresult nsNavHistoryFolderResultNode::OnItemRemoved( int64_t aItemId, int64_t aParentFolder, int32_t aIndex, uint16_t aItemType, nsIURI* aURI, const nsACString& aGUID, const nsACString& aParentGUID, uint16_t aSource) { // Folder shortcuts should not be notified removal of the target folder. MOZ_ASSERT_IF(mItemId != mTargetFolderItemId, aItemId != mTargetFolderItemId); // Concrete folders should not be notified their own removal. // Note aItemId may equal mItemId for recursive folder shortcuts. MOZ_ASSERT_IF(mItemId == mTargetFolderItemId, aItemId != mItemId); // In any case though, here we only care about the children removal. if (mTargetFolderItemId == aItemId || mItemId == aItemId) return NS_OK; MOZ_ASSERT(aParentFolder == mTargetFolderItemId, "Got wrong bookmark update"); RESTART_AND_RETURN_IF_ASYNC_PENDING(); // don't trust the index from the bookmark service, find it ourselves. The // sorting could be different, or the bookmark services indices and ours might // be out of sync somehow. uint32_t index; nsNavHistoryResultNode* node = FindChildById(aItemId, &index); // Bug 1097528. // It's possible our result registered due to a previous notification, for // example the Library left pane could have refreshed and replaced the // right pane as a consequence. In such a case our contents are already // up-to-date. That's OK. if (!node) { return NS_OK; } bool excludeItems = mOptions->ExcludeItems(); if ((node->IsURI() || node->IsSeparator()) && excludeItems) { // don't update items when we aren't displaying them, but we do need to // adjust everybody's bookmark indices to account for the removal ReindexRange(aIndex, INT32_MAX, -1); return NS_OK; } if (!StartIncrementalUpdate()) return NS_OK; // we are completely refreshed // shift all following indices down ReindexRange(aIndex + 1, INT32_MAX, -1); return RemoveChildAt(index); } nsresult nsNavHistoryResultNode::OnItemTagsChanged(int64_t aItemId, const nsAString& aURL) { if (aItemId != mItemId) { return NS_OK; } mTags.SetIsVoid(true); bool shouldNotify = !mParent || mParent->AreChildrenVisible(); if (shouldNotify) { nsNavHistoryResult* result = GetResult(); NS_ENSURE_STATE(result); NOTIFY_RESULT_OBSERVERS(result, NodeTagsChanged(this)); } return NS_OK; } nsresult nsNavHistoryResultNode::OnItemTimeChanged(int64_t aItemId, const nsACString& aGUID, PRTime aDateAdded, PRTime aLastModified) { if (aItemId != mItemId) { return NS_OK; } bool isDateAddedChanged = mDateAdded != aDateAdded; bool isLastModifiedChanged = mLastModified != aLastModified; if (!isDateAddedChanged && !isLastModifiedChanged) { return NS_OK; } mDateAdded = aDateAdded; mLastModified = aLastModified; bool shouldNotify = !mParent || mParent->AreChildrenVisible(); if (shouldNotify) { nsNavHistoryResult* result = GetResult(); NS_ENSURE_STATE(result); if (isDateAddedChanged) { NOTIFY_RESULT_OBSERVERS(result, NodeDateAddedChanged(this, aDateAdded)); } if (isLastModifiedChanged) { NOTIFY_RESULT_OBSERVERS(result, NodeLastModifiedChanged(this, aLastModified)); } } return NS_OK; } nsresult nsNavHistoryResultNode::OnItemTitleChanged(int64_t aItemId, const nsACString& aGUID, const nsACString& aTitle, PRTime aLastModified) { if (aItemId != mItemId) { return NS_OK; } mTitle = aTitle; mLastModified = aLastModified; bool shouldNotify = !mParent || mParent->AreChildrenVisible(); if (shouldNotify) { nsNavHistoryResult* result = GetResult(); NS_ENSURE_STATE(result); NOTIFY_RESULT_OBSERVERS(result, NodeTitleChanged(this, mTitle)); } return NS_OK; } nsresult nsNavHistoryResultNode::OnItemUrlChanged(int64_t aItemId, const nsACString& aGUID, const nsACString& aURL, PRTime aLastModified) { if (aItemId != mItemId) { return NS_OK; } // clear the tags string as well mTags.SetIsVoid(true); nsCString oldURI(mURI); mURI = aURL; mLastModified = aLastModified; bool shouldNotify = !mParent || mParent->AreChildrenVisible(); if (shouldNotify) { nsNavHistoryResult* result = GetResult(); NS_ENSURE_STATE(result); NOTIFY_RESULT_OBSERVERS(result, NodeURIChanged(this, oldURI)); } if (!mParent) return NS_OK; // The sorting methods fall back to each other so we need to re-sort the // result even if it's not set to sort by the given property. int32_t ourIndex = mParent->FindChild(this); NS_ASSERTION(ourIndex >= 0, "Could not find self in parent"); if (ourIndex >= 0) mParent->EnsureItemPosition(ourIndex); return NS_OK; } NS_IMETHODIMP nsNavHistoryResultNode::OnItemChanged( int64_t aItemId, const nsACString& aProperty, bool aIsAnnotationProperty, const nsACString& aNewValue, PRTime aLastModified, uint16_t aItemType, int64_t aParentId, const nsACString& aGUID, const nsACString& aParentGUID, const nsACString& aOldValue, uint16_t aSource) { if (aItemId != mItemId) { return NS_OK; } mLastModified = aLastModified; nsNavHistoryResult* result = GetResult(); NS_ENSURE_STATE(result); bool shouldNotify = !mParent || mParent->AreChildrenVisible(); if (aProperty.EqualsLiteral("cleartime")) { PRTime oldTime = mTime; mTime = 0; if (shouldNotify && !result->CanSkipHistoryDetailsNotifications()) { NOTIFY_RESULT_OBSERVERS( result, NodeHistoryDetailsChanged(this, oldTime, mAccessCount)); } } else if (aProperty.EqualsLiteral("keyword")) { if (shouldNotify) NOTIFY_RESULT_OBSERVERS(result, NodeKeywordChanged(this, aNewValue)); } else MOZ_ASSERT_UNREACHABLE("Unknown bookmark property changing."); if (!mParent) return NS_OK; // DO NOT OPTIMIZE THIS TO CHECK aProperty // The sorting methods fall back to each other so we need to re-sort the // result even if it's not set to sort by the given property. int32_t ourIndex = mParent->FindChild(this); NS_ASSERTION(ourIndex >= 0, "Could not find self in parent"); if (ourIndex >= 0) mParent->EnsureItemPosition(ourIndex); return NS_OK; } NS_IMETHODIMP nsNavHistoryFolderResultNode::OnItemChanged( int64_t aItemId, const nsACString& aProperty, bool aIsAnnotationProperty, const nsACString& aNewValue, PRTime aLastModified, uint16_t aItemType, int64_t aParentId, const nsACString& aGUID, const nsACString& aParentGUID, const nsACString& aOldValue, uint16_t aSource) { RESTART_AND_RETURN_IF_ASYNC_PENDING(); return nsNavHistoryResultNode::OnItemChanged( aItemId, aProperty, aIsAnnotationProperty, aNewValue, aLastModified, aItemType, aParentId, aGUID, aParentGUID, aOldValue, aSource); } /** * Updates visit count and last visit time and refreshes. */ nsresult nsNavHistoryFolderResultNode::OnItemVisited(nsIURI* aURI, int64_t aVisitId, PRTime aTime) { if (mOptions->ExcludeItems()) return NS_OK; // don't update items when we aren't displaying them RESTART_AND_RETURN_IF_ASYNC_PENDING(); if (!StartIncrementalUpdate()) return NS_OK; nsAutoCString spec; nsresult rv = aURI->GetSpec(spec); NS_ENSURE_SUCCESS(rv, rv); nsCOMArray nodes; FindChildrenByURI(spec, &nodes); if (!nodes.Count()) { return NS_OK; } // Update us. int32_t oldAccessCount = mAccessCount; ++mAccessCount; if (aTime > mTime) mTime = aTime; rv = ReverseUpdateStats(mAccessCount - oldAccessCount); NS_ENSURE_SUCCESS(rv, rv); // Update nodes. for (int32_t i = 0; i < nodes.Count(); ++i) { nsNavHistoryResultNode* node = nodes[i]; uint32_t nodeOldAccessCount = node->mAccessCount; PRTime nodeOldTime = node->mTime; node->mTime = aTime; ++node->mAccessCount; // Update frecency for proper frecency ordering. // TODO (bug 832617): we may avoid one query here, by providing the new // frecency value in the notification. nsNavHistory* history = nsNavHistory::GetHistoryService(); NS_ENSURE_TRUE(history, NS_OK); RefPtr visitNode; rv = history->VisitIdToResultNode(aVisitId, mOptions, getter_AddRefs(visitNode)); NS_ENSURE_SUCCESS(rv, rv); if (!visitNode) { // Certain result types manage the nodes by themselves. return NS_OK; } node->mFrecency = visitNode->mFrecency; nsNavHistoryResult* result = GetResult(); if (AreChildrenVisible() && !result->CanSkipHistoryDetailsNotifications()) { // Sorting has not changed, just redraw the row if it's visible. NOTIFY_RESULT_OBSERVERS( result, NodeHistoryDetailsChanged(node, nodeOldTime, nodeOldAccessCount)); } // Update sorting if necessary. uint32_t sortType = GetSortType(); if (sortType == nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_ASCENDING || sortType == nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_DESCENDING || sortType == nsINavHistoryQueryOptions::SORT_BY_DATE_ASCENDING || sortType == nsINavHistoryQueryOptions::SORT_BY_DATE_DESCENDING || sortType == nsINavHistoryQueryOptions::SORT_BY_FRECENCY_ASCENDING || sortType == nsINavHistoryQueryOptions::SORT_BY_FRECENCY_DESCENDING) { int32_t childIndex = FindChild(node); NS_ASSERTION(childIndex >= 0, "Could not find child we just got a reference to"); if (childIndex >= 0) { EnsureItemPosition(childIndex); } } } return NS_OK; } nsresult nsNavHistoryFolderResultNode::OnItemMoved( int64_t aItemId, int32_t aOldIndex, int32_t aNewIndex, uint16_t aItemType, const nsACString& aGUID, const nsACString& aOldParentGUID, const nsACString& aNewParentGUID, uint16_t aSource, const nsACString& aURI) { MOZ_ASSERT(aOldParentGUID.Equals(mTargetFolderGuid) || aNewParentGUID.Equals(mTargetFolderGuid), "Got a bookmark message that doesn't belong to us"); RESTART_AND_RETURN_IF_ASYNC_PENDING(); bool excludeItems = mOptions->ExcludeItems(); if (excludeItems && (aItemType == nsINavBookmarksService::TYPE_SEPARATOR || (aItemType == nsINavBookmarksService::TYPE_BOOKMARK && !StringBeginsWith(aURI, "place:"_ns)))) { // This is a bookmark or a separator, so we don't need to handle this if // we're excluding items. return NS_OK; } uint32_t index; nsNavHistoryResultNode* node = FindChildById(aItemId, &index); // Bug 1097528. // It's possible our result registered due to a previous notification, for // example the Library left pane could have refreshed and replaced the // right pane as a consequence. In such a case our contents are already // up-to-date. That's OK. if (node && aNewParentGUID.Equals(mTargetFolderGuid) && index == static_cast(aNewIndex)) return NS_OK; if (!node && aOldParentGUID.Equals(mTargetFolderGuid)) return NS_OK; if (!StartIncrementalUpdate()) return NS_OK; // entire container was refreshed for us if (aNewParentGUID.Equals(aOldParentGUID)) { // getting moved within the same folder, we don't want to do a remove and // an add because that will lose your tree state. // adjust bookmark indices ReindexRange(aOldIndex + 1, INT32_MAX, -1); ReindexRange(aNewIndex, INT32_MAX, 1); MOZ_ASSERT(node, "Can't find folder that is moving!"); if (!node) { return NS_ERROR_FAILURE; } MOZ_ASSERT(index < uint32_t(mChildren.Count()), "Invalid index!"); node->mBookmarkIndex = aNewIndex; // adjust position EnsureItemPosition(index); return NS_OK; } // moving between two different folders, just do a remove and an add nsCOMPtr itemURI; if (aItemType == nsINavBookmarksService::TYPE_BOOKMARK) { nsresult rv = NS_NewURI(getter_AddRefs(itemURI), aURI); NS_ENSURE_SUCCESS(rv, rv); } if (aOldParentGUID.Equals(mTargetFolderGuid)) { OnItemRemoved(aItemId, mTargetFolderItemId, aOldIndex, aItemType, itemURI, aGUID, aOldParentGUID, aSource); } if (aNewParentGUID.Equals(mTargetFolderGuid)) { OnItemAdded( aItemId, mTargetFolderItemId, aNewIndex, aItemType, itemURI, RoundedPRNow(), // This is a dummy dateAdded, not the real value. aGUID, aNewParentGUID, aSource); } return NS_OK; } /** * Separator nodes do not hold any data. */ nsNavHistorySeparatorResultNode::nsNavHistorySeparatorResultNode() : nsNavHistoryResultNode(""_ns, ""_ns, 0, 0) {} NS_IMPL_CYCLE_COLLECTION_CLASS(nsNavHistoryResult) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsNavHistoryResult) tmp->StopObservingOnUnlink(); NS_IMPL_CYCLE_COLLECTION_UNLINK(mRootNode) NS_IMPL_CYCLE_COLLECTION_UNLINK(mObservers) NS_IMPL_CYCLE_COLLECTION_UNLINK(mMobilePrefObservers) NS_IMPL_CYCLE_COLLECTION_UNLINK(mAllBookmarksObservers) NS_IMPL_CYCLE_COLLECTION_UNLINK(mHistoryObservers) NS_IMPL_CYCLE_COLLECTION_UNLINK(mRefreshParticipants) NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsNavHistoryResult) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRootNode) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mObservers) for (nsNavHistoryResult::FolderObserverList* list : tmp->mBookmarkFolderObservers.Values()) { for (uint32_t i = 0; i < list->Length(); ++i) { NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mBookmarkFolderObservers value[i]"); nsNavHistoryResultNode* node = list->ElementAt(i); cb.NoteXPCOMChild(node); } } NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMobilePrefObservers) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAllBookmarksObservers) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHistoryObservers) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRefreshParticipants) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTING_ADDREF(nsNavHistoryResult) NS_IMPL_CYCLE_COLLECTING_RELEASE(nsNavHistoryResult) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsNavHistoryResult) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsINavHistoryResult) NS_INTERFACE_MAP_STATIC_AMBIGUOUS(nsNavHistoryResult) NS_INTERFACE_MAP_ENTRY(nsINavHistoryResult) NS_INTERFACE_MAP_ENTRY(nsINavBookmarkObserver) NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) NS_INTERFACE_MAP_END nsNavHistoryResult::nsNavHistoryResult( nsNavHistoryContainerResultNode* aRoot, const RefPtr& aQuery, const RefPtr& aOptions) : mRootNode(aRoot), mQuery(aQuery), mOptions(aOptions), mNeedsToApplySortingMode(false), mIsHistoryObserver(false), mIsBookmarksObserver(false), mIsMobilePrefObserver(false), mBookmarkFolderObservers(64), mSuppressNotifications(false), mIsHistoryDetailsObserver(false), mObserversWantHistoryDetails(true), mBatchInProgress(0) { mSortingMode = aOptions->SortingMode(); mRootNode->mResult = this; MOZ_ASSERT(mRootNode->mIndentLevel == -1, "Root node's indent level initialized wrong"); mRootNode->FillStats(); AutoTArray events; events.AppendElement(PlacesEventType::Purge_caches); PlacesObservers::AddListener(events, this); } nsNavHistoryResult::~nsNavHistoryResult() { // Delete all heap-allocated bookmark folder observer arrays. for (auto it = mBookmarkFolderObservers.Iter(); !it.Done(); it.Next()) { delete it.Data(); it.Remove(); } } void nsNavHistoryResult::StopObserving() { AutoTArray events; events.AppendElement(PlacesEventType::Favicon_changed); if (mIsBookmarksObserver) { nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService(); if (bookmarks) { bookmarks->RemoveObserver(this); mIsBookmarksObserver = false; } events.AppendElement(PlacesEventType::Bookmark_added); events.AppendElement(PlacesEventType::Bookmark_removed); events.AppendElement(PlacesEventType::Bookmark_moved); events.AppendElement(PlacesEventType::Bookmark_tags_changed); events.AppendElement(PlacesEventType::Bookmark_time_changed); events.AppendElement(PlacesEventType::Bookmark_title_changed); events.AppendElement(PlacesEventType::Bookmark_url_changed); } if (mIsMobilePrefObserver) { Preferences::UnregisterCallback(OnMobilePrefChangedCallback, MOBILE_BOOKMARKS_PREF, this); mIsMobilePrefObserver = false; } if (mIsHistoryObserver) { mIsHistoryObserver = false; events.AppendElement(PlacesEventType::History_cleared); events.AppendElement(PlacesEventType::Page_removed); } if (mIsHistoryDetailsObserver) { events.AppendElement(PlacesEventType::Page_visited); events.AppendElement(PlacesEventType::Page_title_changed); mIsHistoryDetailsObserver = false; } PlacesObservers::RemoveListener(events, this); } void nsNavHistoryResult::StopObservingOnUnlink() { StopObserving(); AutoTArray events; events.AppendElement(PlacesEventType::Purge_caches); PlacesObservers::RemoveListener(events, this); for (auto it = mBookmarkFolderObservers.Iter(); !it.Done(); it.Next()) { delete it.Data(); it.Remove(); } } bool nsNavHistoryResult::CanSkipHistoryDetailsNotifications() const { return !mObserversWantHistoryDetails && mOptions->QueryType() == nsINavHistoryQueryOptions::QUERY_TYPE_BOOKMARKS && mSortingMode != nsINavHistoryQueryOptions::SORT_BY_DATE_ASCENDING && mSortingMode != nsINavHistoryQueryOptions::SORT_BY_DATE_DESCENDING && mSortingMode != nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_ASCENDING && mSortingMode != nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_DESCENDING && mSortingMode != nsINavHistoryQueryOptions::SORT_BY_FRECENCY_ASCENDING && mSortingMode != nsINavHistoryQueryOptions::SORT_BY_FRECENCY_DESCENDING; } void nsNavHistoryResult::AddHistoryObserver( nsNavHistoryQueryResultNode* aNode) { if (!mIsHistoryObserver) { mIsHistoryObserver = true; AutoTArray events; events.AppendElement(PlacesEventType::History_cleared); events.AppendElement(PlacesEventType::Page_removed); if (!mIsHistoryDetailsObserver) { events.AppendElement(PlacesEventType::Page_visited); events.AppendElement(PlacesEventType::Page_title_changed); mIsHistoryDetailsObserver = true; } PlacesObservers::AddListener(events, this); } // Don't add duplicate observers. In some case we don't unregister when // children are cleared (see ClearChildren) and the next FillChildren call // will try to add the observer again. if (mHistoryObservers.IndexOf(aNode) == QueryObserverList::NoIndex) { mHistoryObservers.AppendElement(aNode); } } void nsNavHistoryResult::AddAllBookmarksObserver( nsNavHistoryQueryResultNode* aNode) { EnsureIsObservingBookmarks(); // Don't add duplicate observers. In some case we don't unregister when // children are cleared (see ClearChildren) and the next FillChildren call // will try to add the observer again. if (mAllBookmarksObservers.IndexOf(aNode) == QueryObserverList::NoIndex) { mAllBookmarksObservers.AppendElement(aNode); } } void nsNavHistoryResult::AddMobilePrefsObserver( nsNavHistoryQueryResultNode* aNode) { if (!mIsMobilePrefObserver) { Preferences::RegisterCallback(OnMobilePrefChangedCallback, MOBILE_BOOKMARKS_PREF, this); mIsMobilePrefObserver = true; } // Don't add duplicate observers. In some case we don't unregister when // children are cleared (see ClearChildren) and the next FillChildren call // will try to add the observer again. if (mMobilePrefObservers.IndexOf(aNode) == mMobilePrefObservers.NoIndex) { mMobilePrefObservers.AppendElement(aNode); } } void nsNavHistoryResult::AddBookmarkFolderObserver( nsNavHistoryFolderResultNode* aNode, const nsACString& aFolderGUID) { MOZ_ASSERT(!aFolderGUID.IsEmpty(), "aFolderGUID should not be empty"); EnsureIsObservingBookmarks(); // Don't add duplicate observers. In some case we don't unregister when // children are cleared (see ClearChildren) and the next FillChildren call // will try to add the observer again. FolderObserverList* list = BookmarkFolderObserversForGUID(aFolderGUID, true); if (list->IndexOf(aNode) == FolderObserverList::NoIndex) { list->AppendElement(aNode); } } void nsNavHistoryResult::EnsureIsObservingBookmarks() { if (mIsBookmarksObserver) { return; } nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService(); if (!bookmarks) { MOZ_ASSERT_UNREACHABLE("Can't create bookmark service"); return; } bookmarks->AddObserver(this, true); AutoTArray events; events.AppendElement(PlacesEventType::Bookmark_added); events.AppendElement(PlacesEventType::Bookmark_removed); events.AppendElement(PlacesEventType::Bookmark_moved); events.AppendElement(PlacesEventType::Bookmark_tags_changed); events.AppendElement(PlacesEventType::Bookmark_time_changed); events.AppendElement(PlacesEventType::Bookmark_title_changed); events.AppendElement(PlacesEventType::Bookmark_url_changed); // If we're not observing visits yet, also add a page-visited observer to // serve onItemVisited. if (!mIsHistoryObserver && !mIsHistoryDetailsObserver) { events.AppendElement(PlacesEventType::Page_visited); mIsHistoryDetailsObserver = true; } PlacesObservers::AddListener(events, this); mIsBookmarksObserver = true; } void nsNavHistoryResult::RemoveHistoryObserver( nsNavHistoryQueryResultNode* aNode) { mHistoryObservers.RemoveElement(aNode); } void nsNavHistoryResult::RemoveAllBookmarksObserver( nsNavHistoryQueryResultNode* aNode) { mAllBookmarksObservers.RemoveElement(aNode); } void nsNavHistoryResult::RemoveMobilePrefsObserver( nsNavHistoryQueryResultNode* aNode) { mMobilePrefObservers.RemoveElement(aNode); } void nsNavHistoryResult::RemoveBookmarkFolderObserver( nsNavHistoryFolderResultNode* aNode, const nsACString& aFolderGUID) { MOZ_ASSERT(!aFolderGUID.IsEmpty(), "aFolderGUID should not be empty"); FolderObserverList* list = BookmarkFolderObserversForGUID(aFolderGUID, false); if (!list) return; // we don't even have an entry for that folder list->RemoveElement(aNode); } nsNavHistoryResult::FolderObserverList* nsNavHistoryResult::BookmarkFolderObserversForGUID( const nsACString& aFolderGUID, bool aCreate) { FolderObserverList* list; if (mBookmarkFolderObservers.Get(aFolderGUID, &list)) return list; if (!aCreate) return nullptr; // need to create a new list list = new FolderObserverList; mBookmarkFolderObservers.InsertOrUpdate(aFolderGUID, list); return list; } NS_IMETHODIMP nsNavHistoryResult::GetSortingMode(uint16_t* aSortingMode) { *aSortingMode = mSortingMode; return NS_OK; } NS_IMETHODIMP nsNavHistoryResult::SetSortingMode(uint16_t aSortingMode) { NS_ENSURE_STATE(mRootNode); if (aSortingMode > nsINavHistoryQueryOptions::SORT_BY_FRECENCY_DESCENDING) return NS_ERROR_INVALID_ARG; // Keep everything in sync. NS_ASSERTION(mOptions, "Options should always be present for a root query"); mSortingMode = aSortingMode; // If the sorting mode changed to one requiring history details, we must // ensure to start observing. bool addedListener = UpdateHistoryDetailsObservers(); if (!mRootNode->mExpanded) { // Need to do this later when node will be expanded. mNeedsToApplySortingMode = true; return NS_OK; } if (addedListener) { // We must do a full refresh because the history details may be stale. if (mRootNode->IsQuery()) { return mRootNode->GetAsQuery()->Refresh(); } if (mRootNode->IsFolder()) { return mRootNode->GetAsFolder()->Refresh(); } } // Actually do sorting. nsNavHistoryContainerResultNode::SortComparator comparator = nsNavHistoryContainerResultNode::GetSortingComparator(aSortingMode); if (comparator) { nsNavHistory* history = nsNavHistory::GetHistoryService(); NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY); mRootNode->RecursiveSort(comparator); } NOTIFY_RESULT_OBSERVERS(this, SortingChanged(aSortingMode)); NOTIFY_RESULT_OBSERVERS(this, InvalidateContainer(mRootNode)); return NS_OK; } NS_IMETHODIMP nsNavHistoryResult::AddObserver(nsINavHistoryResultObserver* aObserver, bool aOwnsWeak) { NS_ENSURE_ARG(aObserver); nsresult rv = mObservers.AppendWeakElementUnlessExists(aObserver, aOwnsWeak); NS_ENSURE_SUCCESS(rv, rv); UpdateHistoryDetailsObservers(); rv = aObserver->SetResult(this); NS_ENSURE_SUCCESS(rv, rv); // If we are batching, notify a fake batch start to the observers. // Not doing so would then notify a not coupled batch end. if (IsBatching()) { NOTIFY_RESULT_OBSERVERS(this, Batching(true)); } if (!mRootNode->IsQuery() || mRootNode->GetAsQuery()->mLiveUpdate != QUERYUPDATE_NONE) { // Pretty much all the views show favicons, thus observe changes to them. AutoTArray events; events.AppendElement(PlacesEventType::Favicon_changed); PlacesObservers::AddListener(events, this); } return NS_OK; } NS_IMETHODIMP nsNavHistoryResult::RemoveObserver(nsINavHistoryResultObserver* aObserver) { NS_ENSURE_ARG(aObserver); nsresult rv = mObservers.RemoveWeakElement(aObserver); NS_ENSURE_SUCCESS(rv, rv); UpdateHistoryDetailsObservers(); return NS_OK; } bool nsNavHistoryResult::UpdateHistoryDetailsObservers() { bool observersWantHistoryDetails = false; // One observer set to true is enough to set observersWantHistoryDetails. for (uint32_t i = 0; i < mObservers.Length() && !observersWantHistoryDetails; ++i) { const nsCOMPtr& entry = mObservers.ElementAt(i).GetValue(); if (entry) { // If the observer doesn't implement the attribute, we assume true. bool observe; observersWantHistoryDetails = NS_FAILED(entry->GetObserveHistoryDetails(&observe)) || observe; } } mObserversWantHistoryDetails = observersWantHistoryDetails; // If one observer wants history details we may have to add the listener. if (!CanSkipHistoryDetailsNotifications()) { if (!mIsHistoryDetailsObserver) { AutoTArray events; events.AppendElement(PlacesEventType::Page_visited); events.AppendElement(PlacesEventType::Page_title_changed); PlacesObservers::AddListener(events, this); mIsHistoryDetailsObserver = true; return true; } } else { AutoTArray events; events.AppendElement(PlacesEventType::Page_visited); events.AppendElement(PlacesEventType::Page_title_changed); PlacesObservers::RemoveListener(events, this); mIsHistoryDetailsObserver = false; } return false; } NS_IMETHODIMP nsNavHistoryResult::GetSuppressNotifications(bool* _retval) { *_retval = mSuppressNotifications; return NS_OK; } NS_IMETHODIMP nsNavHistoryResult::SetSuppressNotifications(bool aSuppressNotifications) { mSuppressNotifications = aSuppressNotifications; return NS_OK; } NS_IMETHODIMP nsNavHistoryResult::GetRoot(nsINavHistoryContainerResultNode** aRoot) { if (!mRootNode) { MOZ_ASSERT_UNREACHABLE("Root is null"); *aRoot = nullptr; return NS_ERROR_FAILURE; } RefPtr node(mRootNode); node.forget(aRoot); return NS_OK; } void nsNavHistoryResult::requestRefresh( nsNavHistoryContainerResultNode* aContainer) { // Don't add twice the same container. if (mRefreshParticipants.IndexOf(aContainer) == ContainerObserverList::NoIndex) mRefreshParticipants.AppendElement(aContainer); } // nsINavBookmarkObserver implementation // Here, it is important that we create a COPY of the observer array. Some // observers will requery themselves, which may cause the observer array to // be modified or added to. #define ENUMERATE_BOOKMARK_FOLDER_OBSERVERS(_folderGUID, _functionCall) \ PR_BEGIN_MACRO \ FolderObserverList* _fol = \ BookmarkFolderObserversForGUID(_folderGUID, false); \ if (_fol) { \ FolderObserverList _listCopy(_fol->Clone()); \ for (uint32_t _fol_i = 0; _fol_i < _listCopy.Length(); ++_fol_i) { \ if (_listCopy[_fol_i]) _listCopy[_fol_i]->_functionCall; \ } \ } \ PR_END_MACRO #define ENUMERATE_LIST_OBSERVERS(_listType, _functionCall, _observersList, \ _conditionCall) \ PR_BEGIN_MACRO \ _listType _listCopy(_observersList.Clone()); \ for (uint32_t _obs_i = 0; _obs_i < _listCopy.Length(); ++_obs_i) { \ if (_listCopy[_obs_i] && _listCopy[_obs_i]->_conditionCall) \ _listCopy[_obs_i]->_functionCall; \ } \ PR_END_MACRO #define ENUMERATE_QUERY_OBSERVERS(_functionCall, _observersList, \ _conditionCall) \ ENUMERATE_LIST_OBSERVERS(QueryObserverList, _functionCall, _observersList, \ _conditionCall) #define ENUMERATE_ALL_BOOKMARKS_OBSERVERS(_functionCall) \ ENUMERATE_QUERY_OBSERVERS(_functionCall, mAllBookmarksObservers, IsQuery()) #define ENUMERATE_HISTORY_OBSERVERS(_functionCall) \ ENUMERATE_QUERY_OBSERVERS(_functionCall, mHistoryObservers, IsQuery()) #define ENUMERATE_MOBILE_PREF_OBSERVERS(_functionCall) \ ENUMERATE_QUERY_OBSERVERS(_functionCall, mMobilePrefObservers, IsQuery()) #define ENUMERATE_BOOKMARK_CHANGED_OBSERVERS(_folderGUID, _targetId, \ _functionCall) \ PR_BEGIN_MACRO \ ENUMERATE_ALL_BOOKMARKS_OBSERVERS(_functionCall); \ FolderObserverList* _fol = \ BookmarkFolderObserversForGUID(_folderGUID, false); \ \ /* \ * Note: folder-nodes set their own bookmark observer only once they're \ * opened, meaning we cannot optimize this code path for changes done to \ * folder-nodes. \ */ \ \ for (uint32_t _fol_i = 0; _fol && _fol_i < _fol->Length(); ++_fol_i) { \ RefPtr _folder = _fol->ElementAt(_fol_i); \ if (_folder) { \ uint32_t _nodeIndex; \ RefPtr _node = \ _folder->FindChildById(_targetId, &_nodeIndex); \ bool _excludeItems = _folder->mOptions->ExcludeItems(); \ if (_node && \ (!_excludeItems || !(_node->IsURI() || _node->IsSeparator())) && \ _folder->StartIncrementalUpdate()) { \ _node->_functionCall; \ } \ } \ } \ \ PR_END_MACRO #define NOTIFY_REFRESH_PARTICIPANTS() \ PR_BEGIN_MACRO \ ENUMERATE_LIST_OBSERVERS(ContainerObserverList, Refresh(), \ mRefreshParticipants, IsContainer()); \ mRefreshParticipants.Clear(); \ PR_END_MACRO NS_IMETHODIMP nsNavHistoryResult::GetSkipTags(bool* aSkipTags) { *aSkipTags = false; return NS_OK; } NS_IMETHODIMP nsNavHistoryResult::OnBeginUpdateBatch() { // Since we could be observing both history and bookmarks, it's possible both // notify the batch. We can safely ignore nested calls. if (++mBatchInProgress == 1) { ENUMERATE_HISTORY_OBSERVERS(OnBeginUpdateBatch()); ENUMERATE_ALL_BOOKMARKS_OBSERVERS(OnBeginUpdateBatch()); NOTIFY_RESULT_OBSERVERS(this, Batching(true)); } return NS_OK; } NS_IMETHODIMP nsNavHistoryResult::OnEndUpdateBatch() { // Since we could be observing both history and bookmarks, it's possible both // notify the batch. We can safely ignore nested calls. // Notice it's possible we are notified OnEndUpdateBatch more times than // onBeginUpdateBatch, since the result could be created in the middle of // nested batches. if (--mBatchInProgress == 0) { ENUMERATE_HISTORY_OBSERVERS(OnEndUpdateBatch()); ENUMERATE_ALL_BOOKMARKS_OBSERVERS(OnEndUpdateBatch()); NOTIFY_REFRESH_PARTICIPANTS(); NOTIFY_RESULT_OBSERVERS(this, Batching(false)); } return NS_OK; } NS_IMETHODIMP nsNavHistoryResult::OnItemChanged( int64_t aItemId, const nsACString& aProperty, bool aIsAnnotationProperty, const nsACString& aNewValue, PRTime aLastModified, uint16_t aItemType, int64_t aParentId, const nsACString& aGUID, const nsACString& aParentGUID, const nsACString& aOldValue, uint16_t aSource) { ENUMERATE_BOOKMARK_CHANGED_OBSERVERS( aParentGUID, aItemId, OnItemChanged(aItemId, aProperty, aIsAnnotationProperty, aNewValue, aLastModified, aItemType, aParentId, aGUID, aParentGUID, aOldValue, aSource)); return NS_OK; } nsresult nsNavHistoryResult::OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime, uint32_t aTransitionType, const nsACString& aGUID, bool aHidden, uint32_t aVisitCount, const nsAString& aLastKnownTitle) { NS_ENSURE_ARG(aURI); // Embed visits are never shown in our views. if (aTransitionType == nsINavHistoryService::TRANSITION_EMBED) { return NS_OK; } uint32_t added = 0; ENUMERATE_HISTORY_OBSERVERS( OnVisit(aURI, aVisitId, aTime, aTransitionType, aHidden, &added)); // When we add visits, we don't bother telling the world that the title // 'changed' from nothing to the first title we ever see for a history entry. // Our consumers here might still care, though, so we have to tell them, but // only for the first visit we add. Subsequent changes will get an usual // ontitlechanged notification. if (!aLastKnownTitle.IsVoid() && aVisitCount == 1) { ENUMERATE_HISTORY_OBSERVERS(OnTitleChanged(aURI, aLastKnownTitle, aGUID)); } if (!mRootNode->mExpanded) return NS_OK; // If this visit is accepted by an overlapped container, and not all // overlapped containers are visible, we should still call Refresh if the // visit falls into any of them. bool todayIsMissing = false; uint32_t resultType = mRootNode->mOptions->ResultType(); if (resultType == nsINavHistoryQueryOptions::RESULTS_AS_DATE_QUERY || resultType == nsINavHistoryQueryOptions::RESULTS_AS_DATE_SITE_QUERY) { uint32_t childCount; nsresult rv = mRootNode->GetChildCount(&childCount); NS_ENSURE_SUCCESS(rv, rv); if (childCount) { nsCOMPtr firstChild; rv = mRootNode->GetChild(0, getter_AddRefs(firstChild)); NS_ENSURE_SUCCESS(rv, rv); nsAutoCString title; rv = firstChild->GetTitle(title); NS_ENSURE_SUCCESS(rv, rv); nsNavHistory* history = nsNavHistory::GetHistoryService(); NS_ENSURE_TRUE(history, NS_OK); nsAutoCString todayLabel; history->GetStringFromName("finduri-AgeInDays-is-0", todayLabel); todayIsMissing = !todayLabel.Equals(title); } } if (!added || todayIsMissing) { // None of registered query observers has accepted our URI. This means, // that a matching query either was not expanded or it does not exist. if (resultType == nsINavHistoryQueryOptions::RESULTS_AS_DATE_QUERY || resultType == nsINavHistoryQueryOptions::RESULTS_AS_DATE_SITE_QUERY) { // If the visit falls into the Today bucket and the bucket exists, it was // just not expanded, thus there's no reason to update. int64_t beginOfToday = nsNavHistory::NormalizeTime( nsINavHistoryQuery::TIME_RELATIVE_TODAY, 0); if (todayIsMissing || aTime < beginOfToday) { (void)mRootNode->GetAsQuery()->Refresh(); } return NS_OK; } if (resultType == nsINavHistoryQueryOptions::RESULTS_AS_SITE_QUERY) { (void)mRootNode->GetAsQuery()->Refresh(); return NS_OK; } // We are result of a folder node, then we should run through history // observers that are containers queries and refresh them. // We use a copy of the observers array since requerying could potentially // cause changes to the array. ENUMERATE_QUERY_OBSERVERS(Refresh(), mHistoryObservers, IsContainersQuery()); // Also notify onItemVisited to bookmark folder observers, that are not // observing history. if (!mIsHistoryObserver && mRootNode->IsFolder()) { nsAutoCString spec; nsresult rv = aURI->GetSpec(spec); NS_ENSURE_SUCCESS(rv, rv); // Find all the folders containing the visited URI, and notify them. nsCOMArray nodes; mRootNode->RecursiveFindURIs(true, mRootNode, spec, &nodes); for (int32_t i = 0; i < nodes.Count(); ++i) { nsNavHistoryResultNode* node = nodes[i]; ENUMERATE_BOOKMARK_FOLDER_OBSERVERS( node->mParent->mBookmarkGuid, OnItemVisited(aURI, aVisitId, aTime)); } } } return NS_OK; } void nsNavHistoryResult::OnIconChanged(nsIURI* aURI, nsIURI* aFaviconURI, const nsACString& aGUID) { if (!mRootNode->mExpanded) { return; } // Find all nodes for the given URI and update them. nsAutoCString spec; if (NS_SUCCEEDED(aURI->GetSpec(spec))) { bool onlyOneEntry = mOptions->QueryType() == nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY && mOptions->ResultType() == nsINavHistoryQueryOptions::RESULTS_AS_URI; mRootNode->UpdateURIs(true, onlyOneEntry, false, spec, setFaviconCallback, nullptr); } } bool nsNavHistoryResult::IsBulkPageRemovedEvent( const PlacesEventSequence& aEvents) { if (IsBatching() || aEvents.Length() <= MAX_PAGE_REMOVES_BEFORE_REFRESH) { return false; } for (const auto& event : aEvents) { if (event->Type() != PlacesEventType::Page_removed) return false; } return true; } void nsNavHistoryResult::HandlePlacesEvent(const PlacesEventSequence& aEvents) { if (IsBulkPageRemovedEvent(aEvents)) { ENUMERATE_HISTORY_OBSERVERS(Refresh()); return; } for (const auto& event : aEvents) { switch (event->Type()) { case PlacesEventType::Favicon_changed: { const dom::PlacesFavicon* faviconEvent = event->AsPlacesFavicon(); if (NS_WARN_IF(!faviconEvent)) { continue; } nsCOMPtr uri, faviconUri; MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), faviconEvent->mUrl)); if (!uri) { continue; } MOZ_ALWAYS_SUCCEEDS( NS_NewURI(getter_AddRefs(faviconUri), faviconEvent->mFaviconUrl)); if (!faviconUri) { continue; } OnIconChanged(uri, faviconUri, faviconEvent->mPageGuid); break; } case PlacesEventType::Page_visited: { const dom::PlacesVisit* visit = event->AsPlacesVisit(); if (NS_WARN_IF(!visit)) { continue; } nsCOMPtr uri; MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), visit->mUrl)); if (!uri) { continue; } OnVisit(uri, visit->mVisitId, visit->mVisitTime * 1000, visit->mTransitionType, visit->mPageGuid, visit->mHidden, visit->mVisitCount, visit->mLastKnownTitle); break; } case PlacesEventType::Bookmark_added: { const dom::PlacesBookmarkAddition* item = event->AsPlacesBookmarkAddition(); if (NS_WARN_IF(!item)) { continue; } nsCOMPtr uri; if (item->mItemType == nsINavBookmarksService::TYPE_BOOKMARK) { MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), item->mUrl)); if (!uri) { continue; } } ENUMERATE_BOOKMARK_FOLDER_OBSERVERS( item->mParentGuid, OnItemAdded(item->mId, item->mParentId, item->mIndex, item->mItemType, uri, item->mDateAdded * 1000, item->mGuid, item->mParentGuid, item->mSource)); ENUMERATE_HISTORY_OBSERVERS( OnItemAdded(item->mId, item->mParentId, item->mIndex, item->mItemType, uri, item->mDateAdded * 1000, item->mGuid, item->mParentGuid, item->mSource)); ENUMERATE_ALL_BOOKMARKS_OBSERVERS( OnItemAdded(item->mId, item->mParentId, item->mIndex, item->mItemType, uri, item->mDateAdded * 1000, item->mGuid, item->mParentGuid, item->mSource)); break; } case PlacesEventType::Bookmark_removed: { const dom::PlacesBookmarkRemoved* item = event->AsPlacesBookmarkRemoved(); if (NS_WARN_IF(!item)) { continue; } nsCOMPtr uri; if (item->mIsDescendantRemoval) { continue; } ENUMERATE_BOOKMARK_FOLDER_OBSERVERS( item->mParentGuid, OnItemRemoved(item->mId, item->mParentId, item->mIndex, item->mItemType, uri, item->mGuid, item->mParentGuid, item->mSource)); ENUMERATE_ALL_BOOKMARKS_OBSERVERS(OnItemRemoved( item->mId, item->mParentId, item->mIndex, item->mItemType, uri, item->mGuid, item->mParentGuid, item->mSource)); ENUMERATE_HISTORY_OBSERVERS(OnItemRemoved( item->mId, item->mParentId, item->mIndex, item->mItemType, uri, item->mGuid, item->mParentGuid, item->mSource)); break; } case PlacesEventType::Bookmark_moved: { const dom::PlacesBookmarkMoved* item = event->AsPlacesBookmarkMoved(); if (NS_WARN_IF(!item)) { continue; } NS_ConvertUTF16toUTF8 url(item->mUrl); ENUMERATE_BOOKMARK_FOLDER_OBSERVERS( item->mOldParentGuid, OnItemMoved(item->mId, item->mOldIndex, item->mIndex, item->mItemType, item->mGuid, item->mOldParentGuid, item->mParentGuid, item->mSource, url)); if (!item->mParentGuid.Equals(item->mOldParentGuid)) { ENUMERATE_BOOKMARK_FOLDER_OBSERVERS( item->mParentGuid, OnItemMoved(item->mId, item->mOldIndex, item->mIndex, item->mItemType, item->mGuid, item->mOldParentGuid, item->mParentGuid, item->mSource, url)); } ENUMERATE_ALL_BOOKMARKS_OBSERVERS( OnItemMoved(item->mId, item->mOldIndex, item->mIndex, item->mItemType, item->mGuid, item->mOldParentGuid, item->mParentGuid, item->mSource, url)); ENUMERATE_HISTORY_OBSERVERS( OnItemMoved(item->mId, item->mOldIndex, item->mIndex, item->mItemType, item->mGuid, item->mOldParentGuid, item->mParentGuid, item->mSource, url)); break; } case PlacesEventType::Bookmark_tags_changed: { const dom::PlacesBookmarkTags* tagsEvent = event->AsPlacesBookmarkTags(); if (NS_WARN_IF(!tagsEvent)) { continue; } ENUMERATE_BOOKMARK_CHANGED_OBSERVERS( tagsEvent->mParentGuid, tagsEvent->mId, OnItemTagsChanged(tagsEvent->mId, tagsEvent->mUrl)); break; } case PlacesEventType::Bookmark_time_changed: { const dom::PlacesBookmarkTime* timeEvent = event->AsPlacesBookmarkTime(); if (NS_WARN_IF(!timeEvent)) { continue; } ENUMERATE_BOOKMARK_CHANGED_OBSERVERS( timeEvent->mParentGuid, timeEvent->mId, OnItemTimeChanged(timeEvent->mId, timeEvent->mGuid, timeEvent->mDateAdded * 1000, timeEvent->mLastModified * 1000)); break; } case PlacesEventType::Bookmark_title_changed: { const dom::PlacesBookmarkTitle* titleEvent = event->AsPlacesBookmarkTitle(); if (NS_WARN_IF(!titleEvent)) { continue; } NS_ConvertUTF16toUTF8 title(titleEvent->mTitle); ENUMERATE_BOOKMARK_CHANGED_OBSERVERS( titleEvent->mParentGuid, titleEvent->mId, OnItemTitleChanged(titleEvent->mId, titleEvent->mGuid, title, titleEvent->mLastModified * 1000)); break; } case PlacesEventType::Bookmark_url_changed: { const dom::PlacesBookmarkUrl* urlEvent = event->AsPlacesBookmarkUrl(); if (NS_WARN_IF(!urlEvent)) { continue; } NS_ConvertUTF16toUTF8 url(urlEvent->mUrl); ENUMERATE_BOOKMARK_CHANGED_OBSERVERS( urlEvent->mParentGuid, urlEvent->mId, OnItemUrlChanged(urlEvent->mId, urlEvent->mGuid, url, urlEvent->mLastModified)); break; } case PlacesEventType::Page_title_changed: { const PlacesVisitTitle* titleEvent = event->AsPlacesVisitTitle(); if (NS_WARN_IF(!titleEvent)) { continue; } nsCOMPtr uri; MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), titleEvent->mUrl)); if (!uri) { continue; } ENUMERATE_HISTORY_OBSERVERS( OnTitleChanged(uri, titleEvent->mTitle, titleEvent->mPageGuid)); break; } case PlacesEventType::History_cleared: { ENUMERATE_HISTORY_OBSERVERS(OnClearHistory()); break; } case PlacesEventType::Page_removed: { const PlacesVisitRemoved* removeEvent = event->AsPlacesVisitRemoved(); if (NS_WARN_IF(!removeEvent)) { continue; } nsCOMPtr uri; MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), removeEvent->mUrl)); if (!uri) { continue; } if (removeEvent->mIsRemovedFromStore) { ENUMERATE_HISTORY_OBSERVERS(OnPageRemovedFromStore( uri, removeEvent->mPageGuid, removeEvent->mReason)); } else { ENUMERATE_HISTORY_OBSERVERS( OnPageRemovedVisits(uri, removeEvent->mIsPartialVisistsRemoval, removeEvent->mPageGuid, removeEvent->mReason, removeEvent->mTransitionType)); } break; } case PlacesEventType::Purge_caches: { mRootNode->Refresh(); break; } default: { MOZ_ASSERT_UNREACHABLE( "Receive notification of a type not subscribed to."); } } } } void nsNavHistoryResult::OnMobilePrefChanged() { ENUMERATE_MOBILE_PREF_OBSERVERS( OnMobilePrefChanged(Preferences::GetBool(MOBILE_BOOKMARKS_PREF, false))); } void nsNavHistoryResult::OnMobilePrefChangedCallback(const char* prefName, void* self) { MOZ_ASSERT(!strcmp(prefName, MOBILE_BOOKMARKS_PREF), "We only expected Mobile Bookmarks pref change."); static_cast(self)->OnMobilePrefChanged(); }