From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- toolkit/components/places/History.cpp | 2977 +++++++++++++++++++++++++++++++++ 1 file changed, 2977 insertions(+) create mode 100644 toolkit/components/places/History.cpp (limited to 'toolkit/components/places/History.cpp') diff --git a/toolkit/components/places/History.cpp b/toolkit/components/places/History.cpp new file mode 100644 index 000000000..61f78cb83 --- /dev/null +++ b/toolkit/components/places/History.cpp @@ -0,0 +1,2977 @@ +/* -*- 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 "mozilla/ArrayUtils.h" +#include "mozilla/Attributes.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/MemoryReporting.h" + +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "nsXULAppAPI.h" + +#include "History.h" +#include "nsNavHistory.h" +#include "nsNavBookmarks.h" +#include "nsAnnotationService.h" +#include "Helpers.h" +#include "PlaceInfo.h" +#include "VisitInfo.h" +#include "nsPlacesMacros.h" + +#include "mozilla/storage.h" +#include "mozilla/dom/Link.h" +#include "nsDocShellCID.h" +#include "mozilla/Services.h" +#include "nsThreadUtils.h" +#include "nsNetUtil.h" +#include "nsIFileURL.h" +#include "nsIXPConnect.h" +#include "mozilla/Unused.h" +#include "nsContentUtils.h" // for nsAutoScriptBlocker +#include "nsJSUtils.h" +#include "mozilla/ipc/URIUtils.h" +#include "nsPrintfCString.h" +#include "nsTHashtable.h" +#include "jsapi.h" + +// Initial size for the cache holding visited status observers. +#define VISIT_OBSERVERS_INITIAL_CACHE_LENGTH 64 + +// Initial length for the visits removal hash. +#define VISITS_REMOVAL_INITIAL_HASH_LENGTH 64 + +using namespace mozilla::dom; +using namespace mozilla::ipc; +using mozilla::Unused; + +namespace mozilla { +namespace places { + +//////////////////////////////////////////////////////////////////////////////// +//// Global Defines + +#define URI_VISITED "visited" +#define URI_NOT_VISITED "not visited" +#define URI_VISITED_RESOLUTION_TOPIC "visited-status-resolution" +// Observer event fired after a visit has been registered in the DB. +#define URI_VISIT_SAVED "uri-visit-saved" + +#define DESTINATIONFILEURI_ANNO \ + NS_LITERAL_CSTRING("downloads/destinationFileURI") +#define DESTINATIONFILENAME_ANNO \ + NS_LITERAL_CSTRING("downloads/destinationFileName") + +//////////////////////////////////////////////////////////////////////////////// +//// VisitData + +struct VisitData { + VisitData() + : placeId(0) + , visitId(0) + , hidden(true) + , shouldUpdateHidden(true) + , typed(false) + , transitionType(UINT32_MAX) + , visitTime(0) + , frecency(-1) + , lastVisitId(0) + , lastVisitTime(0) + , visitCount(0) + , referrerVisitId(0) + , titleChanged(false) + , shouldUpdateFrecency(true) + { + guid.SetIsVoid(true); + title.SetIsVoid(true); + } + + explicit VisitData(nsIURI* aURI, + nsIURI* aReferrer = nullptr) + : placeId(0) + , visitId(0) + , hidden(true) + , shouldUpdateHidden(true) + , typed(false) + , transitionType(UINT32_MAX) + , visitTime(0) + , frecency(-1) + , lastVisitId(0) + , lastVisitTime(0) + , visitCount(0) + , referrerVisitId(0) + , titleChanged(false) + , shouldUpdateFrecency(true) + { + MOZ_ASSERT(aURI); + if (aURI) { + (void)aURI->GetSpec(spec); + (void)GetReversedHostname(aURI, revHost); + } + if (aReferrer) { + (void)aReferrer->GetSpec(referrerSpec); + } + guid.SetIsVoid(true); + title.SetIsVoid(true); + } + + /** + * Sets the transition type of the visit, as well as if it was typed. + * + * @param aTransitionType + * The transition type constant to set. Must be one of the + * TRANSITION_ constants on nsINavHistoryService. + */ + void SetTransitionType(uint32_t aTransitionType) + { + typed = aTransitionType == nsINavHistoryService::TRANSITION_TYPED; + transitionType = aTransitionType; + } + + int64_t placeId; + nsCString guid; + int64_t visitId; + nsCString spec; + nsString revHost; + bool hidden; + bool shouldUpdateHidden; + bool typed; + uint32_t transitionType; + PRTime visitTime; + int32_t frecency; + int64_t lastVisitId; + PRTime lastVisitTime; + uint32_t visitCount; + + /** + * Stores the title. If this is empty (IsEmpty() returns true), then the + * title should be removed from the Place. If the title is void (IsVoid() + * returns true), then no title has been set on this object, and titleChanged + * should remain false. + */ + nsString title; + + nsCString referrerSpec; + int64_t referrerVisitId; + + // TODO bug 626836 hook up hidden and typed change tracking too! + bool titleChanged; + + // Indicates whether frecency should be updated for this visit. + bool shouldUpdateFrecency; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// RemoveVisitsFilter + +/** + * Used to store visit filters for RemoveVisits. + */ +struct RemoveVisitsFilter { + RemoveVisitsFilter() + : transitionType(UINT32_MAX) + { + } + + uint32_t transitionType; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// PlaceHashKey + +class PlaceHashKey : public nsCStringHashKey +{ +public: + explicit PlaceHashKey(const nsACString& aSpec) + : nsCStringHashKey(&aSpec) + , mVisitCount(0) + , mBookmarked(false) +#ifdef DEBUG + , mIsInitialized(false) +#endif + { + } + + explicit PlaceHashKey(const nsACString* aSpec) + : nsCStringHashKey(aSpec) + , mVisitCount(0) + , mBookmarked(false) +#ifdef DEBUG + , mIsInitialized(false) +#endif + { + } + + PlaceHashKey(const PlaceHashKey& aOther) + : nsCStringHashKey(&aOther.GetKey()) + { + MOZ_ASSERT(false, "Do not call me!"); + } + + void SetProperties(uint32_t aVisitCount, bool aBookmarked) + { + mVisitCount = aVisitCount; + mBookmarked = aBookmarked; +#ifdef DEBUG + mIsInitialized = true; +#endif + } + + uint32_t VisitCount() const + { +#ifdef DEBUG + MOZ_ASSERT(mIsInitialized, "PlaceHashKey::mVisitCount not set"); +#endif + return mVisitCount; + } + + bool IsBookmarked() const + { +#ifdef DEBUG + MOZ_ASSERT(mIsInitialized, "PlaceHashKey::mBookmarked not set"); +#endif + return mBookmarked; + } + + // Array of VisitData objects. + nsTArray mVisits; +private: + // Visit count for this place. + uint32_t mVisitCount; + // Whether this place is bookmarked. + bool mBookmarked; +#ifdef DEBUG + // Whether previous attributes are set. + bool mIsInitialized; +#endif +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Anonymous Helpers + +namespace { + +/** + * Convert the given js value to a js array. + * + * @param [in] aValue + * the JS value to convert. + * @param [in] aCtx + * The JSContext for aValue. + * @param [out] _array + * the JS array. + * @param [out] _arrayLength + * _array's length. + */ +nsresult +GetJSArrayFromJSValue(JS::Handle aValue, + JSContext* aCtx, + JS::MutableHandle _array, + uint32_t* _arrayLength) { + if (aValue.isObjectOrNull()) { + JS::Rooted val(aCtx, aValue.toObjectOrNull()); + bool isArray; + if (!JS_IsArrayObject(aCtx, val, &isArray)) { + return NS_ERROR_UNEXPECTED; + } + if (isArray) { + _array.set(val); + (void)JS_GetArrayLength(aCtx, _array, _arrayLength); + NS_ENSURE_ARG(*_arrayLength > 0); + return NS_OK; + } + } + + // Build a temporary array to store this one item so the code below can + // just loop. + *_arrayLength = 1; + _array.set(JS_NewArrayObject(aCtx, 0)); + NS_ENSURE_TRUE(_array, NS_ERROR_OUT_OF_MEMORY); + + bool rc = JS_DefineElement(aCtx, _array, 0, aValue, 0); + NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED); + return NS_OK; +} + +/** + * Attemps to convert a given js value to a nsIURI object. + * @param aCtx + * The JSContext for aValue. + * @param aValue + * The JS value to convert. + * @return the nsIURI object, or null if aValue is not a nsIURI object. + */ +already_AddRefed +GetJSValueAsURI(JSContext* aCtx, + const JS::Value& aValue) { + if (!aValue.isPrimitive()) { + nsCOMPtr xpc = mozilla::services::GetXPConnect(); + + nsCOMPtr wrappedObj; + nsresult rv = xpc->GetWrappedNativeOfJSObject(aCtx, aValue.toObjectOrNull(), + getter_AddRefs(wrappedObj)); + NS_ENSURE_SUCCESS(rv, nullptr); + nsCOMPtr uri = do_QueryWrappedNative(wrappedObj); + return uri.forget(); + } + return nullptr; +} + +/** + * Obtains an nsIURI from the "uri" property of a JSObject. + * + * @param aCtx + * The JSContext for aObject. + * @param aObject + * The JSObject to get the URI from. + * @param aProperty + * The name of the property to get the URI from. + * @return the URI if it exists. + */ +already_AddRefed +GetURIFromJSObject(JSContext* aCtx, + JS::Handle aObject, + const char* aProperty) +{ + JS::Rooted uriVal(aCtx); + bool rc = JS_GetProperty(aCtx, aObject, aProperty, &uriVal); + NS_ENSURE_TRUE(rc, nullptr); + return GetJSValueAsURI(aCtx, uriVal); +} + +/** + * Attemps to convert a JS value to a string. + * @param aCtx + * The JSContext for aObject. + * @param aValue + * The JS value to convert. + * @param _string + * The string to populate with the value, or set it to void. + */ +void +GetJSValueAsString(JSContext* aCtx, + const JS::Value& aValue, + nsString& _string) { + if (aValue.isUndefined() || + !(aValue.isNull() || aValue.isString())) { + _string.SetIsVoid(true); + return; + } + + // |null| in JS maps to the empty string. + if (aValue.isNull()) { + _string.Truncate(); + return; + } + + if (!AssignJSString(aCtx, _string, aValue.toString())) { + _string.SetIsVoid(true); + } +} + +/** + * Obtains the specified property of a JSObject. + * + * @param aCtx + * The JSContext for aObject. + * @param aObject + * The JSObject to get the string from. + * @param aProperty + * The property to get the value from. + * @param _string + * The string to populate with the value, or set it to void. + */ +void +GetStringFromJSObject(JSContext* aCtx, + JS::Handle aObject, + const char* aProperty, + nsString& _string) +{ + JS::Rooted val(aCtx); + bool rc = JS_GetProperty(aCtx, aObject, aProperty, &val); + if (!rc) { + _string.SetIsVoid(true); + return; + } + else { + GetJSValueAsString(aCtx, val, _string); + } +} + +/** + * Obtains the specified property of a JSObject. + * + * @param aCtx + * The JSContext for aObject. + * @param aObject + * The JSObject to get the int from. + * @param aProperty + * The property to get the value from. + * @param _int + * The integer to populate with the value on success. + */ +template +nsresult +GetIntFromJSObject(JSContext* aCtx, + JS::Handle aObject, + const char* aProperty, + IntType* _int) +{ + JS::Rooted value(aCtx); + bool rc = JS_GetProperty(aCtx, aObject, aProperty, &value); + NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED); + if (value.isUndefined()) { + return NS_ERROR_INVALID_ARG; + } + NS_ENSURE_ARG(value.isPrimitive()); + NS_ENSURE_ARG(value.isNumber()); + + double num; + rc = JS::ToNumber(aCtx, value, &num); + NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED); + NS_ENSURE_ARG(IntType(num) == num); + + *_int = IntType(num); + return NS_OK; +} + +/** + * Obtains the specified property of a JSObject. + * + * @pre aArray must be an Array object. + * + * @param aCtx + * The JSContext for aArray. + * @param aArray + * The JSObject to get the object from. + * @param aIndex + * The index to get the object from. + * @param objOut + * Set to the JSObject pointer on success. + */ +nsresult +GetJSObjectFromArray(JSContext* aCtx, + JS::Handle aArray, + uint32_t aIndex, + JS::MutableHandle objOut) +{ + JS::Rooted value(aCtx); + bool rc = JS_GetElement(aCtx, aArray, aIndex, &value); + NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED); + NS_ENSURE_ARG(!value.isPrimitive()); + objOut.set(&value.toObject()); + return NS_OK; +} + +class VisitedQuery final : public AsyncStatementCallback, + public mozIStorageCompletionCallback +{ +public: + NS_DECL_ISUPPORTS_INHERITED + + static nsresult Start(nsIURI* aURI, + mozIVisitedStatusCallback* aCallback=nullptr) + { + NS_PRECONDITION(aURI, "Null URI"); + + // If we are a content process, always remote the request to the + // parent process. + if (XRE_IsContentProcess()) { + URIParams uri; + SerializeURI(aURI, uri); + + mozilla::dom::ContentChild* cpc = + mozilla::dom::ContentChild::GetSingleton(); + NS_ASSERTION(cpc, "Content Protocol is NULL!"); + (void)cpc->SendStartVisitedQuery(uri); + return NS_OK; + } + + nsMainThreadPtrHandle + callback(new nsMainThreadPtrHolder(aCallback)); + + nsNavHistory* navHistory = nsNavHistory::GetHistoryService(); + NS_ENSURE_STATE(navHistory); + if (navHistory->hasEmbedVisit(aURI)) { + RefPtr cb = new VisitedQuery(aURI, callback, true); + NS_ENSURE_TRUE(cb, NS_ERROR_OUT_OF_MEMORY); + // As per IHistory contract, we must notify asynchronously. + NS_DispatchToMainThread(NewRunnableMethod(cb, &VisitedQuery::NotifyVisitedStatus)); + + return NS_OK; + } + + History* history = History::GetService(); + NS_ENSURE_STATE(history); + RefPtr cb = new VisitedQuery(aURI, callback); + NS_ENSURE_TRUE(cb, NS_ERROR_OUT_OF_MEMORY); + nsresult rv = history->GetIsVisitedStatement(cb); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + // Note: the return value matters here. We call into this method, it's not + // just xpcom boilerplate. + NS_IMETHOD Complete(nsresult aResult, nsISupports* aStatement) override + { + NS_ENSURE_SUCCESS(aResult, aResult); + nsCOMPtr stmt = do_QueryInterface(aStatement); + NS_ENSURE_STATE(stmt); + // Bind by index for performance. + nsresult rv = URIBinder::Bind(stmt, 0, mURI); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr handle; + return stmt->ExecuteAsync(this, getter_AddRefs(handle)); + } + + NS_IMETHOD HandleResult(mozIStorageResultSet* aResults) override + { + // If this method is called, we've gotten results, which means we have a + // visit. + mIsVisited = true; + return NS_OK; + } + + NS_IMETHOD HandleError(mozIStorageError* aError) override + { + // mIsVisited is already set to false, and that's the assumption we will + // make if an error occurred. + return NS_OK; + } + + NS_IMETHOD HandleCompletion(uint16_t aReason) override + { + if (aReason != mozIStorageStatementCallback::REASON_FINISHED) { + return NS_OK; + } + + nsresult rv = NotifyVisitedStatus(); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; + } + + nsresult NotifyVisitedStatus() + { + // If an external handling callback is provided, just notify through it. + if (!!mCallback) { + mCallback->IsVisited(mURI, mIsVisited); + return NS_OK; + } + + if (mIsVisited) { + History* history = History::GetService(); + NS_ENSURE_STATE(history); + history->NotifyVisited(mURI); + } + + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + if (observerService) { + nsAutoString status; + if (mIsVisited) { + status.AssignLiteral(URI_VISITED); + } + else { + status.AssignLiteral(URI_NOT_VISITED); + } + (void)observerService->NotifyObservers(mURI, + URI_VISITED_RESOLUTION_TOPIC, + status.get()); + } + + return NS_OK; + } + +private: + explicit VisitedQuery(nsIURI* aURI, + const nsMainThreadPtrHandle& aCallback, + bool aIsVisited=false) + : mURI(aURI) + , mCallback(aCallback) + , mIsVisited(aIsVisited) + { + } + + ~VisitedQuery() + { + } + + nsCOMPtr mURI; + nsMainThreadPtrHandle mCallback; + bool mIsVisited; +}; + +NS_IMPL_ISUPPORTS_INHERITED( + VisitedQuery +, AsyncStatementCallback +, mozIStorageCompletionCallback +) + +/** + * Notifies observers about a visit. + */ +class NotifyVisitObservers : public Runnable +{ +public: + explicit NotifyVisitObservers(VisitData& aPlace) + : mPlace(aPlace) + , mHistory(History::GetService()) + { + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); + + // We are in the main thread, no need to lock. + if (mHistory->IsShuttingDown()) { + // If we are shutting down, we cannot notify the observers. + return NS_OK; + } + + nsNavHistory* navHistory = nsNavHistory::GetHistoryService(); + if (!navHistory) { + NS_WARNING("Trying to notify about a visit but cannot get the history service!"); + return NS_OK; + } + + nsCOMPtr uri; + MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), mPlace.spec)); + if (!uri) { + return NS_ERROR_UNEXPECTED; + } + + // Notify the visit. Note that TRANSITION_EMBED visits are never added + // to the database, thus cannot be queried and we don't notify them. + if (mPlace.transitionType != nsINavHistoryService::TRANSITION_EMBED) { + navHistory->NotifyOnVisit(uri, mPlace.visitId, mPlace.visitTime, + mPlace.referrerVisitId, mPlace.transitionType, + mPlace.guid, mPlace.hidden, + mPlace.visitCount + 1, // Add current visit. + static_cast(mPlace.typed)); + } + + nsCOMPtr obsService = + mozilla::services::GetObserverService(); + if (obsService) { + DebugOnly rv = + obsService->NotifyObservers(uri, URI_VISIT_SAVED, nullptr); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Could not notify observers"); + } + + History* history = History::GetService(); + NS_ENSURE_STATE(history); + history->AppendToRecentlyVisitedURIs(uri); + history->NotifyVisited(uri); + + return NS_OK; + } +private: + VisitData mPlace; + RefPtr mHistory; +}; + +/** + * Notifies observers about a pages title changing. + */ +class NotifyTitleObservers : public Runnable +{ +public: + /** + * Notifies observers on the main thread. + * + * @param aSpec + * The spec of the URI to notify about. + * @param aTitle + * The new title to notify about. + */ + NotifyTitleObservers(const nsCString& aSpec, + const nsString& aTitle, + const nsCString& aGUID) + : mSpec(aSpec) + , mTitle(aTitle) + , mGUID(aGUID) + { + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); + + nsNavHistory* navHistory = nsNavHistory::GetHistoryService(); + NS_ENSURE_TRUE(navHistory, NS_ERROR_OUT_OF_MEMORY); + nsCOMPtr uri; + MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), mSpec)); + if (!uri) { + return NS_ERROR_UNEXPECTED; + } + + navHistory->NotifyTitleChange(uri, mTitle, mGUID); + + return NS_OK; + } +private: + const nsCString mSpec; + const nsString mTitle; + const nsCString mGUID; +}; + +/** + * Helper class for methods which notify their callers through the + * mozIVisitInfoCallback interface. + */ +class NotifyPlaceInfoCallback : public Runnable +{ +public: + NotifyPlaceInfoCallback(const nsMainThreadPtrHandle& aCallback, + const VisitData& aPlace, + bool aIsSingleVisit, + nsresult aResult) + : mCallback(aCallback) + , mPlace(aPlace) + , mResult(aResult) + , mIsSingleVisit(aIsSingleVisit) + { + MOZ_ASSERT(aCallback, "Must pass a non-null callback!"); + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); + + bool hasValidURIs = true; + nsCOMPtr referrerURI; + if (!mPlace.referrerSpec.IsEmpty()) { + MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(referrerURI), mPlace.referrerSpec)); + hasValidURIs = !!referrerURI; + } + + nsCOMPtr uri; + MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), mPlace.spec)); + hasValidURIs = hasValidURIs && !!uri; + + nsCOMPtr place; + if (mIsSingleVisit) { + nsCOMPtr visit = + new VisitInfo(mPlace.visitId, mPlace.visitTime, mPlace.transitionType, + referrerURI.forget()); + PlaceInfo::VisitsArray visits; + (void)visits.AppendElement(visit); + + // The frecency isn't exposed because it may not reflect the updated value + // in the case of InsertVisitedURIs. + place = + new PlaceInfo(mPlace.placeId, mPlace.guid, uri.forget(), mPlace.title, + -1, visits); + } + else { + // Same as above. + place = + new PlaceInfo(mPlace.placeId, mPlace.guid, uri.forget(), mPlace.title, + -1); + } + + if (NS_SUCCEEDED(mResult) && hasValidURIs) { + (void)mCallback->HandleResult(place); + } else { + (void)mCallback->HandleError(mResult, place); + } + + return NS_OK; + } + +private: + nsMainThreadPtrHandle mCallback; + VisitData mPlace; + const nsresult mResult; + bool mIsSingleVisit; +}; + +/** + * Notifies a callback object when the operation is complete. + */ +class NotifyCompletion : public Runnable +{ +public: + explicit NotifyCompletion(const nsMainThreadPtrHandle& aCallback) + : mCallback(aCallback) + { + MOZ_ASSERT(aCallback, "Must pass a non-null callback!"); + } + + NS_IMETHOD Run() override + { + if (NS_IsMainThread()) { + (void)mCallback->HandleCompletion(); + } + else { + (void)NS_DispatchToMainThread(this); + } + return NS_OK; + } + +private: + nsMainThreadPtrHandle mCallback; +}; + +/** + * Checks to see if we can add aURI to history, and dispatches an error to + * aCallback (if provided) if we cannot. + * + * @param aURI + * The URI to check. + * @param [optional] aGUID + * The guid of the URI to check. This is passed back to the callback. + * @param [optional] aCallback + * The callback to notify if the URI cannot be added to history. + * @return true if the URI can be added to history, false otherwise. + */ +bool +CanAddURI(nsIURI* aURI, + const nsCString& aGUID = EmptyCString(), + mozIVisitInfoCallback* aCallback = nullptr) +{ + MOZ_ASSERT(NS_IsMainThread()); + nsNavHistory* navHistory = nsNavHistory::GetHistoryService(); + NS_ENSURE_TRUE(navHistory, false); + + bool canAdd; + nsresult rv = navHistory->CanAddURI(aURI, &canAdd); + if (NS_SUCCEEDED(rv) && canAdd) { + return true; + }; + + // We cannot add the URI. Notify the callback, if we were given one. + if (aCallback) { + VisitData place(aURI); + place.guid = aGUID; + nsMainThreadPtrHandle + callback(new nsMainThreadPtrHolder(aCallback)); + nsCOMPtr event = + new NotifyPlaceInfoCallback(callback, place, true, NS_ERROR_INVALID_ARG); + (void)NS_DispatchToMainThread(event); + } + + return false; +} + +/** + * Adds a visit to the database. + */ +class InsertVisitedURIs final: public Runnable +{ +public: + /** + * Adds a visit to the database asynchronously. + * + * @param aConnection + * The database connection to use for these operations. + * @param aPlaces + * The locations to record visits. + * @param [optional] aCallback + * The callback to notify about the visit. + */ + static nsresult Start(mozIStorageConnection* aConnection, + nsTArray& aPlaces, + mozIVisitInfoCallback* aCallback = nullptr) + { + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); + MOZ_ASSERT(aPlaces.Length() > 0, "Must pass a non-empty array!"); + + // Make sure nsNavHistory service is up before proceeding: + nsNavHistory* navHistory = nsNavHistory::GetHistoryService(); + MOZ_ASSERT(navHistory, "Could not get nsNavHistory?!"); + if (!navHistory) { + return NS_ERROR_FAILURE; + } + + nsMainThreadPtrHandle + callback(new nsMainThreadPtrHolder(aCallback)); + RefPtr event = + new InsertVisitedURIs(aConnection, aPlaces, callback); + + // Get the target thread, and then start the work! + nsCOMPtr target = do_GetInterface(aConnection); + NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED); + nsresult rv = target->Dispatch(event, NS_DISPATCH_NORMAL); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(!NS_IsMainThread(), "This should not be called on the main thread"); + + // Prevent the main thread from shutting down while this is running. + MutexAutoLock lockedScope(mHistory->GetShutdownMutex()); + if (mHistory->IsShuttingDown()) { + // If we were already shutting down, we cannot insert the URIs. + return NS_OK; + } + + mozStorageTransaction transaction(mDBConn, false, + mozIStorageConnection::TRANSACTION_IMMEDIATE); + + VisitData* lastFetchedPlace = nullptr; + for (nsTArray::size_type i = 0; i < mPlaces.Length(); i++) { + VisitData& place = mPlaces.ElementAt(i); + + // Fetching from the database can overwrite this information, so save it + // apart. + bool typed = place.typed; + bool hidden = place.hidden; + + // We can avoid a database lookup if it's the same place as the last + // visit we added. + bool known = lastFetchedPlace && lastFetchedPlace->spec.Equals(place.spec); + if (!known) { + nsresult rv = mHistory->FetchPageInfo(place, &known); + if (NS_FAILED(rv)) { + if (!!mCallback) { + nsCOMPtr event = + new NotifyPlaceInfoCallback(mCallback, place, true, rv); + return NS_DispatchToMainThread(event); + } + return NS_OK; + } + lastFetchedPlace = &mPlaces.ElementAt(i); + } else { + // Copy over the data from the already known place. + place.placeId = lastFetchedPlace->placeId; + place.guid = lastFetchedPlace->guid; + place.lastVisitId = lastFetchedPlace->visitId; + place.lastVisitTime = lastFetchedPlace->visitTime; + place.titleChanged = !lastFetchedPlace->title.Equals(place.title); + place.frecency = lastFetchedPlace->frecency; + // Add one visit for the previous loop. + place.visitCount = ++(*lastFetchedPlace).visitCount; + } + + // If any transition is typed, ensure the page is marked as typed. + if (typed != lastFetchedPlace->typed) { + place.typed = true; + } + + // If any transition is visible, ensure the page is marked as visible. + if (hidden != lastFetchedPlace->hidden) { + place.hidden = false; + } + + // If this is a new page, or the existing page was already visible, + // there's no need to try to unhide it. + if (!known || !lastFetchedPlace->hidden) { + place.shouldUpdateHidden = false; + } + + FetchReferrerInfo(place); + + nsresult rv = DoDatabaseInserts(known, place); + if (!!mCallback) { + nsCOMPtr event = + new NotifyPlaceInfoCallback(mCallback, place, true, rv); + nsresult rv2 = NS_DispatchToMainThread(event); + NS_ENSURE_SUCCESS(rv2, rv2); + } + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr event = new NotifyVisitObservers(place); + rv = NS_DispatchToMainThread(event); + NS_ENSURE_SUCCESS(rv, rv); + + // Notify about title change if needed. + if ((!known && !place.title.IsVoid()) || place.titleChanged) { + event = new NotifyTitleObservers(place.spec, place.title, place.guid); + rv = NS_DispatchToMainThread(event); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + nsresult rv = transaction.Commit(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } +private: + InsertVisitedURIs(mozIStorageConnection* aConnection, + nsTArray& aPlaces, + const nsMainThreadPtrHandle& aCallback) + : mDBConn(aConnection) + , mCallback(aCallback) + , mHistory(History::GetService()) + { + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); + + mPlaces.SwapElements(aPlaces); + +#ifdef DEBUG + for (nsTArray::size_type i = 0; i < mPlaces.Length(); i++) { + nsCOMPtr uri; + MOZ_ASSERT(NS_SUCCEEDED(NS_NewURI(getter_AddRefs(uri), mPlaces[i].spec))); + MOZ_ASSERT(CanAddURI(uri), + "Passed a VisitData with a URI we cannot add to history!"); + } +#endif + } + + /** + * Inserts or updates the entry in moz_places for this visit, adds the visit, + * and updates the frecency of the place. + * + * @param aKnown + * True if we already have an entry for this place in moz_places, false + * otherwise. + * @param aPlace + * The place we are adding a visit for. + */ + nsresult DoDatabaseInserts(bool aKnown, + VisitData& aPlace) + { + MOZ_ASSERT(!NS_IsMainThread(), "This should not be called on the main thread"); + + // If the page was in moz_places, we need to update the entry. + nsresult rv; + if (aKnown) { + rv = mHistory->UpdatePlace(aPlace); + NS_ENSURE_SUCCESS(rv, rv); + } + // Otherwise, the page was not in moz_places, so now we have to add it. + else { + rv = mHistory->InsertPlace(aPlace); + NS_ENSURE_SUCCESS(rv, rv); + aPlace.placeId = nsNavHistory::sLastInsertedPlaceId; + } + MOZ_ASSERT(aPlace.placeId > 0); + + rv = AddVisit(aPlace); + NS_ENSURE_SUCCESS(rv, rv); + + // TODO (bug 623969) we shouldn't update this after each visit, but + // rather only for each unique place to save disk I/O. + + // Don't update frecency if the page should not appear in autocomplete. + if (aPlace.shouldUpdateFrecency) { + rv = UpdateFrecency(aPlace); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; + } + + /** + * Fetches information about a referrer for aPlace if it was a recent + * visit or not. + * + * @param aPlace + * The VisitData for the visit we will eventually add. + * + */ + void FetchReferrerInfo(VisitData& aPlace) + { + if (aPlace.referrerSpec.IsEmpty()) { + return; + } + + VisitData referrer; + referrer.spec = aPlace.referrerSpec; + // If the referrer is the same as the page, we don't need to fetch it. + if (aPlace.referrerSpec.Equals(aPlace.spec)) { + referrer = aPlace; + // The page last visit id is also the referrer visit id. + aPlace.referrerVisitId = aPlace.lastVisitId; + } else { + bool exists = false; + if (NS_SUCCEEDED(mHistory->FetchPageInfo(referrer, &exists)) && exists) { + // Copy the referrer last visit id. + aPlace.referrerVisitId = referrer.lastVisitId; + } + } + + // Check if the page has effectively been visited recently, otherwise + // discard the referrer info. + if (!aPlace.referrerVisitId || !referrer.lastVisitTime || + aPlace.visitTime - referrer.lastVisitTime > RECENT_EVENT_THRESHOLD) { + // We will not be using the referrer data. + aPlace.referrerSpec.Truncate(); + aPlace.referrerVisitId = 0; + } + } + + /** + * Adds a visit for _place and updates it with the right visit id. + * + * @param _place + * The VisitData for the place we need to know visit information about. + */ + nsresult AddVisit(VisitData& _place) + { + MOZ_ASSERT(_place.placeId > 0); + + nsresult rv; + nsCOMPtr stmt; + stmt = mHistory->GetStatement( + "INSERT INTO moz_historyvisits " + "(from_visit, place_id, visit_date, visit_type, session) " + "VALUES (:from_visit, :page_id, :visit_date, :visit_type, 0) " + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), _place.placeId); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("from_visit"), + _place.referrerVisitId); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("visit_date"), + _place.visitTime); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t transitionType = _place.transitionType; + MOZ_ASSERT(transitionType >= nsINavHistoryService::TRANSITION_LINK && + transitionType <= nsINavHistoryService::TRANSITION_RELOAD, + "Invalid transition type!"); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("visit_type"), + transitionType); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + _place.visitId = nsNavHistory::sLastInsertedVisitId; + MOZ_ASSERT(_place.visitId > 0); + + return NS_OK; + } + + /** + * Updates the frecency, and possibly the hidden-ness of aPlace. + * + * @param aPlace + * The VisitData for the place we want to update. + */ + nsresult UpdateFrecency(const VisitData& aPlace) + { + MOZ_ASSERT(aPlace.shouldUpdateFrecency); + MOZ_ASSERT(aPlace.placeId > 0); + + nsresult rv; + { // First, set our frecency to the proper value. + nsCOMPtr stmt; + stmt = mHistory->GetStatement( + "UPDATE moz_places " + "SET frecency = NOTIFY_FRECENCY(" + "CALCULATE_FRECENCY(:page_id), " + "url, guid, hidden, last_visit_date" + ") " + "WHERE id = :page_id" + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), aPlace.placeId); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (!aPlace.hidden && aPlace.shouldUpdateHidden) { + // Mark the page as not hidden if the frecency is now nonzero. + nsCOMPtr stmt; + stmt = mHistory->GetStatement( + "UPDATE moz_places " + "SET hidden = 0 " + "WHERE id = :page_id AND frecency <> 0" + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), aPlace.placeId); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; + } + + mozIStorageConnection* mDBConn; + + nsTArray mPlaces; + + nsMainThreadPtrHandle mCallback; + + /** + * Strong reference to the History object because we do not want it to + * disappear out from under us. + */ + RefPtr mHistory; +}; + +class GetPlaceInfo final : public Runnable { +public: + /** + * Get the place info for a given place (by GUID or URI) asynchronously. + */ + static nsresult Start(mozIStorageConnection* aConnection, + VisitData& aPlace, + mozIVisitInfoCallback* aCallback) { + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); + + nsMainThreadPtrHandle + callback(new nsMainThreadPtrHolder(aCallback)); + RefPtr event = new GetPlaceInfo(aPlace, callback); + + // Get the target thread, and then start the work! + nsCOMPtr target = do_GetInterface(aConnection); + NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED); + nsresult rv = target->Dispatch(event, NS_DISPATCH_NORMAL); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(!NS_IsMainThread(), "This should not be called on the main thread"); + + bool exists; + nsresult rv = mHistory->FetchPageInfo(mPlace, &exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!exists) + rv = NS_ERROR_NOT_AVAILABLE; + + nsCOMPtr event = + new NotifyPlaceInfoCallback(mCallback, mPlace, false, rv); + + rv = NS_DispatchToMainThread(event); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } +private: + GetPlaceInfo(VisitData& aPlace, + const nsMainThreadPtrHandle& aCallback) + : mPlace(aPlace) + , mCallback(aCallback) + , mHistory(History::GetService()) + { + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); + } + + VisitData mPlace; + nsMainThreadPtrHandle mCallback; + RefPtr mHistory; +}; + +/** + * Sets the page title for a page in moz_places (if necessary). + */ +class SetPageTitle : public Runnable +{ +public: + /** + * Sets a pages title in the database asynchronously. + * + * @param aConnection + * The database connection to use for this operation. + * @param aURI + * The URI to set the page title on. + * @param aTitle + * The title to set for the page, if the page exists. + */ + static nsresult Start(mozIStorageConnection* aConnection, + nsIURI* aURI, + const nsAString& aTitle) + { + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); + MOZ_ASSERT(aURI, "Must pass a non-null URI object!"); + + nsCString spec; + nsresult rv = aURI->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr event = new SetPageTitle(spec, aTitle); + + // Get the target thread, and then start the work! + nsCOMPtr target = do_GetInterface(aConnection); + NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED); + rv = target->Dispatch(event, NS_DISPATCH_NORMAL); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(!NS_IsMainThread(), "This should not be called on the main thread"); + + // First, see if the page exists in the database (we'll need its id later). + bool exists; + nsresult rv = mHistory->FetchPageInfo(mPlace, &exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!exists || !mPlace.titleChanged) { + // We have no record of this page, or we have no title change, so there + // is no need to do any further work. + return NS_OK; + } + + MOZ_ASSERT(mPlace.placeId > 0, + "We somehow have an invalid place id here!"); + + // Now we can update our database record. + nsCOMPtr stmt = + mHistory->GetStatement( + "UPDATE moz_places " + "SET title = :page_title " + "WHERE id = :page_id " + ); + NS_ENSURE_STATE(stmt); + + { + mozStorageStatementScoper scoper(stmt); + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), mPlace.placeId); + NS_ENSURE_SUCCESS(rv, rv); + // Empty strings should clear the title, just like + // nsNavHistory::SetPageTitle. + if (mPlace.title.IsEmpty()) { + rv = stmt->BindNullByName(NS_LITERAL_CSTRING("page_title")); + } + else { + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("page_title"), + StringHead(mPlace.title, TITLE_LENGTH_MAX)); + } + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr event = + new NotifyTitleObservers(mPlace.spec, mPlace.title, mPlace.guid); + rv = NS_DispatchToMainThread(event); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + +private: + SetPageTitle(const nsCString& aSpec, + const nsAString& aTitle) + : mHistory(History::GetService()) + { + mPlace.spec = aSpec; + mPlace.title = aTitle; + } + + VisitData mPlace; + + /** + * Strong reference to the History object because we do not want it to + * disappear out from under us. + */ + RefPtr mHistory; +}; + +/** + * Adds download-specific annotations to a download page. + */ +class SetDownloadAnnotations final : public mozIVisitInfoCallback +{ +public: + NS_DECL_ISUPPORTS + + explicit SetDownloadAnnotations(nsIURI* aDestination) + : mDestination(aDestination) + , mHistory(History::GetService()) + { + MOZ_ASSERT(mDestination); + MOZ_ASSERT(NS_IsMainThread()); + } + + NS_IMETHOD HandleError(nsresult aResultCode, mozIPlaceInfo *aPlaceInfo) override + { + // Just don't add the annotations in case the visit isn't added. + return NS_OK; + } + + NS_IMETHOD HandleResult(mozIPlaceInfo *aPlaceInfo) override + { + // Exit silently if the download destination is not a local file. + nsCOMPtr destinationFileURL = do_QueryInterface(mDestination); + if (!destinationFileURL) { + return NS_OK; + } + + nsCOMPtr source; + nsresult rv = aPlaceInfo->GetUri(getter_AddRefs(source)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr destinationFile; + rv = destinationFileURL->GetFile(getter_AddRefs(destinationFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString destinationFileName; + rv = destinationFile->GetLeafName(destinationFileName); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString destinationURISpec; + rv = destinationFileURL->GetSpec(destinationURISpec); + NS_ENSURE_SUCCESS(rv, rv); + + // Use annotations for storing the additional download metadata. + nsAnnotationService* annosvc = nsAnnotationService::GetAnnotationService(); + NS_ENSURE_TRUE(annosvc, NS_ERROR_OUT_OF_MEMORY); + + rv = annosvc->SetPageAnnotationString( + source, + DESTINATIONFILEURI_ANNO, + NS_ConvertUTF8toUTF16(destinationURISpec), + 0, + nsIAnnotationService::EXPIRE_WITH_HISTORY + ); + NS_ENSURE_SUCCESS(rv, rv); + + rv = annosvc->SetPageAnnotationString( + source, + DESTINATIONFILENAME_ANNO, + destinationFileName, + 0, + nsIAnnotationService::EXPIRE_WITH_HISTORY + ); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString title; + rv = aPlaceInfo->GetTitle(title); + NS_ENSURE_SUCCESS(rv, rv); + + // In case we are downloading a file that does not correspond to a web + // page for which the title is present, we populate the otherwise empty + // history title with the name of the destination file, to allow it to be + // visible and searchable in history results. + if (title.IsEmpty()) { + rv = mHistory->SetURITitle(source, destinationFileName); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; + } + + NS_IMETHOD HandleCompletion() override + { + return NS_OK; + } + +private: + ~SetDownloadAnnotations() {} + + nsCOMPtr mDestination; + + /** + * Strong reference to the History object because we do not want it to + * disappear out from under us. + */ + RefPtr mHistory; +}; +NS_IMPL_ISUPPORTS( + SetDownloadAnnotations, + mozIVisitInfoCallback +) + +/** + * Notify removed visits to observers. + */ +class NotifyRemoveVisits : public Runnable +{ +public: + + explicit NotifyRemoveVisits(nsTHashtable& aPlaces) + : mPlaces(VISITS_REMOVAL_INITIAL_HASH_LENGTH) + , mHistory(History::GetService()) + { + MOZ_ASSERT(!NS_IsMainThread(), + "This should not be called on the main thread"); + for (auto iter = aPlaces.Iter(); !iter.Done(); iter.Next()) { + PlaceHashKey* entry = iter.Get(); + PlaceHashKey* copy = mPlaces.PutEntry(entry->GetKey()); + copy->SetProperties(entry->VisitCount(), entry->IsBookmarked()); + entry->mVisits.SwapElements(copy->mVisits); + } + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); + + // We are in the main thread, no need to lock. + if (mHistory->IsShuttingDown()) { + // If we are shutting down, we cannot notify the observers. + return NS_OK; + } + + nsNavHistory* navHistory = nsNavHistory::GetHistoryService(); + if (!navHistory) { + NS_WARNING("Cannot notify without the history service!"); + return NS_OK; + } + + // Wrap all notifications in a batch, so the view can handle changes in a + // more performant way, by initiating a refresh after a limited number of + // single changes. + (void)navHistory->BeginUpdateBatch(); + for (auto iter = mPlaces.Iter(); !iter.Done(); iter.Next()) { + PlaceHashKey* entry = iter.Get(); + const nsTArray& visits = entry->mVisits; + nsCOMPtr uri; + MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), visits[0].spec)); + // Notify an expiration only if we have a valid uri, otherwise + // the observer couldn't gather any useful data from the notification. + // This should be false only if there's a bug in the code preceding us. + if (uri) { + bool removingPage = visits.Length() == entry->VisitCount() && + !entry->IsBookmarked(); + + // FindRemovableVisits only sets the transition type on the VisitData + // objects it collects if the visits were filtered by transition type. + // RemoveVisitsFilter currently only supports filtering by transition + // type, so FindRemovableVisits will either find all visits, or all + // visits of a given type. Therefore, if transitionType is set on this + // visit, we pass the transition type to NotifyOnPageExpired which in + // turns passes it to OnDeleteVisits to indicate that all visits of a + // given type were removed. + uint32_t transition = visits[0].transitionType < UINT32_MAX + ? visits[0].transitionType + : 0; + navHistory->NotifyOnPageExpired(uri, visits[0].visitTime, removingPage, + visits[0].guid, + nsINavHistoryObserver::REASON_DELETED, + transition); + } + } + (void)navHistory->EndUpdateBatch(); + + return NS_OK; + } + +private: + nsTHashtable mPlaces; + + /** + * Strong reference to the History object because we do not want it to + * disappear out from under us. + */ + RefPtr mHistory; +}; + +/** + * Remove visits from history. + */ +class RemoveVisits : public Runnable +{ +public: + /** + * Asynchronously removes visits from history. + * + * @param aConnection + * The database connection to use for these operations. + * @param aFilter + * Filter to remove visits. + */ + static nsresult Start(mozIStorageConnection* aConnection, + RemoveVisitsFilter& aFilter) + { + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); + + RefPtr event = new RemoveVisits(aConnection, aFilter); + + // Get the target thread, and then start the work! + nsCOMPtr target = do_GetInterface(aConnection); + NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED); + nsresult rv = target->Dispatch(event, NS_DISPATCH_NORMAL); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(!NS_IsMainThread(), + "This should not be called on the main thread"); + + // Prevent the main thread from shutting down while this is running. + MutexAutoLock lockedScope(mHistory->GetShutdownMutex()); + if (mHistory->IsShuttingDown()) { + // If we were already shutting down, we cannot remove the visits. + return NS_OK; + } + + // Find all the visits relative to the current filters and whether their + // pages will be removed or not. + nsTHashtable places(VISITS_REMOVAL_INITIAL_HASH_LENGTH); + nsresult rv = FindRemovableVisits(places); + NS_ENSURE_SUCCESS(rv, rv); + + if (places.Count() == 0) + return NS_OK; + + mozStorageTransaction transaction(mDBConn, false, + mozIStorageConnection::TRANSACTION_IMMEDIATE); + + rv = RemoveVisitsFromDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + rv = RemovePagesFromDatabase(places); + NS_ENSURE_SUCCESS(rv, rv); + + rv = transaction.Commit(); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr event = new NotifyRemoveVisits(places); + rv = NS_DispatchToMainThread(event); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + +private: + RemoveVisits(mozIStorageConnection* aConnection, + RemoveVisitsFilter& aFilter) + : mDBConn(aConnection) + , mHasTransitionType(false) + , mHistory(History::GetService()) + { + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); + + // Build query conditions. + nsTArray conditions; + // TODO: add support for binding params when adding further stuff here. + if (aFilter.transitionType < UINT32_MAX) { + conditions.AppendElement(nsPrintfCString("visit_type = %d", aFilter.transitionType)); + mHasTransitionType = true; + } + if (conditions.Length() > 0) { + mWhereClause.AppendLiteral (" WHERE "); + for (uint32_t i = 0; i < conditions.Length(); ++i) { + if (i > 0) + mWhereClause.AppendLiteral(" AND "); + mWhereClause.Append(conditions[i]); + } + } + } + + /** + * Find the list of entries that may be removed from `moz_places`. + * + * Calling this method makes sense only if we are not clearing the entire history. + */ + nsresult + FindRemovableVisits(nsTHashtable& aPlaces) + { + MOZ_ASSERT(!NS_IsMainThread(), + "This should not be called on the main thread"); + + nsCString query("SELECT h.id, url, guid, visit_date, visit_type, " + "(SELECT count(*) FROM moz_historyvisits WHERE place_id = h.id) as full_visit_count, " + "EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) as bookmarked " + "FROM moz_historyvisits " + "JOIN moz_places h ON place_id = h.id"); + query.Append(mWhereClause); + + nsCOMPtr stmt = mHistory->GetStatement(query); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + bool hasResult; + nsresult rv; + while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) { + VisitData visit; + rv = stmt->GetInt64(0, &visit.placeId); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->GetUTF8String(1, visit.spec); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->GetUTF8String(2, visit.guid); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->GetInt64(3, &visit.visitTime); + NS_ENSURE_SUCCESS(rv, rv); + if (mHasTransitionType) { + int32_t transition; + rv = stmt->GetInt32(4, &transition); + NS_ENSURE_SUCCESS(rv, rv); + visit.transitionType = static_cast(transition); + } + int32_t visitCount, bookmarked; + rv = stmt->GetInt32(5, &visitCount); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->GetInt32(6, &bookmarked); + NS_ENSURE_SUCCESS(rv, rv); + + PlaceHashKey* entry = aPlaces.GetEntry(visit.spec); + if (!entry) { + entry = aPlaces.PutEntry(visit.spec); + } + entry->SetProperties(static_cast(visitCount), static_cast(bookmarked)); + entry->mVisits.AppendElement(visit); + } + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + nsresult + RemoveVisitsFromDatabase() + { + MOZ_ASSERT(!NS_IsMainThread(), + "This should not be called on the main thread"); + + nsCString query("DELETE FROM moz_historyvisits"); + query.Append(mWhereClause); + + nsCOMPtr stmt = mHistory->GetStatement(query); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + nsresult rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + nsresult + RemovePagesFromDatabase(nsTHashtable& aPlaces) + { + MOZ_ASSERT(!NS_IsMainThread(), + "This should not be called on the main thread"); + + nsCString placeIdsToRemove; + for (auto iter = aPlaces.Iter(); !iter.Done(); iter.Next()) { + PlaceHashKey* entry = iter.Get(); + const nsTArray& visits = entry->mVisits; + // Only orphan ids should be listed. + if (visits.Length() == entry->VisitCount() && !entry->IsBookmarked()) { + if (!placeIdsToRemove.IsEmpty()) + placeIdsToRemove.Append(','); + placeIdsToRemove.AppendInt(visits[0].placeId); + } + } + +#ifdef DEBUG + { + // Ensure that we are not removing any problematic entry. + nsCString query("SELECT id FROM moz_places h WHERE id IN ("); + query.Append(placeIdsToRemove); + query.AppendLiteral(") AND (" + "EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) OR " + "EXISTS(SELECT 1 FROM moz_historyvisits WHERE place_id = h.id) OR " + "SUBSTR(h.url, 1, 6) = 'place:' " + ")"); + nsCOMPtr stmt = mHistory->GetStatement(query); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + bool hasResult; + MOZ_ASSERT(NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && !hasResult, + "Trying to remove a non-oprhan place from the database"); + } +#endif + + { + nsCString query("DELETE FROM moz_places " + "WHERE id IN ("); + query.Append(placeIdsToRemove); + query.Append(')'); + + nsCOMPtr stmt = mHistory->GetStatement(query); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + nsresult rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + } + + { + // Hosts accumulated during the places delete are updated through a trigger + // (see nsPlacesTriggers.h). + nsAutoCString query("DELETE FROM moz_updatehosts_temp"); + nsCOMPtr stmt = mHistory->GetStatement(query); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + nsresult rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; + } + + mozIStorageConnection* mDBConn; + bool mHasTransitionType; + nsCString mWhereClause; + + /** + * Strong reference to the History object because we do not want it to + * disappear out from under us. + */ + RefPtr mHistory; +}; + +/** + * Stores an embed visit, and notifies observers. + * + * @param aPlace + * The VisitData of the visit to store as an embed visit. + * @param [optional] aCallback + * The mozIVisitInfoCallback to notify, if provided. + */ +void +StoreAndNotifyEmbedVisit(VisitData& aPlace, + mozIVisitInfoCallback* aCallback = nullptr) +{ + MOZ_ASSERT(aPlace.transitionType == nsINavHistoryService::TRANSITION_EMBED, + "Must only pass TRANSITION_EMBED visits to this!"); + MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread!"); + + nsCOMPtr uri; + MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), aPlace.spec)); + + nsNavHistory* navHistory = nsNavHistory::GetHistoryService(); + if (!navHistory || !uri) { + return; + } + + navHistory->registerEmbedVisit(uri, aPlace.visitTime); + + if (!!aCallback) { + nsMainThreadPtrHandle + callback(new nsMainThreadPtrHolder(aCallback)); + nsCOMPtr event = + new NotifyPlaceInfoCallback(callback, aPlace, true, NS_OK); + (void)NS_DispatchToMainThread(event); + } + + nsCOMPtr event = new NotifyVisitObservers(aPlace); + (void)NS_DispatchToMainThread(event); +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +//// History + +History* History::gService = nullptr; + +History::History() + : mShuttingDown(false) + , mShutdownMutex("History::mShutdownMutex") + , mObservers(VISIT_OBSERVERS_INITIAL_CACHE_LENGTH) + , mRecentlyVisitedURIs(RECENTLY_VISITED_URIS_SIZE) +{ + NS_ASSERTION(!gService, "Ruh-roh! This service has already been created!"); + gService = this; + + nsCOMPtr os = services::GetObserverService(); + NS_WARNING_ASSERTION(os, "Observer service was not found!"); + if (os) { + (void)os->AddObserver(this, TOPIC_PLACES_SHUTDOWN, false); + } +} + +History::~History() +{ + UnregisterWeakMemoryReporter(this); + + gService = nullptr; + + NS_ASSERTION(mObservers.Count() == 0, + "Not all Links were removed before we disappear!"); +} + +void +History::InitMemoryReporter() +{ + RegisterWeakMemoryReporter(this); +} + +NS_IMETHODIMP +History::NotifyVisited(nsIURI* aURI) +{ + MOZ_ASSERT(NS_IsMainThread()); + NS_ENSURE_ARG(aURI); + + nsAutoScriptBlocker scriptBlocker; + + if (XRE_IsParentProcess()) { + nsTArray cplist; + ContentParent::GetAll(cplist); + + if (!cplist.IsEmpty()) { + URIParams uri; + SerializeURI(aURI, uri); + for (uint32_t i = 0; i < cplist.Length(); ++i) { + Unused << cplist[i]->SendNotifyVisited(uri); + } + } + } + + // If we have no observers for this URI, we have nothing to notify about. + KeyClass* key = mObservers.GetEntry(aURI); + if (!key) { + return NS_OK; + } + + // Update status of each Link node. + { + // RemoveEntry will destroy the array, this iterator should not survive it. + ObserverArray::ForwardIterator iter(key->array); + while (iter.HasMore()) { + Link* link = iter.GetNext(); + link->SetLinkState(eLinkState_Visited); + // Verify that the observers hash doesn't mutate while looping through + // the links associated with this URI. + MOZ_ASSERT(key == mObservers.GetEntry(aURI), + "The URIs hash mutated!"); + } + } + + // All the registered nodes can now be removed for this URI. + mObservers.RemoveEntry(key); + return NS_OK; +} + +class ConcurrentStatementsHolder final : public mozIStorageCompletionCallback { +public: + NS_DECL_ISUPPORTS + + explicit ConcurrentStatementsHolder(mozIStorageConnection* aDBConn) + { + DebugOnly rv = aDBConn->AsyncClone(true, this); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + NS_IMETHOD Complete(nsresult aStatus, nsISupports* aConnection) override { + if (NS_FAILED(aStatus)) + return NS_OK; + mReadOnlyDBConn = do_QueryInterface(aConnection); + + // Now we can create our cached statements. + + if (!mIsVisitedStatement) { + (void)mReadOnlyDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING( + "SELECT 1 FROM moz_places h " + "WHERE url_hash = hash(?1) AND url = ?1 AND last_visit_date NOTNULL " + ), getter_AddRefs(mIsVisitedStatement)); + MOZ_ASSERT(mIsVisitedStatement); + nsresult result = mIsVisitedStatement ? NS_OK : NS_ERROR_NOT_AVAILABLE; + for (int32_t i = 0; i < mIsVisitedCallbacks.Count(); ++i) { + DebugOnly rv; + rv = mIsVisitedCallbacks[i]->Complete(result, mIsVisitedStatement); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + mIsVisitedCallbacks.Clear(); + } + + return NS_OK; + } + + void GetIsVisitedStatement(mozIStorageCompletionCallback* aCallback) + { + if (mIsVisitedStatement) { + DebugOnly rv; + rv = aCallback->Complete(NS_OK, mIsVisitedStatement); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } else { + DebugOnly added = mIsVisitedCallbacks.AppendObject(aCallback); + MOZ_ASSERT(added); + } + } + + void Shutdown() { + if (mReadOnlyDBConn) { + mIsVisitedCallbacks.Clear(); + DebugOnly rv; + if (mIsVisitedStatement) { + rv = mIsVisitedStatement->Finalize(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + rv = mReadOnlyDBConn->AsyncClose(nullptr); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + +private: + ~ConcurrentStatementsHolder() + { + } + + nsCOMPtr mReadOnlyDBConn; + nsCOMPtr mIsVisitedStatement; + nsCOMArray mIsVisitedCallbacks; +}; + +NS_IMPL_ISUPPORTS( + ConcurrentStatementsHolder +, mozIStorageCompletionCallback +) + +nsresult +History::GetIsVisitedStatement(mozIStorageCompletionCallback* aCallback) +{ + MOZ_ASSERT(NS_IsMainThread()); + if (mShuttingDown) + return NS_ERROR_NOT_AVAILABLE; + + if (!mConcurrentStatementsHolder) { + mozIStorageConnection* dbConn = GetDBConn(); + NS_ENSURE_STATE(dbConn); + mConcurrentStatementsHolder = new ConcurrentStatementsHolder(dbConn); + } + mConcurrentStatementsHolder->GetIsVisitedStatement(aCallback); + return NS_OK; +} + +nsresult +History::InsertPlace(VisitData& aPlace) +{ + MOZ_ASSERT(aPlace.placeId == 0, "should not have a valid place id!"); + MOZ_ASSERT(!aPlace.shouldUpdateHidden, "We should not need to update hidden"); + MOZ_ASSERT(!NS_IsMainThread(), "must be called off of the main thread!"); + + nsCOMPtr stmt = GetStatement( + "INSERT INTO moz_places " + "(url, url_hash, title, rev_host, hidden, typed, frecency, guid) " + "VALUES (:url, hash(:url), :title, :rev_host, :hidden, :typed, :frecency, :guid) " + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + nsresult rv = stmt->BindStringByName(NS_LITERAL_CSTRING("rev_host"), + aPlace.revHost); + NS_ENSURE_SUCCESS(rv, rv); + rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("url"), aPlace.spec); + NS_ENSURE_SUCCESS(rv, rv); + nsString title = aPlace.title; + // Empty strings should have no title, just like nsNavHistory::SetPageTitle. + if (title.IsEmpty()) { + rv = stmt->BindNullByName(NS_LITERAL_CSTRING("title")); + } + else { + title.Assign(StringHead(aPlace.title, TITLE_LENGTH_MAX)); + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("title"), title); + } + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("typed"), aPlace.typed); + NS_ENSURE_SUCCESS(rv, rv); + // When inserting a page for a first visit that should not appear in + // autocomplete, for example an error page, use a zero frecency. + int32_t frecency = aPlace.shouldUpdateFrecency ? aPlace.frecency : 0; + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("frecency"), frecency); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("hidden"), aPlace.hidden); + NS_ENSURE_SUCCESS(rv, rv); + if (aPlace.guid.IsVoid()) { + rv = GenerateGUID(aPlace.guid); + NS_ENSURE_SUCCESS(rv, rv); + } + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), aPlace.guid); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + // Post an onFrecencyChanged observer notification. + const nsNavHistory* navHistory = nsNavHistory::GetConstHistoryService(); + NS_ENSURE_STATE(navHistory); + navHistory->DispatchFrecencyChangedNotification(aPlace.spec, frecency, + aPlace.guid, + aPlace.hidden, + aPlace.visitTime); + + return NS_OK; +} + +nsresult +History::UpdatePlace(const VisitData& aPlace) +{ + MOZ_ASSERT(!NS_IsMainThread(), "must be called off of the main thread!"); + MOZ_ASSERT(aPlace.placeId > 0, "must have a valid place id!"); + MOZ_ASSERT(!aPlace.guid.IsVoid(), "must have a guid!"); + + nsCOMPtr stmt = GetStatement( + "UPDATE moz_places " + "SET title = :title, " + "hidden = :hidden, " + "typed = :typed, " + "guid = :guid " + "WHERE id = :page_id " + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + nsresult rv; + // Empty strings should clear the title, just like nsNavHistory::SetPageTitle. + if (aPlace.title.IsEmpty()) { + rv = stmt->BindNullByName(NS_LITERAL_CSTRING("title")); + } + else { + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("title"), + StringHead(aPlace.title, TITLE_LENGTH_MAX)); + } + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("typed"), aPlace.typed); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("hidden"), aPlace.hidden); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), aPlace.guid); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), + aPlace.placeId); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +History::FetchPageInfo(VisitData& _place, bool* _exists) +{ + MOZ_ASSERT(!_place.spec.IsEmpty() || !_place.guid.IsEmpty(), "must have either a non-empty spec or guid!"); + MOZ_ASSERT(!NS_IsMainThread(), "must be called off of the main thread!"); + + nsresult rv; + + // URI takes precedence. + nsCOMPtr stmt; + bool selectByURI = !_place.spec.IsEmpty(); + if (selectByURI) { + stmt = GetStatement( + "SELECT guid, id, title, hidden, typed, frecency, visit_count, last_visit_date, " + "(SELECT id FROM moz_historyvisits " + "WHERE place_id = h.id AND visit_date = h.last_visit_date) AS last_visit_id " + "FROM moz_places h " + "WHERE url_hash = hash(:page_url) AND url = :page_url " + ); + NS_ENSURE_STATE(stmt); + + rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), _place.spec); + NS_ENSURE_SUCCESS(rv, rv); + } + else { + stmt = GetStatement( + "SELECT url, id, title, hidden, typed, frecency, visit_count, last_visit_date, " + "(SELECT id FROM moz_historyvisits " + "WHERE place_id = h.id AND visit_date = h.last_visit_date) AS last_visit_id " + "FROM moz_places h " + "WHERE guid = :guid " + ); + NS_ENSURE_STATE(stmt); + + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), _place.guid); + NS_ENSURE_SUCCESS(rv, rv); + } + + mozStorageStatementScoper scoper(stmt); + + rv = stmt->ExecuteStep(_exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!*_exists) { + return NS_OK; + } + + if (selectByURI) { + if (_place.guid.IsEmpty()) { + rv = stmt->GetUTF8String(0, _place.guid); + NS_ENSURE_SUCCESS(rv, rv); + } + } + else { + nsAutoCString spec; + rv = stmt->GetUTF8String(0, spec); + NS_ENSURE_SUCCESS(rv, rv); + _place.spec = spec; + } + + rv = stmt->GetInt64(1, &_place.placeId); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString title; + rv = stmt->GetString(2, title); + NS_ENSURE_SUCCESS(rv, rv); + + // If the title we were given was void, that means we did not bother to set + // it to anything. As a result, ignore the fact that we may have changed the + // title (because we don't want to, that would be empty), and set the title + // to what is currently stored in the datbase. + if (_place.title.IsVoid()) { + _place.title = title; + } + // Otherwise, just indicate if the title has changed. + else { + _place.titleChanged = !(_place.title.Equals(title) || + (_place.title.IsEmpty() && title.IsVoid())); + } + + int32_t hidden; + rv = stmt->GetInt32(3, &hidden); + NS_ENSURE_SUCCESS(rv, rv); + _place.hidden = !!hidden; + + int32_t typed; + rv = stmt->GetInt32(4, &typed); + NS_ENSURE_SUCCESS(rv, rv); + _place.typed = !!typed; + + rv = stmt->GetInt32(5, &_place.frecency); + NS_ENSURE_SUCCESS(rv, rv); + int32_t visitCount; + rv = stmt->GetInt32(6, &visitCount); + NS_ENSURE_SUCCESS(rv, rv); + _place.visitCount = visitCount; + rv = stmt->GetInt64(7, &_place.lastVisitTime); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->GetInt64(8, &_place.lastVisitId); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +MOZ_DEFINE_MALLOC_SIZE_OF(HistoryMallocSizeOf) + +NS_IMETHODIMP +History::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) +{ + MOZ_COLLECT_REPORT( + "explicit/history-links-hashtable", KIND_HEAP, UNITS_BYTES, + SizeOfIncludingThis(HistoryMallocSizeOf), + "Memory used by the hashtable that records changes to the visited state " + "of links."); + + return NS_OK; +} + +size_t +History::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOfThis) +{ + return aMallocSizeOfThis(this) + + mObservers.SizeOfExcludingThis(aMallocSizeOfThis); +} + +/* static */ +History* +History::GetService() +{ + if (gService) { + return gService; + } + + nsCOMPtr service(do_GetService(NS_IHISTORY_CONTRACTID)); + MOZ_ASSERT(service, "Cannot obtain IHistory service!"); + NS_ASSERTION(gService, "Our constructor was not run?!"); + + return gService; +} + +/* static */ +History* +History::GetSingleton() +{ + if (!gService) { + gService = new History(); + NS_ENSURE_TRUE(gService, nullptr); + gService->InitMemoryReporter(); + } + + NS_ADDREF(gService); + return gService; +} + +mozIStorageConnection* +History::GetDBConn() +{ + if (mShuttingDown) + return nullptr; + if (!mDB) { + mDB = Database::GetDatabase(); + NS_ENSURE_TRUE(mDB, nullptr); + } + return mDB->MainConn(); +} + +void +History::Shutdown() +{ + MOZ_ASSERT(NS_IsMainThread()); + + // Prevent other threads from scheduling uses of the DB while we mark + // ourselves as shutting down. + MutexAutoLock lockedScope(mShutdownMutex); + MOZ_ASSERT(!mShuttingDown && "Shutdown was called more than once!"); + + mShuttingDown = true; + + if (mConcurrentStatementsHolder) { + mConcurrentStatementsHolder->Shutdown(); + } +} + +void +History::AppendToRecentlyVisitedURIs(nsIURI* aURI) { + // Add a new entry, if necessary. + RecentURIKey* entry = mRecentlyVisitedURIs.GetEntry(aURI); + if (!entry) { + entry = mRecentlyVisitedURIs.PutEntry(aURI); + } + if (entry) { + entry->time = PR_Now(); + } + + // Remove entries older than RECENTLY_VISITED_URIS_MAX_AGE. + for (auto iter = mRecentlyVisitedURIs.Iter(); !iter.Done(); iter.Next()) { + RecentURIKey* entry = iter.Get(); + if ((PR_Now() - entry->time) > RECENTLY_VISITED_URIS_MAX_AGE) { + iter.Remove(); + } + } +} + +inline bool +History::IsRecentlyVisitedURI(nsIURI* aURI) { + RecentURIKey* entry = mRecentlyVisitedURIs.GetEntry(aURI); + // Check if the entry exists and is younger than RECENTLY_VISITED_URIS_MAX_AGE. + return entry && (PR_Now() - entry->time) < RECENTLY_VISITED_URIS_MAX_AGE; +} + +//////////////////////////////////////////////////////////////////////////////// +//// IHistory + +NS_IMETHODIMP +History::VisitURI(nsIURI* aURI, + nsIURI* aLastVisitedURI, + uint32_t aFlags) +{ + NS_ENSURE_ARG(aURI); + + if (mShuttingDown) { + return NS_OK; + } + + if (XRE_IsContentProcess()) { + URIParams uri; + SerializeURI(aURI, uri); + + OptionalURIParams lastVisitedURI; + SerializeURI(aLastVisitedURI, lastVisitedURI); + + mozilla::dom::ContentChild* cpc = + mozilla::dom::ContentChild::GetSingleton(); + NS_ASSERTION(cpc, "Content Protocol is NULL!"); + (void)cpc->SendVisitURI(uri, lastVisitedURI, aFlags); + return NS_OK; + } + + nsNavHistory* navHistory = nsNavHistory::GetHistoryService(); + NS_ENSURE_TRUE(navHistory, NS_ERROR_OUT_OF_MEMORY); + + // Silently return if URI is something we shouldn't add to DB. + bool canAdd; + nsresult rv = navHistory->CanAddURI(aURI, &canAdd); + NS_ENSURE_SUCCESS(rv, rv); + if (!canAdd) { + return NS_OK; + } + + // Do not save a reloaded uri if we have visited the same URI recently. + bool reload = false; + if (aLastVisitedURI) { + rv = aURI->Equals(aLastVisitedURI, &reload); + NS_ENSURE_SUCCESS(rv, rv); + if (reload && IsRecentlyVisitedURI(aURI)) { + // Regardless we must update the stored visit time. + AppendToRecentlyVisitedURIs(aURI); + return NS_OK; + } + } + + nsTArray placeArray(1); + NS_ENSURE_TRUE(placeArray.AppendElement(VisitData(aURI, aLastVisitedURI)), + NS_ERROR_OUT_OF_MEMORY); + VisitData& place = placeArray.ElementAt(0); + NS_ENSURE_FALSE(place.spec.IsEmpty(), NS_ERROR_INVALID_ARG); + + place.visitTime = PR_Now(); + + // Assigns a type to the edge in the visit linked list. Each type will be + // considered differently when weighting the frecency of a location. + uint32_t recentFlags = navHistory->GetRecentFlags(aURI); + bool isFollowedLink = recentFlags & nsNavHistory::RECENT_ACTIVATED; + + // Embed visits should never be added to the database, and the same is valid + // for redirects across frames. + // For the above reasoning non-toplevel transitions are handled at first. + // if the visit is toplevel or a non-toplevel followed link, then it can be + // handled as usual and stored on disk. + + uint32_t transitionType = nsINavHistoryService::TRANSITION_LINK; + + if (!(aFlags & IHistory::TOP_LEVEL) && !isFollowedLink) { + // A frame redirected to a new site without user interaction. + transitionType = nsINavHistoryService::TRANSITION_EMBED; + } + else if (aFlags & IHistory::REDIRECT_TEMPORARY) { + transitionType = nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY; + } + else if (aFlags & IHistory::REDIRECT_PERMANENT) { + transitionType = nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT; + } + else if (reload) { + transitionType = nsINavHistoryService::TRANSITION_RELOAD; + } + else if ((recentFlags & nsNavHistory::RECENT_TYPED) && + !(aFlags & IHistory::UNRECOVERABLE_ERROR)) { + // Don't mark error pages as typed, even if they were actually typed by + // the user. This is useful to limit their score in autocomplete. + transitionType = nsINavHistoryService::TRANSITION_TYPED; + } + else if (recentFlags & nsNavHistory::RECENT_BOOKMARKED) { + transitionType = nsINavHistoryService::TRANSITION_BOOKMARK; + } + else if (!(aFlags & IHistory::TOP_LEVEL) && isFollowedLink) { + // User activated a link in a frame. + transitionType = nsINavHistoryService::TRANSITION_FRAMED_LINK; + } + + place.SetTransitionType(transitionType); + place.hidden = GetHiddenState(aFlags & IHistory::REDIRECT_SOURCE, + transitionType); + + // Error pages should never be autocompleted. + if (aFlags & IHistory::UNRECOVERABLE_ERROR) { + place.shouldUpdateFrecency = false; + } + + // EMBED visits are session-persistent and should not go through the database. + // They exist only to keep track of isVisited status during the session. + if (place.transitionType == nsINavHistoryService::TRANSITION_EMBED) { + StoreAndNotifyEmbedVisit(place); + } + else { + mozIStorageConnection* dbConn = GetDBConn(); + NS_ENSURE_STATE(dbConn); + + rv = InsertVisitedURIs::Start(dbConn, placeArray); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Finally, notify that we've been visited. + nsCOMPtr obsService = + mozilla::services::GetObserverService(); + if (obsService) { + obsService->NotifyObservers(aURI, NS_LINK_VISITED_EVENT_TOPIC, nullptr); + } + + return NS_OK; +} + +NS_IMETHODIMP +History::RegisterVisitedCallback(nsIURI* aURI, + Link* aLink) +{ + MOZ_ASSERT(NS_IsMainThread()); + NS_ASSERTION(aURI, "Must pass a non-null URI!"); + if (XRE_IsContentProcess()) { + NS_PRECONDITION(aLink, "Must pass a non-null Link!"); + } + + // Obtain our array of observers for this URI. +#ifdef DEBUG + bool keyAlreadyExists = !!mObservers.GetEntry(aURI); +#endif + KeyClass* key = mObservers.PutEntry(aURI); + NS_ENSURE_TRUE(key, NS_ERROR_OUT_OF_MEMORY); + ObserverArray& observers = key->array; + + if (observers.IsEmpty()) { + NS_ASSERTION(!keyAlreadyExists, + "An empty key was kept around in our hashtable!"); + + // We are the first Link node to ask about this URI, or there are no pending + // Links wanting to know about this URI. Therefore, we should query the + // database now. + nsresult rv = VisitedQuery::Start(aURI); + + // In IPC builds, we are passed a nullptr Link from + // ContentParent::RecvStartVisitedQuery. Since we won't be adding a + // nullptr entry to our list of observers, and the code after this point + // assumes that aLink is non-nullptr, we will need to return now. + if (NS_FAILED(rv) || !aLink) { + // Remove our array from the hashtable so we don't keep it around. + // In some case calling RemoveEntry on the key obtained by PutEntry + // crashes for currently unknown reasons. Our suspect is that something + // between PutEntry and this call causes a nested loop that either removes + // the entry or reallocs the hash. + // TODO (Bug 1412647): we must figure the root cause for these issues and + // remove this stop-gap crash fix. + key = mObservers.GetEntry(aURI); + if (key) { + mObservers.RemoveEntry(key); + } + return rv; + } + } + // In IPC builds, we are passed a nullptr Link from + // ContentParent::RecvStartVisitedQuery. All of our code after this point + // assumes aLink is non-nullptr, so we have to return now. + else if (!aLink) { + NS_ASSERTION(XRE_IsParentProcess(), + "We should only ever get a null Link in the default process!"); + return NS_OK; + } + + // Sanity check that Links are not registered more than once for a given URI. + // This will not catch a case where it is registered for two different URIs. + NS_ASSERTION(!observers.Contains(aLink), + "Already tracking this Link object!"); + + // Start tracking our Link. + if (!observers.AppendElement(aLink)) { + // Curses - unregister and return failure. + (void)UnregisterVisitedCallback(aURI, aLink); + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +NS_IMETHODIMP +History::UnregisterVisitedCallback(nsIURI* aURI, + Link* aLink) +{ + MOZ_ASSERT(NS_IsMainThread()); + // TODO: aURI is sometimes null - see bug 548685 + NS_ASSERTION(aURI, "Must pass a non-null URI!"); + NS_ASSERTION(aLink, "Must pass a non-null Link object!"); + + // Get the array, and remove the item from it. + KeyClass* key = mObservers.GetEntry(aURI); + if (!key) { + NS_ERROR("Trying to unregister for a URI that wasn't registered!"); + return NS_ERROR_UNEXPECTED; + } + ObserverArray& observers = key->array; + if (!observers.RemoveElement(aLink)) { + NS_ERROR("Trying to unregister a node that wasn't registered!"); + return NS_ERROR_UNEXPECTED; + } + + // If the array is now empty, we should remove it from the hashtable. + if (observers.IsEmpty()) { + mObservers.RemoveEntry(aURI); + } + + return NS_OK; +} + +NS_IMETHODIMP +History::SetURITitle(nsIURI* aURI, const nsAString& aTitle) +{ + NS_ENSURE_ARG(aURI); + + if (mShuttingDown) { + return NS_OK; + } + + if (XRE_IsContentProcess()) { + URIParams uri; + SerializeURI(aURI, uri); + + mozilla::dom::ContentChild * cpc = + mozilla::dom::ContentChild::GetSingleton(); + NS_ASSERTION(cpc, "Content Protocol is NULL!"); + (void)cpc->SendSetURITitle(uri, PromiseFlatString(aTitle)); + return NS_OK; + } + + nsNavHistory* navHistory = nsNavHistory::GetHistoryService(); + + // At first, it seems like nav history should always be available here, no + // matter what. + // + // nsNavHistory fails to register as a service if there is no profile in + // place (for instance, if user is choosing a profile). + // + // Maybe the correct thing to do is to not register this service if no + // profile has been selected? + // + NS_ENSURE_TRUE(navHistory, NS_ERROR_FAILURE); + + bool canAdd; + nsresult rv = navHistory->CanAddURI(aURI, &canAdd); + NS_ENSURE_SUCCESS(rv, rv); + if (!canAdd) { + return NS_OK; + } + + // Embed visits don't have a database entry, thus don't set a title on them. + if (navHistory->hasEmbedVisit(aURI)) { + return NS_OK; + } + + mozIStorageConnection* dbConn = GetDBConn(); + NS_ENSURE_STATE(dbConn); + + rv = SetPageTitle::Start(dbConn, aURI, aTitle); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIDownloadHistory + +NS_IMETHODIMP +History::AddDownload(nsIURI* aSource, nsIURI* aReferrer, + PRTime aStartTime, nsIURI* aDestination) +{ + MOZ_ASSERT(NS_IsMainThread()); + NS_ENSURE_ARG(aSource); + + if (mShuttingDown) { + return NS_OK; + } + + if (XRE_IsContentProcess()) { + NS_ERROR("Cannot add downloads to history from content process!"); + return NS_ERROR_NOT_AVAILABLE; + } + + nsNavHistory* navHistory = nsNavHistory::GetHistoryService(); + NS_ENSURE_TRUE(navHistory, NS_ERROR_OUT_OF_MEMORY); + + // Silently return if URI is something we shouldn't add to DB. + bool canAdd; + nsresult rv = navHistory->CanAddURI(aSource, &canAdd); + NS_ENSURE_SUCCESS(rv, rv); + if (!canAdd) { + return NS_OK; + } + + nsTArray placeArray(1); + NS_ENSURE_TRUE(placeArray.AppendElement(VisitData(aSource, aReferrer)), + NS_ERROR_OUT_OF_MEMORY); + VisitData& place = placeArray.ElementAt(0); + NS_ENSURE_FALSE(place.spec.IsEmpty(), NS_ERROR_INVALID_ARG); + + place.visitTime = aStartTime; + place.SetTransitionType(nsINavHistoryService::TRANSITION_DOWNLOAD); + place.hidden = false; + + mozIStorageConnection* dbConn = GetDBConn(); + NS_ENSURE_STATE(dbConn); + + nsMainThreadPtrHandle callback; + if (aDestination) { + callback = new nsMainThreadPtrHolder(new SetDownloadAnnotations(aDestination)); + } + + rv = InsertVisitedURIs::Start(dbConn, placeArray, callback); + NS_ENSURE_SUCCESS(rv, rv); + + // Finally, notify that we've been visited. + nsCOMPtr obsService = + mozilla::services::GetObserverService(); + if (obsService) { + obsService->NotifyObservers(aSource, NS_LINK_VISITED_EVENT_TOPIC, nullptr); + } + + return NS_OK; +} + +NS_IMETHODIMP +History::RemoveAllDownloads() +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (mShuttingDown) { + return NS_OK; + } + + if (XRE_IsContentProcess()) { + NS_ERROR("Cannot remove downloads to history from content process!"); + return NS_ERROR_NOT_AVAILABLE; + } + + // Ensure navHistory is initialized. + nsNavHistory* navHistory = nsNavHistory::GetHistoryService(); + NS_ENSURE_TRUE(navHistory, NS_ERROR_OUT_OF_MEMORY); + mozIStorageConnection* dbConn = GetDBConn(); + NS_ENSURE_STATE(dbConn); + + RemoveVisitsFilter filter; + filter.transitionType = nsINavHistoryService::TRANSITION_DOWNLOAD; + + nsresult rv = RemoveVisits::Start(dbConn, filter); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIAsyncHistory + +NS_IMETHODIMP +History::GetPlacesInfo(JS::Handle aPlaceIdentifiers, + mozIVisitInfoCallback* aCallback, + JSContext* aCtx) +{ + // Make sure nsNavHistory service is up before proceeding: + nsNavHistory* navHistory = nsNavHistory::GetHistoryService(); + MOZ_ASSERT(navHistory, "Could not get nsNavHistory?!"); + if (!navHistory) { + return NS_ERROR_FAILURE; + } + + uint32_t placesIndentifiersLength; + JS::Rooted placesIndentifiers(aCtx); + nsresult rv = GetJSArrayFromJSValue(aPlaceIdentifiers, aCtx, + &placesIndentifiers, + &placesIndentifiersLength); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray placesInfo; + placesInfo.SetCapacity(placesIndentifiersLength); + for (uint32_t i = 0; i < placesIndentifiersLength; i++) { + JS::Rooted placeIdentifier(aCtx); + bool rc = JS_GetElement(aCtx, placesIndentifiers, i, &placeIdentifier); + NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED); + + // GUID + nsAutoString fatGUID; + GetJSValueAsString(aCtx, placeIdentifier, fatGUID); + if (!fatGUID.IsVoid()) { + NS_ConvertUTF16toUTF8 guid(fatGUID); + if (!IsValidGUID(guid)) + return NS_ERROR_INVALID_ARG; + + VisitData& placeInfo = *placesInfo.AppendElement(VisitData()); + placeInfo.guid = guid; + } + else { + nsCOMPtr uri = GetJSValueAsURI(aCtx, placeIdentifier); + if (!uri) + return NS_ERROR_INVALID_ARG; // neither a guid, nor a uri. + placesInfo.AppendElement(VisitData(uri)); + } + } + + mozIStorageConnection* dbConn = GetDBConn(); + NS_ENSURE_STATE(dbConn); + + for (nsTArray::size_type i = 0; i < placesInfo.Length(); i++) { + nsresult rv = GetPlaceInfo::Start(dbConn, placesInfo.ElementAt(i), aCallback); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Be sure to notify that all of our operations are complete. This + // is dispatched to the background thread first and redirected to the + // main thread from there to make sure that all database notifications + // and all embed or canAddURI notifications have finished. + if (aCallback) { + nsMainThreadPtrHandle + callback(new nsMainThreadPtrHolder(aCallback)); + nsCOMPtr backgroundThread = do_GetInterface(dbConn); + NS_ENSURE_TRUE(backgroundThread, NS_ERROR_UNEXPECTED); + nsCOMPtr event = new NotifyCompletion(callback); + return backgroundThread->Dispatch(event, NS_DISPATCH_NORMAL); + } + + return NS_OK; +} + +NS_IMETHODIMP +History::UpdatePlaces(JS::Handle aPlaceInfos, + mozIVisitInfoCallback* aCallback, + JSContext* aCtx) +{ + NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED); + NS_ENSURE_TRUE(!aPlaceInfos.isPrimitive(), NS_ERROR_INVALID_ARG); + + uint32_t infosLength; + JS::Rooted infos(aCtx); + nsresult rv = GetJSArrayFromJSValue(aPlaceInfos, aCtx, &infos, &infosLength); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray visitData; + for (uint32_t i = 0; i < infosLength; i++) { + JS::Rooted info(aCtx); + nsresult rv = GetJSObjectFromArray(aCtx, infos, i, &info); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr uri = GetURIFromJSObject(aCtx, info, "uri"); + nsCString guid; + { + nsString fatGUID; + GetStringFromJSObject(aCtx, info, "guid", fatGUID); + if (fatGUID.IsVoid()) { + guid.SetIsVoid(true); + } + else { + guid = NS_ConvertUTF16toUTF8(fatGUID); + } + } + + // Make sure that any uri we are given can be added to history, and if not, + // skip it (CanAddURI will notify our callback for us). + if (uri && !CanAddURI(uri, guid, aCallback)) { + continue; + } + + // We must have at least one of uri or guid. + NS_ENSURE_ARG(uri || !guid.IsVoid()); + + // If we were given a guid, make sure it is valid. + bool isValidGUID = IsValidGUID(guid); + NS_ENSURE_ARG(guid.IsVoid() || isValidGUID); + + nsString title; + GetStringFromJSObject(aCtx, info, "title", title); + + JS::Rooted visits(aCtx, nullptr); + { + JS::Rooted visitsVal(aCtx); + bool rc = JS_GetProperty(aCtx, info, "visits", &visitsVal); + NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED); + if (!visitsVal.isPrimitive()) { + visits = visitsVal.toObjectOrNull(); + bool isArray; + if (!JS_IsArrayObject(aCtx, visits, &isArray)) { + return NS_ERROR_UNEXPECTED; + } + if (!isArray) { + return NS_ERROR_INVALID_ARG; + } + } + } + NS_ENSURE_ARG(visits); + + uint32_t visitsLength = 0; + if (visits) { + (void)JS_GetArrayLength(aCtx, visits, &visitsLength); + } + NS_ENSURE_ARG(visitsLength > 0); + + // Check each visit, and build our array of VisitData objects. + visitData.SetCapacity(visitData.Length() + visitsLength); + for (uint32_t j = 0; j < visitsLength; j++) { + JS::Rooted visit(aCtx); + rv = GetJSObjectFromArray(aCtx, visits, j, &visit); + NS_ENSURE_SUCCESS(rv, rv); + + VisitData& data = *visitData.AppendElement(VisitData(uri)); + data.title = title; + data.guid = guid; + + // We must have a date and a transaction type! + rv = GetIntFromJSObject(aCtx, visit, "visitDate", &data.visitTime); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t transitionType = 0; + rv = GetIntFromJSObject(aCtx, visit, "transitionType", &transitionType); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_ARG_RANGE(transitionType, + nsINavHistoryService::TRANSITION_LINK, + nsINavHistoryService::TRANSITION_RELOAD); + data.SetTransitionType(transitionType); + data.hidden = GetHiddenState(false, transitionType); + + // If the visit is an embed visit, we do not actually add it to the + // database. + if (transitionType == nsINavHistoryService::TRANSITION_EMBED) { + StoreAndNotifyEmbedVisit(data, aCallback); + visitData.RemoveElementAt(visitData.Length() - 1); + continue; + } + + // The referrer is optional. + nsCOMPtr referrer = GetURIFromJSObject(aCtx, visit, + "referrerURI"); + if (referrer) { + (void)referrer->GetSpec(data.referrerSpec); + } + } + } + + mozIStorageConnection* dbConn = GetDBConn(); + NS_ENSURE_STATE(dbConn); + + nsMainThreadPtrHandle + callback(new nsMainThreadPtrHolder(aCallback)); + + // It is possible that all of the visits we were passed were dissallowed by + // CanAddURI, which isn't an error. If we have no visits to add, however, + // we should not call InsertVisitedURIs::Start. + if (visitData.Length()) { + nsresult rv = InsertVisitedURIs::Start(dbConn, visitData, callback); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Be sure to notify that all of our operations are complete. This + // is dispatched to the background thread first and redirected to the + // main thread from there to make sure that all database notifications + // and all embed or canAddURI notifications have finished. + if (aCallback) { + nsCOMPtr backgroundThread = do_GetInterface(dbConn); + NS_ENSURE_TRUE(backgroundThread, NS_ERROR_UNEXPECTED); + nsCOMPtr event = new NotifyCompletion(callback); + return backgroundThread->Dispatch(event, NS_DISPATCH_NORMAL); + } + + return NS_OK; +} + +NS_IMETHODIMP +History::IsURIVisited(nsIURI* aURI, + mozIVisitedStatusCallback* aCallback) +{ + NS_ENSURE_STATE(NS_IsMainThread()); + NS_ENSURE_ARG(aURI); + NS_ENSURE_ARG(aCallback); + + nsresult rv = VisitedQuery::Start(aURI, aCallback); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIObserver + +NS_IMETHODIMP +History::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) +{ + if (strcmp(aTopic, TOPIC_PLACES_SHUTDOWN) == 0) { + Shutdown(); + + nsCOMPtr os = mozilla::services::GetObserverService(); + if (os) { + (void)os->RemoveObserver(this, TOPIC_PLACES_SHUTDOWN); + } + } + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsISupports + +NS_IMPL_ISUPPORTS( + History +, IHistory +, nsIDownloadHistory +, mozIAsyncHistory +, nsIObserver +, nsIMemoryReporter +) + +} // namespace places +} // namespace mozilla -- cgit v1.2.3