diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /toolkit/components/places/History.cpp | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/places/History.cpp')
-rw-r--r-- | toolkit/components/places/History.cpp | 2349 |
1 files changed, 2349 insertions, 0 deletions
diff --git a/toolkit/components/places/History.cpp b/toolkit/components/places/History.cpp new file mode 100644 index 0000000000..eeb11402b9 --- /dev/null +++ b/toolkit/components/places/History.cpp @@ -0,0 +1,2349 @@ +/* -*- 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/Attributes.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/MemoryReporting.h" + +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/BrowserChild.h" +#include "nsXULAppAPI.h" + +#include "History.h" +#include "nsNavHistory.h" +#include "nsNavBookmarks.h" +#include "Helpers.h" +#include "PlaceInfo.h" +#include "VisitInfo.h" +#include "nsPlacesMacros.h" +#include "NotifyRankingChanged.h" + +#include "mozilla/storage.h" +#include "mozilla/dom/Link.h" +#include "nsDocShellCID.h" +#include "mozilla/Components.h" +#include "nsThreadUtils.h" +#include "nsNetUtil.h" +#include "nsIWidget.h" +#include "nsIXPConnect.h" +#include "nsIXULRuntime.h" +#include "mozilla/Unused.h" +#include "nsContentUtils.h" // for nsAutoScriptBlocker +#include "nsJSUtils.h" +#include "nsStandardURL.h" +#include "mozilla/ipc/URIUtils.h" +#include "nsPrintfCString.h" +#include "nsTHashtable.h" +#include "jsapi.h" +#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject, JS::NewArrayObject +#include "js/PropertyAndElement.h" // JS_DefineElement, JS_GetElement, JS_GetProperty +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/dom/ContentProcessMessageManager.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/PlacesObservers.h" +#include "mozilla/dom/PlacesVisit.h" +#include "mozilla/dom/PlacesVisitTitle.h" +#include "mozilla/dom/ScriptSettings.h" + +#include "nsIBrowserWindowTracker.h" +#include "nsImportModule.h" +#include "mozilla/StaticPrefs_browser.h" + +using namespace mozilla::dom; +using namespace mozilla::ipc; +using mozilla::Unused; + +namespace mozilla { +namespace places { + +//////////////////////////////////////////////////////////////////////////////// +//// Global Defines + +// Observer event fired after a visit has been registered in the DB. +#define URI_VISIT_SAVED "uri-visit-saved" + +#define DESTINATIONFILEURI_ANNO "downloads/destinationFileURI"_ns + +//////////////////////////////////////////////////////////////////////////////// +//// 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), + useFrecencyRedirectBonus(false), + source(nsINavHistoryService::VISIT_SOURCE_ORGANIC), + triggeringPlaceId(0), + triggeringSponsoredURLVisitTimeMS(0), + bookmarked(false) { + guid.SetIsVoid(true); + title.SetIsVoid(true); + baseDomain.SetIsVoid(true); + triggeringSearchEngine.SetIsVoid(true); + triggeringSponsoredURL.SetIsVoid(true); + triggeringSponsoredURLBaseDomain.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), + useFrecencyRedirectBonus(false), + source(nsINavHistoryService::VISIT_SOURCE_ORGANIC), + triggeringPlaceId(0), + triggeringSponsoredURLVisitTimeMS(0), + bookmarked(false) { + 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); + baseDomain.SetIsVoid(true); + triggeringSearchEngine.SetIsVoid(true); + triggeringSponsoredURL.SetIsVoid(true); + triggeringSponsoredURLBaseDomain.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; + nsCString baseDomain; + 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; + + // Whether to override the visit type bonus with a redirect bonus when + // calculating frecency on the most recent visit. + bool useFrecencyRedirectBonus; + + uint16_t source; + nsCString triggeringSearchEngine; + int64_t triggeringPlaceId; + nsCString triggeringSponsoredURL; + nsCString triggeringSponsoredURLBaseDomain; + int64_t triggeringSponsoredURLVisitTimeMS; + bool bookmarked; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// 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<JS::Value> aValue, JSContext* aCtx, + JS::MutableHandle<JSObject*> _array, + uint32_t* _arrayLength) { + if (aValue.isObjectOrNull()) { + JS::Rooted<JSObject*> 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<nsIURI> GetJSValueAsURI(JSContext* aCtx, + const JS::Value& aValue) { + if (!aValue.isPrimitive()) { + nsCOMPtr<nsIXPConnect> xpc = nsIXPConnect::XPConnect(); + + nsCOMPtr<nsIXPConnectWrappedNative> wrappedObj; + JS::Rooted<JSObject*> obj(aCtx, aValue.toObjectOrNull()); + nsresult rv = + xpc->GetWrappedNativeOfJSObject(aCtx, obj, getter_AddRefs(wrappedObj)); + NS_ENSURE_SUCCESS(rv, nullptr); + nsCOMPtr<nsIURI> uri = do_QueryInterface(wrappedObj->Native()); + 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<nsIURI> GetURIFromJSObject(JSContext* aCtx, + JS::Handle<JSObject*> aObject, + const char* aProperty) { + JS::Rooted<JS::Value> 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<JSObject*> aObject, + const char* aProperty, nsString& _string) { + JS::Rooted<JS::Value> 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 <typename IntType> +nsresult GetIntFromJSObject(JSContext* aCtx, JS::Handle<JSObject*> aObject, + const char* aProperty, IntType* _int) { + JS::Rooted<JS::Value> 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<JSObject*> aArray, + uint32_t aIndex, + JS::MutableHandle<JSObject*> objOut) { + JS::Rooted<JS::Value> 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; +} + +} // namespace + +class VisitedQuery final : public AsyncStatementCallback { + public: + NS_DECL_ISUPPORTS_INHERITED + + static nsresult Start(nsIURI* aURI, + History::ContentParentSet&& aContentProcessesToNotify) { + MOZ_ASSERT(aURI, "Null URI"); + MOZ_ASSERT(XRE_IsParentProcess()); + + History* history = History::GetService(); + NS_ENSURE_STATE(history); + RefPtr<VisitedQuery> query = + new VisitedQuery(aURI, std::move(aContentProcessesToNotify)); + return history->QueueVisitedStatement(std::move(query)); + } + + static nsresult Start(nsIURI* aURI, + mozIVisitedStatusCallback* aCallback = nullptr) { + MOZ_ASSERT(aURI, "Null URI"); + MOZ_ASSERT(XRE_IsParentProcess()); + + nsMainThreadPtrHandle<mozIVisitedStatusCallback> callback( + new nsMainThreadPtrHolder<mozIVisitedStatusCallback>( + "mozIVisitedStatusCallback", aCallback)); + + History* history = History::GetService(); + NS_ENSURE_STATE(history); + RefPtr<VisitedQuery> query = new VisitedQuery(aURI, callback); + return history->QueueVisitedStatement(std::move(query)); + } + + void Execute(mozIStorageAsyncStatement& aStatement) { + // Bind by index for performance. + nsresult rv = URIBinder::Bind(&aStatement, 0, mURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsCOMPtr<mozIStoragePendingStatement> handle; + rv = aStatement.ExecuteAsync(this, getter_AddRefs(handle)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + Unused << rv; + } + + 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) { + NotifyVisitedStatus(); + } + return NS_OK; + } + + void NotifyVisitedStatus() { + // If an external handling callback is provided, just notify through it. + if (mCallback) { + mCallback->IsVisited(mURI, mIsVisited); + return; + } + + if (History* history = History::GetService()) { + auto status = mIsVisited ? IHistory::VisitedStatus::Visited + : IHistory::VisitedStatus::Unvisited; + history->NotifyVisited(mURI, status, &mContentProcessesToNotify); + } + } + + private: + explicit VisitedQuery( + nsIURI* aURI, + const nsMainThreadPtrHandle<mozIVisitedStatusCallback>& aCallback) + : mURI(aURI), mCallback(aCallback) {} + + explicit VisitedQuery(nsIURI* aURI, + History::ContentParentSet&& aContentProcessesToNotify) + : mURI(aURI), + mContentProcessesToNotify(std::move(aContentProcessesToNotify)) {} + + ~VisitedQuery() = default; + + nsCOMPtr<nsIURI> mURI; + nsMainThreadPtrHandle<mozIVisitedStatusCallback> mCallback; + History::ContentParentSet mContentProcessesToNotify; + bool mIsVisited = false; +}; + +NS_IMPL_ISUPPORTS_INHERITED0(VisitedQuery, AsyncStatementCallback) + +/** + * Notifies observers about a visit or an array of visits. + */ +class NotifyManyVisitsObservers : public Runnable { + public: + explicit NotifyManyVisitsObservers(const VisitData& aPlace) + : Runnable("places::NotifyManyVisitsObservers"), + mPlaces({aPlace}), + mHistory(History::GetService()) {} + + explicit NotifyManyVisitsObservers(nsTArray<VisitData>&& aPlaces) + : Runnable("places::NotifyManyVisitsObservers"), + mPlaces(std::move(aPlaces)), + mHistory(History::GetService()) {} + + nsresult NotifyVisit(nsNavHistory* aNavHistory, + nsCOMPtr<nsIObserverService>& aObsService, PRTime aNow, + nsIURI* aURI, const VisitData& aPlace) { + if (aObsService) { + DebugOnly<nsresult> rv = + aObsService->NotifyObservers(aURI, URI_VISIT_SAVED, nullptr); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Could not notify observers"); + } + + if (aNow - aPlace.visitTime < RECENTLY_VISITED_URIS_MAX_AGE) { + mHistory->AppendToRecentlyVisitedURIs(aURI, aPlace.hidden); + } + mHistory->NotifyVisited(aURI, IHistory::VisitedStatus::Visited); + + aNavHistory->UpdateDaysOfHistory(aPlace.visitTime); + + return NS_OK; + } + + void AddPlaceForNotify(const VisitData& aPlace, + Sequence<OwningNonNull<PlacesEvent>>& aEvents) { + if (aPlace.transitionType == nsINavHistoryService::TRANSITION_EMBED) { + return; + } + + RefPtr<PlacesVisit> visitEvent = new PlacesVisit(); + visitEvent->mVisitId = aPlace.visitId; + visitEvent->mUrl.Assign(NS_ConvertUTF8toUTF16(aPlace.spec)); + visitEvent->mVisitTime = aPlace.visitTime / 1000; + visitEvent->mReferringVisitId = aPlace.referrerVisitId; + visitEvent->mTransitionType = aPlace.transitionType; + visitEvent->mPageGuid.Assign(aPlace.guid); + visitEvent->mHidden = aPlace.hidden; + visitEvent->mVisitCount = aPlace.visitCount + 1; // Add current visit + visitEvent->mTypedCount = static_cast<uint32_t>(aPlace.typed); + visitEvent->mLastKnownTitle.Assign(aPlace.title); + bool success = !!aEvents.AppendElement(visitEvent.forget(), fallible); + MOZ_RELEASE_ASSERT(success); + + if (aPlace.titleChanged) { + RefPtr<PlacesVisitTitle> titleEvent = new PlacesVisitTitle(); + titleEvent->mUrl.Assign(NS_ConvertUTF8toUTF16(aPlace.spec)); + titleEvent->mPageGuid.Assign(aPlace.guid); + titleEvent->mTitle.Assign(aPlace.title); + bool success = !!aEvents.AppendElement(titleEvent.forget(), fallible); + MOZ_RELEASE_ASSERT(success); + } + } + + // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is marked + // MOZ_CAN_RUN_SCRIPT. See bug 1535398. + MOZ_CAN_RUN_SCRIPT_BOUNDARY + 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 visits observers but cannot get the history " + "service!"); + return NS_OK; + } + + nsCOMPtr<nsIObserverService> obsService = + mozilla::services::GetObserverService(); + + Sequence<OwningNonNull<PlacesEvent>> events; + PRTime now = PR_Now(); + for (uint32_t i = 0; i < mPlaces.Length(); ++i) { + nsCOMPtr<nsIURI> uri; + MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), mPlaces[i].spec)); + if (!uri) { + return NS_ERROR_UNEXPECTED; + } + AddPlaceForNotify(mPlaces[i], events); + + nsresult rv = NotifyVisit(navHistory, obsService, now, uri, mPlaces[i]); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (events.Length() > 0) { + PlacesObservers::NotifyListeners(events); + } + + return NS_OK; + } + + private: + AutoTArray<VisitData, 1> mPlaces; + RefPtr<History> 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) + : Runnable("places::NotifyTitleObservers"), + mSpec(aSpec), + mTitle(aTitle), + mGUID(aGUID) {} + + // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is marked + // MOZ_CAN_RUN_SCRIPT. See bug 1535398. + MOZ_CAN_RUN_SCRIPT_BOUNDARY + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); + + RefPtr<PlacesVisitTitle> titleEvent = new PlacesVisitTitle(); + titleEvent->mUrl.Assign(NS_ConvertUTF8toUTF16(mSpec)); + titleEvent->mPageGuid.Assign(mGUID); + titleEvent->mTitle.Assign(mTitle); + + Sequence<OwningNonNull<PlacesEvent>> events; + bool success = !!events.AppendElement(titleEvent.forget(), fallible); + MOZ_RELEASE_ASSERT(success); + + PlacesObservers::NotifyListeners(events); + + 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<mozIVisitInfoCallback>& aCallback, + const VisitData& aPlace, bool aIsSingleVisit, nsresult aResult) + : Runnable("places::NotifyPlaceInfoCallback"), + 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<nsIURI> referrerURI; + if (!mPlace.referrerSpec.IsEmpty()) { + MOZ_ALWAYS_SUCCEEDS( + NS_NewURI(getter_AddRefs(referrerURI), mPlace.referrerSpec)); + hasValidURIs = !!referrerURI; + } + + nsCOMPtr<nsIURI> uri; + MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), mPlace.spec)); + hasValidURIs = hasValidURIs && !!uri; + + nsCOMPtr<mozIPlaceInfo> place; + if (mIsSingleVisit) { + nsCOMPtr<mozIVisitInfo> 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<mozIVisitInfoCallback> 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<mozIVisitInfoCallback>& aCallback, + uint32_t aUpdatedCount = 0) + : Runnable("places::NotifyCompletion"), + mCallback(aCallback), + mUpdatedCount(aUpdatedCount) { + MOZ_ASSERT(aCallback, "Must pass a non-null callback!"); + } + + NS_IMETHOD Run() override { + if (NS_IsMainThread()) { + (void)mCallback->HandleCompletion(mUpdatedCount); + } else { + (void)NS_DispatchToMainThread(this); + } + return NS_OK; + } + + private: + nsMainThreadPtrHandle<mozIVisitInfoCallback> mCallback; + uint32_t mUpdatedCount; +}; + +/** + * 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 = ""_ns, + 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<mozIVisitInfoCallback> callback( + new nsMainThreadPtrHolder<mozIVisitInfoCallback>( + "mozIVisitInfoCallback", aCallback)); + nsCOMPtr<nsIRunnable> 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<VisitData>&& aPlaces, + mozIVisitInfoCallback* aCallback = nullptr, + uint32_t aInitialUpdatedCount = 0) { + 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<mozIVisitInfoCallback> callback( + new nsMainThreadPtrHolder<mozIVisitInfoCallback>( + "mozIVisitInfoCallback", aCallback)); + bool ignoreErrors = false, ignoreResults = false; + if (aCallback) { + // We ignore errors from either of these methods in case old JS consumers + // don't implement them (in which case they will get error/result + // notifications as normal). + Unused << aCallback->GetIgnoreErrors(&ignoreErrors); + Unused << aCallback->GetIgnoreResults(&ignoreResults); + } + RefPtr<InsertVisitedURIs> event = new InsertVisitedURIs( + aConnection, std::move(aPlaces), callback, ignoreErrors, ignoreResults, + aInitialUpdatedCount); + + // Get the target thread, and then start the work! + nsCOMPtr<nsIEventTarget> 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"); + + // The inner run method may bail out at any point, so we ensure we do + // whatever we can and then notify the main thread we're done. + nsresult rv = InnerRun(); + + if (mSuccessfulUpdatedCount > 0) { + NS_DispatchToMainThread(new NotifyRankingChanged()); + } + if (!!mCallback) { + NS_DispatchToMainThread( + new NotifyCompletion(mCallback, mSuccessfulUpdatedCount)); + } + return rv; + } + + nsresult InnerRun() { + MOZ_ASSERT(!NS_IsMainThread()); + // Prevent Shutdown() from proceeding while this is running. + MutexAutoLock lockedScope(mHistory->mBlockShutdownMutex); + // Check if we were already shutting down. + if (mHistory->IsShuttingDown()) { + return NS_OK; + } + + mozStorageTransaction transaction( + mDBConn, false, mozIStorageConnection::TRANSACTION_IMMEDIATE); + + // XXX Handle the error, bug 1696133. + Unused << NS_WARN_IF(NS_FAILED(transaction.Start())); + + const VisitData* lastFetchedPlace = nullptr; + uint32_t lastFetchedVisitCount = 0; + bool shouldChunkNotifications = mPlaces.Length() > NOTIFY_VISITS_CHUNK_SIZE; + nsTArray<VisitData> notificationChunk; + if (shouldChunkNotifications) { + notificationChunk.SetCapacity(NOTIFY_VISITS_CHUNK_SIZE); + } + for (nsTArray<VisitData>::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 && !mIgnoreErrors) { + nsCOMPtr<nsIRunnable> event = + new NotifyPlaceInfoCallback(mCallback, place, true, rv); + return NS_DispatchToMainThread(event); + } + return NS_OK; + } + lastFetchedPlace = &mPlaces.ElementAt(i); + lastFetchedVisitCount = lastFetchedPlace->visitCount; + } 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; + if (!place.title.IsVoid()) { + place.titleChanged = !lastFetchedPlace->title.Equals(place.title); + } + place.frecency = lastFetchedPlace->frecency; + // Add one visit for the previous loop. + place.visitCount = ++lastFetchedVisitCount; + } + + // 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); + UpdateVisitSource(place, mHistory); + + nsresult rv = DoDatabaseInserts(known, place); + if (!!mCallback) { + // Check if consumers wanted to be notified about success/failure, + // depending on whether this action succeeded or not. + if ((NS_SUCCEEDED(rv) && !mIgnoreResults) || + (NS_FAILED(rv) && !mIgnoreErrors)) { + nsCOMPtr<nsIRunnable> event = + new NotifyPlaceInfoCallback(mCallback, place, true, rv); + nsresult rv2 = NS_DispatchToMainThread(event); + NS_ENSURE_SUCCESS(rv2, rv2); + } + } + NS_ENSURE_SUCCESS(rv, rv); + + if (shouldChunkNotifications) { + int32_t numRemaining = mPlaces.Length() - (i + 1); + notificationChunk.AppendElement(place); + if (notificationChunk.Length() == NOTIFY_VISITS_CHUNK_SIZE || + numRemaining == 0) { + nsCOMPtr<nsIRunnable> event = + new NotifyManyVisitsObservers(std::move(notificationChunk)); + rv = NS_DispatchToMainThread(event); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t nextCapacity = + std::min(NOTIFY_VISITS_CHUNK_SIZE, numRemaining); + notificationChunk.SetCapacity(nextCapacity); + } + } + + // If we get here, we must have been successful adding/updating this + // visit/place, so update the count: + mSuccessfulUpdatedCount++; + } + + { + // Trigger insertions for all the new origins of the places we inserted. + nsAutoCString query("DELETE FROM moz_updateoriginsinsert_temp"); + nsCOMPtr<mozIStorageStatement> stmt = mHistory->GetStatement(query); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + nsresult rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + } + + { + // Trigger frecency updates for all those origins. + nsAutoCString query("DELETE FROM moz_updateoriginsupdate_temp"); + nsCOMPtr<mozIStorageStatement> stmt = mHistory->GetStatement(query); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + nsresult rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsresult rv = transaction.Commit(); + NS_ENSURE_SUCCESS(rv, rv); + + // If we don't need to chunk the notifications, just notify using the + // original mPlaces array. + if (!shouldChunkNotifications) { + nsCOMPtr<nsIRunnable> event = + new NotifyManyVisitsObservers(std::move(mPlaces)); + rv = NS_DispatchToMainThread(event); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; + } + + private: + InsertVisitedURIs( + mozIStorageConnection* aConnection, nsTArray<VisitData>&& aPlaces, + const nsMainThreadPtrHandle<mozIVisitInfoCallback>& aCallback, + bool aIgnoreErrors, bool aIgnoreResults, uint32_t aInitialUpdatedCount) + : Runnable("places::InsertVisitedURIs"), + mDBConn(aConnection), + mPlaces(std::move(aPlaces)), + mCallback(aCallback), + mIgnoreErrors(aIgnoreErrors), + mIgnoreResults(aIgnoreResults), + mSuccessfulUpdatedCount(aInitialUpdatedCount), + mHistory(History::GetService()) { + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); + +#ifdef DEBUG + for (nsTArray<VisitData>::size_type i = 0; i < mPlaces.Length(); i++) { + nsCOMPtr<nsIURI> 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<mozIStorageStatement> stmt; + stmt = mHistory->GetStatement( + "INSERT INTO moz_historyvisits " + "(from_visit, place_id, visit_date, visit_type, session, source, " + "triggeringPlaceId) " + "VALUES (:from_visit, :page_id, :visit_date, :visit_type, 0, :source, " + ":triggeringPlaceId) "); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + rv = stmt->BindInt64ByName("page_id"_ns, _place.placeId); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt64ByName("from_visit"_ns, _place.referrerVisitId); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt64ByName("visit_date"_ns, _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("visit_type"_ns, transitionType); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName("source"_ns, _place.source); + NS_ENSURE_SUCCESS(rv, rv); + if (_place.triggeringPlaceId != 0) { + rv = stmt->BindInt64ByName("triggeringPlaceId"_ns, + _place.triggeringPlaceId); + } else { + rv = stmt->BindNullByName("triggeringPlaceId"_ns); + } + 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<mozIStorageStatement> stmt = mHistory->GetStatement( + "UPDATE moz_places " + "SET frecency = CALCULATE_FRECENCY(:page_id, :redirect) " + "WHERE id = :page_id"); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + rv = stmt->BindInt64ByName("page_id"_ns, aPlace.placeId); + NS_ENSURE_SUCCESS(rv, rv); + rv = + stmt->BindInt32ByName("redirect"_ns, aPlace.useFrecencyRedirectBonus); + 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<mozIStorageStatement> 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("page_id"_ns, aPlace.placeId); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; + } + + nsresult UpdateVisitSource(VisitData& aPlace, History* aHistory) { + if (aPlace.bookmarked) { + aPlace.source = nsINavHistoryService::VISIT_SOURCE_BOOKMARKED; + } else if (!aPlace.triggeringSearchEngine.IsEmpty()) { + aPlace.source = nsINavHistoryService::VISIT_SOURCE_SEARCHED; + } else { + aPlace.source = nsINavHistoryService::VISIT_SOURCE_ORGANIC; + } + + if (aPlace.triggeringSponsoredURL.IsEmpty()) { + // No triggeringSponsoredURL. + return NS_OK; + } + + if ((aPlace.visitTime - + aPlace.triggeringSponsoredURLVisitTimeMS * PR_USEC_PER_MSEC) > + StaticPrefs::browser_places_sponsoredSession_timeoutSecs() * + PR_USEC_PER_SEC) { + // Sponsored session timeout. + return NS_OK; + } + + if (aPlace.spec.Equals(aPlace.triggeringSponsoredURL)) { + // This place is the triggeringSponsoredURL. + aPlace.source = nsINavHistoryService::VISIT_SOURCE_SPONSORED; + return NS_OK; + } + + if (!aPlace.baseDomain.Equals(aPlace.triggeringSponsoredURLBaseDomain)) { + // The base domain is not same. + return NS_OK; + } + + nsCOMPtr<mozIStorageStatement> stmt; + stmt = aHistory->GetStatement( + "SELECT id FROM moz_places h " + "WHERE url_hash = hash(:url) AND url = :url"); + NS_ENSURE_STATE(stmt); + nsresult rv = + URIBinder::Bind(stmt, "url"_ns, aPlace.triggeringSponsoredURL); + NS_ENSURE_SUCCESS(rv, rv); + + mozStorageStatementScoper scoper(stmt); + + bool exists; + rv = stmt->ExecuteStep(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (exists) { + rv = stmt->GetInt64(0, &aPlace.triggeringPlaceId); + NS_ENSURE_SUCCESS(rv, rv); + } else { + Telemetry::ScalarAdd( + Telemetry::ScalarID::PLACES_SPONSORED_VISIT_NO_TRIGGERING_URL, 1); + } + + aPlace.source = nsINavHistoryService::VISIT_SOURCE_SPONSORED; + + return NS_OK; + } + + mozIStorageConnection* mDBConn; + + nsTArray<VisitData> mPlaces; + + nsMainThreadPtrHandle<mozIVisitInfoCallback> mCallback; + + bool mIgnoreErrors; + + bool mIgnoreResults; + + uint32_t mSuccessfulUpdatedCount; + + /** + * Strong reference to the History object because we do not want it to + * disappear out from under us. + */ + RefPtr<History> 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<SetPageTitle> event = new SetPageTitle(spec, aTitle); + + // Get the target thread, and then start the work! + nsCOMPtr<nsIEventTarget> 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<mozIStorageStatement> stmt = mHistory->GetStatement( + "UPDATE moz_places " + "SET title = :page_title " + "WHERE id = :page_id "); + NS_ENSURE_STATE(stmt); + + { + mozStorageStatementScoper scoper(stmt); + rv = stmt->BindInt64ByName("page_id"_ns, mPlace.placeId); + NS_ENSURE_SUCCESS(rv, rv); + // Empty strings should clear the title, just like + // nsNavHistory::SetPageTitle. + if (mPlace.title.IsEmpty()) { + rv = stmt->BindNullByName("page_title"_ns); + } else { + rv = stmt->BindStringByName("page_title"_ns, + StringHead(mPlace.title, TITLE_LENGTH_MAX)); + } + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIRunnable> 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) + : Runnable("places::SetPageTitle"), 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<History> 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. + * + * FIXME(emilio, bug 1595484): We should get rid of EMBED visits completely. + */ +void NotifyEmbedVisit(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<nsIURI> uri; + MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), aPlace.spec)); + + if (!uri) { + return; + } + + if (!!aCallback) { + nsMainThreadPtrHandle<mozIVisitInfoCallback> callback( + new nsMainThreadPtrHolder<mozIVisitInfoCallback>( + "mozIVisitInfoCallback", aCallback)); + bool ignoreResults = false; + Unused << aCallback->GetIgnoreResults(&ignoreResults); + if (!ignoreResults) { + nsCOMPtr<nsIRunnable> event = + new NotifyPlaceInfoCallback(callback, aPlace, true, NS_OK); + (void)NS_DispatchToMainThread(event); + } + } + + nsCOMPtr<nsIRunnable> event = new NotifyManyVisitsObservers(aPlace); + (void)NS_DispatchToMainThread(event); +} + +//////////////////////////////////////////////////////////////////////////////// +//// History + +History* History::gService = nullptr; + +History::History() + : mShuttingDown(false), + mShuttingDownMutex("History::mShuttingDownMutex"), + mBlockShutdownMutex("History::mBlockShutdownMutex"), + mRecentlyVisitedURIs(RECENTLY_VISITED_URIS_SIZE) { + NS_ASSERTION(!gService, "Ruh-roh! This service has already been created!"); + if (XRE_IsParentProcess()) { + nsCOMPtr<nsIProperties> dirsvc = components::Directory::Service(); + bool haveProfile = false; + MOZ_RELEASE_ASSERT( + dirsvc && + NS_SUCCEEDED( + dirsvc->Has(NS_APP_USER_PROFILE_50_DIR, &haveProfile)) && + haveProfile, + "Can't construct history service if there is no profile."); + } + gService = this; + + nsCOMPtr<nsIObserverService> 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); + + MOZ_ASSERT(gService == this); + gService = nullptr; +} + +void History::InitMemoryReporter() { RegisterWeakMemoryReporter(this); } + +class ConcurrentStatementsHolder final : public mozIStorageCompletionCallback { + public: + NS_DECL_ISUPPORTS + + ConcurrentStatementsHolder() : mShutdownWasInvoked(false) {} + + static RefPtr<ConcurrentStatementsHolder> Create( + mozIStorageConnection* aDBConn) { + RefPtr<ConcurrentStatementsHolder> holder = + new ConcurrentStatementsHolder(); + nsresult rv = aDBConn->AsyncClone(true, holder); + if (NS_FAILED(rv)) { + return nullptr; + } + return holder; + } + + NS_IMETHOD Complete(nsresult aStatus, nsISupports* aConnection) override { + if (NS_FAILED(aStatus)) { + return NS_OK; + } + mReadOnlyDBConn = do_QueryInterface(aConnection); + // It's possible Shutdown was invoked before we were handed back the + // cloned connection handle. + if (mShutdownWasInvoked) { + Shutdown(); + return NS_OK; + } + + // Now we can create our cached statements. + + if (!mIsVisitedStatement) { + (void)mReadOnlyDBConn->CreateAsyncStatement( + nsLiteralCString("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); + auto queries = std::move(mVisitedQueries); + if (mIsVisitedStatement) { + for (auto& query : queries) { + query->Execute(*mIsVisitedStatement); + } + } + } + + return NS_OK; + } + + void QueueVisitedStatement(RefPtr<VisitedQuery> aCallback) { + if (mIsVisitedStatement) { + aCallback->Execute(*mIsVisitedStatement); + } else { + mVisitedQueries.AppendElement(std::move(aCallback)); + } + } + + void Shutdown() { + mShutdownWasInvoked = true; + if (mReadOnlyDBConn) { + mVisitedQueries.Clear(); + DebugOnly<nsresult> rv; + if (mIsVisitedStatement) { + rv = mIsVisitedStatement->Finalize(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + rv = mReadOnlyDBConn->AsyncClose(nullptr); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + mReadOnlyDBConn = nullptr; + } + } + + private: + ~ConcurrentStatementsHolder() = default; + + nsCOMPtr<mozIStorageAsyncConnection> mReadOnlyDBConn; + nsCOMPtr<mozIStorageAsyncStatement> mIsVisitedStatement; + nsTArray<RefPtr<VisitedQuery>> mVisitedQueries; + bool mShutdownWasInvoked; +}; + +NS_IMPL_ISUPPORTS(ConcurrentStatementsHolder, mozIStorageCompletionCallback) + +nsresult History::QueueVisitedStatement(RefPtr<VisitedQuery> aQuery) { + MOZ_ASSERT(NS_IsMainThread()); + if (IsShuttingDown()) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (!mConcurrentStatementsHolder) { + mozIStorageConnection* dbConn = GetDBConn(); + NS_ENSURE_STATE(dbConn); + mConcurrentStatementsHolder = ConcurrentStatementsHolder::Create(dbConn); + if (!mConcurrentStatementsHolder) { + return NS_ERROR_NOT_AVAILABLE; + } + } + mConcurrentStatementsHolder->QueueVisitedStatement(std::move(aQuery)); + 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<mozIStorageStatement> 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("rev_host"_ns, aPlace.revHost); + NS_ENSURE_SUCCESS(rv, rv); + rv = URIBinder::Bind(stmt, "url"_ns, 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("title"_ns); + } else { + title.Assign(StringHead(aPlace.title, TITLE_LENGTH_MAX)); + rv = stmt->BindStringByName("title"_ns, title); + } + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName("typed"_ns, 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("frecency"_ns, frecency); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName("hidden"_ns, aPlace.hidden); + NS_ENSURE_SUCCESS(rv, rv); + if (aPlace.guid.IsVoid()) { + rv = GenerateGUID(aPlace.guid); + NS_ENSURE_SUCCESS(rv, rv); + } + rv = stmt->BindUTF8StringByName("guid"_ns, aPlace.guid); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + 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<mozIStorageStatement> stmt; + bool titleIsVoid = aPlace.title.IsVoid(); + if (titleIsVoid) { + // Don't change the title. + stmt = GetStatement( + "UPDATE moz_places " + "SET hidden = :hidden, " + "typed = :typed, " + "guid = :guid " + "WHERE id = :page_id "); + } else { + 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; + if (!titleIsVoid) { + // An empty string clears the title. + if (aPlace.title.IsEmpty()) { + rv = stmt->BindNullByName("title"_ns); + } else { + rv = stmt->BindStringByName("title"_ns, + StringHead(aPlace.title, TITLE_LENGTH_MAX)); + } + NS_ENSURE_SUCCESS(rv, rv); + } + rv = stmt->BindInt32ByName("typed"_ns, aPlace.typed); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName("hidden"_ns, aPlace.hidden); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindUTF8StringByName("guid"_ns, aPlace.guid); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt64ByName("page_id"_ns, 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<mozIStorageStatement> 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, " + "EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) AS bookmarked " + "FROM moz_places h " + "WHERE url_hash = hash(:page_url) AND url = :page_url "); + NS_ENSURE_STATE(stmt); + + rv = URIBinder::Bind(stmt, "page_url"_ns, _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, " + "EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) AS bookmarked " + "FROM moz_places h " + "WHERE guid = :guid "); + NS_ENSURE_STATE(stmt); + + rv = stmt->BindUTF8StringByName("guid"_ns, _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); + int32_t bookmarked; + rv = stmt->GetInt32(9, &bookmarked); + NS_ENSURE_SUCCESS(rv, rv); + _place.bookmarked = bookmarked == 1; + + 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(MallocSizeOf aMallocSizeOf) { + size_t size = aMallocSizeOf(this); + size += mTrackedURIs.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (const auto& entry : mTrackedURIs.Values()) { + size += entry.SizeOfExcludingThis(aMallocSizeOf); + } + return size; +} + +/* static */ +History* History::GetService() { + if (gService) { + return gService; + } + + nsCOMPtr<IHistory> service = components::History::Service(); + if (service) { + NS_ASSERTION(gService, "Our constructor was not run?!"); + } + + return gService; +} + +/* static */ +already_AddRefed<History> History::GetSingleton() { + if (!gService) { + RefPtr<History> svc = new History(); + MOZ_ASSERT(gService == svc.get()); + svc->InitMemoryReporter(); + return svc.forget(); + } + + return do_AddRef(gService); +} + +mozIStorageConnection* History::GetDBConn() { + MOZ_ASSERT(NS_IsMainThread()); + if (IsShuttingDown()) { + return nullptr; + } + if (!mDB) { + mDB = Database::GetDatabase(); + NS_ENSURE_TRUE(mDB, nullptr); + // This must happen on the main-thread, so when we try to use the connection + // later it's initialized. + mDB->EnsureConnection(); + NS_ENSURE_TRUE(mDB, nullptr); + } + return mDB->MainConn(); +} + +const mozIStorageConnection* History::GetConstDBConn() { + MOZ_ASSERT(!NS_IsMainThread()); + { + MOZ_ASSERT(mDB || IsShuttingDown()); + if (IsShuttingDown() || !mDB) { + return nullptr; + } + } + return mDB->MainConn(); +} + +void History::Shutdown() { + MOZ_ASSERT(NS_IsMainThread()); + MutexAutoLock lockedScope(mBlockShutdownMutex); + { + MutexAutoLock lockedScope(mShuttingDownMutex); + MOZ_ASSERT(!mShuttingDown && "Shutdown was called more than once!"); + mShuttingDown = true; + } + if (mConcurrentStatementsHolder) { + mConcurrentStatementsHolder->Shutdown(); + } +} + +void History::AppendToRecentlyVisitedURIs(nsIURI* aURI, bool aHidden) { + PRTime now = PR_Now(); + + mRecentlyVisitedURIs.InsertOrUpdate(aURI, RecentURIVisit{now, aHidden}); + + // Remove entries older than RECENTLY_VISITED_URIS_MAX_AGE. + for (auto iter = mRecentlyVisitedURIs.Iter(); !iter.Done(); iter.Next()) { + if ((now - iter.Data().mTime) > RECENTLY_VISITED_URIS_MAX_AGE) { + iter.Remove(); + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// IHistory + +NS_IMETHODIMP +History::VisitURI(nsIWidget* aWidget, nsIURI* aURI, nsIURI* aLastVisitedURI, + uint32_t aFlags, uint64_t aBrowserId) { + MOZ_ASSERT(NS_IsMainThread()); + NS_ENSURE_ARG(aURI); + + if (IsShuttingDown()) { + return NS_OK; + } + + if (XRE_IsContentProcess()) { + if (!BaseHistory::CanStore(aURI)) { + return NS_OK; + } + + NS_ENSURE_ARG(aWidget); + BrowserChild* browserChild = aWidget->GetOwningBrowserChild(); + NS_ENSURE_TRUE(browserChild, NS_ERROR_FAILURE); + (void)browserChild->SendVisitURI(aURI, aLastVisitedURI, aFlags, aBrowserId); + 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; + } + + bool reload = false; + if (aLastVisitedURI) { + rv = aURI->Equals(aLastVisitedURI, &reload); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsTArray<VisitData> placeArray(1); + placeArray.AppendElement(VisitData(aURI, aLastVisitedURI)); + 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); + bool isRedirect = aFlags & IHistory::REDIRECT_SOURCE; + if (isRedirect) { + place.useFrecencyRedirectBonus = + (aFlags & IHistory::REDIRECT_SOURCE_PERMANENT) || + transitionType != nsINavHistoryService::TRANSITION_TYPED; + } + place.hidden = GetHiddenState(isRedirect, place.transitionType); + + // Error pages should never be autocompleted. + if (aFlags & IHistory::UNRECOVERABLE_ERROR) { + place.shouldUpdateFrecency = false; + } + + // Do not save a reloaded uri if we have visited the same URI recently. + if (reload) { + auto entry = mRecentlyVisitedURIs.Lookup(aURI); + // Check if the entry exists and is younger than + // RECENTLY_VISITED_URIS_MAX_AGE. + if (entry && (PR_Now() - entry->mTime) < RECENTLY_VISITED_URIS_MAX_AGE) { + bool wasHidden = entry->mHidden; + // Regardless of whether we store the visit or not, we must update the + // stored visit time. + AppendToRecentlyVisitedURIs(aURI, place.hidden); + // We always want to store an unhidden visit, if the previous visits were + // hidden, because otherwise the page may not appear in the history UI. + // This can happen for example at a page redirecting to itself. + if (!wasHidden || place.hidden) { + // We can skip this visit. + return NS_OK; + } + } + } + + nsCOMPtr<nsIBrowserWindowTracker> bwt = + do_ImportModule("resource:///modules/BrowserWindowTracker.jsm", + "BrowserWindowTracker", &rv); + if (NS_SUCCEEDED(rv)) { + // Only if it is running on Firefox, continue to process the followings. + nsCOMPtr<nsISupports> browser; + rv = bwt->GetBrowserById(aBrowserId, getter_AddRefs(browser)); + NS_ENSURE_SUCCESS(rv, rv); + if (browser) { + RefPtr<Element> browserElement = static_cast<Element*>(browser.get()); + + nsAutoString triggeringSearchEngineURL; + browserElement->GetAttribute(u"triggeringSearchEngineURL"_ns, + triggeringSearchEngineURL); + if (!triggeringSearchEngineURL.IsEmpty() && + place.spec.Equals(NS_ConvertUTF16toUTF8(triggeringSearchEngineURL))) { + nsAutoString triggeringSearchEngine; + browserElement->GetAttribute(u"triggeringSearchEngine"_ns, + triggeringSearchEngine); + place.triggeringSearchEngine.Assign( + NS_ConvertUTF16toUTF8(triggeringSearchEngine)); + } + + nsAutoString triggeringSponsoredURL; + browserElement->GetAttribute(u"triggeringSponsoredURL"_ns, + triggeringSponsoredURL); + if (!triggeringSponsoredURL.IsEmpty()) { + place.triggeringSponsoredURL.Assign( + NS_ConvertUTF16toUTF8(triggeringSponsoredURL)); + + nsAutoString triggeringSponsoredURLVisitTimeMS; + browserElement->GetAttribute(u"triggeringSponsoredURLVisitTimeMS"_ns, + triggeringSponsoredURLVisitTimeMS); + place.triggeringSponsoredURLVisitTimeMS = + triggeringSponsoredURLVisitTimeMS.ToInteger64(&rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Get base domain. We need to get it here since nsIEffectiveTLDService + // referred in DomainNameFromURI should access on main thread. + nsCOMPtr<nsIURI> currentURL; + rv = NS_MutateURI(new net::nsStandardURL::Mutator()) + .SetSpec(place.spec) + .Finalize(currentURL); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIURI> sponsoredURL; + rv = NS_MutateURI(new net::nsStandardURL::Mutator()) + .SetSpec(place.triggeringSponsoredURL) + .Finalize(sponsoredURL); + NS_ENSURE_SUCCESS(rv, rv); + navHistory->DomainNameFromURI(currentURL, place.baseDomain); + navHistory->DomainNameFromURI(sponsoredURL, + place.triggeringSponsoredURLBaseDomain); + } + } + } + + // EMBED visits should not go through the database. + // They exist only to keep track of isVisited status during the session. + if (place.transitionType == nsINavHistoryService::TRANSITION_EMBED) { + NotifyEmbedVisit(place); + } else { + mozIStorageConnection* dbConn = GetDBConn(); + NS_ENSURE_STATE(dbConn); + + rv = InsertVisitedURIs::Start(dbConn, std::move(placeArray)); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +NS_IMETHODIMP +History::SetURITitle(nsIURI* aURI, const nsAString& aTitle) { + MOZ_ASSERT(NS_IsMainThread()); + NS_ENSURE_ARG(aURI); + + if (IsShuttingDown()) { + return NS_OK; + } + + if (XRE_IsContentProcess()) { + auto* cpc = dom::ContentChild::GetSingleton(); + MOZ_ASSERT(cpc, "Content Protocol is NULL!"); + Unused << cpc->SendSetURITitle(aURI, 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; + } + + mozIStorageConnection* dbConn = GetDBConn(); + NS_ENSURE_STATE(dbConn); + + return SetPageTitle::Start(dbConn, aURI, aTitle); +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIAsyncHistory + +NS_IMETHODIMP +History::UpdatePlaces(JS::Handle<JS::Value> 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<JSObject*> infos(aCtx); + nsresult rv = GetJSArrayFromJSValue(aPlaceInfos, aCtx, &infos, &infosLength); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t initialUpdatedCount = 0; + + nsTArray<VisitData> visitData; + for (uint32_t i = 0; i < infosLength; i++) { + JS::Rooted<JSObject*> info(aCtx); + nsresult rv = GetJSObjectFromArray(aCtx, infos, i, &info); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> uri = GetURIFromJSObject(aCtx, info, "uri"); + nsCString guid; + { + nsString fatGUID; + GetStringFromJSObject(aCtx, info, "guid", fatGUID); + if (fatGUID.IsVoid()) { + guid.SetIsVoid(true); + } else { + CopyUTF16toUTF8(fatGUID, guid); + } + } + + // 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<JSObject*> visits(aCtx, nullptr); + { + JS::Rooted<JS::Value> 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<JSObject*> visit(aCtx); + rv = GetJSObjectFromArray(aCtx, visits, j, &visit); + NS_ENSURE_SUCCESS(rv, rv); + + VisitData& data = *visitData.AppendElement(VisitData(uri)); + if (!title.IsEmpty()) { + data.title = title; + } else if (!title.IsVoid()) { + // Setting data.title to an empty string wouldn't make it non-void. + data.title.SetIsVoid(false); + } + data.guid = guid; + + // We must have a date and a transaction type! + rv = GetIntFromJSObject(aCtx, visit, "visitDate", &data.visitTime); + NS_ENSURE_SUCCESS(rv, rv); + // visitDate should be in microseconds. It's easy to do the wrong thing + // and pass milliseconds to updatePlaces, so we lazily check for that. + // While it's not easily distinguishable, since both are integers, we can + // check if the value is very far in the past, and assume it's probably + // a mistake. + if (data.visitTime < (PR_Now() / 1000)) { +#ifdef DEBUG + nsCOMPtr<nsIXPConnect> xpc = nsIXPConnect::XPConnect(); + Unused << xpc->DebugDumpJSStack(false, false, false); + MOZ_CRASH("invalid time format passed to updatePlaces"); +#endif + return NS_ERROR_INVALID_ARG; + } + 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) { + NotifyEmbedVisit(data, aCallback); + visitData.RemoveLastElement(); + initialUpdatedCount++; + continue; + } + + // The referrer is optional. + nsCOMPtr<nsIURI> referrer = + GetURIFromJSObject(aCtx, visit, "referrerURI"); + if (referrer) { + (void)referrer->GetSpec(data.referrerSpec); + } + } + } + + mozIStorageConnection* dbConn = GetDBConn(); + NS_ENSURE_STATE(dbConn); + + nsMainThreadPtrHandle<mozIVisitInfoCallback> callback( + new nsMainThreadPtrHolder<mozIVisitInfoCallback>("mozIVisitInfoCallback", + 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, std::move(visitData), + callback, initialUpdatedCount); + NS_ENSURE_SUCCESS(rv, rv); + } else if (aCallback) { + // 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. + + // Note: if we're inserting anything, it's the responsibility of + // InsertVisitedURIs to call the completion callback, as here we won't + // know how yet many items we will successfully insert/update. + nsCOMPtr<nsIEventTarget> backgroundThread = do_GetInterface(dbConn); + NS_ENSURE_TRUE(backgroundThread, NS_ERROR_UNEXPECTED); + nsCOMPtr<nsIRunnable> event = + new NotifyCompletion(callback, initialUpdatedCount); + 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); + + return VisitedQuery::Start(aURI, aCallback); +} + +void History::StartPendingVisitedQueries(PendingVisitedQueries&& aQueries) { + if (XRE_IsContentProcess()) { + auto* cpc = dom::ContentChild::GetSingleton(); + MOZ_ASSERT(cpc, "Content Protocol is NULL!"); + + // Fairly arbitrary limit on the number of URLs we send at a time, to avoid + // going over the IPC message size limit... Note that this is imperfect (we + // could have very long URIs), so this is a best-effort kind of thing. See + // bug 1775265. + constexpr size_t kBatchLimit = 4000; + + nsTArray<RefPtr<nsIURI>> uris(aQueries.Count()); + for (const auto& entry : aQueries) { + uris.AppendElement(entry.GetKey()); + MOZ_ASSERT(entry.GetData().IsEmpty(), + "Child process shouldn't have parent requests"); + if (uris.Length() == kBatchLimit) { + Unused << cpc->SendStartVisitedQueries(uris); + uris.ClearAndRetainStorage(); + } + } + + if (!uris.IsEmpty()) { + Unused << cpc->SendStartVisitedQueries(uris); + } + } else { + // TODO(bug 1594368): We could do a single query, as long as we can + // then notify each URI individually. + for (auto& entry : aQueries) { + nsresult queryStatus = VisitedQuery::Start( + entry.GetKey(), std::move(*entry.GetModifiableData())); + Unused << NS_WARN_IF(NS_FAILED(queryStatus)); + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIObserver + +NS_IMETHODIMP +History::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (strcmp(aTopic, TOPIC_PLACES_SHUTDOWN) == 0) { + Shutdown(); + + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (os) { + (void)os->RemoveObserver(this, TOPIC_PLACES_SHUTDOWN); + } + } + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsISupports + +NS_IMPL_ISUPPORTS(History, IHistory, mozIAsyncHistory, nsIObserver, + nsIMemoryReporter) + +} // namespace places +} // namespace mozilla |