/* 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 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 mPreloader; nsCOMPtr mCallbacks; nsCOMPtr 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 uri; aNewChannel->GetOriginalURI(getter_AddRefs(uri)); if (mPreloader) { mPreloader->mRedirectRecords.AppendElement( RedirectRecord(aFlags, uri.forget())); } if (mCallbacks) { nsCOMPtr 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 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 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(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 callbacks; mChannel->GetNotificationCallbacks(getter_AddRefs(callbacks)); RefPtr 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 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 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 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 nodes = std::move(mNodes); for (nsWeakPtr& weak : nodes) { nsCOMPtr 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 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(mKey.As()) - 1) * 2; if (!mIsUsed) { ++index; } auto label = static_cast(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 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(&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({std::move(spec)})); return NS_OK; } NS_IMETHODIMP PreloaderBase::UsageTimer::GetName(nsACString& aName) { aName.AssignLiteral("PreloaderBase::UsageTimer"); return NS_OK; } } // namespace mozilla