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