/* -*- 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 aValue, JSContext* aCtx, JS::MutableHandle _array, uint32_t* _arrayLength) { if (aValue.isObjectOrNull()) { JS::Rooted val(aCtx, aValue.toObjectOrNull()); bool isArray; if (!JS::IsArrayObject(aCtx, val, &isArray)) { return NS_ERROR_UNEXPECTED; } if (isArray) { _array.set(val); (void)JS::GetArrayLength(aCtx, _array, _arrayLength); NS_ENSURE_ARG(*_arrayLength > 0); return NS_OK; } } // Build a temporary array to store this one item so the code below can // just loop. *_arrayLength = 1; _array.set(JS::NewArrayObject(aCtx, 0)); NS_ENSURE_TRUE(_array, NS_ERROR_OUT_OF_MEMORY); bool rc = JS_DefineElement(aCtx, _array, 0, aValue, 0); NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED); return NS_OK; } /** * Attemps to convert a given js value to a nsIURI object. * @param aCtx * The JSContext for aValue. * @param aValue * The JS value to convert. * @return the nsIURI object, or null if aValue is not a nsIURI object. */ already_AddRefed GetJSValueAsURI(JSContext* aCtx, const JS::Value& aValue) { if (!aValue.isPrimitive()) { nsCOMPtr xpc = nsIXPConnect::XPConnect(); nsCOMPtr wrappedObj; JS::Rooted obj(aCtx, aValue.toObjectOrNull()); nsresult rv = xpc->GetWrappedNativeOfJSObject(aCtx, obj, getter_AddRefs(wrappedObj)); NS_ENSURE_SUCCESS(rv, nullptr); nsCOMPtr 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 GetURIFromJSObject(JSContext* aCtx, JS::Handle aObject, const char* aProperty) { JS::Rooted uriVal(aCtx); bool rc = JS_GetProperty(aCtx, aObject, aProperty, &uriVal); NS_ENSURE_TRUE(rc, nullptr); return GetJSValueAsURI(aCtx, uriVal); } /** * Attemps to convert a JS value to a string. * @param aCtx * The JSContext for aObject. * @param aValue * The JS value to convert. * @param _string * The string to populate with the value, or set it to void. */ void GetJSValueAsString(JSContext* aCtx, const JS::Value& aValue, nsString& _string) { if (aValue.isUndefined() || !(aValue.isNull() || aValue.isString())) { _string.SetIsVoid(true); return; } // |null| in JS maps to the empty string. if (aValue.isNull()) { _string.Truncate(); return; } if (!AssignJSString(aCtx, _string, aValue.toString())) { _string.SetIsVoid(true); } } /** * Obtains the specified property of a JSObject. * * @param aCtx * The JSContext for aObject. * @param aObject * The JSObject to get the string from. * @param aProperty * The property to get the value from. * @param _string * The string to populate with the value, or set it to void. */ void GetStringFromJSObject(JSContext* aCtx, JS::Handle aObject, const char* aProperty, nsString& _string) { JS::Rooted val(aCtx); bool rc = JS_GetProperty(aCtx, aObject, aProperty, &val); if (!rc) { _string.SetIsVoid(true); return; } GetJSValueAsString(aCtx, val, _string); } /** * Obtains the specified property of a JSObject. * * @param aCtx * The JSContext for aObject. * @param aObject * The JSObject to get the int from. * @param aProperty * The property to get the value from. * @param _int * The integer to populate with the value on success. */ template nsresult GetIntFromJSObject(JSContext* aCtx, JS::Handle aObject, const char* aProperty, IntType* _int) { JS::Rooted value(aCtx); bool rc = JS_GetProperty(aCtx, aObject, aProperty, &value); NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED); if (value.isUndefined()) { return NS_ERROR_INVALID_ARG; } NS_ENSURE_ARG(value.isPrimitive()); NS_ENSURE_ARG(value.isNumber()); double num; rc = JS::ToNumber(aCtx, value, &num); NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED); NS_ENSURE_ARG(IntType(num) == num); *_int = IntType(num); return NS_OK; } /** * Obtains the specified property of a JSObject. * * @pre aArray must be an Array object. * * @param aCtx * The JSContext for aArray. * @param aArray * The JSObject to get the object from. * @param aIndex * The index to get the object from. * @param objOut * Set to the JSObject pointer on success. */ nsresult GetJSObjectFromArray(JSContext* aCtx, JS::Handle aArray, uint32_t aIndex, JS::MutableHandle objOut) { JS::Rooted value(aCtx); bool rc = JS_GetElement(aCtx, aArray, aIndex, &value); NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED); NS_ENSURE_ARG(!value.isPrimitive()); objOut.set(&value.toObject()); return NS_OK; } } // 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 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 callback( new nsMainThreadPtrHolder( "mozIVisitedStatusCallback", aCallback)); History* history = History::GetService(); NS_ENSURE_STATE(history); RefPtr 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 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& aCallback) : mURI(aURI), mCallback(aCallback) {} explicit VisitedQuery(nsIURI* aURI, History::ContentParentSet&& aContentProcessesToNotify) : mURI(aURI), mContentProcessesToNotify(std::move(aContentProcessesToNotify)) {} ~VisitedQuery() = default; nsCOMPtr mURI; nsMainThreadPtrHandle 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&& aPlaces) : Runnable("places::NotifyManyVisitsObservers"), mPlaces(std::move(aPlaces)), mHistory(History::GetService()) {} nsresult NotifyVisit(nsNavHistory* aNavHistory, nsCOMPtr& aObsService, PRTime aNow, nsIURI* aURI, const VisitData& aPlace) { if (aObsService) { DebugOnly 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>& aEvents) { if (aPlace.transitionType == nsINavHistoryService::TRANSITION_EMBED) { return; } RefPtr 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(aPlace.typed); visitEvent->mLastKnownTitle.Assign(aPlace.title); bool success = !!aEvents.AppendElement(visitEvent.forget(), fallible); MOZ_RELEASE_ASSERT(success); if (aPlace.titleChanged) { RefPtr 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 obsService = mozilla::services::GetObserverService(); Sequence> events; PRTime now = PR_Now(); for (uint32_t i = 0; i < mPlaces.Length(); ++i) { nsCOMPtr 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 mPlaces; RefPtr mHistory; }; /** * Notifies observers about a pages title changing. */ class NotifyTitleObservers : public Runnable { public: /** * Notifies observers on the main thread. * * @param aSpec * The spec of the URI to notify about. * @param aTitle * The new title to notify about. */ NotifyTitleObservers(const nsCString& aSpec, const nsString& aTitle, const nsCString& aGUID) : 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 titleEvent = new PlacesVisitTitle(); titleEvent->mUrl.Assign(NS_ConvertUTF8toUTF16(mSpec)); titleEvent->mPageGuid.Assign(mGUID); titleEvent->mTitle.Assign(mTitle); Sequence> 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& 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 referrerURI; if (!mPlace.referrerSpec.IsEmpty()) { hasValidURIs = !NS_WARN_IF(NS_FAILED( NS_NewURI(getter_AddRefs(referrerURI), mPlace.referrerSpec))); } nsCOMPtr uri; hasValidURIs = hasValidURIs && !NS_WARN_IF(NS_FAILED(NS_NewURI(getter_AddRefs(uri), mPlace.spec))); nsCOMPtr place; if (mIsSingleVisit) { nsCOMPtr visit = new VisitInfo(mPlace.visitId, mPlace.visitTime, mPlace.transitionType, referrerURI.forget()); PlaceInfo::VisitsArray visits; (void)visits.AppendElement(visit); // The frecency isn't exposed because it may not reflect the updated value // in the case of InsertVisitedURIs. place = new PlaceInfo(mPlace.placeId, mPlace.guid, uri.forget(), mPlace.title, -1, visits); } else { // Same as above. place = new PlaceInfo(mPlace.placeId, mPlace.guid, uri.forget(), mPlace.title, -1); } if (NS_SUCCEEDED(mResult) && hasValidURIs) { (void)mCallback->HandleResult(place); } else { (void)mCallback->HandleError(mResult, place); } return NS_OK; } private: nsMainThreadPtrHandle mCallback; VisitData mPlace; const nsresult mResult; bool mIsSingleVisit; }; /** * Notifies a callback object when the operation is complete. */ class NotifyCompletion : public Runnable { public: explicit NotifyCompletion( const nsMainThreadPtrHandle& aCallback, 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 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 callback( new nsMainThreadPtrHolder( "mozIVisitInfoCallback", aCallback)); nsCOMPtr event = new NotifyPlaceInfoCallback( callback, place, true, NS_ERROR_INVALID_ARG); (void)NS_DispatchToMainThread(event); } return false; } /** * Adds a visit to the database. */ class InsertVisitedURIs final : public Runnable { public: /** * Adds a visit to the database asynchronously. * * @param aConnection * The database connection to use for these operations. * @param aPlaces * The locations to record visits. * @param [optional] aCallback * The callback to notify about the visit. */ static nsresult Start(mozIStorageConnection* aConnection, nsTArray&& aPlaces, mozIVisitInfoCallback* aCallback = nullptr, 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 callback( new nsMainThreadPtrHolder( "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 event = new InsertVisitedURIs( aConnection, std::move(aPlaces), callback, ignoreErrors, ignoreResults, aInitialUpdatedCount); // Get the target thread, and then start the work! nsCOMPtr target = do_GetInterface(aConnection); NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED); nsresult rv = target->Dispatch(event, NS_DISPATCH_NORMAL); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } NS_IMETHOD Run() override { MOZ_ASSERT(!NS_IsMainThread(), "This should not be called on the main thread"); // 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 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::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 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 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 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 event = new NotifyManyVisitsObservers(std::move(mPlaces)); rv = NS_DispatchToMainThread(event); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } private: InsertVisitedURIs( mozIStorageConnection* aConnection, nsTArray&& aPlaces, const nsMainThreadPtrHandle& 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::size_type i = 0; i < mPlaces.Length(); i++) { nsCOMPtr uri; MOZ_ASSERT(NS_SUCCEEDED(NS_NewURI(getter_AddRefs(uri), mPlaces[i].spec))); MOZ_ASSERT(CanAddURI(uri), "Passed a VisitData with a URI we cannot add to history!"); } #endif } /** * Inserts or updates the entry in moz_places for this visit, adds the visit, * and updates the frecency of the place. * * @param {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 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 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 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 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 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 mPlaces; nsMainThreadPtrHandle 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 mHistory; }; /** * Sets the page title for a page in moz_places (if necessary). */ class SetPageTitle : public Runnable { public: /** * Sets a pages title in the database asynchronously. * * @param aConnection * The database connection to use for this operation. * @param aURI * The URI to set the page title on. * @param aTitle * The title to set for the page, if the page exists. */ static nsresult Start(mozIStorageConnection* aConnection, nsIURI* aURI, const nsAString& aTitle) { MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); MOZ_ASSERT(aURI, "Must pass a non-null URI object!"); nsCString spec; nsresult rv = aURI->GetSpec(spec); NS_ENSURE_SUCCESS(rv, rv); RefPtr event = new SetPageTitle(spec, aTitle); // Get the target thread, and then start the work! nsCOMPtr target = do_GetInterface(aConnection); NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED); rv = target->Dispatch(event, NS_DISPATCH_NORMAL); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } NS_IMETHOD Run() override { MOZ_ASSERT(!NS_IsMainThread(), "This should not be called on the main thread"); // First, see if the page exists in the database (we'll need its id later). bool exists; nsresult rv = mHistory->FetchPageInfo(mPlace, &exists); NS_ENSURE_SUCCESS(rv, rv); if (!exists || !mPlace.titleChanged) { // We have no record of this page, or we have no title change, so there // is no need to do any further work. return NS_OK; } MOZ_ASSERT(mPlace.placeId > 0, "We somehow have an invalid place id here!"); // Now we can update our database record. nsCOMPtr stmt = mHistory->GetStatement( "UPDATE moz_places " "SET title = :page_title " "WHERE id = :page_id "); NS_ENSURE_STATE(stmt); { mozStorageStatementScoper scoper(stmt); rv = stmt->BindInt64ByName("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 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 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 uri; if (NS_WARN_IF(NS_FAILED(NS_NewURI(getter_AddRefs(uri), aPlace.spec)))) { return; } if (!!aCallback) { nsMainThreadPtrHandle callback( new nsMainThreadPtrHolder( "mozIVisitInfoCallback", aCallback)); bool ignoreResults = false; Unused << aCallback->GetIgnoreResults(&ignoreResults); if (!ignoreResults) { nsCOMPtr event = new NotifyPlaceInfoCallback(callback, aPlace, true, NS_OK); (void)NS_DispatchToMainThread(event); } } nsCOMPtr event = new NotifyManyVisitsObservers(aPlace); (void)NS_DispatchToMainThread(event); } void NotifyVisitIfHavingUserPass(nsIURI* aURI) { MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread!"); bool hasUserPass; if (NS_SUCCEEDED(aURI->GetHasUserPass(&hasUserPass)) && hasUserPass) { nsCOMPtr event = new NotifyManyVisitsObservers(VisitData(aURI)); (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 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 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 Create( mozIStorageConnection* aDBConn) { RefPtr 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 aCallback) { if (mIsVisitedStatement) { aCallback->Execute(*mIsVisitedStatement); } else { mVisitedQueries.AppendElement(std::move(aCallback)); } } void Shutdown() { mShutdownWasInvoked = true; if (mReadOnlyDBConn) { mVisitedQueries.Clear(); DebugOnly 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 mReadOnlyDBConn; nsCOMPtr mIsVisitedStatement; nsTArray> mVisitedQueries; bool mShutdownWasInvoked; }; NS_IMPL_ISUPPORTS(ConcurrentStatementsHolder, mozIStorageCompletionCallback) nsresult History::QueueVisitedStatement(RefPtr 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 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 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 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 service = components::History::Service(); if (service) { NS_ASSERTION(gService, "Our constructor was not run?!"); } return gService; } /* static */ already_AddRefed History::GetSingleton() { if (!gService) { RefPtr 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); } // 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; } bool isRedirect = aFlags & IHistory::REDIRECT_SOURCE; bool isHidden = GetHiddenState(isRedirect, transitionType); // 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, isHidden); // 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 || isHidden) { // We can skip this visit. return NS_OK; } } } // Never store the URL having userpass to database. nsCOMPtr visitedURI = GetExposableURI(aURI); nsCOMPtr lastVisitedURI; if (aLastVisitedURI) { lastVisitedURI = GetExposableURI(aLastVisitedURI); } nsTArray placeArray(1); placeArray.AppendElement(VisitData(visitedURI, lastVisitedURI)); VisitData& place = placeArray.ElementAt(0); NS_ENSURE_FALSE(place.spec.IsEmpty(), NS_ERROR_INVALID_ARG); place.visitTime = PR_Now(); place.SetTransitionType(transitionType); place.hidden = isHidden; if (isRedirect) { place.useFrecencyRedirectBonus = (aFlags & (IHistory::REDIRECT_SOURCE_PERMANENT | IHistory::REDIRECT_SOURCE_UPGRADED)) || transitionType != nsINavHistoryService::TRANSITION_TYPED; } // Error pages should never be autocompleted. place.isUnrecoverableError = aFlags & IHistory::UNRECOVERABLE_ERROR; nsCOMPtr 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 browser; rv = bwt->GetBrowserById(aBrowserId, getter_AddRefs(browser)); NS_ENSURE_SUCCESS(rv, rv); if (browser) { RefPtr browserElement = static_cast(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 currentURL; rv = NS_MutateURI(new net::nsStandardURL::Mutator()) .SetSpec(place.spec) .Finalize(currentURL); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr 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); } // URIs with a userpass component are not stored in the database for security // reasons, we store the exposable URI version of them instead. // The original link pointing at the URI with userpass must still be marked as // visited, to properly react to the user interaction, so we notify a visit. // The visited status is not going to survive a reload of course, though the // alternative of marking any userpass URI as visited if the exposable URI is // visited also feels wrong from the user point of view. NotifyVisitIfHavingUserPass(aURI); 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); nsCOMPtr uri = GetExposableURI(aURI); bool canAdd; nsresult rv = navHistory->CanAddURI(uri, &canAdd); NS_ENSURE_SUCCESS(rv, rv); if (!canAdd) { return NS_OK; } mozIStorageConnection* dbConn = GetDBConn(); NS_ENSURE_STATE(dbConn); return SetPageTitle::Start(dbConn, uri, aTitle); } //////////////////////////////////////////////////////////////////////////////// //// mozIAsyncHistory NS_IMETHODIMP History::UpdatePlaces(JS::Handle aPlaceInfos, mozIVisitInfoCallback* aCallback, JSContext* aCtx) { NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED); NS_ENSURE_TRUE(!aPlaceInfos.isPrimitive(), NS_ERROR_INVALID_ARG); uint32_t infosLength; JS::Rooted infos(aCtx); nsresult rv = GetJSArrayFromJSValue(aPlaceInfos, aCtx, &infos, &infosLength); NS_ENSURE_SUCCESS(rv, rv); uint32_t initialUpdatedCount = 0; nsTArray visitData; for (uint32_t i = 0; i < infosLength; i++) { JS::Rooted info(aCtx); nsresult rv = GetJSObjectFromArray(aCtx, infos, i, &info); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr uri = GetURIFromJSObject(aCtx, info, "uri"); if (uri) { uri = GetExposableURI(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 visits(aCtx, nullptr); { JS::Rooted visitsVal(aCtx); bool rc = JS_GetProperty(aCtx, info, "visits", &visitsVal); NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED); if (!visitsVal.isPrimitive()) { visits = visitsVal.toObjectOrNull(); bool isArray; if (!JS::IsArrayObject(aCtx, visits, &isArray)) { return NS_ERROR_UNEXPECTED; } if (!isArray) { return NS_ERROR_INVALID_ARG; } } } NS_ENSURE_ARG(visits); uint32_t visitsLength = 0; if (visits) { (void)JS::GetArrayLength(aCtx, visits, &visitsLength); } NS_ENSURE_ARG(visitsLength > 0); // Check each visit, and build our array of VisitData objects. visitData.SetCapacity(visitData.Length() + visitsLength); for (uint32_t j = 0; j < visitsLength; j++) { JS::Rooted visit(aCtx); rv = GetJSObjectFromArray(aCtx, visits, j, &visit); NS_ENSURE_SUCCESS(rv, rv); VisitData& data = *visitData.AppendElement(VisitData(uri)); 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 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 referrer = GetURIFromJSObject(aCtx, visit, "referrerURI"); if (referrer) { (void)referrer->GetSpec(data.referrerSpec); } } } mozIStorageConnection* dbConn = GetDBConn(); NS_ENSURE_STATE(dbConn); nsMainThreadPtrHandle callback( new nsMainThreadPtrHolder("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 backgroundThread = do_GetInterface(dbConn); NS_ENSURE_TRUE(backgroundThread, NS_ERROR_UNEXPECTED); nsCOMPtr 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> 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 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