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