diff --git a/toolkit/components/places/Helpers.cpp b/toolkit/components/places/Helpers.cpp index 4ebb661eb63..e798b40cc3a 100644 --- a/toolkit/components/places/Helpers.cpp +++ b/toolkit/components/places/Helpers.cpp @@ -300,7 +300,7 @@ GenerateGUID(nsCString& _guid) } bool -IsValidGUID(const nsCString& aGUID) +IsValidGUID(const nsACString& aGUID) { nsCString::size_type len = aGUID.Length(); if (len != GUID_LENGTH) { diff --git a/toolkit/components/places/Helpers.h b/toolkit/components/places/Helpers.h index 61870014149..65be98f6b89 100644 --- a/toolkit/components/places/Helpers.h +++ b/toolkit/components/places/Helpers.h @@ -137,7 +137,7 @@ nsresult GenerateGUID(nsCString& _guid); * The guid to test. * @return true if it is a valid guid, false otherwise. */ -bool IsValidGUID(const nsCString& aGUID); +bool IsValidGUID(const nsACString& aGUID); /** * Truncates the title if it's longer than TITLE_LENGTH_MAX. diff --git a/toolkit/components/places/History.cpp b/toolkit/components/places/History.cpp index 4cadd3bf5b3..ffdc699bce3 100644 --- a/toolkit/components/places/History.cpp +++ b/toolkit/components/places/History.cpp @@ -212,6 +212,68 @@ class PlaceHashKey : public nsCStringHashKey namespace { +/** + * Convert the given js value to a js array. + * + * @param [in] aValue + * the JS value to convert. + * @param [in] aCtx + * The JSContext for aValue. + * @param [out] _array + * the JS array. + * @param [out] _arrayLength + * _array's length. + */ +nsresult +GetJSArrayFromJSValue(const JS::Value& aValue, + JSContext* aCtx, + JSObject** _array, + uint32_t* _arrayLength) { + if (aValue.isObjectOrNull()) { + JS::Rooted val(aCtx, aValue.toObjectOrNull()); + if (JS_IsArrayObject(aCtx, val)) { + *_array = val; + (void)JS_GetArrayLength(aCtx, *_array, _arrayLength); + NS_ENSURE_ARG(*_arrayLength > 0); + return NS_OK; + } + } + + // Build a temporary array to store this one item so the code below can + // just loop. + *_arrayLength = 1; + *_array = JS_NewArrayObject(aCtx, 0, nullptr); + NS_ENSURE_TRUE(*_array, NS_ERROR_OUT_OF_MEMORY); + + JSBool rc = JS_DefineElement(aCtx, *_array, 0, aValue, nullptr, nullptr, 0); + NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED); + return NS_OK; +} + +/** + * Attemps to convert a given js value to a nsIURI object. + * @param aCtx + * The JSContext for aValue. + * @param aValue + * The JS value to convert. + * @return the nsIURI object, or null if aValue is not a nsIURI object. + */ +already_AddRefed +GetJSValueAsURI(JSContext* aCtx, + const JS::Value& aValue) { + if (!JSVAL_IS_PRIMITIVE(aValue)) { + nsCOMPtr xpc = mozilla::services::GetXPConnect(); + + nsCOMPtr wrappedObj; + nsresult rv = xpc->GetWrappedNativeOfJSObject(aCtx, JSVAL_TO_OBJECT(aValue), + getter_AddRefs(wrappedObj)); + NS_ENSURE_SUCCESS(rv, nullptr); + nsCOMPtr uri = do_QueryWrappedNative(wrappedObj); + return uri.forget(); + } + return nullptr; +} + /** * Obtains an nsIURI from the "uri" property of a JSObject. * @@ -231,18 +293,41 @@ GetURIFromJSObject(JSContext* aCtx, JS::Rooted uriVal(aCtx); JSBool rc = JS_GetProperty(aCtx, aObject, aProperty, uriVal.address()); NS_ENSURE_TRUE(rc, nullptr); + return GetJSValueAsURI(aCtx, uriVal); +} - if (!JSVAL_IS_PRIMITIVE(uriVal)) { - nsCOMPtr xpc = mozilla::services::GetXPConnect(); - - nsCOMPtr wrappedObj; - nsresult rv = xpc->GetWrappedNativeOfJSObject(aCtx, JSVAL_TO_OBJECT(uriVal), - getter_AddRefs(wrappedObj)); - NS_ENSURE_SUCCESS(rv, nullptr); - nsCOMPtr uri = do_QueryWrappedNative(wrappedObj); - return uri.forget(); +/** + * Attemps to convert a JS value to a string. + * @param aCtx + * The JSContext for aObject. + * @param aValue + * The JS value to convert. + * @param _string + * The string to populate with the value, or set it to void. + */ +void +GetJSValueAsString(JSContext* aCtx, + const JS::Value& aValue, + nsString& _string) { + if (JSVAL_IS_VOID(aValue) || + !(JSVAL_IS_NULL(aValue) || JSVAL_IS_STRING(aValue))) { + _string.SetIsVoid(true); + return; } - return nullptr; + + // |null| in JS maps to the empty string. + if (JSVAL_IS_NULL(aValue)) { + _string.Truncate(); + return; + } + size_t length; + const jschar* chars = + JS_GetStringCharsZAndLength(aCtx, JSVAL_TO_STRING(aValue), &length); + if (!chars) { + _string.SetIsVoid(true); + return; + } + _string.Assign(static_cast(chars), length); } /** @@ -265,24 +350,13 @@ GetStringFromJSObject(JSContext* aCtx, { JS::Rooted val(aCtx); JSBool rc = JS_GetProperty(aCtx, aObject, aProperty, val.address()); - if (!rc || JSVAL_IS_VOID(val) || - !(JSVAL_IS_NULL(val) || JSVAL_IS_STRING(val))) { + if (!rc) { _string.SetIsVoid(true); return; } - // |null| in JS maps to the empty string. - if (JSVAL_IS_NULL(val)) { - _string.Truncate(); - return; + else { + GetJSValueAsString(aCtx, val, _string); } - size_t length; - const jschar* chars = - JS_GetStringCharsZAndLength(aCtx, JSVAL_TO_STRING(val), &length); - if (!chars) { - _string.SetIsVoid(true); - return; - } - _string.Assign(static_cast(chars), length); } /** @@ -492,8 +566,8 @@ public: NS_IMETHOD Run() { - NS_PRECONDITION(NS_IsMainThread(), - "This should be called on the main thread"); + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); + // We are in the main thread, no need to lock. if (mHistory->IsShuttingDown()) { // If we are shutting down, we cannot notify the observers. @@ -563,8 +637,7 @@ public: NS_IMETHOD Run() { - NS_PRECONDITION(NS_IsMainThread(), - "This should be called on the main thread"); + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); nsNavHistory* navHistory = nsNavHistory::GetHistoryService(); NS_ENSURE_TRUE(navHistory, NS_ERROR_OUT_OF_MEMORY); @@ -581,44 +654,57 @@ private: }; /** - * Notifies a callback object when a visit has been handled. + * Helper class for methods which notify their callers through the + * mozIVisitInfoCallback interface. */ -class NotifyVisitInfoCallback : public nsRunnable +class NotifyPlaceInfoCallback : public nsRunnable { public: - NotifyVisitInfoCallback(mozIVisitInfoCallback* aCallback, + NotifyPlaceInfoCallback(mozIVisitInfoCallback* aCallback, const VisitData& aPlace, + bool aIsSingleVisit, nsresult aResult) : mCallback(aCallback) , mPlace(aPlace) , mResult(aResult) + , mIsSingleVisit(aIsSingleVisit) { - NS_PRECONDITION(aCallback, "Must pass a non-null callback!"); + MOZ_ASSERT(aCallback, "Must pass a non-null callback!"); } NS_IMETHOD Run() { - NS_PRECONDITION(NS_IsMainThread(), - "This should be called on the main thread"); + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); nsCOMPtr referrerURI; if (!mPlace.referrerSpec.IsEmpty()) { (void)NS_NewURI(getter_AddRefs(referrerURI), mPlace.referrerSpec); } - nsCOMPtr visit = - new VisitInfo(mPlace.visitId, mPlace.visitTime, mPlace.transitionType, - referrerURI.forget()); - PlaceInfo::VisitsArray visits; - (void)visits.AppendElement(visit); - nsCOMPtr uri; (void)NS_NewURI(getter_AddRefs(uri), mPlace.spec); - // We do not notify about the frecency of the place. - nsCOMPtr place = - new PlaceInfo(mPlace.placeId, mPlace.guid, uri.forget(), mPlace.title, - -1, visits); + nsCOMPtr place; + if (mIsSingleVisit) { + nsCOMPtr visit = + new VisitInfo(mPlace.visitId, mPlace.visitTime, mPlace.transitionType, + referrerURI.forget()); + PlaceInfo::VisitsArray visits; + (void)visits.AppendElement(visit); + + // The frecency isn't exposed because it may not reflect the updated value + // in the case of InsertVisitedURIs. + place = + new PlaceInfo(mPlace.placeId, mPlace.guid, uri.forget(), mPlace.title, + -1, visits); + } + else { + // Same as above. + place = + new PlaceInfo(mPlace.placeId, mPlace.guid, uri.forget(), mPlace.title, + -1); + } + if (NS_SUCCEEDED(mResult)) { (void)mCallback->HandleResult(place); } @@ -638,6 +724,7 @@ private: mozIVisitInfoCallback* mCallback; VisitData mPlace; const nsresult mResult; + bool mIsSingleVisit; }; /** @@ -649,7 +736,7 @@ public: NotifyCompletion(mozIVisitInfoCallback* aCallback) : mCallback(aCallback) { - NS_PRECONDITION(aCallback, "Must pass a non-null callback!"); + MOZ_ASSERT(aCallback, "Must pass a non-null callback!"); } NS_IMETHOD Run() @@ -706,7 +793,7 @@ CanAddURI(nsIURI* aURI, // We cannot add the URI. Notify the callback, if we were given one. if (aCallback) { - // NotifyVisitInfoCallback does not hold a strong reference to the callback, so we + // NotifyPlaceInfoCallback does not hold a strong reference to the callback, so we // have to manage it by AddRefing now and then releasing it after the event // has run. NS_ADDREF(aCallback); @@ -714,11 +801,11 @@ CanAddURI(nsIURI* aURI, VisitData place(aURI); place.guid = aGUID; nsCOMPtr event = - new NotifyVisitInfoCallback(aCallback, place, NS_ERROR_INVALID_ARG); + new NotifyPlaceInfoCallback(aCallback, place, true, NS_ERROR_INVALID_ARG); (void)NS_DispatchToMainThread(event); // Also dispatch an event to release our reference to the callback after - // NotifyVisitInfoCallback has run. + // NotifyPlaceInfoCallback has run. nsCOMPtr mainThread = do_GetMainThread(); (void)NS_ProxyRelease(mainThread, aCallback, true); } @@ -746,9 +833,8 @@ public: nsTArray& aPlaces, mozIVisitInfoCallback* aCallback = NULL) { - NS_PRECONDITION(NS_IsMainThread(), - "This should be called on the main thread"); - NS_PRECONDITION(aPlaces.Length() > 0, "Must pass a non-empty array!"); + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); + MOZ_ASSERT(aPlaces.Length() > 0, "Must pass a non-empty array!"); nsRefPtr event = new InsertVisitedURIs(aConnection, aPlaces, aCallback); @@ -764,8 +850,7 @@ public: NS_IMETHOD Run() { - NS_PRECONDITION(!NS_IsMainThread(), - "This should not be called on the main thread"); + MOZ_ASSERT(!NS_IsMainThread(), "This should not be called on the main thread"); // Prevent the main thread from shutting down while this is running. MutexAutoLock lockedScope(mHistory->GetShutdownMutex()); @@ -784,15 +869,22 @@ public: // We can avoid a database lookup if it's the same place as the last // visit we added. - bool known = (lastPlace && lastPlace->IsSamePlaceAs(place)) || - mHistory->FetchPageInfo(place); + bool known = lastPlace && lastPlace->IsSamePlaceAs(place); + if (!known) { + nsresult rv = mHistory->FetchPageInfo(place, &known); + if (NS_FAILED(rv)) { + nsCOMPtr event = + new NotifyPlaceInfoCallback(mCallback, place, true, rv); + return NS_DispatchToMainThread(event); + } + } FetchReferrerInfo(referrer, place); nsresult rv = DoDatabaseInserts(known, place, referrer); if (mCallback) { nsCOMPtr event = - new NotifyVisitInfoCallback(mCallback, place, rv); + new NotifyPlaceInfoCallback(mCallback, place, true, rv); nsresult rv2 = NS_DispatchToMainThread(event); NS_ENSURE_SUCCESS(rv2, rv2); } @@ -825,8 +917,7 @@ private: , mCallback(aCallback) , mHistory(History::GetService()) { - NS_PRECONDITION(NS_IsMainThread(), - "This should be called on the main thread"); + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); (void)mPlaces.SwapElements(aPlaces); (void)mReferrers.SetLength(mPlaces.Length()); @@ -844,9 +935,6 @@ private: "Passed a VisitData with a URI we cannot add to history!"); #endif } - - // We AddRef on the main thread, and release it when we are destroyed. - NS_IF_ADDREF(mCallback); } virtual ~InsertVisitedURIs() @@ -873,8 +961,7 @@ private: VisitData& aPlace, VisitData& aReferrer) { - NS_PRECONDITION(!NS_IsMainThread(), - "This should not be called on the main thread"); + MOZ_ASSERT(!NS_IsMainThread(), "This should not be called on the main thread"); // If the page was in moz_places, we need to update the entry. nsresult rv; @@ -891,7 +978,10 @@ private: // have a callback or when the GUID isn't known. No point in doing the // disk I/O if we do not need it. if (mCallback || aPlace.guid.IsEmpty()) { - bool exists = mHistory->FetchPageInfo(aPlace); + bool exists; + rv = mHistory->FetchPageInfo(aPlace, &exists); + NS_ENSURE_SUCCESS(rv, rv); + if (!exists) { NS_NOTREACHED("should have an entry in moz_places"); } @@ -1145,12 +1235,7 @@ private: nsTArray mPlaces; nsTArray mReferrers; - /** - * We own a strong reference to this, but in an indirect way. We call AddRef - * in our constructor, which happens on the main thread, and proxy the relase - * of the object to the main thread in our destructor. - */ - mozIVisitInfoCallback* mCallback; + nsCOMPtr mCallback; /** * Strong reference to the History object because we do not want it to @@ -1159,6 +1244,69 @@ private: nsRefPtr mHistory; }; +class GetPlaceInfo MOZ_FINAL : public nsRunnable { +public: + /** + * Get the place info for a given place (by GUID or URI) asynchronously. + */ + static nsresult Start(mozIStorageConnection* aConnection, + VisitData& aPlace, + mozIVisitInfoCallback* aCallback) { + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); + + nsRefPtr event = new GetPlaceInfo(aPlace, aCallback); + + // Get the target thread, and then start the work! + nsCOMPtr target = do_GetInterface(aConnection); + NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED); + nsresult rv = target->Dispatch(event, NS_DISPATCH_NORMAL); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + NS_IMETHOD Run() + { + MOZ_ASSERT(!NS_IsMainThread(), "This should not be called on the main thread"); + + bool exists; + nsresult rv = mHistory->FetchPageInfo(mPlace, &exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!exists) + rv = NS_ERROR_NOT_AVAILABLE; + + nsCOMPtr event = + new NotifyPlaceInfoCallback(mCallback, mPlace, false, rv); + + rv = NS_DispatchToMainThread(event); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } +private: + GetPlaceInfo(VisitData& aPlace, + mozIVisitInfoCallback* aCallback) + : mPlace(aPlace) + , mCallback(aCallback) + , mHistory(History::GetService()) + { + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); + } + + virtual ~GetPlaceInfo() + { + if (mCallback) { + nsCOMPtr mainThread = do_GetMainThread(); + (void)NS_ProxyRelease(mainThread, mCallback, true); + } + } + + VisitData mPlace; + nsCOMPtr mCallback; + nsRefPtr mHistory; +}; + /** * Sets the page title for a page in moz_places (if necessary). */ @@ -1179,9 +1327,8 @@ public: nsIURI* aURI, const nsAString& aTitle) { - NS_PRECONDITION(NS_IsMainThread(), - "This should be called on the main thread"); - NS_PRECONDITION(aURI, "Must pass a non-null URI object!"); + MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread"); + MOZ_ASSERT(aURI, "Must pass a non-null URI object!"); nsCString spec; nsresult rv = aURI->GetSpec(spec); @@ -1200,11 +1347,13 @@ public: NS_IMETHOD Run() { - NS_PRECONDITION(!NS_IsMainThread(), - "This should not be called on the main thread"); + MOZ_ASSERT(!NS_IsMainThread(), "This should not be called on the main thread"); // First, see if the page exists in the database (we'll need its id later). - bool exists = mHistory->FetchPageInfo(mPlace); + bool exists; + nsresult rv = mHistory->FetchPageInfo(mPlace, &exists); + NS_ENSURE_SUCCESS(rv, rv); + if (!exists || !mPlace.titleChanged) { // We have no record of this page, or we have no title change, so there // is no need to do any further work. @@ -1225,8 +1374,7 @@ public: { mozStorageStatementScoper scoper(stmt); - nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), - mPlace.placeId); + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), mPlace.placeId); NS_ENSURE_SUCCESS(rv, rv); // Empty strings should clear the title, just like // nsNavHistory::SetPageTitle. @@ -1244,7 +1392,7 @@ public: nsCOMPtr event = new NotifyTitleObservers(mPlace.spec, mPlace.title, mPlace.guid); - nsresult rv = NS_DispatchToMainThread(event); + rv = NS_DispatchToMainThread(event); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; @@ -1716,9 +1864,9 @@ void StoreAndNotifyEmbedVisit(VisitData& aPlace, mozIVisitInfoCallback* aCallback = NULL) { - NS_PRECONDITION(aPlace.transitionType == nsINavHistoryService::TRANSITION_EMBED, - "Must only pass TRANSITION_EMBED visits to this!"); - NS_PRECONDITION(NS_IsMainThread(), "Must be called on the main thread!"); + MOZ_ASSERT(aPlace.transitionType == nsINavHistoryService::TRANSITION_EMBED, + "Must only pass TRANSITION_EMBED visits to this!"); + MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread!"); nsCOMPtr uri; (void)NS_NewURI(getter_AddRefs(uri), aPlace.spec); @@ -1731,16 +1879,16 @@ StoreAndNotifyEmbedVisit(VisitData& aPlace, navHistory->registerEmbedVisit(uri, aPlace.visitTime); if (aCallback) { - // NotifyVisitInfoCallback does not hold a strong reference to the callback, + // NotifyPlaceInfoCallback does not hold a strong reference to the callback, // so we have to manage it by AddRefing now and then releasing it after the // event has run. NS_ADDREF(aCallback); nsCOMPtr event = - new NotifyVisitInfoCallback(aCallback, aPlace, NS_OK); + new NotifyPlaceInfoCallback(aCallback, aPlace, true, NS_OK); (void)NS_DispatchToMainThread(event); // Also dispatch an event to release our reference to the callback after - // NotifyVisitInfoCallback has run. + // NotifyPlaceInfoCallback has run. nsCOMPtr mainThread = do_GetMainThread(); (void)NS_ProxyRelease(mainThread, aCallback, true); } @@ -1972,37 +2120,65 @@ History::UpdatePlace(const VisitData& aPlace) return NS_OK; } -bool -History::FetchPageInfo(VisitData& _place) +nsresult +History::FetchPageInfo(VisitData& _place, bool* _exists) { - NS_PRECONDITION(!_place.spec.IsEmpty(), "must have a non-empty spec!"); + NS_PRECONDITION(!_place.spec.IsEmpty() || !_place.guid.IsEmpty(), "must have either a non-empty spec or guid!"); NS_PRECONDITION(!NS_IsMainThread(), "must be called off of the main thread!"); - nsCOMPtr stmt = GetStatement( - "SELECT id, title, hidden, typed, guid " + nsresult rv; + + // URI takes precedence. + nsCOMPtr stmt; + bool selectByURI = !_place.spec.IsEmpty(); + if (selectByURI) { + stmt = GetStatement( + "SELECT guid, id, title, hidden, typed, frecency " "FROM moz_places " "WHERE url = :page_url " ); - NS_ENSURE_TRUE(stmt, false); - mozStorageStatementScoper scoper(stmt); - - nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), - _place.spec); - NS_ENSURE_SUCCESS(rv, false); - - bool hasResult; - rv = stmt->ExecuteStep(&hasResult); - NS_ENSURE_SUCCESS(rv, false); - if (!hasResult) { - return false; + rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), _place.spec); + NS_ENSURE_SUCCESS(rv, rv); + } + else { + stmt = GetStatement( + "SELECT url, id, title, hidden, typed, frecency " + "FROM moz_places " + "WHERE guid = :guid " + ); + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), _place.guid); + NS_ENSURE_SUCCESS(rv, rv); } - rv = stmt->GetInt64(0, &_place.placeId); - NS_ENSURE_SUCCESS(rv, false); + NS_ENSURE_TRUE(stmt, rv); + mozStorageStatementScoper scoper(stmt); + + rv = stmt->ExecuteStep(_exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!*_exists) { + return NS_OK; + } + + if (selectByURI) { + if (_place.guid.IsEmpty()) { + rv = stmt->GetUTF8String(0, _place.guid); + NS_ENSURE_SUCCESS(rv, rv); + } + } + else { + nsAutoCString spec; + rv = stmt->GetUTF8String(0, spec); + NS_ENSURE_SUCCESS(rv, rv); + _place.spec = spec; + } + + rv = stmt->GetInt64(1, &_place.placeId); + NS_ENSURE_SUCCESS(rv, rv); nsAutoString title; - rv = stmt->GetString(1, title); - NS_ENSURE_SUCCESS(rv, true); + rv = stmt->GetString(2, title); + NS_ENSURE_SUCCESS(rv, rv); // If the title we were given was void, that means we did not bother to set // it to anything. As a result, ignore the fact that we may have changed the @@ -2022,26 +2198,23 @@ History::FetchPageInfo(VisitData& _place) // Any one visible transition makes this location visible. If database // has location as visible, reflect that in our data structure. int32_t hidden; - rv = stmt->GetInt32(2, &hidden); + rv = stmt->GetInt32(3, &hidden); + NS_ENSURE_SUCCESS(rv, rv); _place.hidden = !!hidden; - NS_ENSURE_SUCCESS(rv, true); } if (!_place.typed) { // If this transition wasn't typed, others might have been. If database // has location as typed, reflect that in our data structure. int32_t typed; - rv = stmt->GetInt32(3, &typed); + rv = stmt->GetInt32(4, &typed); + NS_ENSURE_SUCCESS(rv, rv); _place.typed = !!typed; - NS_ENSURE_SUCCESS(rv, true); } - if (_place.guid.IsVoid()) { - rv = stmt->GetUTF8String(4, _place.guid); - NS_ENSURE_SUCCESS(rv, true); - } - - return true; + rv = stmt->GetInt32(5, &_place.frecency); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; } /* static */ size_t @@ -2500,6 +2673,74 @@ History::RemoveAllDownloads() //////////////////////////////////////////////////////////////////////////////// //// mozIAsyncHistory +NS_IMETHODIMP +History::GetPlacesInfo(const JS::Value& aPlaceIdentifiers, + mozIVisitInfoCallback* aCallback, + JSContext* aCtx) { + nsNavHistory* navHistory = nsNavHistory::GetHistoryService(); + NS_ABORT_IF_FALSE(navHistory, "Could not get nsNavHistory?!"); + + uint32_t placesIndentifiersLength; + JS::Rooted placesIndentifiers(aCtx); + nsresult rv = GetJSArrayFromJSValue(aPlaceIdentifiers, aCtx, + placesIndentifiers.address(), + &placesIndentifiersLength); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray placesInfo; + placesInfo.SetCapacity(placesIndentifiersLength); + for (uint32_t i = 0; i < placesIndentifiersLength; i++) { + JS::Value placeIdentifier; + JSBool rc = JS_GetElement(aCtx, placesIndentifiers, i, &placeIdentifier); + NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED); + + // GUID + nsAutoString fatGUID; + GetJSValueAsString(aCtx, placeIdentifier, fatGUID); + if (!fatGUID.IsVoid()) { + NS_ConvertUTF16toUTF8 guid(fatGUID); + if (!IsValidGUID(guid)) + return NS_ERROR_INVALID_ARG; + + VisitData& placeInfo = *placesInfo.AppendElement(VisitData()); + placeInfo.guid = guid; + } + else { + nsCOMPtr uri = GetJSValueAsURI(aCtx, placeIdentifier); + if (!uri) + return NS_ERROR_INVALID_ARG; // neither a guid, nor a uri. + *placesInfo.AppendElement(VisitData(uri)); + } + } + + mozIStorageConnection* dbConn = GetDBConn(); + NS_ENSURE_STATE(dbConn); + + for (nsTArray::size_type i = 0; i < placesInfo.Length(); i++) { + nsresult rv = GetPlaceInfo::Start(dbConn, placesInfo.ElementAt(i), aCallback); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Be sure to notify that all of our operations are complete. This + // is dispatched to the background thread first and redirected to the + // main thread from there to make sure that all database notifications + // and all embed or canAddURI notifications have finished. + if (aCallback) { + // NotifyCompletion does not hold a strong reference to the callback, + // so we have to manage it by AddRefing now. NotifyCompletion will + // release it for us once it has dispatched the callback to the main + // thread. + NS_ADDREF(aCallback); + + nsCOMPtr backgroundThread = do_GetInterface(dbConn); + NS_ENSURE_TRUE(backgroundThread, NS_ERROR_UNEXPECTED); + nsCOMPtr event = new NotifyCompletion(aCallback); + return backgroundThread->Dispatch(event, NS_DISPATCH_NORMAL); + } + + return NS_OK; +} + NS_IMETHODIMP History::UpdatePlaces(const JS::Value& aPlaceInfos, mozIVisitInfoCallback* aCallback, @@ -2508,22 +2749,10 @@ History::UpdatePlaces(const JS::Value& aPlaceInfos, NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED); NS_ENSURE_TRUE(!JSVAL_IS_PRIMITIVE(aPlaceInfos), NS_ERROR_INVALID_ARG); - uint32_t infosLength = 1; + uint32_t infosLength; JS::Rooted infos(aCtx); - if (JS_IsArrayObject(aCtx, aPlaceInfos.toObjectOrNull())) { - infos = aPlaceInfos.toObjectOrNull(); - (void)JS_GetArrayLength(aCtx, infos, &infosLength); - NS_ENSURE_ARG(infosLength > 0); - } - else { - // Build a temporary array to store this one item so the code below can - // just loop. - infos = JS_NewArrayObject(aCtx, 0, NULL); - NS_ENSURE_TRUE(infos, NS_ERROR_OUT_OF_MEMORY); - - JSBool rc = JS_DefineElement(aCtx, infos, 0, aPlaceInfos, NULL, NULL, 0); - NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED); - } + nsresult rv = GetJSArrayFromJSValue(aPlaceInfos, aCtx, infos.address(), &infosLength); + NS_ENSURE_SUCCESS(rv, rv); nsTArray visitData; for (uint32_t i = 0; i < infosLength; i++) { @@ -2643,7 +2872,7 @@ History::UpdatePlaces(const JS::Value& aPlaceInfos, nsCOMPtr backgroundThread = do_GetInterface(dbConn); NS_ENSURE_TRUE(backgroundThread, NS_ERROR_UNEXPECTED); nsCOMPtr event = new NotifyCompletion(aCallback); - (void)backgroundThread->Dispatch(event, NS_DISPATCH_NORMAL); + return backgroundThread->Dispatch(event, NS_DISPATCH_NORMAL); } return NS_OK; diff --git a/toolkit/components/places/History.h b/toolkit/components/places/History.h index a6eea95e688..91197b9b36f 100644 --- a/toolkit/components/places/History.h +++ b/toolkit/components/places/History.h @@ -73,9 +73,10 @@ public: * * @param _place * The VisitData for the place we need to know information about. - * @return true if the page was recorded in moz_places, false otherwise. + * @param [out] _exists + * Whether or the page was recorded in moz_places, false otherwise. */ - bool FetchPageInfo(VisitData& _place); + nsresult FetchPageInfo(VisitData& _place, bool* _exists); /** * Get the number of bytes of memory this History object is using, diff --git a/toolkit/components/places/PlaceInfo.cpp b/toolkit/components/places/PlaceInfo.cpp index e9ea95df826..a508fbaf80c 100644 --- a/toolkit/components/places/PlaceInfo.cpp +++ b/toolkit/components/places/PlaceInfo.cpp @@ -15,6 +15,21 @@ namespace places { //////////////////////////////////////////////////////////////////////////////// //// PlaceInfo +PlaceInfo::PlaceInfo(int64_t aId, + const nsCString& aGUID, + already_AddRefed aURI, + const nsString& aTitle, + int64_t aFrecency) +: mId(aId) +, mGUID(aGUID) +, mURI(aURI) +, mTitle(aTitle) +, mFrecency(aFrecency) +, mVisitsAvailable(false) +{ + NS_PRECONDITION(mURI, "Must provide a non-null uri!"); +} + PlaceInfo::PlaceInfo(int64_t aId, const nsCString& aGUID, already_AddRefed aURI, @@ -27,6 +42,7 @@ PlaceInfo::PlaceInfo(int64_t aId, , mTitle(aTitle) , mFrecency(aFrecency) , mVisits(aVisits) +, mVisitsAvailable(true) { NS_PRECONDITION(mURI, "Must provide a non-null uri!"); } @@ -73,6 +89,14 @@ NS_IMETHODIMP PlaceInfo::GetVisits(JSContext* aContext, JS::Value* _visits) { + // If the visits data was not provided, return null rather + // than an empty array to distinguish this case from the case + // of a place without any visit. + if (!mVisitsAvailable) { + *_visits = JSVAL_NULL; + return NS_OK; + } + // TODO bug 625913 when we use this in situations that have more than one // visit here, we will likely want to make this cache the value. JS::Rooted visits(aContext, JS_NewArrayObject(aContext, 0, NULL)); diff --git a/toolkit/components/places/PlaceInfo.h b/toolkit/components/places/PlaceInfo.h index 3e47c6da060..b9a7716a595 100644 --- a/toolkit/components/places/PlaceInfo.h +++ b/toolkit/components/places/PlaceInfo.h @@ -26,6 +26,8 @@ public: typedef nsTArray< nsCOMPtr > VisitsArray; + PlaceInfo(int64_t aId, const nsCString& aGUID, already_AddRefed aURI, + const nsString& aTitle, int64_t aFrecency); PlaceInfo(int64_t aId, const nsCString& aGUID, already_AddRefed aURI, const nsString& aTitle, int64_t aFrecency, const VisitsArray& aVisits); @@ -37,6 +39,7 @@ private: const nsString mTitle; const int64_t mFrecency; const VisitsArray mVisits; + bool mVisitsAvailable; }; } // namespace places diff --git a/toolkit/components/places/PlacesUtils.jsm b/toolkit/components/places/PlacesUtils.jsm index 2c69329a1d3..a00d7c59957 100644 --- a/toolkit/components/places/PlacesUtils.jsm +++ b/toolkit/components/places/PlacesUtils.jsm @@ -1754,6 +1754,56 @@ this.PlacesUtils = { deferred.resolve(charset); }, Ci.nsIThread.DISPATCH_NORMAL); + return deferred.promise; + }, + + /** + * Promised wrapper for mozIAsyncHistory::updatePlaces for a single place. + * + * @param aPlaces + * a single mozIPlaceInfo object + * @resolves {Promise} + */ + promiseUpdatePlace: function PU_promiseUpdatePlaces(aPlace) { + let deferred = Promise.defer(); + PlacesUtils.asyncHistory.updatePlaces(aPlace, { + _placeInfo: null, + handleResult: function handleResult(aPlaceInfo) { + this._placeInfo = aPlaceInfo; + }, + handleError: function handleError(aResultCode, aPlaceInfo) { + deferred.reject(new Components.Exception("Error", aResultCode)); + }, + handleCompletion: function() { + deferred.resolve(this._placeInfo); + } + }); + + return deferred.promise; + }, + + /** + * Promised wrapper for mozIAsyncHistory::getPlacesInfo for a single place. + * + * @param aPlaceIdentifier + * either an nsIURI or a GUID (@see getPlacesInfo) + * @resolves to the place info object handed to handleResult. + */ + promisePlaceInfo: function PU_promisePlaceInfo(aPlaceIdentifier) { + let deferred = Promise.defer(); + PlacesUtils.asyncHistory.getPlacesInfo(aPlaceIdentifier, { + _placeInfo: null, + handleResult: function handleResult(aPlaceInfo) { + this._placeInfo = aPlaceInfo; + }, + handleError: function handleError(aResultCode, aPlaceInfo) { + deferred.reject(new Components.Exception("Error", aResultCode)); + }, + handleCompletion: function() { + deferred.resolve(this._placeInfo); + } + }); + return deferred.promise; } }; diff --git a/toolkit/components/places/mozIAsyncHistory.idl b/toolkit/components/places/mozIAsyncHistory.idl index 63471682230..925a62aaec7 100644 --- a/toolkit/components/places/mozIAsyncHistory.idl +++ b/toolkit/components/places/mozIAsyncHistory.idl @@ -73,34 +73,34 @@ interface mozIPlaceInfo : nsISupports readonly attribute jsval visits; }; +/** + * Shared Callback interface for mozIAsyncHistory methods. The semantics + * for each method are detailed in mozIAsyncHistory. + */ [scriptable, uuid(1f266877-2859-418b-a11b-ec3ae4f4f93d)] interface mozIVisitInfoCallback : nsISupports { /** - * Called when the given mozIPlaceInfo object could not be processed. + * Called when the given place could not be processed. * * @param aResultCode * nsresult indicating the failure reason. * @param aPlaceInfo - * The information that was being entered into the database. + * The information that was given to the caller for the place. */ void handleError(in nsresult aResultCode, in mozIPlaceInfo aPlaceInfo); /** - * Called for each visit added, title change, or guid change when passed to - * mozIAsyncHistory::updatePlaces. If more than one operation is done for - * a given visit, only one callback will be given (i.e. title change and - * add visit). + * Called for each place processed successfully. * * @param aPlaceInfo - * The information that was being entered into the database. + * The current info stored for the place. */ void handleResult(in mozIPlaceInfo aPlaceInfo); /** - * Called when the mozIAsyncHistory::updatePlaces has finished processing - * all mozIPlaceInfo records. + * Called when all records were processed. */ void handleCompletion(); @@ -121,18 +121,48 @@ interface mozIVisitedStatusCallback : nsISupports in boolean aVisitedStatus); }; -[scriptable, uuid(b7edc16e-9f3c-4bf5-981b-4e8000b02d89)] +[scriptable, uuid(1643EFD2-A329-4733-A39D-17069C8D3B2D)] interface mozIAsyncHistory : nsISupports { + /** + * Gets the available information for the given array of places, each + * identified by either nsIURI or places GUID (string). + * + * The retrieved places info objects DO NOT include the visits data (the + * |visits| attribute is set to null). + * + * If a given place does not exist in the database, aCallback.handleError is + * called for it with NS_ERROR_NOT_AVAILABLE result code. + * + * @param aPlaceIdentifiers + * The place[s] for which to retrieve information, identified by either + * a single place GUID, a single URI, or a JS array of URIs and/or GUIDs. + * @param aCallback + * A mozIVisitInfoCallback object which consists of callbacks to be + * notified for successful or failed retrievals. + * If there's no information available for a given place, aCallback + * is called with a stub place info object, containing just the provided + * data (GUID or URI). + * + * @throws NS_ERROR_INVALID_ARG + * - Passing in NULL for aPlaceIdentifiers or aCallback. + * - Not providing at least one valid GUID or URI. + */ + [implicit_jscontext] + void getPlacesInfo(in jsval aPlaceIdentifiers, + in mozIVisitInfoCallback aCallback); + /** * Adds a set of visits for one or more mozIPlaceInfo objects, and updates * each mozIPlaceInfo's title or guid. * + * aCallback.handleResult is called for each visit added. + * * @param aPlaceInfo * The mozIPlaceInfo object[s] containing the information to store or * update. This can be a single object, or an array of objects. * @param [optional] aCallback - * A mozIVisitInfoCallback object which consists of callbacks to be + * A mozIVisitInfoCallback object which consists of callbacks to be * notified for successful and/or failed changes. * * @throws NS_ERROR_INVALID_ARG diff --git a/toolkit/components/places/nsINavHistoryService.idl b/toolkit/components/places/nsINavHistoryService.idl index 60c1c046dd6..26537c88d3f 100644 --- a/toolkit/components/places/nsINavHistoryService.idl +++ b/toolkit/components/places/nsINavHistoryService.idl @@ -1241,6 +1241,7 @@ interface nsINavHistoryService : nsISupports /** * Gets the original title of the page. + * @deprecated use mozIAsyncHistory.getPlacesInfo instead. */ AString getPageTitle(in nsIURI aURI); diff --git a/toolkit/components/places/nsNavHistory.cpp b/toolkit/components/places/nsNavHistory.cpp index 8578a52ddcf..c0a4302e5e7 100644 --- a/toolkit/components/places/nsNavHistory.cpp +++ b/toolkit/components/places/nsNavHistory.cpp @@ -2828,6 +2828,8 @@ nsNavHistory::GetCharsetForURI(nsIURI* aURI, NS_IMETHODIMP nsNavHistory::GetPageTitle(nsIURI* aURI, nsAString& aTitle) { + PLACES_WARN_DEPRECATED(); + NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread"); NS_ENSURE_ARG(aURI); diff --git a/toolkit/components/places/tests/unit/test_getPlacesInfo.js b/toolkit/components/places/tests/unit/test_getPlacesInfo.js new file mode 100644 index 00000000000..3551a51e915 --- /dev/null +++ b/toolkit/components/places/tests/unit/test_getPlacesInfo.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function promiseGetPlacesInfo(aPlacesIdentifiers) { + let deferred = Promise.defer(); + PlacesUtils.asyncHistory.getPlacesInfo(aPlacesIdentifiers, { + _results: [], + _errors: [], + + handleResult: function handleResult(aPlaceInfo) { + this._results.push(aPlaceInfo); + }, + handleError: function handleError(aResultCode, aPlaceInfo) { + this._errors.push({ resultCode: aResultCode, info: aPlaceInfo }); + }, + handleCompletion: function handleCompletion() { + deferred.resolve({ errors: this._errors, results: this._results }); + } + }); + + return deferred.promise; +} + +function ensurePlacesInfoObjectsAreEqual(a, b) { + do_check_true(a.uri.equals(b.uri)); + do_check_eq(a.title, b.title); + do_check_eq(a.guid, b.guid); + do_check_eq(a.placeId, b.placeId); +} + +function test_getPlacesInfoExistentPlace() { + let testURI = NetUtil.newURI("http://www.example.tld"); + yield promiseAddVisits(testURI); + + let getPlacesInfoResult = yield promiseGetPlacesInfo([testURI]); + do_check_eq(getPlacesInfoResult.results.length, 1); + do_check_eq(getPlacesInfoResult.errors.length, 0); + + let placeInfo = getPlacesInfoResult.results[0]; + do_check_true(placeInfo instanceof Ci.mozIPlaceInfo); + + do_check_true(placeInfo.uri.equals(testURI)); + do_check_eq(placeInfo.title, "test visit for " + testURI.spec); + do_check_true(placeInfo.guid.length > 0); + do_check_eq(placeInfo.visits, null); +} +add_task(test_getPlacesInfoExistentPlace); + +function test_getPlacesInfoNonExistentPlace() { + let testURI = NetUtil.newURI("http://www.example_non_existent.tld"); + let getPlacesInfoResult = yield promiseGetPlacesInfo(testURI); + do_check_eq(getPlacesInfoResult.results.length, 0); + do_check_eq(getPlacesInfoResult.errors.length, 1); +} +add_task(test_getPlacesInfoNonExistentPlace); + +function test_promisedHelper() { + let (uri = NetUtil.newURI("http://www.helper_existent_example.tld")) { + yield promiseAddVisits(uri); + let placeInfo = yield PlacesUtils.promisePlaceInfo(uri); + do_check_true(placeInfo instanceof Ci.mozIPlaceInfo); + }; + + let (uri = NetUtil.newURI("http://www.helper_non_existent_example.tld")) { + try { + let placeInfo = yield PlacesUtils.promisePlaceInfo(uri); + do_throw("PlacesUtils.promisePlaceInfo should have rejected the promise"); + } + catch(ex) { } + }; +} +add_task(test_promisedHelper); + +function test_infoByGUID() { + let testURI = NetUtil.newURI("http://www.guid_example.tld"); + yield promiseAddVisits(testURI); + + let placeInfoByURI = yield PlacesUtils.promisePlaceInfo(testURI); + let placeInfoByGUID = yield PlacesUtils.promisePlaceInfo(placeInfoByURI.guid); + ensurePlacesInfoObjectsAreEqual(placeInfoByURI, placeInfoByGUID); +} +add_task(test_infoByGUID); + +function test_invalid_guid() { + try { + let placeInfoByGUID = yield PlacesUtils.promisePlaceInfo("###"); + do_throw("getPlacesInfo should fail for invalid guids") + } + catch(ex) { } +} +add_task(test_invalid_guid); + +function test_mixed_selection() { + let placeInfo1, placeInfo2; + let (uri = NetUtil.newURI("http://www.mixed_selection_test_1.tld")) { + yield promiseAddVisits(uri); + placeInfo1 = yield PlacesUtils.promisePlaceInfo(uri); + }; + + let (uri = NetUtil.newURI("http://www.mixed_selection_test_2.tld")) { + yield promiseAddVisits(uri); + placeInfo2 = yield PlacesUtils.promisePlaceInfo(uri); + }; + + let getPlacesInfoResult = yield promiseGetPlacesInfo([placeInfo1.uri, placeInfo2.guid]); + do_check_eq(getPlacesInfoResult.results.length, 2); + do_check_eq(getPlacesInfoResult.errors.length, 0); + + do_check_eq(getPlacesInfoResult.results[0].uri.spec, placeInfo1.uri.spec); + do_check_eq(getPlacesInfoResult.results[1].guid, placeInfo2.guid); +} +add_task(test_mixed_selection); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/components/places/tests/unit/xpcshell.ini b/toolkit/components/places/tests/unit/xpcshell.ini index 28ce39308ca..cc6fe4d9794 100644 --- a/toolkit/components/places/tests/unit/xpcshell.ini +++ b/toolkit/components/places/tests/unit/xpcshell.ini @@ -123,3 +123,4 @@ skip-if = os == "android" [test_PlacesUtils_lazyobservers.js] [test_placesTxn.js] [test_telemetry.js] +[test_getPlacesInfo.js]