Source code

Revision control

Other Tools

/* -*- 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 <stdio.h>
#include "nsNavHistory.h"
#include "nsNavBookmarks.h"
#include "nsFaviconService.h"
#include "nsAnnotationService.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 "nsCycleCollectionParticipant.h"
// Thanks, Windows.h :(
#undef CompareString
#define TO_ICONTAINER(_node) \
static_cast<nsINavHistoryContainerResultNode*>(_node)
#define TO_CONTAINER(_node) static_cast<nsNavHistoryContainerResultNode*>(_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
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<nsNavHistoryQuery>& aQuery,
const RefPtr<nsNavHistoryQueryOptions>& 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<nsIURI> 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<nsNavHistoryQuery>& aQuery,
const RefPtr<nsNavHistoryQueryOptions>& aOptions,
const RefPtr<nsNavHistoryResultNode>& 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<nsNavHistoryResultNode> inputSet;
inputSet.AppendObject(aNode);
nsCOMArray<nsNavHistoryResultNode> 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<nsIURI> 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<nsIURI> 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<uint32_t>& 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),
mFolderId(-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<nsCString> tags;
ParseString(NS_ConvertUTF16toUTF8(mTags), ',', tags);
tags.Sort();
mTags.SetIsVoid(true);
for (nsTArray<nsCString>::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<Database> DB = Database::GetDatabase();
NS_ENSURE_STATE(DB);
nsCOMPtr<mozIStorageStatement> 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(NS_LITERAL_CSTRING("tags_folder"),
history->GetTagsFolder());
NS_ENSURE_SUCCESS(rv, rv);
rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), 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) {
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<uint32_t>(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);
nsICollation* collation = history->GetCollation();
NS_ENSURE_TRUE(collation, 0);
int32_t res = 0;
collation->CompareString(nsICollation::kCollationCaseInSensitive, a, b, &res);
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 = a->mURI.Compare(b->mURI.get());
}
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 = a->mURI.Compare(b->mURI.get());
} 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::FindChildURI(
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 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()) {
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<nsNavHistoryResultNode> 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<nsNavHistoryResultNode> 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 visit
* style results that may have multiple copies of any given URI.
*/
void nsNavHistoryContainerResultNode::RecursiveFindURIs(
bool aOnlyOne, nsNavHistoryContainerResultNode* aContainer,
const nsCString& aSpec, nsCOMArray<nsNavHistoryResultNode>* aMatches) {
for (int32_t child = 0; child < aContainer->mChildren.Count(); ++child) {
uint32_t type;
aContainer->mChildren[child]->GetType(&type);
if (nsNavHistoryResultNode::IsTypeURI(type)) {
// compare URIs
nsNavHistoryResultNode* uriNode = aContainer->mChildren[child];
if (uriNode->mURI.Equals(aSpec)) {
// found
aMatches->AppendObject(uriNode);
if (aOnlyOne) return;
}
}
}
}
/**
* 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<nsNavHistoryResultNode> matches;
if (aRecursive) {
RecursiveFindURIs(aOnlyOne, this, aSpec, &matches);
} else if (aOnlyOne) {
uint32_t nodeIndex;
nsNavHistoryResultNode* node = FindChildURI(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()) {
NOTIFY_RESULT_OBSERVERS_RET(
result,
NodeHistoryDetailsChanged(TO_ICONTAINER(parent), parentOldTime,
parentOldAccessCount),
true);
}
DebugOnly<nsresult> 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<const nsACString*>(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<const void*>(&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<nsINavHistoryResultNode> 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<nsNavHistoryResultNode*>(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<nsNavHistoryQuery>& aQuery,
const RefPtr<nsNavHistoryQueryOptions>& 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 = EmptyCString();
return NS_OK;
}
NS_IMETHODIMP
nsNavHistoryQueryResultNode::GetQuery(nsINavHistoryQuery** _query) {
RefPtr<nsNavHistoryQuery> query = mQuery;
query.forget(_query);
return NS_OK;
}
NS_IMETHODIMP
nsNavHistoryQueryResultNode::GetQueryOptions(
nsINavHistoryQueryOptions** _options) {
MOZ_ASSERT(mOptions, "Options should be valid");
RefPtr<nsNavHistoryQueryOptions> 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