Bug 1618535 - Implement preloading for as="fetch" type r=kershaw,baku

Depends on D69860

Differential Revision: https://phabricator.services.mozilla.com/D69628
This commit is contained in:
Honza Bambas 2020-05-11 14:12:33 +00:00
Родитель 3f250fba44
Коммит 9e027ce534
14 изменённых файлов: 1490 добавлений и 2 удалений

Просмотреть файл

@ -121,6 +121,10 @@ void nsHtml5SpeculativeLoad::Perform(nsHtml5TreeOpExecutor* aExecutor) {
case eSpeculativeLoadPreconnect:
aExecutor->Preconnect(mUrlOrSizes, mCrossOriginOrMedia);
break;
case eSpeculativeLoadFetch:
aExecutor->PreloadFetch(mUrlOrSizes, mCrossOriginOrMedia,
mReferrerPolicyOrIntegrity);
break;
default:
MOZ_ASSERT_UNREACHABLE("Bogus speculative load.");
break;

Просмотреть файл

@ -30,7 +30,8 @@ enum eHtml5SpeculativeLoad {
eSpeculativeLoadManifest,
eSpeculativeLoadSetDocumentCharset,
eSpeculativeLoadSetDocumentMode,
eSpeculativeLoadPreconnect
eSpeculativeLoadPreconnect,
eSpeculativeLoadFetch
};
class nsHtml5SpeculativeLoad {
@ -91,6 +92,25 @@ class nsHtml5SpeculativeLoad {
mIsLinkPreload = aLinkPreload;
}
inline void InitFetch(nsHtml5String aUrl, nsHtml5String aCrossOrigin,
nsHtml5String aReferrerPolicy) {
MOZ_ASSERT(mOpCode == eSpeculativeLoadUninitialized,
"Trying to reinitialize a speculative load!");
mOpCode = eSpeculativeLoadFetch;
aUrl.ToString(mUrlOrSizes);
aCrossOrigin.ToString(mCrossOriginOrMedia);
nsString
referrerPolicy; // Not Auto, because using it to hold nsStringBuffer*
aReferrerPolicy.ToString(referrerPolicy);
mReferrerPolicyOrIntegrity.Assign(
nsContentUtils::TrimWhitespace<nsContentUtils::IsHTMLWhitespace>(
referrerPolicy));
// This method can be only be triggered by <link rel=preload type=fetch>,
// hence this operation is always a preload.
mIsLinkPreload = true;
}
// <picture> elements have multiple <source> nodes followed by an <img>,
// where we use the first valid source, which may be the img. Because we
// can't determine validity at this point without parsing CSS and getting

Просмотреть файл

@ -313,6 +313,9 @@ nsIContentHandle* nsHtml5TreeBuilder::createElement(
nsHtml5AttributeName::ATTR_IMAGESIZES);
mSpeculativeLoadQueue.AppendElement()->InitImage(
url, crossOrigin, referrerPolicy, srcset, sizes, true);
} else if (as.LowerCaseEqualsASCII("fetch")) {
mSpeculativeLoadQueue.AppendElement()->InitFetch(
url, crossOrigin, referrerPolicy);
}
// Other "as" values will be supported later.
}

Просмотреть файл

@ -1031,6 +1031,17 @@ void nsHtml5TreeOpExecutor::PreloadPictureSource(const nsAString& aSrcset,
mDocument->PreloadPictureImageSource(aSrcset, aSizes, aType, aMedia);
}
void nsHtml5TreeOpExecutor::PreloadFetch(const nsAString& aURL,
const nsAString& aCrossOrigin,
const nsAString& aReferrerPolicy) {
nsCOMPtr<nsIURI> uri = ConvertIfNotPreloadedYet(aURL);
if (!uri) {
return;
}
mDocument->Preloads().PreloadFetch(uri, aCrossOrigin, aReferrerPolicy);
}
void nsHtml5TreeOpExecutor::PreloadOpenPicture() {
mDocument->PreloadPictureOpened();
}

Просмотреть файл

@ -252,6 +252,9 @@ class nsHtml5TreeOpExecutor final
void PreloadPictureSource(const nsAString& aSrcset, const nsAString& aSizes,
const nsAString& aType, const nsAString& aMedia);
void PreloadFetch(const nsAString& aURL, const nsAString& aCrossOrigin,
const nsAString& aReferrerPolicy);
void SetSpeculationBase(const nsAString& aURL);
void UpdateReferrerInfoFromMeta(const nsAString& aMetaReferrer);

Просмотреть файл

