summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/History.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/places/History.cpp
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
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.cpp2355
1 files changed, 2355 insertions, 0 deletions
diff --git a/toolkit/components/places/History.cpp b/toolkit/components/places/History.cpp
new file mode 100644
index 0000000000..81cccdcb55
--- /dev/null
+++ b/toolkit/components/places/History.cpp
@@ -0,0 +1,2355 @@
+/* -*- 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 "nsAppDirectoryServiceDefs.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/StaticPrefs_places.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;
+
+namespace mozilla::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),
+ typed(false),
+ transitionType(UINT32_MAX),
+ visitTime(0),
+ frecency(-1),
+ lastVisitId(0),
+ lastVisitTime(0),
+ visitCount(0),
+ referrerVisitId(0),
+ titleChanged(false),
+ isUnrecoverableError(false),
+ 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),
+ typed(false),
+ transitionType(UINT32_MAX),
+ visitTime(0),
+ frecency(-1),
+ lastVisitId(0),
+ lastVisitTime(0),
+ visitCount(0),
+ referrerVisitId(0),
+ titleChanged(false),
+ isUnrecoverableError(false),
+ 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 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 the visit ended up in an unrecoverable error.
+ bool isUnrecoverableError;
+
+ // 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;
+ }
+ 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->mFrecency = aPlace.frecency;
+ 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;
+ if (NS_WARN_IF(
+ NS_FAILED(NS_NewURI(getter_AddRefs(uri), mPlaces[i].spec)))) {
+ 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()) {
+ hasValidURIs = !NS_WARN_IF(NS_FAILED(
+ NS_NewURI(getter_AddRefs(referrerURI), mPlace.referrerSpec)));
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ hasValidURIs =
+ hasValidURIs &&
+ !NS_WARN_IF(NS_FAILED(NS_NewURI(getter_AddRefs(uri), mPlace.spec)));
+
+ 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 (!!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);
+ }
+
+ // This is an optimization for frecency updating, if all the entries point
+ // to the same URL (inserting multiple visits for the same url), then we can
+ // update frecency once at the end of the loop. Otherwise, if there's
+ // multiple pages, we'll delay frecency recalculation to a later time.
+ bool shouldUpdateFrecency = false;
+
+ for (nsTArray<VisitData>::size_type i = 0; i < mPlaces.Length(); i++) {
+ VisitData& place = mPlaces.ElementAt(i);
+
+ if (i == 0) {
+ // isUnrecoverableError can only be defined when this is invoked by
+ // VisitURI, to insert a single visit. When it's defined, the page
+ // will be hidden, thus it's not worth updating.
+ shouldUpdateFrecency = !place.isUnrecoverableError;
+ } else if (shouldUpdateFrecency &&
+ (!place.spec.Equals(mPlaces.ElementAt(i - 1).spec))) {
+ // We have multiple entries with different URLs, delay recalculation.
+ // A SQL trigger will set recalc_frecency automatically when a visit
+ // is added.
+ shouldUpdateFrecency = false;
+ }
+ // 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;
+ }
+
+ 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 = (int32_t)(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++;
+ }
+
+ if (shouldUpdateFrecency) {
+ VisitData& place = mPlaces.ElementAt(0);
+ if (NS_SUCCEEDED(UpdateFrecency(
+ place.placeId,
+ place.useFrecencyRedirectBonus && mPlaces.Length() == 1))) {
+ // Notifying a new visit should be sufficient to know that frecency
+ // changed, but since historically we notified a frecency change, for
+ // now we'll continue doing it, and re-evaluate in the future.
+ NS_DispatchToMainThread(new NotifyRankingChanged());
+ }
+ }
+
+ 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 {boolean} aKnown
+ * True if we already have an entry for this place in moz_places, false
+ * otherwise.
+ * @param {VisitData} 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);
+
+ // Adding a visit sets the recalculation flags through a trigger, but for
+ // error pages we don't want that, because recalculation considers this a
+ // normal successful visit. In the future we should store the error state
+ // along with the visit, so that recalculation can do a better job and
+ // we can remove this workaround update. See Bug 1842008.
+ if (aPlace.isUnrecoverableError) {
+ nsCOMPtr<mozIStorageStatement> stmt = mHistory->GetStatement(
+ "UPDATE moz_places "
+ "SET recalc_frecency = 0, recalc_alt_frecency = 0 "
+ "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->Execute();
+ 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, (int32_t)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 int64_t aPlaceId, bool aIsRedirect) {
+ 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, aPlaceId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName("redirect"_ns, aIsRedirect);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (StaticPrefs::
+ places_frecency_pages_alternative_featureGate_AtStartup()) {
+ nsCOMPtr<mozIStorageStatement> stmt = mHistory->GetStatement(
+ "UPDATE moz_places "
+ "SET alt_frecency = CALCULATE_ALT_FRECENCY(id, :redirect), "
+ "recalc_alt_frecency = 0 "
+ "WHERE id = :page_id");
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+ rv = stmt->BindInt64ByName("page_id"_ns, aPlaceId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName("redirect"_ns, aIsRedirect);
+ 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;
+ if (NS_WARN_IF(NS_FAILED(NS_NewURI(getter_AddRefs(uri), aPlace.spec)))) {
+ 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(!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.isUnrecoverableError ? 0 : aPlace.frecency;
+ 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 |
+ IHistory::REDIRECT_SOURCE_UPGRADED)) ||
+ transitionType != nsINavHistoryService::TRANSITION_TYPED;
+ }
+ place.hidden = GetHiddenState(isRedirect, place.transitionType);
+
+ // Error pages should never be autocompleted.
+ place.isUnrecoverableError = aFlags & IHistory::UNRECOVERABLE_ERROR;
+
+ // 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_ImportESModule("resource:///modules/BrowserWindowTracker.sys.mjs",
+ "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);
+}
+
+NS_IMETHODIMP
+History::ClearCache() {
+ mRecentlyVisitedURIs.Clear();
+ return NS_OK;
+}
+
+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 mozilla::places