summaryrefslogtreecommitdiffstats
path: root/uriloader/preload
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /uriloader/preload
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'uriloader/preload')
-rw-r--r--uriloader/preload/FetchPreloader.cpp353
-rw-r--r--uriloader/preload/FetchPreloader.h101
-rw-r--r--uriloader/preload/PreloadHashKey.cpp213
-rw-r--r--uriloader/preload/PreloadHashKey.h109
-rw-r--r--uriloader/preload/PreloadService.cpp296
-rw-r--r--uriloader/preload/PreloadService.h135
-rw-r--r--uriloader/preload/PreloaderBase.cpp391
-rw-r--r--uriloader/preload/PreloaderBase.h200
-rw-r--r--uriloader/preload/gtest/TestFetchPreloader.cpp950
-rw-r--r--uriloader/preload/gtest/moz.build18
-rw-r--r--uriloader/preload/moz.build26
11 files changed, 2792 insertions, 0 deletions
diff --git a/uriloader/preload/FetchPreloader.cpp b/uriloader/preload/FetchPreloader.cpp
new file mode 100644
index 0000000000..6e848bbee8
--- /dev/null
+++ b/uriloader/preload/FetchPreloader.cpp
@@ -0,0 +1,353 @@
+/* -*- 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/CORSMode.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/LoadInfo.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/ReferrerInfo.h"
+#include "nsContentPolicyUtils.h"
+#include "nsContentSecurityManager.h"
+#include "nsContentUtils.h"
+#include "nsIChannel.h"
+#include "nsIChildChannel.h"
+#include "nsIClassOfService.h"
+#include "nsIHttpChannel.h"
+#include "nsIHttpChannelInternal.h"
+#include "nsITimedChannel.h"
+#include "nsNetUtil.h"
+#include "nsStringStream.h"
+#include "nsIDocShell.h"
+
+namespace mozilla {
+
+NS_IMPL_ISUPPORTS(FetchPreloader, nsIStreamListener, nsIRequestObserver)
+
+FetchPreloader::FetchPreloader()
+ : FetchPreloader(nsIContentPolicy::TYPE_INTERNAL_FETCH_PRELOAD) {}
+
+FetchPreloader::FetchPreloader(nsContentPolicyType aContentPolicyType)
+ : mContentPolicyType(aContentPolicyType) {}
+
+nsresult FetchPreloader::OpenChannel(const PreloadHashKey& aKey, nsIURI* aURI,
+ const CORSMode aCORSMode,
+ const dom::ReferrerPolicy& aReferrerPolicy,
+ dom::Document* aDocument,
+ uint64_t aEarlyHintPreloaderId) {
+ nsresult rv;
+ nsCOMPtr<nsIChannel> channel;
+
+ auto notify = MakeScopeExit([&]() {
+ if (NS_FAILED(rv)) {
+ // Make sure we notify any <link preload> elements when opening fails
+ // because of various technical or security reasons.
+ NotifyStart(channel);
+ // Using the non-channel overload of this method to make it work even
+ // before NotifyOpen has been called on this preload. We are not
+ // switching between channels, so it's safe to do so.
+ NotifyStop(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, aEarlyHintPreloaderId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Doing this now so that we have the channel and tainting set on it properly
+ // to notify the proper event (load or error) on the associated preload tags
+ // when the CSP check fails.
+ rv = CheckContentPolicy(aURI, aDocument);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ PrioritizeAsPreload(channel);
+ AddLoadBackgroundFlag(channel);
+
+ NotifyOpen(aKey, channel, aDocument, true);
+
+ if (aEarlyHintPreloaderId) {
+ nsCOMPtr<nsIHttpChannelInternal> channelInternal =
+ do_QueryInterface(channel);
+ NS_ENSURE_TRUE(channelInternal != nullptr, NS_ERROR_FAILURE);
+
+ rv = channelInternal->SetEarlyHintPreloaderId(aEarlyHintPreloaderId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ 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,
+ uint64_t aEarlyHintPreloaderId) {
+ nsresult rv;
+
+ nsSecurityFlags securityFlags =
+ nsContentSecurityManager::ComputeSecurityFlags(
+ aCORSMode, nsContentSecurityManager::CORSSecurityMapping::
+ CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS);
+
+ 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)) {
+ if (aEarlyHintPreloaderId) {
+ timedChannel->SetInitiatorType(u"early-hints"_ns);
+ } else {
+ timedChannel->SetInitiatorType(u"link"_ns);
+ }
+ }
+
+ 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, ""_ns, &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;
+ }
+ }
+
+ // Fetch preloader wants to keep the channel around so that consumers like XHR
+ // can access it even after the preload is done.
+ nsCOMPtr<nsIChannel> channel = mChannel;
+ NotifyStop(request, status);
+ mChannel.swap(channel);
+ 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
diff --git a/uriloader/preload/FetchPreloader.h b/uriloader/preload/FetchPreloader.h
new file mode 100644
index 0000000000..b6b667fad3
--- /dev/null
+++ b/uriloader/preload/FetchPreloader.h
@@ -0,0 +1,101 @@
+/* -*- 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 "mozilla/Variant.h"
+#include "nsCOMPtr.h"
+#include "nsIAsyncOutputStream.h"
+#include "nsIAsyncInputStream.h"
+#include "nsIContentPolicy.h"
+#include "nsIStreamListener.h"
+
+class nsIChannel;
+class nsILoadGroup;
+class nsIInterfaceRequestor;
+
+namespace mozilla {
+
+namespace dom {
+enum class ReferrerPolicy : uint8_t;
+}
+
+class FetchPreloader : public PreloaderBase, public nsIStreamListener {
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+
+ FetchPreloader();
+ nsresult OpenChannel(const PreloadHashKey& aKey, nsIURI* aURI,
+ const CORSMode aCORSMode,
+ const dom::ReferrerPolicy& aReferrerPolicy,
+ dom::Document* aDocument,
+ uint64_t aEarlyHintPreloaderId);
+
+ // 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,
+ uint64_t aEarlyHintPreloaderId);
+
+ 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
diff --git a/uriloader/preload/PreloadHashKey.cpp b/uriloader/preload/PreloadHashKey.cpp
new file mode 100644
index 0000000000..989bb16949
--- /dev/null
+++ b/uriloader/preload/PreloadHashKey.cpp
@@ -0,0 +1,213 @@
+/* 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 "PreloadHashKey.h"
+
+#include "mozilla/dom/Element.h" // StringToCORSMode
+#include "mozilla/css/SheetLoadData.h"
+#include "mozilla/dom/ReferrerPolicyBinding.h"
+#include "nsIPrincipal.h"
+#include "nsIReferrerInfo.h"
+
+namespace mozilla {
+
+PreloadHashKey::PreloadHashKey(const nsIURI* aKey, ResourceType aAs)
+ : nsURIHashKey(aKey), mAs(aAs) {}
+
+PreloadHashKey::PreloadHashKey(const PreloadHashKey* aKey)
+ : nsURIHashKey(aKey->mKey) {
+ *this = *aKey;
+}
+
+PreloadHashKey::PreloadHashKey(PreloadHashKey&& aToMove)
+ : nsURIHashKey(std::move(aToMove)) {
+ mAs = std::move(aToMove.mAs);
+ mCORSMode = std::move(aToMove.mCORSMode);
+ mPrincipal = std::move(aToMove.mPrincipal);
+
+ switch (mAs) {
+ case ResourceType::SCRIPT:
+ mScript = std::move(aToMove.mScript);
+ break;
+ case ResourceType::STYLE:
+ mStyle = std::move(aToMove.mStyle);
+ break;
+ case ResourceType::IMAGE:
+ break;
+ case ResourceType::FONT:
+ break;
+ case ResourceType::FETCH:
+ break;
+ case ResourceType::NONE:
+ break;
+ }
+}
+
+PreloadHashKey& PreloadHashKey::operator=(const PreloadHashKey& aOther) {
+ MOZ_ASSERT(mAs == ResourceType::NONE || aOther.mAs == ResourceType::NONE,
+ "Assigning more than once, only reset is allowed");
+
+ nsURIHashKey::operator=(aOther);
+
+ mAs = aOther.mAs;
+ mCORSMode = aOther.mCORSMode;
+ mPrincipal = aOther.mPrincipal;
+
+ switch (mAs) {
+ case ResourceType::SCRIPT:
+ mScript = aOther.mScript;
+ break;
+ case ResourceType::STYLE:
+ mStyle = aOther.mStyle;
+ break;
+ case ResourceType::IMAGE:
+ break;
+ case ResourceType::FONT:
+ break;
+ case ResourceType::FETCH:
+ break;
+ case ResourceType::NONE:
+ break;
+ }
+
+ return *this;
+}
+
+// static
+PreloadHashKey PreloadHashKey::CreateAsScript(
+ nsIURI* aURI, CORSMode aCORSMode, JS::loader::ScriptKind aScriptKind) {
+ PreloadHashKey key(aURI, ResourceType::SCRIPT);
+ key.mCORSMode = aCORSMode;
+
+ key.mScript.mScriptKind = aScriptKind;
+
+ return key;
+}
+
+// static
+PreloadHashKey PreloadHashKey::CreateAsScript(nsIURI* aURI,
+ const nsAString& aCrossOrigin,
+ const nsAString& aType) {
+ JS::loader::ScriptKind scriptKind = JS::loader::ScriptKind::eClassic;
+ if (aType.LowerCaseEqualsASCII("module")) {
+ scriptKind = JS::loader::ScriptKind::eModule;
+ }
+ CORSMode crossOrigin = dom::Element::StringToCORSMode(aCrossOrigin);
+ return CreateAsScript(aURI, crossOrigin, scriptKind);
+}
+
+// static
+PreloadHashKey PreloadHashKey::CreateAsStyle(
+ nsIURI* aURI, nsIPrincipal* aPrincipal, CORSMode aCORSMode,
+ css::SheetParsingMode aParsingMode) {
+ PreloadHashKey key(aURI, ResourceType::STYLE);
+ key.mCORSMode = aCORSMode;
+ key.mPrincipal = aPrincipal;
+
+ key.mStyle.mParsingMode = aParsingMode;
+
+ return key;
+}
+
+// static
+PreloadHashKey PreloadHashKey::CreateAsStyle(
+ css::SheetLoadData& aSheetLoadData) {
+ return CreateAsStyle(aSheetLoadData.mURI, aSheetLoadData.mTriggeringPrincipal,
+ aSheetLoadData.mSheet->GetCORSMode(),
+ aSheetLoadData.mSheet->ParsingMode());
+}
+
+// static
+PreloadHashKey PreloadHashKey::CreateAsImage(nsIURI* aURI,
+ nsIPrincipal* aPrincipal,
+ CORSMode aCORSMode) {
+ PreloadHashKey key(aURI, ResourceType::IMAGE);
+ key.mCORSMode = aCORSMode;
+ key.mPrincipal = aPrincipal;
+
+ return key;
+}
+
+// static
+PreloadHashKey PreloadHashKey::CreateAsFetch(nsIURI* aURI, CORSMode aCORSMode) {
+ PreloadHashKey key(aURI, ResourceType::FETCH);
+ key.mCORSMode = aCORSMode;
+
+ return key;
+}
+
+// static
+PreloadHashKey PreloadHashKey::CreateAsFetch(nsIURI* aURI,
+ const nsAString& aCrossOrigin) {
+ return CreateAsFetch(aURI, dom::Element::StringToCORSMode(aCrossOrigin));
+}
+
+PreloadHashKey PreloadHashKey::CreateAsFont(nsIURI* aURI, CORSMode aCORSMode) {
+ PreloadHashKey key(aURI, ResourceType::FONT);
+ key.mCORSMode = aCORSMode;
+
+ return key;
+}
+
+bool PreloadHashKey::KeyEquals(KeyTypePointer aOther) const {
+ if (mAs != aOther->mAs || mCORSMode != aOther->mCORSMode) {
+ return false;
+ }
+
+ if (!mPrincipal != !aOther->mPrincipal) {
+ // One or the other has a principal, but not both... not equal
+ return false;
+ }
+
+ if (mPrincipal && !mPrincipal->Equals(aOther->mPrincipal)) {
+ return false;
+ }
+
+ if (!nsURIHashKey::KeyEquals(
+ static_cast<const nsURIHashKey*>(aOther)->GetKey())) {
+ return false;
+ }
+
+ switch (mAs) {
+ case ResourceType::SCRIPT:
+ if (mScript.mScriptKind != aOther->mScript.mScriptKind) {
+ return false;
+ }
+ break;
+ case ResourceType::STYLE: {
+ if (mStyle.mParsingMode != aOther->mStyle.mParsingMode) {
+ return false;
+ }
+ break;
+ }
+ case ResourceType::IMAGE:
+ // No more checks needed. The image cache key consists of the document
+ // (which we are scoped into), origin attributes (which we compare as part
+ // of the principal check) and the URL. imgLoader::ValidateEntry compares
+ // CORS, referrer info and principal, which we do by default.
+ break;
+ case ResourceType::FONT:
+ break;
+ case ResourceType::FETCH:
+ // No more checks needed.
+ break;
+ case ResourceType::NONE:
+ break;
+ }
+
+ return true;
+}
+
+// static
+PLDHashNumber PreloadHashKey::HashKey(KeyTypePointer aKey) {
+ PLDHashNumber hash = nsURIHashKey::HashKey(aKey->mKey);
+
+ // Enough to use the common attributes
+ hash = AddToHash(hash, static_cast<uint32_t>(aKey->mAs));
+ hash = AddToHash(hash, static_cast<uint32_t>(aKey->mCORSMode));
+
+ return hash;
+}
+
+} // namespace mozilla
diff --git a/uriloader/preload/PreloadHashKey.h b/uriloader/preload/PreloadHashKey.h
new file mode 100644
index 0000000000..8db49c1b18
--- /dev/null
+++ b/uriloader/preload/PreloadHashKey.h
@@ -0,0 +1,109 @@
+/* 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 PreloadHashKey_h__
+#define PreloadHashKey_h__
+
+#include "mozilla/CORSMode.h"
+#include "mozilla/css/SheetParsingMode.h"
+#include "js/loader/ScriptKind.h"
+#include "nsURIHashKey.h"
+
+class nsIPrincipal;
+
+namespace JS::loader {
+enum class ScriptKind;
+}
+
+namespace mozilla {
+
+namespace css {
+class SheetLoadData;
+}
+
+/**
+ * This key is used for coalescing and lookup of preloading or regular
+ * speculative loads. It consists of:
+ * - the resource type, which is the value of the 'as' attribute
+ * - the URI of the resource
+ * - set of attributes that is common to all resource types
+ * - resource-type-specific attributes that we use to distinguish loads that has
+ * to be treated separately, some of these attributes may remain at their
+ * default values
+ */
+class PreloadHashKey : public nsURIHashKey {
+ public:
+ enum class ResourceType : uint8_t { NONE, SCRIPT, STYLE, IMAGE, FONT, FETCH };
+
+ using KeyType = const PreloadHashKey&;
+ using KeyTypePointer = const PreloadHashKey*;
+
+ PreloadHashKey() = default;
+ PreloadHashKey(const nsIURI* aKey, ResourceType aAs);
+ explicit PreloadHashKey(const PreloadHashKey* aKey);
+ PreloadHashKey(PreloadHashKey&& aToMove);
+
+ PreloadHashKey& operator=(const PreloadHashKey& aOther);
+
+ // Construct key for "script"
+ static PreloadHashKey CreateAsScript(nsIURI* aURI, CORSMode aCORSMode,
+ JS::loader::ScriptKind aScriptKind);
+ static PreloadHashKey CreateAsScript(nsIURI* aURI,
+ const nsAString& aCrossOrigin,
+ const nsAString& aType);
+
+ // Construct key for "style"
+ static PreloadHashKey CreateAsStyle(nsIURI* aURI, nsIPrincipal* aPrincipal,
+ CORSMode aCORSMode,
+ css::SheetParsingMode aParsingMode);
+ static PreloadHashKey CreateAsStyle(css::SheetLoadData&);
+
+ // Construct key for "image"
+ static PreloadHashKey CreateAsImage(nsIURI* aURI, nsIPrincipal* aPrincipal,
+ CORSMode aCORSMode);
+
+ // Construct key for "fetch"
+ static PreloadHashKey CreateAsFetch(nsIURI* aURI, CORSMode aCORSMode);
+ static PreloadHashKey CreateAsFetch(nsIURI* aURI,
+ const nsAString& aCrossOrigin);
+
+ // Construct key for "font"
+ static PreloadHashKey CreateAsFont(nsIURI* aURI, CORSMode aCORSMode);
+
+ KeyType GetKey() const { return *this; }
+ KeyTypePointer GetKeyPointer() const { return this; }
+ static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; }
+
+ bool KeyEquals(KeyTypePointer aOther) const;
+ static PLDHashNumber HashKey(KeyTypePointer aKey);
+ ResourceType As() const { return mAs; }
+
+#ifdef MOZILLA_INTERNAL_API
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
+ // Bug 1627752
+ return 0;
+ }
+#endif
+
+ enum { ALLOW_MEMMOVE = true };
+
+ private:
+ // Attributes common to all resource types
+ ResourceType mAs = ResourceType::NONE;
+
+ CORSMode mCORSMode = CORS_NONE;
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+
+ struct {
+ JS::loader::ScriptKind mScriptKind = JS::loader::ScriptKind::eClassic;
+ } mScript;
+
+ struct {
+ css::SheetParsingMode mParsingMode = css::eAuthorSheetFeatures;
+ } mStyle;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/uriloader/preload/PreloadService.cpp b/uriloader/preload/PreloadService.cpp
new file mode 100644
index 0000000000..6766dc5799
--- /dev/null
+++ b/uriloader/preload/PreloadService.cpp
@@ -0,0 +1,296 @@
+/* -*- 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 "PreloadService.h"
+
+#include "FetchPreloader.h"
+#include "PreloaderBase.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/dom/HTMLLinkElement.h"
+#include "mozilla/dom/ScriptLoader.h"
+#include "mozilla/dom/ReferrerInfo.h"
+#include "mozilla/Encoding.h"
+#include "mozilla/FontPreloader.h"
+#include "mozilla/StaticPrefs_network.h"
+#include "nsNetUtil.h"
+
+namespace mozilla {
+
+PreloadService::PreloadService(dom::Document* aDoc) : mDocument(aDoc) {}
+PreloadService::~PreloadService() = default;
+
+bool PreloadService::RegisterPreload(const PreloadHashKey& aKey,
+ PreloaderBase* aPreload) {
+ return mPreloads.WithEntryHandle(aKey, [&](auto&& lookup) {
+ if (lookup) {
+ lookup.Data() = aPreload;
+ return true;
+ }
+ lookup.Insert(aPreload);
+ return false;
+ });
+}
+
+void PreloadService::DeregisterPreload(const PreloadHashKey& aKey) {
+ mPreloads.Remove(aKey);
+}
+
+void PreloadService::ClearAllPreloads() { mPreloads.Clear(); }
+
+bool PreloadService::PreloadExists(const PreloadHashKey& aKey) {
+ return mPreloads.Contains(aKey);
+}
+
+already_AddRefed<PreloaderBase> PreloadService::LookupPreload(
+ const PreloadHashKey& aKey) const {
+ return mPreloads.Get(aKey);
+}
+
+already_AddRefed<nsIURI> PreloadService::GetPreloadURI(const nsAString& aURL) {
+ nsIURI* base = BaseURIForPreload();
+ auto encoding = mDocument->GetDocumentCharacterSet();
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL, encoding, base);
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+
+ return uri.forget();
+}
+
+already_AddRefed<PreloaderBase> PreloadService::PreloadLinkElement(
+ dom::HTMLLinkElement* aLinkElement, nsContentPolicyType aPolicyType) {
+ if (aPolicyType == nsIContentPolicy::TYPE_INVALID) {
+ MOZ_ASSERT_UNREACHABLE("Caller should check");
+ return nullptr;
+ }
+
+ if (!StaticPrefs::network_preload()) {
+ return nullptr;
+ }
+
+ nsAutoString as, charset, crossOrigin, integrity, referrerPolicy, rel, srcset,
+ sizes, type, url;
+
+ nsCOMPtr<nsIURI> uri = aLinkElement->GetURI();
+ aLinkElement->GetCharset(charset);
+ aLinkElement->GetImageSrcset(srcset);
+ aLinkElement->GetImageSizes(sizes);
+ aLinkElement->GetHref(url);
+ aLinkElement->GetCrossOrigin(crossOrigin);
+ aLinkElement->GetIntegrity(integrity);
+ aLinkElement->GetReferrerPolicy(referrerPolicy);
+ aLinkElement->GetRel(rel);
+
+ if (rel.LowerCaseEqualsASCII("modulepreload")) {
+ as = u"script"_ns;
+ type = u"module"_ns;
+ } else {
+ aLinkElement->GetAs(as);
+ aLinkElement->GetType(type);
+ }
+
+ auto result = PreloadOrCoalesce(uri, url, aPolicyType, as, type, charset,
+ srcset, sizes, integrity, crossOrigin,
+ referrerPolicy, /* aFromHeader = */ false, 0);
+
+ if (!result.mPreloader) {
+ NotifyNodeEvent(aLinkElement, result.mAlreadyComplete);
+ return nullptr;
+ }
+
+ result.mPreloader->AddLinkPreloadNode(aLinkElement);
+ return result.mPreloader.forget();
+}
+
+void PreloadService::PreloadLinkHeader(
+ nsIURI* aURI, const nsAString& aURL, nsContentPolicyType aPolicyType,
+ const nsAString& aAs, const nsAString& aType, const nsAString& aIntegrity,
+ const nsAString& aSrcset, const nsAString& aSizes, const nsAString& aCORS,
+ const nsAString& aReferrerPolicy, uint64_t aEarlyHintPreloaderId) {
+ if (aPolicyType == nsIContentPolicy::TYPE_INVALID) {
+ MOZ_ASSERT_UNREACHABLE("Caller should check");
+ return;
+ }
+
+ if (!StaticPrefs::network_preload()) {
+ return;
+ }
+
+ PreloadOrCoalesce(aURI, aURL, aPolicyType, aAs, aType, u""_ns, aSrcset,
+ aSizes, aIntegrity, aCORS, aReferrerPolicy,
+ /* aFromHeader = */ true, aEarlyHintPreloaderId);
+}
+
+PreloadService::PreloadOrCoalesceResult PreloadService::PreloadOrCoalesce(
+ nsIURI* aURI, const nsAString& aURL, nsContentPolicyType aPolicyType,
+ const nsAString& aAs, const nsAString& aType, const nsAString& aCharset,
+ const nsAString& aSrcset, const nsAString& aSizes,
+ const nsAString& aIntegrity, const nsAString& aCORS,
+ const nsAString& aReferrerPolicy, bool aFromHeader,
+ uint64_t aEarlyHintPreloaderId) {
+ if (!aURI) {
+ MOZ_ASSERT_UNREACHABLE("Should not pass null nsIURI");
+ return {nullptr, false};
+ }
+
+ bool isImgSet = false;
+ PreloadHashKey preloadKey;
+ nsCOMPtr<nsIURI> uri = aURI;
+
+ if (aAs.LowerCaseEqualsASCII("script")) {
+ preloadKey = PreloadHashKey::CreateAsScript(uri, aCORS, aType);
+ } else if (aAs.LowerCaseEqualsASCII("style")) {
+ preloadKey = PreloadHashKey::CreateAsStyle(
+ uri, mDocument->NodePrincipal(), dom::Element::StringToCORSMode(aCORS),
+ css::eAuthorSheetFeatures /* see Loader::LoadSheet */);
+ } else if (aAs.LowerCaseEqualsASCII("image")) {
+ uri = mDocument->ResolvePreloadImage(BaseURIForPreload(), aURL, aSrcset,
+ aSizes, &isImgSet);
+ if (!uri) {
+ return {nullptr, false};
+ }
+
+ preloadKey = PreloadHashKey::CreateAsImage(
+ uri, mDocument->NodePrincipal(), dom::Element::StringToCORSMode(aCORS));
+ } else if (aAs.LowerCaseEqualsASCII("font")) {
+ preloadKey = PreloadHashKey::CreateAsFont(
+ uri, dom::Element::StringToCORSMode(aCORS));
+ } else if (aAs.LowerCaseEqualsASCII("fetch")) {
+ preloadKey = PreloadHashKey::CreateAsFetch(
+ uri, dom::Element::StringToCORSMode(aCORS));
+ } else {
+ return {nullptr, false};
+ }
+
+ if (RefPtr<PreloaderBase> preload = LookupPreload(preloadKey)) {
+ return {std::move(preload), false};
+ }
+
+ if (aAs.LowerCaseEqualsASCII("script")) {
+ PreloadScript(uri, aType, aCharset, aCORS, aReferrerPolicy, aIntegrity,
+ true /* isInHead - TODO */, aEarlyHintPreloaderId);
+ } else if (aAs.LowerCaseEqualsASCII("style")) {
+ auto status = mDocument->PreloadStyle(
+ aURI, Encoding::ForLabel(aCharset), aCORS,
+ PreloadReferrerPolicy(aReferrerPolicy), aIntegrity,
+ aFromHeader ? css::StylePreloadKind::FromLinkRelPreloadHeader
+ : css::StylePreloadKind::FromLinkRelPreloadElement,
+ aEarlyHintPreloaderId);
+ switch (status) {
+ case dom::SheetPreloadStatus::AlreadyComplete:
+ return {nullptr, /* already_complete = */ true};
+ case dom::SheetPreloadStatus::Errored:
+ case dom::SheetPreloadStatus::InProgress:
+ break;
+ }
+ } else if (aAs.LowerCaseEqualsASCII("image")) {
+ PreloadImage(uri, aCORS, aReferrerPolicy, isImgSet, aEarlyHintPreloaderId);
+ } else if (aAs.LowerCaseEqualsASCII("font")) {
+ PreloadFont(uri, aCORS, aReferrerPolicy, aEarlyHintPreloaderId);
+ } else if (aAs.LowerCaseEqualsASCII("fetch")) {
+ PreloadFetch(uri, aCORS, aReferrerPolicy, aEarlyHintPreloaderId);
+ }
+
+ RefPtr<PreloaderBase> preload = LookupPreload(preloadKey);
+ if (preload && aEarlyHintPreloaderId) {
+ preload->SetForEarlyHints();
+ }
+
+ return {preload, false};
+}
+
+void PreloadService::PreloadScript(nsIURI* aURI, const nsAString& aType,
+ const nsAString& aCharset,
+ const nsAString& aCrossOrigin,
+ const nsAString& aReferrerPolicy,
+ const nsAString& aIntegrity,
+ bool aScriptFromHead,
+ uint64_t aEarlyHintPreloaderId) {
+ mDocument->ScriptLoader()->PreloadURI(
+ aURI, aCharset, aType, aCrossOrigin, aIntegrity, aScriptFromHead, false,
+ false, false, true, PreloadReferrerPolicy(aReferrerPolicy),
+ aEarlyHintPreloaderId);
+}
+
+void PreloadService::PreloadImage(nsIURI* aURI, const nsAString& aCrossOrigin,
+ const nsAString& aImageReferrerPolicy,
+ bool aIsImgSet,
+ uint64_t aEarlyHintPreloaderId) {
+ mDocument->PreLoadImage(aURI, aCrossOrigin,
+ PreloadReferrerPolicy(aImageReferrerPolicy),
+ aIsImgSet, true, aEarlyHintPreloaderId);
+}
+
+void PreloadService::PreloadFont(nsIURI* aURI, const nsAString& aCrossOrigin,
+ const nsAString& aReferrerPolicy,
+ uint64_t aEarlyHintPreloaderId) {
+ CORSMode cors = dom::Element::StringToCORSMode(aCrossOrigin);
+ auto key = PreloadHashKey::CreateAsFont(aURI, cors);
+
+ if (PreloadExists(key)) {
+ return;
+ }
+
+ RefPtr<FontPreloader> preloader = new FontPreloader();
+ dom::ReferrerPolicy referrerPolicy = PreloadReferrerPolicy(aReferrerPolicy);
+ preloader->OpenChannel(key, aURI, cors, referrerPolicy, mDocument,
+ aEarlyHintPreloaderId);
+}
+
+void PreloadService::PreloadFetch(nsIURI* aURI, const nsAString& aCrossOrigin,
+ const nsAString& aReferrerPolicy,
+ uint64_t aEarlyHintPreloaderId) {
+ CORSMode cors = dom::Element::StringToCORSMode(aCrossOrigin);
+ auto key = PreloadHashKey::CreateAsFetch(aURI, cors);
+
+ if (PreloadExists(key)) {
+ return;
+ }
+
+ RefPtr<FetchPreloader> preloader = new FetchPreloader();
+ dom::ReferrerPolicy referrerPolicy = PreloadReferrerPolicy(aReferrerPolicy);
+ preloader->OpenChannel(key, aURI, cors, referrerPolicy, mDocument,
+ aEarlyHintPreloaderId);
+}
+
+// static
+void PreloadService::NotifyNodeEvent(nsINode* aNode, bool aSuccess) {
+ if (!aNode->IsInComposedDoc()) {
+ return;
+ }
+
+ // We don't dispatch synchronously since |node| might be in a DocGroup
+ // that we're not allowed to touch. (Our network request happens in the
+ // DocGroup of one of the mSources nodes--not necessarily this one).
+
+ RefPtr<AsyncEventDispatcher> dispatcher = new AsyncEventDispatcher(
+ aNode, aSuccess ? u"load"_ns : u"error"_ns, CanBubble::eNo);
+
+ dispatcher->RequireNodeInDocument();
+ dispatcher->PostDOMEvent();
+}
+
+dom::ReferrerPolicy PreloadService::PreloadReferrerPolicy(
+ const nsAString& aReferrerPolicy) {
+ dom::ReferrerPolicy referrerPolicy =
+ dom::ReferrerInfo::ReferrerPolicyAttributeFromString(aReferrerPolicy);
+ if (referrerPolicy == dom::ReferrerPolicy::_empty) {
+ referrerPolicy = mDocument->GetPreloadReferrerInfo()->ReferrerPolicy();
+ }
+
+ return referrerPolicy;
+}
+
+nsIURI* PreloadService::BaseURIForPreload() {
+ nsIURI* documentURI = mDocument->GetDocumentURI();
+ nsIURI* documentBaseURI = mDocument->GetDocBaseURI();
+ return (documentURI == documentBaseURI)
+ ? (mSpeculationBaseURI ? mSpeculationBaseURI.get() : documentURI)
+ : documentBaseURI;
+}
+
+} // namespace mozilla
diff --git a/uriloader/preload/PreloadService.h b/uriloader/preload/PreloadService.h
new file mode 100644
index 0000000000..d785fd0ae5
--- /dev/null
+++ b/uriloader/preload/PreloadService.h
@@ -0,0 +1,135 @@
+/* -*- 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 PreloadService_h__
+#define PreloadService_h__
+
+#include "nsIContentPolicy.h"
+#include "nsIURI.h"
+#include "nsRefPtrHashtable.h"
+#include "mozilla/PreloadHashKey.h"
+
+class nsINode;
+
+namespace mozilla {
+
+class PreloaderBase;
+
+namespace dom {
+
+class HTMLLinkElement;
+class Document;
+enum class ReferrerPolicy : uint8_t;
+
+} // namespace dom
+
+/**
+ * Intended to scope preloads and speculative loads under one roof. This class
+ * is intended to be a member of dom::Document. Provides registration of
+ * speculative loads via a `key` which is defined to consist of the URL,
+ * resource type, and resource-specific attributes that are further
+ * distinguishing loads with otherwise same type and url.
+ */
+class PreloadService {
+ public:
+ explicit PreloadService(dom::Document*);
+ ~PreloadService();
+
+ // Called by resource loaders to register a running resource load. This is
+ // called for a speculative load when it's started the first time, being it
+ // either regular speculative load or a preload.
+ //
+ // Returns false and does nothing if a preload is already registered under
+ // this key, true otherwise.
+ bool RegisterPreload(const PreloadHashKey& aKey, PreloaderBase* aPreload);
+
+ // Called when the load is about to be cancelled. Exact behavior is to be
+ // determined yet.
+ void DeregisterPreload(const PreloadHashKey& aKey);
+
+ // Called when the scope is to go away.
+ void ClearAllPreloads();
+
+ // True when there is a preload registered under the key.
+ bool PreloadExists(const PreloadHashKey& aKey);
+
+ // Returns an existing preload under the key or null, when there is none
+ // registered under the key.
+ already_AddRefed<PreloaderBase> LookupPreload(
+ const PreloadHashKey& aKey) const;
+
+ void SetSpeculationBase(nsIURI* aURI) { mSpeculationBaseURI = aURI; }
+ already_AddRefed<nsIURI> GetPreloadURI(const nsAString& aURL);
+
+ already_AddRefed<PreloaderBase> PreloadLinkElement(
+ dom::HTMLLinkElement* aLink, nsContentPolicyType aPolicyType);
+
+ // a non-zero aEarlyHintPreloaderId tells this service that a preload for this
+ // link was started by the EarlyHintPreloader and the preloaders should
+ // connect back by setting earlyHintPreloaderId in nsIChannelInternal before
+ // AsyncOpen.
+ void PreloadLinkHeader(nsIURI* aURI, const nsAString& aURL,
+ nsContentPolicyType aPolicyType, const nsAString& aAs,
+ const nsAString& aType, const nsAString& aIntegrity,
+ const nsAString& aSrcset, const nsAString& aSizes,
+ const nsAString& aCORS,
+ const nsAString& aReferrerPolicy,
+ uint64_t aEarlyHintPreloaderId);
+
+ void PreloadScript(nsIURI* aURI, const nsAString& aType,
+ const nsAString& aCharset, const nsAString& aCrossOrigin,
+ const nsAString& aReferrerPolicy,
+ const nsAString& aIntegrity, bool aScriptFromHead,
+ uint64_t aEarlyHintPreloaderId);
+
+ void PreloadImage(nsIURI* aURI, const nsAString& aCrossOrigin,
+ const nsAString& aImageReferrerPolicy, bool aIsImgSet,
+ uint64_t aEarlyHintPreloaderId);
+
+ void PreloadFont(nsIURI* aURI, const nsAString& aCrossOrigin,
+ const nsAString& aReferrerPolicy,
+ uint64_t aEarlyHintPreloaderId);
+
+ void PreloadFetch(nsIURI* aURI, const nsAString& aCrossOrigin,
+ const nsAString& aReferrerPolicy,
+ uint64_t aEarlyHintPreloaderId);
+
+ static void NotifyNodeEvent(nsINode* aNode, bool aSuccess);
+
+ void SetEarlyHintUsed() { mEarlyHintUsed = true; }
+ bool GetEarlyHintUsed() const { return mEarlyHintUsed; }
+
+ private:
+ dom::ReferrerPolicy PreloadReferrerPolicy(const nsAString& aReferrerPolicy);
+ nsIURI* BaseURIForPreload();
+
+ struct PreloadOrCoalesceResult {
+ RefPtr<PreloaderBase> mPreloader;
+ bool mAlreadyComplete;
+ };
+
+ PreloadOrCoalesceResult PreloadOrCoalesce(
+ nsIURI* aURI, const nsAString& aURL, nsContentPolicyType aPolicyType,
+ const nsAString& aAs, const nsAString& aType, const nsAString& aCharset,
+ const nsAString& aSrcset, const nsAString& aSizes,
+ const nsAString& aIntegrity, const nsAString& aCORS,
+ const nsAString& aReferrerPolicy, bool aFromHeader,
+ uint64_t aEarlyHintPreloaderId);
+
+ private:
+ nsRefPtrHashtable<PreloadHashKey, PreloaderBase> mPreloads;
+
+ // Raw pointer only, we are intended to be a direct member of dom::Document
+ dom::Document* mDocument;
+
+ // Set by `nsHtml5TreeOpExecutor::SetSpeculationBase`.
+ nsCOMPtr<nsIURI> mSpeculationBaseURI;
+
+ bool mEarlyHintUsed = false;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/uriloader/preload/PreloaderBase.cpp b/uriloader/preload/PreloaderBase.cpp
new file mode 100644
index 0000000000..2c3d4fb60b
--- /dev/null
+++ b/uriloader/preload/PreloaderBase.cpp
@@ -0,0 +1,391 @@
+/* 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 "PreloaderBase.h"
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/Telemetry.h"
+#include "nsContentUtils.h"
+#include "nsIAsyncVerifyRedirectCallback.h"
+#include "nsIHttpChannel.h"
+#include "nsILoadGroup.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIRedirectResultListener.h"
+#include "nsNetUtil.h"
+
+// Change this if we want to cancel and remove the associated preload on removal
+// of all <link rel=preload> tags from the tree.
+constexpr static bool kCancelAndRemovePreloadOnZeroReferences = false;
+
+namespace mozilla {
+
+PreloaderBase::UsageTimer::UsageTimer(PreloaderBase* aPreload,
+ dom::Document* aDocument)
+ : mDocument(aDocument), mPreload(aPreload) {}
+
+class PreloaderBase::RedirectSink final : public nsIInterfaceRequestor,
+ public nsIChannelEventSink,
+ public nsIRedirectResultListener {
+ RedirectSink() = delete;
+ virtual ~RedirectSink();
+
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIINTERFACEREQUESTOR
+ NS_DECL_NSICHANNELEVENTSINK
+ NS_DECL_NSIREDIRECTRESULTLISTENER
+
+ RedirectSink(PreloaderBase* aPreloader, nsIInterfaceRequestor* aCallbacks);
+
+ private:
+ MainThreadWeakPtr<PreloaderBase> mPreloader;
+ nsCOMPtr<nsIInterfaceRequestor> mCallbacks;
+ nsCOMPtr<nsIChannel> mRedirectChannel;
+};
+
+PreloaderBase::RedirectSink::RedirectSink(PreloaderBase* aPreloader,
+ nsIInterfaceRequestor* aCallbacks)
+ : mPreloader(aPreloader), mCallbacks(aCallbacks) {}
+
+PreloaderBase::RedirectSink::~RedirectSink() = default;
+
+NS_IMPL_ISUPPORTS(PreloaderBase::RedirectSink, nsIInterfaceRequestor,
+ nsIChannelEventSink, nsIRedirectResultListener)
+
+NS_IMETHODIMP PreloaderBase::RedirectSink::AsyncOnChannelRedirect(
+ nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags,
+ nsIAsyncVerifyRedirectCallback* aCallback) {
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+
+ mRedirectChannel = aNewChannel;
+
+ // Deliberately adding this before confirmation.
+ nsCOMPtr<nsIURI> uri;
+ aNewChannel->GetOriginalURI(getter_AddRefs(uri));
+ if (mPreloader) {
+ mPreloader->mRedirectRecords.AppendElement(
+ RedirectRecord(aFlags, uri.forget()));
+ }
+
+ if (mCallbacks) {
+ nsCOMPtr<nsIChannelEventSink> sink(do_GetInterface(mCallbacks));
+ if (sink) {
+ return sink->AsyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags,
+ aCallback);
+ }
+ }
+
+ aCallback->OnRedirectVerifyCallback(NS_OK);
+ return NS_OK;
+}
+
+NS_IMETHODIMP PreloaderBase::RedirectSink::OnRedirectResult(nsresult status) {
+ if (NS_SUCCEEDED(status) && mRedirectChannel) {
+ mPreloader->mChannel = std::move(mRedirectChannel);
+ } else {
+ mRedirectChannel = nullptr;
+ }
+
+ if (mCallbacks) {
+ nsCOMPtr<nsIRedirectResultListener> sink(do_GetInterface(mCallbacks));
+ if (sink) {
+ return sink->OnRedirectResult(status);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP PreloaderBase::RedirectSink::GetInterface(const nsIID& aIID,
+ void** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ if (aIID.Equals(NS_GET_IID(nsIChannelEventSink)) ||
+ aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) {
+ return QueryInterface(aIID, aResult);
+ }
+
+ if (mCallbacks) {
+ return mCallbacks->GetInterface(aIID, aResult);
+ }
+
+ *aResult = nullptr;
+ return NS_ERROR_NO_INTERFACE;
+}
+
+PreloaderBase::~PreloaderBase() { MOZ_ASSERT(NS_IsMainThread()); }
+
+// static
+void PreloaderBase::AddLoadBackgroundFlag(nsIChannel* aChannel) {
+ nsLoadFlags loadFlags;
+ aChannel->GetLoadFlags(&loadFlags);
+ aChannel->SetLoadFlags(loadFlags | nsIRequest::LOAD_BACKGROUND);
+}
+
+void PreloaderBase::NotifyOpen(const PreloadHashKey& aKey,
+ dom::Document* aDocument, bool aIsPreload,
+ bool aIsModule) {
+ if (aDocument) {
+ DebugOnly<bool> alreadyRegistered =
+ aDocument->Preloads().RegisterPreload(aKey, this);
+ // This means there is already a preload registered under this key in this
+ // document. We only allow replacement when this is a regular load.
+ // Otherwise, this should never happen and is a suspected misuse of the API.
+ MOZ_ASSERT_IF(alreadyRegistered, !aIsPreload);
+ }
+
+ mKey = aKey;
+ mIsUsed = !aIsPreload;
+
+ // Start usage timer for rel="preload", but not for rel="modulepreload"
+ // because modules may be loaded for functionality the user does not
+ // immediately interact with after page load (e.g. a docs search box)
+ if (!aIsModule && !mIsUsed && !mUsageTimer) {
+ auto callback = MakeRefPtr<UsageTimer>(this, aDocument);
+ NS_NewTimerWithCallback(getter_AddRefs(mUsageTimer), callback, 10000,
+ nsITimer::TYPE_ONE_SHOT);
+ }
+
+ ReportUsageTelemetry();
+}
+
+void PreloaderBase::NotifyOpen(const PreloadHashKey& aKey, nsIChannel* aChannel,
+ dom::Document* aDocument, bool aIsPreload,
+ bool aIsModule) {
+ NotifyOpen(aKey, aDocument, aIsPreload, aIsModule);
+ mChannel = aChannel;
+
+ nsCOMPtr<nsIInterfaceRequestor> callbacks;
+ mChannel->GetNotificationCallbacks(getter_AddRefs(callbacks));
+ RefPtr<RedirectSink> sink(new RedirectSink(this, callbacks));
+ mChannel->SetNotificationCallbacks(sink);
+}
+
+void PreloaderBase::NotifyUsage(dom::Document* aDocument,
+ LoadBackground aLoadBackground) {
+ if (!mIsUsed && mChannel && aLoadBackground == LoadBackground::Drop) {
+ nsLoadFlags loadFlags;
+ mChannel->GetLoadFlags(&loadFlags);
+
+ // Preloads are initially set the LOAD_BACKGROUND flag. When becoming
+ // regular loads by hitting its consuming tag, we need to drop that flag,
+ // which also means to re-add the request from/to it's loadgroup to reflect
+ // that flag change.
+ if (loadFlags & nsIRequest::LOAD_BACKGROUND) {
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ mChannel->GetLoadGroup(getter_AddRefs(loadGroup));
+
+ if (loadGroup) {
+ nsresult status;
+ mChannel->GetStatus(&status);
+
+ nsresult rv = loadGroup->RemoveRequest(mChannel, nullptr, status);
+ mChannel->SetLoadFlags(loadFlags & ~nsIRequest::LOAD_BACKGROUND);
+ if (NS_SUCCEEDED(rv)) {
+ loadGroup->AddRequest(mChannel, nullptr);
+ }
+ }
+ }
+ }
+
+ mIsUsed = true;
+ ReportUsageTelemetry();
+ CancelUsageTimer();
+ if (mIsEarlyHintsPreload) {
+ aDocument->Preloads().SetEarlyHintUsed();
+ }
+}
+
+void PreloaderBase::RemoveSelf(dom::Document* aDocument) {
+ if (aDocument) {
+ aDocument->Preloads().DeregisterPreload(mKey);
+ }
+}
+
+void PreloaderBase::NotifyRestart(dom::Document* aDocument,
+ PreloaderBase* aNewPreloader) {
+ RemoveSelf(aDocument);
+ mKey = PreloadHashKey();
+
+ CancelUsageTimer();
+
+ if (aNewPreloader) {
+ aNewPreloader->mNodes = std::move(mNodes);
+ }
+}
+
+void PreloaderBase::NotifyStart(nsIRequest* aRequest) {
+ // If there is no channel assigned on this preloader, we are not between
+ // channel switching, so we can freely update the mShouldFireLoadEvent using
+ // the given channel.
+ if (mChannel && !SameCOMIdentity(aRequest, mChannel)) {
+ return;
+ }
+
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest);
+ if (!httpChannel) {
+ return;
+ }
+
+ // if the load is cross origin without CORS, or the CORS access is rejected,
+ // always fire load event to avoid leaking site information.
+ nsresult rv;
+ nsCOMPtr<nsILoadInfo> loadInfo = httpChannel->LoadInfo();
+ mShouldFireLoadEvent =
+ loadInfo->GetTainting() == LoadTainting::Opaque ||
+ (loadInfo->GetTainting() == LoadTainting::CORS &&
+ (NS_FAILED(httpChannel->GetStatus(&rv)) || NS_FAILED(rv)));
+}
+
+void PreloaderBase::NotifyStop(nsIRequest* aRequest, nsresult aStatus) {
+ // Filter out notifications that may be arriving from the old channel before
+ // restarting this request.
+ if (!SameCOMIdentity(aRequest, mChannel)) {
+ return;
+ }
+
+ NotifyStop(aStatus);
+}
+
+void PreloaderBase::NotifyStop(nsresult aStatus) {
+ mOnStopStatus.emplace(aStatus);
+
+ nsTArray<nsWeakPtr> nodes = std::move(mNodes);
+
+ for (nsWeakPtr& weak : nodes) {
+ nsCOMPtr<nsINode> node = do_QueryReferent(weak);
+ if (node) {
+ NotifyNodeEvent(node);
+ }
+ }
+
+ mChannel = nullptr;
+}
+
+void PreloaderBase::AddLinkPreloadNode(nsINode* aNode) {
+ if (mOnStopStatus) {
+ return NotifyNodeEvent(aNode);
+ }
+
+ mNodes.AppendElement(do_GetWeakReference(aNode));
+}
+
+void PreloaderBase::RemoveLinkPreloadNode(nsINode* aNode) {
+ // Note that do_GetWeakReference returns the internal weak proxy, which is
+ // always the same, so we can use it to search the array using default
+ // comparator.
+ nsWeakPtr node = do_GetWeakReference(aNode);
+ mNodes.RemoveElement(node);
+
+ if (kCancelAndRemovePreloadOnZeroReferences && mNodes.Length() == 0 &&
+ !mIsUsed) {
+ // Keep a reference, because the following call may release us. The caller
+ // may use a WeakPtr to access this.
+ RefPtr<PreloaderBase> self(this);
+ RemoveSelf(aNode->OwnerDoc());
+
+ if (mChannel) {
+ mChannel->CancelWithReason(NS_BINDING_ABORTED,
+ "PreloaderBase::RemoveLinkPreloadNode"_ns);
+ }
+ }
+}
+
+void PreloaderBase::NotifyNodeEvent(nsINode* aNode) {
+ PreloadService::NotifyNodeEvent(
+ aNode, mShouldFireLoadEvent || NS_SUCCEEDED(*mOnStopStatus));
+}
+
+void PreloaderBase::CancelUsageTimer() {
+ if (mUsageTimer) {
+ mUsageTimer->Cancel();
+ mUsageTimer = nullptr;
+ }
+}
+
+void PreloaderBase::ReportUsageTelemetry() {
+ if (mUsageTelementryReported) {
+ return;
+ }
+ mUsageTelementryReported = true;
+
+ if (mKey.As() == PreloadHashKey::ResourceType::NONE) {
+ return;
+ }
+
+ // The labels are structured as type1-used, type1-unused, type2-used, ...
+ // The first "as" resource type is NONE with value 0.
+ auto index = (static_cast<uint32_t>(mKey.As()) - 1) * 2;
+ if (!mIsUsed) {
+ ++index;
+ }
+
+ auto label = static_cast<Telemetry::LABELS_REL_PRELOAD_MISS_RATIO>(index);
+ Telemetry::AccumulateCategorical(label);
+}
+
+nsresult PreloaderBase::AsyncConsume(nsIStreamListener* aListener) {
+ // We want to return an error so that consumers can't ever use a preload to
+ // consume data unless it's properly implemented.
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+// PreloaderBase::RedirectRecord
+
+nsCString PreloaderBase::RedirectRecord::Spec() const {
+ nsCOMPtr<nsIURI> noFragment;
+ NS_GetURIWithoutRef(mURI, getter_AddRefs(noFragment));
+ MOZ_ASSERT(noFragment);
+ return noFragment->GetSpecOrDefault();
+}
+
+nsCString PreloaderBase::RedirectRecord::Fragment() const {
+ nsCString fragment;
+ mURI->GetRef(fragment);
+ return fragment;
+}
+
+// PreloaderBase::UsageTimer
+
+NS_IMPL_ISUPPORTS(PreloaderBase::UsageTimer, nsITimerCallback, nsINamed)
+
+NS_IMETHODIMP PreloaderBase::UsageTimer::Notify(nsITimer* aTimer) {
+ if (!mPreload || !mDocument) {
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(aTimer == mPreload->mUsageTimer);
+ mPreload->mUsageTimer = nullptr;
+
+ if (mPreload->IsUsed()) {
+ // Left in the hashtable, but marked as used. This is a valid case, and we
+ // don't want to emit a warning for this preload then.
+ return NS_OK;
+ }
+
+ mPreload->ReportUsageTelemetry();
+
+ // PreloadHashKey overrides GetKey, we need to use the nsURIHashKey one to get
+ // the URI.
+ nsIURI* uri = static_cast<nsURIHashKey*>(&mPreload->mKey)->GetKey();
+ if (!uri) {
+ return NS_OK;
+ }
+
+ nsString spec;
+ NS_GetSanitizedURIStringFromURI(uri, spec);
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns,
+ mDocument, nsContentUtils::eDOM_PROPERTIES,
+ "UnusedLinkPreloadPending",
+ nsTArray<nsString>({std::move(spec)}));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PreloaderBase::UsageTimer::GetName(nsACString& aName) {
+ aName.AssignLiteral("PreloaderBase::UsageTimer");
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/uriloader/preload/PreloaderBase.h b/uriloader/preload/PreloaderBase.h
new file mode 100644
index 0000000000..2ecb37a1fd
--- /dev/null
+++ b/uriloader/preload/PreloaderBase.h
@@ -0,0 +1,200 @@
+/* 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 PreloaderBase_h__
+#define PreloaderBase_h__
+
+#include "mozilla/Maybe.h"
+#include "mozilla/PreloadHashKey.h"
+#include "mozilla/WeakPtr.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsITimer.h"
+#include "nsIURI.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsTArray.h"
+
+class nsIChannel;
+class nsINode;
+class nsIRequest;
+class nsIStreamListener;
+
+namespace mozilla {
+
+namespace dom {
+
+class Document;
+
+} // namespace dom
+
+/**
+ * A half-abstract base class that resource loaders' respective
+ * channel-listening classes should derive from. Provides a unified API to
+ * register the load or preload in a document scoped service, links <link
+ * rel="preload"> DOM nodes with the load progress and provides API to possibly
+ * consume the data by later, dynamically discovered consumers.
+ *
+ * This class is designed to be used only on the main thread.
+ */
+class PreloaderBase : public SupportsWeakPtr, public nsISupports {
+ public:
+ PreloaderBase() = default;
+
+ // Called by resource loaders to register this preload in the document's
+ // preload service to provide coalescing, and access to the preload when it
+ // should be used for an actual load.
+ void NotifyOpen(const PreloadHashKey& aKey, dom::Document* aDocument,
+ bool aIsPreload, bool aIsModule = false);
+ void NotifyOpen(const PreloadHashKey& aKey, nsIChannel* aChannel,
+ dom::Document* aDocument, bool aIsPreload,
+ bool aIsModule = false);
+
+ // Called when the load is about to be started all over again and thus this
+ // PreloaderBase will be registered again with the same key. This method
+ // taks care to deregister this preload prior to that.
+ // @param aNewPreloader: If there is a new request and loader created for the
+ // restarted load, we will pass any necessary information into it.
+ void NotifyRestart(dom::Document* aDocument,
+ PreloaderBase* aNewPreloader = nullptr);
+
+ // Called by the loading object when the channel started to load
+ // (OnStartRequest or equal) and when it finished (OnStopRequest or equal)
+ void NotifyStart(nsIRequest* aRequest);
+ void NotifyStop(nsIRequest* aRequest, nsresult aStatus);
+ // Use this variant only in complement to NotifyOpen without providing a
+ // channel.
+ void NotifyStop(nsresult aStatus);
+
+ // Called by resource loaders or any suitable component to notify the preload
+ // has been used for an actual load. This is intended to stop any usage
+ // timers.
+ // @param aDropLoadBackground: If `Keep` then the loading channel, if still in
+ // progress, will not be removed the LOAD_BACKGROUND flag, for instance XHR is
+ // the user here.
+ enum class LoadBackground { Keep, Drop };
+ void NotifyUsage(dom::Document* aDocument,
+ LoadBackground aLoadBackground = LoadBackground::Drop);
+ // Whether this preloader has been used for a regular/actual load or not.
+ bool IsUsed() const { return mIsUsed; }
+
+ // Removes itself from the document's preloads hashtable
+ void RemoveSelf(dom::Document* aDocument);
+
+ // When a loader starting an actual load finds a preload, the data can be
+ // delivered using this method. It will deliver stream listener notifications
+ // as if it were coming from the resource loading channel. The |request|
+ // argument will be the channel that loaded/loads the resource.
+ // This method must keep to the nsIChannel.AsyncOpen contract. A loader is
+ // not obligated to re-implement this method when not necessarily needed.
+ virtual nsresult AsyncConsume(nsIStreamListener* aListener);
+
+ // Accessor to the resource loading channel.
+ nsIChannel* Channel() const { return mChannel; }
+
+ // May change priority of the resource loading channel so that it's treated as
+ // preload when this was initially representing a normal speculative load but
+ // later <link rel="preload"> was found for this resource.
+ virtual void PrioritizeAsPreload() = 0;
+
+ // Helper function to set the LOAD_BACKGROUND flag on channel initiated by
+ // <link rel=preload>. This MUST be used before the channel is AsyncOpen'ed.
+ static void AddLoadBackgroundFlag(nsIChannel* aChannel);
+
+ // These are linking this preload to <link rel="preload"> DOM nodes. If we
+ // are already loaded, immediately notify events on the node, otherwise wait
+ // for NotifyStop() call.
+ void AddLinkPreloadNode(nsINode* aNode);
+ void RemoveLinkPreloadNode(nsINode* aNode);
+
+ // A collection of redirects, the main consumer is fetch.
+ class RedirectRecord {
+ public:
+ RedirectRecord(uint32_t aFlags, already_AddRefed<nsIURI> aURI)
+ : mFlags(aFlags), mURI(aURI) {}
+
+ uint32_t Flags() const { return mFlags; }
+ nsCString Spec() const;
+ nsCString Fragment() const;
+
+ private:
+ uint32_t mFlags;
+ nsCOMPtr<nsIURI> mURI;
+ };
+
+ const nsTArray<RedirectRecord>& Redirects() { return mRedirectRecords; }
+
+ void SetForEarlyHints() { mIsEarlyHintsPreload = true; }
+
+ protected:
+ virtual ~PreloaderBase();
+
+ // The loading channel. This will update when a redirect occurs.
+ nsCOMPtr<nsIChannel> mChannel;
+
+ private:
+ void NotifyNodeEvent(nsINode* aNode);
+ void CancelUsageTimer();
+
+ void ReportUsageTelemetry();
+
+ // A helper class that will update the PreloaderBase.mChannel member when a
+ // redirect happens, so that we can reprioritize or cancel when needed.
+ // Having a separate class instead of implementing this on PreloaderBase
+ // directly is to keep PreloaderBase as simple as possible so that derived
+ // classes don't have to deal with calling super when implementing these
+ // interfaces from some reason as well.
+ class RedirectSink;
+
+ // A timer callback to trigger the unuse warning for this preload
+ class UsageTimer final : public nsITimerCallback, public nsINamed {
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+
+ UsageTimer(PreloaderBase* aPreload, dom::Document* aDocument);
+
+ private:
+ ~UsageTimer() = default;
+
+ WeakPtr<dom::Document> mDocument;
+ WeakPtr<PreloaderBase> mPreload;
+ };
+
+ private:
+ // Reference to HTMLLinkElement DOM nodes to deliver onload and onerror
+ // notifications to.
+ nsTArray<nsWeakPtr> mNodes;
+
+ // History of redirects.
+ nsTArray<RedirectRecord> mRedirectRecords;
+
+ // Usage timer, emits warning when the preload is not used in time. Started
+ // in NotifyOpen and stopped in NotifyUsage.
+ nsCOMPtr<nsITimer> mUsageTimer;
+
+ // The key this preload has been registered under. We want to remember it to
+ // be able to deregister itself from the document's preloads.
+ PreloadHashKey mKey;
+
+ // This overrides the final event we send to DOM nodes to be always 'load'.
+ // Modified in NotifyStart based on LoadInfo data of the loading channel.
+ bool mShouldFireLoadEvent = false;
+
+ // True after call to NotifyUsage.
+ bool mIsUsed = false;
+
+ // True after we have reported the usage telemetry. Prevent duplicates.
+ bool mUsageTelementryReported = false;
+
+ // True when this is used to Early Hints preload.
+ bool mIsEarlyHintsPreload = false;
+
+ // Emplaced when the data delivery has finished, in NotifyStop, holds the
+ // result of the load.
+ Maybe<nsresult> mOnStopStatus;
+};
+
+} // namespace mozilla
+
+#endif // !PreloaderBase_h__
diff --git a/uriloader/preload/gtest/TestFetchPreloader.cpp b/uriloader/preload/gtest/TestFetchPreloader.cpp
new file mode 100644
index 0000000000..67f6eed6f4
--- /dev/null
+++ b/uriloader/preload/gtest/TestFetchPreloader.cpp
@@ -0,0 +1,950 @@
+#include "gtest/gtest.h"
+
+#include "mozilla/CORSMode.h"
+#include "mozilla/dom/XMLDocument.h"
+#include "mozilla/dom/ReferrerPolicyBinding.h"
+#include "mozilla/FetchPreloader.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/PreloadHashKey.h"
+#include "mozilla/SpinEventLoopUntil.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::SetCanceledReason(const nsACString& aReason) {
+ return SetCanceledReasonImpl(aReason);
+}
+NS_IMETHODIMP FakeChannel::GetCanceledReason(nsACString& aReason) {
+ return GetCanceledReasonImpl(aReason);
+}
+NS_IMETHODIMP FakeChannel::CancelWithReason(nsresult aStatus,
+ const nsACString& aReason) {
+ return CancelWithReasonImpl(aStatus, aReason);
+}
+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(
+ nsITransportSecurityInfo** 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, uint64_t aHttpChannelId) 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;
+ mozilla::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(
+ "uriloader:TestFetchPreloader:Await"_ns, [&]() {
+ 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), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ 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, 0)));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener));
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+
+ EXPECT_NS_SUCCEEDED(channel->Start());
+ EXPECT_NS_SUCCEEDED(channel->Data("one"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("two"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("three"_ns));
+ EXPECT_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), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ 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, 0)));
+
+ EXPECT_NS_SUCCEEDED(channel->Start());
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener));
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(listener->mOnStart);
+
+ EXPECT_NS_SUCCEEDED(channel->Data("one"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("two"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("three"_ns));
+ EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree"));
+
+ EXPECT_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), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ 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, 0)));
+
+ EXPECT_NS_SUCCEEDED(channel->Start());
+ EXPECT_NS_SUCCEEDED(channel->Data("one"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("two"_ns));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener));
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(listener->mOnStart);
+
+ EXPECT_NS_SUCCEEDED(channel->Data("three"_ns));
+ EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree"));
+
+ EXPECT_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), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ 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, 0)));
+
+ EXPECT_NS_SUCCEEDED(channel->Start());
+ EXPECT_NS_SUCCEEDED(channel->Data("one"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("two"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("three"_ns));
+
+ // Request consumation of the preload...
+ RefPtr<FakeListener> listener = new FakeListener();
+ EXPECT_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_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), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ 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, 0)));
+
+ EXPECT_NS_SUCCEEDED(channel->Start());
+ EXPECT_NS_SUCCEEDED(channel->Data("one"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("two"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("three"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ EXPECT_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), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ 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, 0)));
+
+ EXPECT_NS_SUCCEEDED(channel->Start());
+ EXPECT_NS_SUCCEEDED(channel->Data("one"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("two"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("three"_ns));
+ EXPECT_NS_FAILED(channel->Stop(ERROR_ONSTOP));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ EXPECT_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), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ 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, 0)));
+
+ EXPECT_NS_SUCCEEDED(channel->Start());
+ EXPECT_NS_SUCCEEDED(channel->Data("one"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("two"_ns));
+ channel->Cancel(ERROR_CANCEL);
+ EXPECT_NS_FAILED(channel->Stop(ERROR_CANCEL));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ EXPECT_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), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ 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, 0)));
+
+ EXPECT_NS_SUCCEEDED(channel->Start());
+ EXPECT_NS_SUCCEEDED(channel->Data("one"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("two"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("three"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ listener->mOnStartResult = ERROR_THROW;
+
+ EXPECT_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), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ 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, 0)));
+
+ EXPECT_NS_SUCCEEDED(channel->Start());
+ EXPECT_NS_SUCCEEDED(channel->Data("one"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("two"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("three"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ listener->mOnDataResult = ERROR_THROW;
+
+ EXPECT_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), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ 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, 0)));
+
+ EXPECT_NS_SUCCEEDED(channel->Start());
+ EXPECT_NS_SUCCEEDED(channel->Data("one"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("two"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("three"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ listener->mOnStopResult = ERROR_THROW;
+
+ EXPECT_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), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ 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, 0)));
+
+ EXPECT_NS_SUCCEEDED(channel->Start());
+ EXPECT_NS_SUCCEEDED(channel->Data("one"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("two"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("three"_ns));
+ EXPECT_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_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), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ 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, 0)));
+
+ EXPECT_NS_SUCCEEDED(channel->Start());
+ EXPECT_NS_SUCCEEDED(channel->Data("one"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("two"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("three"_ns));
+ EXPECT_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_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), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ 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, 0)));
+
+ EXPECT_NS_SUCCEEDED(channel->Start());
+ EXPECT_NS_SUCCEEDED(channel->Data("one"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("two"_ns));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ listener->mCancelIn = FakeListener::OnData;
+
+ EXPECT_NS_SUCCEEDED(preloader->AsyncConsume(listener));
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_NS_FAILED(channel->Data("three"_ns));
+ EXPECT_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), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ 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, 0)));
+
+ EXPECT_NS_SUCCEEDED(channel->Start());
+ EXPECT_NS_SUCCEEDED(channel->Data("one"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("two"_ns));
+
+ // 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_NS_SUCCEEDED(channel->Data("three"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK));
+ }));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ listener->mCancelIn = FakeListener::OnStart;
+
+ EXPECT_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), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ 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, 0)));
+
+ EXPECT_NS_SUCCEEDED(channel->Start());
+ EXPECT_NS_SUCCEEDED(channel->Data("one"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("two"_ns));
+
+ // 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_NS_SUCCEEDED(channel->Data("three"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK));
+ }));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ listener->mCancelIn = FakeListener::OnData;
+
+ EXPECT_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), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ 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, 0)));
+
+ EXPECT_NS_SUCCEEDED(channel->Start());
+ EXPECT_NS_SUCCEEDED(channel->Data("one"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Data("two"_ns));
+
+ // 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_NS_SUCCEEDED(channel->Data("three"_ns));
+ EXPECT_NS_SUCCEEDED(channel->Stop(NS_OK));
+ }));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ listener->mOnStartResult = ERROR_THROW;
+
+ EXPECT_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)));
+}
diff --git a/uriloader/preload/gtest/moz.build b/uriloader/preload/gtest/moz.build
new file mode 100644
index 0000000000..72c2afa539
--- /dev/null
+++ b/uriloader/preload/gtest/moz.build
@@ -0,0 +1,18 @@
+# -*- 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"]
diff --git a/uriloader/preload/moz.build b/uriloader/preload/moz.build
new file mode 100644
index 0000000000..27a2f8cf00
--- /dev/null
+++ b/uriloader/preload/moz.build
@@ -0,0 +1,26 @@
+# -*- Mode: python; 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/.
+
+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",
+]
+
+FINAL_LIBRARY = "xul"