@ -0,0 +1,327 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
*
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "FetchPreloader.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/dom/Document.h"
#include "mozilla/LoadInfo.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Unused.h"
#include "nsContentPolicyUtils.h"
#include "nsContentUtils.h"
#include "nsIChannel.h"
#include "nsIClassOfService.h"
#include "nsIHttpChannel.h"
#include "nsITimedChannel.h"
#include "nsNetUtil.h"
#include "nsStringStream.h"
namespace mozilla {
NS_IMPL_ISUPPORTS(FetchPreloader, nsIStreamListener, nsIRequestObserver)
FetchPreloader::FetchPreloader()
: FetchPreloader(nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST) {}
FetchPreloader::FetchPreloader(nsContentPolicyType aContentPolicyType)
: mContentPolicyType(aContentPolicyType) {}
nsresult FetchPreloader::OpenChannel(PreloadHashKey* aKey, nsIURI* aURI,
const CORSMode aCORSMode,
const dom::ReferrerPolicy& aReferrerPolicy,
dom::Document* aDocument) {
nsresult rv;
nsCOMPtr<nsIChannel> channel;
auto notify = MakeScopeExit([&]() {
if (NS_WARN_IF(NS_FAILED(rv)) && channel) {
// Make sure we notify any <link preload> elements when opening because of
// various technical or security reasons fails.
NotifyStart(channel);
NotifyStop(channel, rv);
}
});
rv = CheckContentPolicy(aURI, aDocument);
if (NS_FAILED(rv)) {
return rv;
}
nsCOMPtr<nsILoadGroup> loadGroup = aDocument->GetDocumentLoadGroup();
nsCOMPtr<nsPIDOMWindowOuter> window = aDocument->GetWindow();
nsCOMPtr<nsIInterfaceRequestor> prompter;
if (window) {
nsIDocShell* docshell = window->GetDocShell();
prompter = do_QueryInterface(docshell);
}
rv = CreateChannel(getter_AddRefs(channel), aURI, aCORSMode, aReferrerPolicy,
aDocument, loadGroup, prompter);
NS_ENSURE_SUCCESS(rv, rv);
PrioritizeAsPreload(channel);
AddLoadBackgroundFlag(channel);
NotifyOpen(aKey, channel, aDocument, true);
return mAsyncConsumeResult = rv = channel->AsyncOpen(this);
}
nsresult FetchPreloader::CreateChannel(
nsIChannel** aChannel, nsIURI* aURI, const CORSMode aCORSMode,
const dom::ReferrerPolicy& aReferrerPolicy, dom::Document* aDocument,
nsILoadGroup* aLoadGroup, nsIInterfaceRequestor* aCallbacks) {
nsresult rv;
nsSecurityFlags securityFlags =
aCORSMode == CORS_NONE ? nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL
: nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS;
if (aCORSMode == CORS_ANONYMOUS) {
securityFlags |= nsILoadInfo::SEC_COOKIES_SAME_ORIGIN;
} else if (aCORSMode == CORS_USE_CREDENTIALS) {
securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE;
}
nsCOMPtr<nsIChannel> channel;
rv = NS_NewChannelWithTriggeringPrincipal(
getter_AddRefs(channel), aURI, aDocument, aDocument->NodePrincipal(),
securityFlags, nsIContentPolicy::TYPE_FETCH, nullptr, aLoadGroup,
aCallbacks);
if (NS_FAILED(rv)) {
return rv;
}
if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(channel)) {
nsCOMPtr<nsIReferrerInfo> referrerInfo = new dom::ReferrerInfo(
aDocument->GetDocumentURIAsReferrer(), aReferrerPolicy);
rv = httpChannel->SetReferrerInfoWithoutClone(referrerInfo);
MOZ_ASSERT(NS_SUCCEEDED(rv));
}
if (nsCOMPtr<nsITimedChannel> timedChannel = do_QueryInterface(channel)) {
timedChannel->SetInitiatorType(NS_LITERAL_STRING("link"));
}
channel.forget(aChannel);
return NS_OK;
}
nsresult FetchPreloader::CheckContentPolicy(nsIURI* aURI,
dom::Document* aDocument) {
if (!aDocument) {
return NS_OK;
}
nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new net::LoadInfo(
aDocument->NodePrincipal(), aDocument->NodePrincipal(), aDocument,
nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, mContentPolicyType);
int16_t shouldLoad = nsIContentPolicy::ACCEPT;
nsresult rv = NS_CheckContentLoadPolicy(aURI, secCheckLoadInfo,
EmptyCString(), &shouldLoad,
nsContentUtils::GetContentPolicy());
if (NS_SUCCEEDED(rv) && NS_CP_ACCEPTED(shouldLoad)) {
return NS_OK;
}
return NS_ERROR_CONTENT_BLOCKED;
}
// PreloaderBase
nsresult FetchPreloader::AsyncConsume(nsIStreamListener* aListener) {
if (NS_FAILED(mAsyncConsumeResult)) {
// Already being consumed or failed to open.
return mAsyncConsumeResult;
}
// Prevent duplicate calls.
mAsyncConsumeResult = NS_ERROR_NOT_AVAILABLE;
if (!mConsumeListener) {
// Called before we are getting response from the channel.
mConsumeListener = aListener;
} else {
// Channel already started, push cached calls to this listener.
// Can't be anything else than the `Cache`, hence a safe static_cast.
Cache* cache = static_cast<Cache*>(mConsumeListener.get());
cache->AsyncConsume(aListener);
}
return NS_OK;
}
// static
void FetchPreloader::PrioritizeAsPreload(nsIChannel* aChannel) {
if (nsCOMPtr<nsIClassOfService> cos = do_QueryInterface(aChannel)) {
cos->AddClassFlags(nsIClassOfService::Unblocked);
}
}
void FetchPreloader::PrioritizeAsPreload() { PrioritizeAsPreload(Channel()); }
// nsIRequestObserver + nsIStreamListener
NS_IMETHODIMP FetchPreloader::OnStartRequest(nsIRequest* request) {
NotifyStart(request);
if (!mConsumeListener) {
// AsyncConsume not called yet.
mConsumeListener = new Cache();
}
return mConsumeListener->OnStartRequest(request);
}
NS_IMETHODIMP FetchPreloader::OnDataAvailable(nsIRequest* request,
nsIInputStream* input,
uint64_t offset, uint32_t count) {
return mConsumeListener->OnDataAvailable(request, input, offset, count);
}
NS_IMETHODIMP FetchPreloader::OnStopRequest(nsIRequest* request,
nsresult status) {
mConsumeListener->OnStopRequest(request, status);
// We want 404 or other types of server responses to be treated as 'error'.
if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(request)) {
uint32_t responseStatus = 0;
Unused << httpChannel->GetResponseStatus(&responseStatus);
if (responseStatus / 100 != 2) {
status = NS_ERROR_FAILURE;
}
}
NotifyStop(request, status);
return NS_OK;
}
// FetchPreloader::Cache
NS_IMPL_ISUPPORTS(FetchPreloader::Cache, nsIStreamListener, nsIRequestObserver)
NS_IMETHODIMP FetchPreloader::Cache::OnStartRequest(nsIRequest* request) {
mRequest = request;
if (mFinalListener) {
return mFinalListener->OnStartRequest(mRequest);
}
mCalls.AppendElement(Call{VariantIndex<0>{}, StartRequest{}});
return NS_OK;
}
NS_IMETHODIMP FetchPreloader::Cache::OnDataAvailable(nsIRequest* request,
nsIInputStream* input,
uint64_t offset,
uint32_t count) {
if (mFinalListener) {
return mFinalListener->OnDataAvailable(mRequest, input, offset, count);
}
DataAvailable data;
if (!data.mData.SetLength(count, fallible)) {
return NS_ERROR_OUT_OF_MEMORY;
}
uint32_t read;
nsresult rv = input->Read(data.mData.BeginWriting(), count, &read);
if (NS_FAILED(rv)) {
return rv;
}
mCalls.AppendElement(Call{VariantIndex<1>{}, std::move(data)});
return NS_OK;
}
NS_IMETHODIMP FetchPreloader::Cache::OnStopRequest(nsIRequest* request,
nsresult status) {
if (mFinalListener) {
return mFinalListener->OnStopRequest(mRequest, status);
}
mCalls.AppendElement(Call{VariantIndex<2>{}, StopRequest{status}});
return NS_OK;
}
void FetchPreloader::Cache::AsyncConsume(nsIStreamListener* aListener) {
// Must dispatch for two reasons:
// 1. This is called directly from FetchLoader::AsyncConsume, which must
// behave the same way as nsIChannel::AsyncOpen.
// 2. In case there are already stream listener events scheduled on the main
// thread we preserve the order - those will still end up in Cache.
// * The `Cache` object is fully main thread only for now, doesn't support
// retargeting, but it can be improved to allow it.
nsCOMPtr<nsIStreamListener> listener(aListener);
NS_DispatchToMainThread(NewRunnableMethod<nsCOMPtr<nsIStreamListener>>(
"FetchPreloader::Cache::Consume", this, &FetchPreloader::Cache::Consume,
listener));
}
void FetchPreloader::Cache::Consume(nsCOMPtr<nsIStreamListener> aListener) {
MOZ_ASSERT(!mFinalListener, "Duplicate call");
mFinalListener = std::move(aListener);
// Status of the channel read after each call.
nsresult status = NS_OK;
nsCOMPtr<nsIChannel> channel(do_QueryInterface(mRequest));
RefPtr<Cache> self(this);
for (auto& call : mCalls) {
nsresult rv = call.match(
[&](const StartRequest& startRequest) mutable {
return self->mFinalListener->OnStartRequest(self->mRequest);
},
[&](const DataAvailable& dataAvailable) mutable {
if (NS_FAILED(status)) {
// Channel has been cancelled during this mCalls loop.
return NS_OK;
}
nsCOMPtr<nsIInputStream> input;
rv = NS_NewCStringInputStream(getter_AddRefs(input),
dataAvailable.mData);
if (NS_FAILED(rv)) {
return rv;
}
return self->mFinalListener->OnDataAvailable(
self->mRequest, input, 0, dataAvailable.mData.Length());
},
[&](const StopRequest& stopRequest) {
// First cancellation overrides mStatus in nsHttpChannel.
nsresult stopStatus =
NS_FAILED(status) ? status : stopRequest.mStatus;
self->mFinalListener->OnStopRequest(self->mRequest, stopStatus);
self->mFinalListener = nullptr;
self->mRequest = nullptr;
return NS_OK;
});
if (!mRequest) {
// We are done!
break;
}
bool isCancelled = false;
Unused << channel->GetCanceled(&isCancelled);
if (isCancelled) {
mRequest->GetStatus(&status);
} else if (NS_FAILED(rv)) {
status = rv;
mRequest->Cancel(status);
}
}
mCalls.Clear();
}
} // namespace mozilla

Просмотреть файл

@ -0,0 +1,93 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef FetchPreloader_h_
#define FetchPreloader_h_
#include "mozilla/PreloaderBase.h"
#include "nsCOMPtr.h"
#include "nsIAsyncOutputStream.h"
#include "nsIAsyncInputStream.h"
#include "nsIStreamListener.h"
class nsIChannel;
class nsILoadGroup;
class nsIInterfaceRequestor;
namespace mozilla {
class FetchPreloader : public PreloaderBase, public nsIStreamListener {
NS_DECL_ISUPPORTS
NS_DECL_NSIREQUESTOBSERVER
NS_DECL_NSISTREAMLISTENER
FetchPreloader();
nsresult OpenChannel(PreloadHashKey* aKey, nsIURI* aURI,
const CORSMode aCORSMode,
const dom::ReferrerPolicy& aReferrerPolicy,
dom::Document* aDocument);
// PreloaderBase
nsresult AsyncConsume(nsIStreamListener* aListener) override;
static void PrioritizeAsPreload(nsIChannel* aChannel);
void PrioritizeAsPreload() override;
protected:
explicit FetchPreloader(nsContentPolicyType aContentPolicyType);
virtual ~FetchPreloader() = default;
// Create and setup the channel with necessary security properties. This is
// overridable by subclasses to allow different initial conditions.
virtual nsresult CreateChannel(nsIChannel** aChannel, nsIURI* aURI,
const CORSMode aCORSMode,
const dom::ReferrerPolicy& aReferrerPolicy,
dom::Document* aDocument,
nsILoadGroup* aLoadGroup,
nsIInterfaceRequestor* aCallbacks);
private:
nsresult CheckContentPolicy(nsIURI* aURI, dom::Document* aDocument);
class Cache final : public nsIStreamListener {
NS_DECL_ISUPPORTS
NS_DECL_NSIREQUESTOBSERVER
NS_DECL_NSISTREAMLISTENER
void AsyncConsume(nsIStreamListener* aListener);
void Consume(nsCOMPtr<nsIStreamListener> aListener);
private:
virtual ~Cache() = default;
struct StartRequest {};
struct DataAvailable {
nsCString mData;
};
struct StopRequest {
nsresult mStatus;
};
typedef Variant<StartRequest, DataAvailable, StopRequest> Call;
nsCOMPtr<nsIRequest> mRequest;
nsCOMPtr<nsIStreamListener> mFinalListener;
nsTArray<Call> mCalls;
};
// The listener passed to AsyncConsume in case we haven't started getting the
// data from the channel yet.
nsCOMPtr<nsIStreamListener> mConsumeListener;
// Returned by AsyncConsume when a failure. This remembers the result of
// opening the channel and prevents duplicate consumation.
nsresult mAsyncConsumeResult = NS_ERROR_NOT_AVAILABLE;
// The CP type to check against. Derived classes have to override call to CSP
// constructor of this class.
nsContentPolicyType mContentPolicyType;
};
} // namespace mozilla
#endif

Просмотреть файл

@ -135,6 +135,25 @@ PreloadHashKey PreloadHashKey::CreateAsImage(
return key;
}
// static
PreloadHashKey PreloadHashKey::CreateAsFetch(
nsIURI* aURI, const CORSMode aCORSMode,
const dom::ReferrerPolicy& aReferrerPolicy) {
PreloadHashKey key(aURI, ResourceType::FETCH);
key.mReferrerPolicy = aReferrerPolicy;
key.mCORSMode = aCORSMode;
return key;
}
// static
PreloadHashKey PreloadHashKey::CreateAsFetch(
nsIURI* aURI, const nsAString& aCrossOrigin,
const dom::ReferrerPolicy& aReferrerPolicy) {
return CreateAsFetch(aURI, dom::Element::StringToCORSMode(aCrossOrigin),
aReferrerPolicy);
}
bool PreloadHashKey::KeyEquals(KeyTypePointer aOther) const {
if (mAs != aOther->mAs || mCORSMode != aOther->mCORSMode ||
mReferrerPolicy != aOther->mReferrerPolicy) {
@ -181,6 +200,7 @@ bool PreloadHashKey::KeyEquals(KeyTypePointer aOther) const {
case ResourceType::FONT:
break;
case ResourceType::FETCH:
// No more checks needed.
break;
case ResourceType::NONE:
break;

Просмотреть файл

@ -61,9 +61,16 @@ class PreloadHashKey : public nsURIHashKey {
nsIURI* aURI, nsIPrincipal* aPrincipal, CORSMode aCORSMode,
dom::ReferrerPolicy const& aReferrerPolicy);
// Construct key for "fetch"
static PreloadHashKey CreateAsFetch(
nsIURI* aURI, const CORSMode aCORSMode,
const dom::ReferrerPolicy& aReferrerPolicy);
static PreloadHashKey CreateAsFetch(
nsIURI* aURI, const nsAString& aCrossOrigin,
const dom::ReferrerPolicy& aReferrerPolicy);
// TODO
// static CreateAsFont(...);
// static CreateAsFetch(...);
KeyType GetKey() const { return const_cast<PreloadHashKey*>(this); }
KeyTypePointer GetKeyPointer() const { return this; }

Просмотреть файл

@ -5,10 +5,12 @@
#include "PreloadService.h"
#include "FetchPreloader.h"
#include "mozilla/AsyncEventDispatcher.h"
#include "mozilla/dom/HTMLLinkElement.h"
#include "mozilla/dom/ScriptLoader.h"
#include "mozilla/StaticPrefs_network.h"
#include "nsIReferrerInfo.h"
#include "nsNetUtil.h"
namespace mozilla {
@ -112,6 +114,9 @@ already_AddRefed<PreloaderBase> PreloadService::PreloadLinkElement(
preloadKey = PreloadHashKey::CreateAsImage(
uri, mDocument->NodePrincipal(),
dom::Element::StringToCORSMode(crossOrigin), referrerPolicy);
} else if (as.LowerCaseEqualsASCII("fetch")) {
preloadKey = PreloadHashKey::CreateAsFetch(
uri, dom::Element::StringToCORSMode(crossOrigin), referrerPolicy);
} else {
NotifyNodeEvent(aLinkElement, false);
return nullptr;
@ -126,6 +131,8 @@ already_AddRefed<PreloaderBase> PreloadService::PreloadLinkElement(
PreloadStyle(uri, charset, crossOrigin, referrerPolicyAttr, integrity);
} else if (as.LowerCaseEqualsASCII("image")) {
PreloadImage(uri, crossOrigin, referrerPolicyAttr, isImgSet);
} else if (as.LowerCaseEqualsASCII("fetch")) {
PreloadFetch(uri, crossOrigin, referrerPolicyAttr);
}
preload = LookupPreload(&preloadKey);
@ -168,6 +175,19 @@ void PreloadService::PreloadImage(nsIURI* aURI, const nsAString& aCrossOrigin,
aIsImgSet, true);
}
void PreloadService::PreloadFetch(nsIURI* aURI, const nsAString& aCrossOrigin,
const nsAString& aReferrerPolicy) {
CORSMode cors = dom::Element::StringToCORSMode(aCrossOrigin);
dom::ReferrerPolicy referrerPolicy = PreloadReferrerPolicy(aReferrerPolicy);
auto key = PreloadHashKey::CreateAsFetch(aURI, cors, referrerPolicy);
// * Bug 1618549: Depending on where we decide to do the deduplication, we may
// want to check if a fetch is already being preloaded here.
RefPtr<FetchPreloader> preloader = new FetchPreloader();
preloader->OpenChannel(&key, aURI, cors, referrerPolicy, mDocument);
}
// static
void PreloadService::NotifyNodeEvent(nsINode* aNode, bool aSuccess) {
if (!aNode->IsInComposedDoc()) {

Просмотреть файл

@ -75,6 +75,9 @@ class PreloadService {
void PreloadImage(nsIURI* aURI, const nsAString& aCrossOrigin,
const nsAString& aImageReferrerPolicy, bool aIsImgSet);
void PreloadFetch(nsIURI* aURI, const nsAString& aCrossOrigin,
const nsAString& aReferrerPolicy);
static void NotifyNodeEvent(nsINode* aNode, bool aSuccess);
private:

Просмотреть файл

@ -0,0 +1,952 @@
#include "gtest/gtest.h"
#include "mozilla/CORSMode.h"
#include "mozilla/dom/XMLDocument.h"
#include "mozilla/dom/ReferrerPolicyBinding.h"
#include "mozilla/FetchPreloader.h"
#include "mozilla/Maybe.h"
#include "mozilla/PreloadHashKey.h"
#include "nsNetUtil.h"
#include "nsIChannel.h"
#include "nsIStreamListener.h"
#include "nsThreadUtils.h"
#include "nsStringStream.h"
namespace {
auto const ERROR_CANCEL = NS_ERROR_ABORT;
auto const ERROR_ONSTOP = NS_ERROR_NET_INTERRUPT;
auto const ERROR_THROW = NS_ERROR_FAILURE;
class FakeChannel : public nsIChannel {
public:
NS_DECL_ISUPPORTS
NS_DECL_NSICHANNEL
NS_DECL_NSIREQUEST
nsresult Start() { return mListener->OnStartRequest(this); }
nsresult Data(const nsACString& aData) {
if (NS_FAILED(mStatus)) {
return mStatus;
}
nsCOMPtr<nsIInputStream> is;
NS_NewCStringInputStream(getter_AddRefs(is), aData);
return mListener->OnDataAvailable(this, is, 0, aData.Length());
}
nsresult Stop(nsresult status) {
if (NS_SUCCEEDED(mStatus)) {
mStatus = status;
}
mListener->OnStopRequest(this, mStatus);
mListener = nullptr;
return mStatus;
}
private:
virtual ~FakeChannel() = default;
bool mIsPending = false;
bool mCanceled = false;
nsresult mStatus = NS_OK;
nsCOMPtr<nsIStreamListener> mListener;
};
NS_IMPL_ISUPPORTS(FakeChannel, nsIChannel, nsIRequest)
NS_IMETHODIMP FakeChannel::GetName(nsACString& result) { return NS_OK; }
NS_IMETHODIMP FakeChannel::IsPending(bool* result) {
*result = mIsPending;
return NS_OK;
}
NS_IMETHODIMP FakeChannel::GetStatus(nsresult* status) {
*status = mStatus;
return NS_OK;
}
NS_IMETHODIMP FakeChannel::Cancel(nsresult status) {
if (!mCanceled) {
mCanceled = true;
mStatus = status;
}
return NS_OK;
}
NS_IMETHODIMP FakeChannel::Suspend() { return NS_OK; }
NS_IMETHODIMP FakeChannel::Resume() { return NS_OK; }
NS_IMETHODIMP FakeChannel::GetLoadFlags(nsLoadFlags* aLoadFlags) {
*aLoadFlags = 0;
return NS_OK;
}
NS_IMETHODIMP FakeChannel::SetLoadFlags(nsLoadFlags aLoadFlags) {
return NS_OK;
}
NS_IMETHODIMP FakeChannel::GetTRRMode(nsIRequest::TRRMode* aTRRMode) {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP FakeChannel::SetTRRMode(nsIRequest::TRRMode aTRRMode) {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP FakeChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) {
return NS_OK;
}
NS_IMETHODIMP FakeChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) {
return NS_OK;
}
NS_IMETHODIMP FakeChannel::GetOriginalURI(nsIURI** aURI) { return NS_OK; }
NS_IMETHODIMP FakeChannel::SetOriginalURI(nsIURI* aURI) { return NS_OK; }
NS_IMETHODIMP FakeChannel::GetURI(nsIURI** aURI) { return NS_OK; }
NS_IMETHODIMP FakeChannel::GetOwner(nsISupports** aOwner) { return NS_OK; }
NS_IMETHODIMP FakeChannel::SetOwner(nsISupports* aOwner) { return NS_OK; }
NS_IMETHODIMP FakeChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) { return NS_OK; }
NS_IMETHODIMP FakeChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) {
return NS_OK;
}
NS_IMETHODIMP FakeChannel::GetIsDocument(bool* aIsDocument) {
*aIsDocument = false;
return NS_OK;
}
NS_IMETHODIMP FakeChannel::GetNotificationCallbacks(
nsIInterfaceRequestor** aCallbacks) {
return NS_OK;
}
NS_IMETHODIMP FakeChannel::SetNotificationCallbacks(
nsIInterfaceRequestor* aCallbacks) {
return NS_OK;
}
NS_IMETHODIMP FakeChannel::GetSecurityInfo(nsISupports** aSecurityInfo) {
return NS_OK;
}
NS_IMETHODIMP FakeChannel::GetContentType(nsACString& aContentType) {
return NS_OK;
}
NS_IMETHODIMP FakeChannel::SetContentType(const nsACString& aContentType) {
return NS_OK;
}
NS_IMETHODIMP FakeChannel::GetContentCharset(nsACString& aContentCharset) {
return NS_OK;
}
NS_IMETHODIMP FakeChannel::SetContentCharset(
const nsACString& aContentCharset) {
return NS_OK;
}
NS_IMETHODIMP FakeChannel::GetContentDisposition(
uint32_t* aContentDisposition) {
return NS_OK;
}
NS_IMETHODIMP FakeChannel::SetContentDisposition(uint32_t aContentDisposition) {
return NS_OK;
}
NS_IMETHODIMP FakeChannel::GetContentDispositionFilename(
nsAString& aContentDispositionFilename) {
return NS_OK;
}
NS_IMETHODIMP FakeChannel::SetContentDispositionFilename(
const nsAString& aContentDispositionFilename) {
return NS_OK;
}
NS_IMETHODIMP FakeChannel::GetContentDispositionHeader(
nsACString& aContentDispositionHeader) {
return NS_ERROR_NOT_AVAILABLE;
}
NS_IMETHODIMP FakeChannel::GetContentLength(int64_t* aContentLength) {
return NS_OK;
}
NS_IMETHODIMP FakeChannel::SetContentLength(int64_t aContentLength) {
return NS_OK;
}
NS_IMETHODIMP FakeChannel::GetCanceled(bool* aCanceled) {
*aCanceled = mCanceled;
return NS_OK;
}
NS_IMETHODIMP FakeChannel::Open(nsIInputStream** aStream) {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
FakeChannel::AsyncOpen(nsIStreamListener* aListener) {
mIsPending = true;
mListener = aListener;
return NS_OK;
}
class FakePreloader : public mozilla::FetchPreloader {
public:
explicit FakePreloader(FakeChannel* aChannel) : mDrivingChannel(aChannel) {}
private:
RefPtr<FakeChannel> mDrivingChannel;
virtual nsresult CreateChannel(
nsIChannel** aChannel, nsIURI* aURI, const mozilla::CORSMode aCORSMode,
const mozilla::dom::ReferrerPolicy& aReferrerPolicy,
mozilla::dom::Document* aDocument, nsILoadGroup* aLoadGroup,
nsIInterfaceRequestor* aCallbacks) override {
mDrivingChannel.forget(aChannel);
return NS_OK;
}
};
class FakeListener : public nsIStreamListener {
NS_DECL_ISUPPORTS
NS_DECL_NSIREQUESTOBSERVER
NS_DECL_NSISTREAMLISTENER
enum { Never, OnStart, OnData, OnStop } mCancelIn = Never;
nsresult mOnStartResult = NS_OK;
nsresult mOnDataResult = NS_OK;
nsresult mOnStopResult = NS_OK;
bool mOnStart = false;
nsCString mOnData;
Maybe<nsresult> mOnStop;
private:
virtual ~FakeListener() = default;
};
NS_IMPL_ISUPPORTS(FakeListener, nsIStreamListener, nsIRequestObserver)
NS_IMETHODIMP FakeListener::OnStartRequest(nsIRequest* request) {
EXPECT_FALSE(mOnStart);
mOnStart = true;
if (mCancelIn == OnStart) {
request->Cancel(ERROR_CANCEL);
}
return mOnStartResult;
}
NS_IMETHODIMP FakeListener::OnDataAvailable(nsIRequest* request,
nsIInputStream* input,
uint64_t offset, uint32_t count) {
nsAutoCString data;
data.SetLength(count);
uint32_t read;
input->Read(data.BeginWriting(), count, &read);
mOnData += data;
if (mCancelIn == OnData) {
request->Cancel(ERROR_CANCEL);
}
return mOnDataResult;
}
NS_IMETHODIMP FakeListener::OnStopRequest(nsIRequest* request,
nsresult status) {
EXPECT_FALSE(mOnStop);
mOnStop.emplace(status);
if (mCancelIn == OnStop) {
request->Cancel(ERROR_CANCEL);
}
return mOnStopResult;
}
bool eventInProgress = true;
void Await() {
MOZ_ALWAYS_TRUE(mozilla::SpinEventLoopUntil([&]() {
bool yield = !eventInProgress;
eventInProgress = true; // Just for convenience
return yield;
}));
}
void Yield() { eventInProgress = false; }
} // namespace
// ****************************************************************************
// Test body
// ****************************************************************************
// Caching with all good results (data + NS_OK)
TEST(TestFetchPreloader, CacheNoneBeforeConsume)
{
nsCOMPtr<nsIURI> uri;
NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://example.com"));
auto key = mozilla::PreloadHashKey::CreateAsFetch(
uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty);
RefPtr<FakeChannel> channel = new FakeChannel();
RefPtr<FakePreloader> preloader = new FakePreloader(channel);
RefPtr<mozilla::dom::Document> doc;
NS_NewXMLDocument(getter_AddRefs(doc));
EXPECT_TRUE(NS_SUCCEEDED(
preloader->OpenChannel(&key, uri, mozilla::CORS_NONE,
mozilla::dom::ReferrerPolicy::_empty, doc)));
RefPtr<FakeListener> listener = new FakeListener();
EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("one"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("two"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("three"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
EXPECT_TRUE(listener->mOnStart);
EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree"));
EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK);
Yield();
}));
Await();
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
}
TEST(TestFetchPreloader, CacheStartBeforeConsume)
{
nsCOMPtr<nsIURI> uri;
NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://example.com"));
auto key = mozilla::PreloadHashKey::CreateAsFetch(
uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty);
RefPtr<FakeChannel> channel = new FakeChannel();
RefPtr<FakePreloader> preloader = new FakePreloader(channel);
RefPtr<mozilla::dom::Document> doc;
NS_NewXMLDocument(getter_AddRefs(doc));
EXPECT_TRUE(NS_SUCCEEDED(
preloader->OpenChannel(&key, uri, mozilla::CORS_NONE,
mozilla::dom::ReferrerPolicy::_empty, doc)));
EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
RefPtr<FakeListener> listener = new FakeListener();
EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
EXPECT_TRUE(listener->mOnStart);
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("one"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("two"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("three"))));
EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree"));
EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK);
Yield();
}));
Await();
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
}
TEST(TestFetchPreloader, CachePartOfDataBeforeConsume)
{
nsCOMPtr<nsIURI> uri;
NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://example.com"));
auto key = mozilla::PreloadHashKey::CreateAsFetch(
uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty);
RefPtr<FakeChannel> channel = new FakeChannel();
RefPtr<FakePreloader> preloader = new FakePreloader(channel);
RefPtr<mozilla::dom::Document> doc;
NS_NewXMLDocument(getter_AddRefs(doc));
EXPECT_TRUE(NS_SUCCEEDED(
preloader->OpenChannel(&key, uri, mozilla::CORS_NONE,
mozilla::dom::ReferrerPolicy::_empty, doc)));
EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("one"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("two"))));
RefPtr<FakeListener> listener = new FakeListener();
EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
EXPECT_TRUE(listener->mOnStart);
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("three"))));
EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree"));
EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK);
Yield();
}));
Await();
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
}
TEST(TestFetchPreloader, CacheAllDataBeforeConsume)
{
nsCOMPtr<nsIURI> uri;
NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://example.com"));
auto key = mozilla::PreloadHashKey::CreateAsFetch(
uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty);
RefPtr<FakeChannel> channel = new FakeChannel();
RefPtr<FakePreloader> preloader = new FakePreloader(channel);
RefPtr<mozilla::dom::Document> doc;
NS_NewXMLDocument(getter_AddRefs(doc));
EXPECT_TRUE(NS_SUCCEEDED(
preloader->OpenChannel(&key, uri, mozilla::CORS_NONE,
mozilla::dom::ReferrerPolicy::_empty, doc)));
EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("one"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("two"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("three"))));
// Request consumation of the preload...
RefPtr<FakeListener> listener = new FakeListener();
EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
EXPECT_TRUE(listener->mOnStart);
EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree"));
EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK);
Yield();
}));
Await();
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
}
TEST(TestFetchPreloader, CacheAllBeforeConsume)
{
nsCOMPtr<nsIURI> uri;
NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://example.com"));
auto key = mozilla::PreloadHashKey::CreateAsFetch(
uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty);
RefPtr<FakeChannel> channel = new FakeChannel();
RefPtr<FakePreloader> preloader = new FakePreloader(channel);
RefPtr<mozilla::dom::Document> doc;
NS_NewXMLDocument(getter_AddRefs(doc));
EXPECT_TRUE(NS_SUCCEEDED(
preloader->OpenChannel(&key, uri, mozilla::CORS_NONE,
mozilla::dom::ReferrerPolicy::_empty, doc)));
EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("one"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("two"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("three"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
RefPtr<FakeListener> listener = new FakeListener();
EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
EXPECT_TRUE(listener->mOnStart);
EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree"));
EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK);
Yield();
}));
Await();
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
}
// Get data before the channel fails
TEST(TestFetchPreloader, CacheAllBeforeConsumeWithChannelError)
{
nsCOMPtr<nsIURI> uri;
NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://example.com"));
auto key = mozilla::PreloadHashKey::CreateAsFetch(
uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty);
RefPtr<FakeChannel> channel = new FakeChannel();
RefPtr<FakePreloader> preloader = new FakePreloader(channel);
RefPtr<mozilla::dom::Document> doc;
NS_NewXMLDocument(getter_AddRefs(doc));
EXPECT_TRUE(NS_SUCCEEDED(
preloader->OpenChannel(&key, uri, mozilla::CORS_NONE,
mozilla::dom::ReferrerPolicy::_empty, doc)));
EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("one"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("two"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("three"))));
EXPECT_TRUE(NS_FAILED(channel->Stop(ERROR_ONSTOP)));
RefPtr<FakeListener> listener = new FakeListener();
EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
EXPECT_TRUE(listener->mOnStart);
EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree"));
EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_ONSTOP);
Yield();
}));
Await();
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
}
// Cancel the channel between caching and consuming
TEST(TestFetchPreloader, CacheAllBeforeConsumeWithChannelCancel)
{
nsCOMPtr<nsIURI> uri;
NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://example.com"));
auto key = mozilla::PreloadHashKey::CreateAsFetch(
uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty);
RefPtr<FakeChannel> channel = new FakeChannel();
RefPtr<FakePreloader> preloader = new FakePreloader(channel);
RefPtr<mozilla::dom::Document> doc;
NS_NewXMLDocument(getter_AddRefs(doc));
EXPECT_TRUE(NS_SUCCEEDED(
preloader->OpenChannel(&key, uri, mozilla::CORS_NONE,
mozilla::dom::ReferrerPolicy::_empty, doc)));
EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("one"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("two"))));
channel->Cancel(ERROR_CANCEL);
EXPECT_TRUE(NS_FAILED(channel->Stop(ERROR_CANCEL)));
RefPtr<FakeListener> listener = new FakeListener();
EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
EXPECT_TRUE(listener->mOnStart);
// XXX - This is hard to solve; the data is there but we won't deliver it.
// This is a bit different case than e.g. a network error. We want to
// deliver some data in that case. Cancellation probably happens because of
// navigation or a demand to not consume the channel anyway.
EXPECT_TRUE(listener->mOnData.IsEmpty());
EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL);
Yield();
}));
Await();
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
}
// Let the listener throw while data is already cached
TEST(TestFetchPreloader, CacheAllBeforeConsumeThrowFromOnStartRequest)
{
nsCOMPtr<nsIURI> uri;
NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://example.com"));
auto key = mozilla::PreloadHashKey::CreateAsFetch(
uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty);
RefPtr<FakeChannel> channel = new FakeChannel();
RefPtr<FakePreloader> preloader = new FakePreloader(channel);
RefPtr<mozilla::dom::Document> doc;
NS_NewXMLDocument(getter_AddRefs(doc));
EXPECT_TRUE(NS_SUCCEEDED(
preloader->OpenChannel(&key, uri, mozilla::CORS_NONE,
mozilla::dom::ReferrerPolicy::_empty, doc)));
EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("one"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("two"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("three"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
RefPtr<FakeListener> listener = new FakeListener();
listener->mOnStartResult = ERROR_THROW;
EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
EXPECT_TRUE(listener->mOnStart);
EXPECT_TRUE(listener->mOnData.IsEmpty());
EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_THROW);
Yield();
}));
Await();
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
}
TEST(TestFetchPreloader, CacheAllBeforeConsumeThrowFromOnDataAvailable)
{
nsCOMPtr<nsIURI> uri;
NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://example.com"));
auto key = mozilla::PreloadHashKey::CreateAsFetch(
uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty);
RefPtr<FakeChannel> channel = new FakeChannel();
RefPtr<FakePreloader> preloader = new FakePreloader(channel);
RefPtr<mozilla::dom::Document> doc;
NS_NewXMLDocument(getter_AddRefs(doc));
EXPECT_TRUE(NS_SUCCEEDED(
preloader->OpenChannel(&key, uri, mozilla::CORS_NONE,
mozilla::dom::ReferrerPolicy::_empty, doc)));
EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("one"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("two"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("three"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
RefPtr<FakeListener> listener = new FakeListener();
listener->mOnDataResult = ERROR_THROW;
EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
EXPECT_TRUE(listener->mOnStart);
EXPECT_TRUE(listener->mOnData.EqualsLiteral("one"));
EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_THROW);
Yield();
}));
Await();
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
}
TEST(TestFetchPreloader, CacheAllBeforeConsumeThrowFromOnStopRequest)
{
nsCOMPtr<nsIURI> uri;
NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://example.com"));
auto key = mozilla::PreloadHashKey::CreateAsFetch(
uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty);
RefPtr<FakeChannel> channel = new FakeChannel();
RefPtr<FakePreloader> preloader = new FakePreloader(channel);
RefPtr<mozilla::dom::Document> doc;
NS_NewXMLDocument(getter_AddRefs(doc));
EXPECT_TRUE(NS_SUCCEEDED(
preloader->OpenChannel(&key, uri, mozilla::CORS_NONE,
mozilla::dom::ReferrerPolicy::_empty, doc)));
EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("one"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("two"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("three"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
RefPtr<FakeListener> listener = new FakeListener();
listener->mOnStopResult = ERROR_THROW;
EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
EXPECT_TRUE(listener->mOnStart);
EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree"));
// Throwing from OnStopRequest is generally ignored.
EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK);
Yield();
}));
Await();
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
}
// Cancel the channel in various callbacks
TEST(TestFetchPreloader, CacheAllBeforeConsumeCancelInOnStartRequest)
{
nsCOMPtr<nsIURI> uri;
NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://example.com"));
auto key = mozilla::PreloadHashKey::CreateAsFetch(
uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty);
RefPtr<FakeChannel> channel = new FakeChannel();
RefPtr<FakePreloader> preloader = new FakePreloader(channel);
RefPtr<mozilla::dom::Document> doc;
NS_NewXMLDocument(getter_AddRefs(doc));
EXPECT_TRUE(NS_SUCCEEDED(
preloader->OpenChannel(&key, uri, mozilla::CORS_NONE,
mozilla::dom::ReferrerPolicy::_empty, doc)));
EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("one"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("two"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("three"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
RefPtr<FakeListener> listener = new FakeListener();
listener->mCancelIn = FakeListener::OnStart;
// check that throwing from OnStartRequest doesn't affect the cancellation
// status.
listener->mOnStartResult = ERROR_THROW;
EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
EXPECT_TRUE(listener->mOnStart);
EXPECT_TRUE(listener->mOnData.IsEmpty());
EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL);
Yield();
}));
Await();
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
}
TEST(TestFetchPreloader, CacheAllBeforeConsumeCancelInOnDataAvailable)
{
nsCOMPtr<nsIURI> uri;
NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://example.com"));
auto key = mozilla::PreloadHashKey::CreateAsFetch(
uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty);
RefPtr<FakeChannel> channel = new FakeChannel();
RefPtr<FakePreloader> preloader = new FakePreloader(channel);
RefPtr<mozilla::dom::Document> doc;
NS_NewXMLDocument(getter_AddRefs(doc));
EXPECT_TRUE(NS_SUCCEEDED(
preloader->OpenChannel(&key, uri, mozilla::CORS_NONE,
mozilla::dom::ReferrerPolicy::_empty, doc)));
EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("one"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("two"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("three"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
RefPtr<FakeListener> listener = new FakeListener();
listener->mCancelIn = FakeListener::OnData;
// check that throwing from OnStartRequest doesn't affect the cancellation
// status.
listener->mOnDataResult = ERROR_THROW;
EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
EXPECT_TRUE(listener->mOnStart);
EXPECT_TRUE(listener->mOnData.EqualsLiteral("one"));
EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL);
Yield();
}));
Await();
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
}
// Corner cases
TEST(TestFetchPreloader, CachePartlyBeforeConsumeCancelInOnDataAvailable)
{
nsCOMPtr<nsIURI> uri;
NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://example.com"));
auto key = mozilla::PreloadHashKey::CreateAsFetch(
uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty);
RefPtr<FakeChannel> channel = new FakeChannel();
RefPtr<FakePreloader> preloader = new FakePreloader(channel);
RefPtr<mozilla::dom::Document> doc;
NS_NewXMLDocument(getter_AddRefs(doc));
EXPECT_TRUE(NS_SUCCEEDED(
preloader->OpenChannel(&key, uri, mozilla::CORS_NONE,
mozilla::dom::ReferrerPolicy::_empty, doc)));
EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("one"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("two"))));
RefPtr<FakeListener> listener = new FakeListener();
listener->mCancelIn = FakeListener::OnData;
EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
EXPECT_TRUE(NS_FAILED(channel->Data(NS_LITERAL_CSTRING("three"))));
EXPECT_TRUE(NS_FAILED(channel->Stop(NS_OK)));
EXPECT_TRUE(listener->mOnStart);
EXPECT_TRUE(listener->mOnData.EqualsLiteral("one"));
EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL);
Yield();
}));
Await();
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
}
TEST(TestFetchPreloader, CachePartlyBeforeConsumeCancelInOnStartRequestAndRace)
{
nsCOMPtr<nsIURI> uri;
NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://example.com"));
auto key = mozilla::PreloadHashKey::CreateAsFetch(
uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty);
RefPtr<FakeChannel> channel = new FakeChannel();
RefPtr<FakePreloader> preloader = new FakePreloader(channel);
RefPtr<mozilla::dom::Document> doc;
NS_NewXMLDocument(getter_AddRefs(doc));
EXPECT_TRUE(NS_SUCCEEDED(
preloader->OpenChannel(&key, uri, mozilla::CORS_NONE,
mozilla::dom::ReferrerPolicy::_empty, doc)));
EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("one"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("two"))));
// This has to simulate a possibiilty when stream listener notifications from
// the channel are already pending in the queue while AsyncConsume is called.
// At this moment the listener has not been notified yet.
NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("three"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
}));
RefPtr<FakeListener> listener = new FakeListener();
listener->mCancelIn = FakeListener::OnStart;
EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
// Check listener's been fed properly. Expected is to NOT get any data and
// propagate the cancellation code and not being called duplicated
// OnStopRequest.
NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
EXPECT_TRUE(listener->mOnStart);
EXPECT_TRUE(listener->mOnData.IsEmpty());
EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL);
Yield();
}));
Await();
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
}
TEST(TestFetchPreloader, CachePartlyBeforeConsumeCancelInOnDataAvailableAndRace)
{
nsCOMPtr<nsIURI> uri;
NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://example.com"));
auto key = mozilla::PreloadHashKey::CreateAsFetch(
uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty);
RefPtr<FakeChannel> channel = new FakeChannel();
RefPtr<FakePreloader> preloader = new FakePreloader(channel);
RefPtr<mozilla::dom::Document> doc;
NS_NewXMLDocument(getter_AddRefs(doc));
EXPECT_TRUE(NS_SUCCEEDED(
preloader->OpenChannel(&key, uri, mozilla::CORS_NONE,
mozilla::dom::ReferrerPolicy::_empty, doc)));
EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("one"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("two"))));
// This has to simulate a possibiilty when stream listener notifications from
// the channel are already pending in the queue while AsyncConsume is called.
// At this moment the listener has not been notified yet.
NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("three"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
}));
RefPtr<FakeListener> listener = new FakeListener();
listener->mCancelIn = FakeListener::OnData;
EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
// Check listener's been fed properly. Expected is to NOT get anything after
// the first OnData and propagate the cancellation code and not being called
// duplicated OnStopRequest.
NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
EXPECT_TRUE(listener->mOnStart);
EXPECT_TRUE(listener->mOnData.EqualsLiteral("one"));
EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL);
Yield();
}));
Await();
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
}
TEST(TestFetchPreloader, CachePartlyBeforeConsumeThrowFromOnStartRequestAndRace)
{
nsCOMPtr<nsIURI> uri;
NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://example.com"));
auto key = mozilla::PreloadHashKey::CreateAsFetch(
uri, mozilla::CORS_NONE, mozilla::dom::ReferrerPolicy::_empty);
RefPtr<FakeChannel> channel = new FakeChannel();
RefPtr<FakePreloader> preloader = new FakePreloader(channel);
RefPtr<mozilla::dom::Document> doc;
NS_NewXMLDocument(getter_AddRefs(doc));
EXPECT_TRUE(NS_SUCCEEDED(
preloader->OpenChannel(&key, uri, mozilla::CORS_NONE,
mozilla::dom::ReferrerPolicy::_empty, doc)));
EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("one"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("two"))));
// This has to simulate a possibiilty when stream listener notifications from
// the channel are already pending in the queue while AsyncConsume is called.
// At this moment the listener has not been notified yet.
NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
EXPECT_TRUE(NS_SUCCEEDED(channel->Data(NS_LITERAL_CSTRING("three"))));
EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
}));
RefPtr<FakeListener> listener = new FakeListener();
listener->mOnStartResult = ERROR_THROW;
EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
// Check listener's been fed properly. Expected is to NOT get any data and
// propagate the throwing code and not being called duplicated OnStopRequest.
NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
EXPECT_TRUE(listener->mOnStart);
EXPECT_TRUE(listener->mOnData.IsEmpty());
EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_THROW);
Yield();
}));
Await();
EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
}

Просмотреть файл

@ -0,0 +1,21 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
UNIFIED_SOURCES += [
'TestFetchPreloader.cpp',
]
LOCAL_INCLUDES += [
'/netwerk/base',
'/xpcom/tests/gtest',
]
FINAL_LIBRARY = 'xul-gtest'
LOCAL_INCLUDES += [
'!/xpcom',
'/xpcom/components'
]

Просмотреть файл

@ -7,13 +7,17 @@
with Files("**"):
BUG_COMPONENT = ("Core", "Networking")
TEST_DIRS += ['gtest']
EXPORTS.mozilla += [
'FetchPreloader.h',
'PreloaderBase.h',
'PreloadHashKey.h',
'PreloadService.h',
]
UNIFIED_SOURCES += [
'FetchPreloader.cpp',
'PreloaderBase.cpp',
'PreloadHashKey.cpp',
'PreloadService.cpp',