/* -*- 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/Assertions.h" #include "mozilla/AsyncEventDispatcher.h" #include "mozilla/Maybe.h" #include "mozilla/dom/FetchPriority.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 "nsGenericHTMLElement.h" #include "nsNetUtil.h" namespace mozilla { using namespace dom; static LazyLogModule sPreloadServiceLog{"PreloadService"}; 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 PreloadService::LookupPreload( const PreloadHashKey& aKey) const { return mPreloads.Get(aKey); } already_AddRefed PreloadService::GetPreloadURI(const nsAString& aURL) { nsIURI* base = BaseURIForPreload(); auto encoding = mDocument->GetDocumentCharacterSet(); nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL, encoding, base); if (NS_FAILED(rv)) { return nullptr; } return uri.forget(); } already_AddRefed PreloadService::PreloadLinkElement( dom::HTMLLinkElement* aLinkElement, nsContentPolicyType aPolicyType) { if (aPolicyType == nsIContentPolicy::TYPE_INVALID) { MOZ_ASSERT_UNREACHABLE("Caller should check"); return nullptr; } nsAutoString as, charset, crossOrigin, integrity, referrerPolicy, fetchPriority, rel, srcset, sizes, type, url; nsCOMPtr 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->GetFetchPriority(fetchPriority); aLinkElement->GetRel(rel); nsAutoString nonce; if (nsString* cspNonce = static_cast(aLinkElement->GetProperty(nsGkAtoms::nonce))) { nonce = *cspNonce; } 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, nonce, integrity, crossOrigin, referrerPolicy, fetchPriority, /* 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& aNonce, const nsAString& aIntegrity, const nsAString& aSrcset, const nsAString& aSizes, const nsAString& aCORS, const nsAString& aReferrerPolicy, uint64_t aEarlyHintPreloaderId, const nsAString& aFetchPriority) { if (aPolicyType == nsIContentPolicy::TYPE_INVALID) { MOZ_ASSERT_UNREACHABLE("Caller should check"); return; } PreloadOrCoalesce(aURI, aURL, aPolicyType, aAs, aType, u""_ns, aSrcset, aSizes, aNonce, aIntegrity, aCORS, aReferrerPolicy, aFetchPriority, /* aFromHeader = */ true, aEarlyHintPreloaderId); } // The mapping is specified as implementation-defined, see step 15 of // . For web-compatibility, // Chromium's mapping described at // // is chosen. class SupportsPriorityValueFor { public: static int32_t LinkRelPreloadFont(const FetchPriority aFetchPriority) { if (!StaticPrefs::network_fetchpriority_enabled()) { return nsISupportsPriority::PRIORITY_HIGH; } switch (aFetchPriority) { case FetchPriority::Auto: return nsISupportsPriority::PRIORITY_HIGH; case FetchPriority::High: return nsISupportsPriority::PRIORITY_HIGH; case FetchPriority::Low: return nsISupportsPriority::PRIORITY_LOW; } MOZ_ASSERT_UNREACHABLE(); return nsISupportsPriority::PRIORITY_HIGH; } static int32_t LinkRelPreloadFetch(const FetchPriority aFetchPriority) { if (!StaticPrefs::network_fetchpriority_enabled()) { return nsISupportsPriority::PRIORITY_NORMAL; } switch (aFetchPriority) { case FetchPriority::Auto: return nsISupportsPriority::PRIORITY_NORMAL; case FetchPriority::High: return nsISupportsPriority::PRIORITY_HIGH; case FetchPriority::Low: return nsISupportsPriority::PRIORITY_LOW; } MOZ_ASSERT_UNREACHABLE(); return nsISupportsPriority::PRIORITY_NORMAL; } }; 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& aNonce, const nsAString& aIntegrity, const nsAString& aCORS, const nsAString& aReferrerPolicy, const nsAString& aFetchPriority, bool aFromHeader, uint64_t aEarlyHintPreloaderId) { if (!aURI) { MOZ_ASSERT_UNREACHABLE("Should not pass null nsIURI"); return {nullptr, false}; } bool isImgSet = false; PreloadHashKey preloadKey; nsCOMPtr 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 preload = LookupPreload(preloadKey)) { return {std::move(preload), false}; } if (aAs.LowerCaseEqualsASCII("script")) { PreloadScript(uri, aType, aCharset, aCORS, aReferrerPolicy, aNonce, aFetchPriority, aIntegrity, true /* isInHead - TODO */, aEarlyHintPreloaderId); } else if (aAs.LowerCaseEqualsASCII("style")) { auto status = mDocument->PreloadStyle( aURI, Encoding::ForLabel(aCharset), aCORS, PreloadReferrerPolicy(aReferrerPolicy), aNonce, aIntegrity, aFromHeader ? css::StylePreloadKind::FromLinkRelPreloadHeader : css::StylePreloadKind::FromLinkRelPreloadElement, aEarlyHintPreloaderId, aFetchPriority); 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, aFetchPriority); } else if (aAs.LowerCaseEqualsASCII("fetch")) { PreloadFetch(uri, aCORS, aReferrerPolicy, aEarlyHintPreloaderId, aFetchPriority); } RefPtr 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& aNonce, const nsAString& aFetchPriority, const nsAString& aIntegrity, bool aScriptFromHead, uint64_t aEarlyHintPreloaderId) { mDocument->ScriptLoader()->PreloadURI( aURI, aCharset, aType, aCrossOrigin, aNonce, aFetchPriority, aIntegrity, aScriptFromHead, 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, const nsAString& aFetchPriority) { CORSMode cors = dom::Element::StringToCORSMode(aCrossOrigin); auto key = PreloadHashKey::CreateAsFont(aURI, cors); if (PreloadExists(key)) { return; } const auto fetchPriority = nsGenericHTMLElement::ToFetchPriority(aFetchPriority); const auto supportsPriorityValue = SupportsPriorityValueFor::LinkRelPreloadFont(fetchPriority); LogPriorityMapping(sPreloadServiceLog, fetchPriority, supportsPriorityValue); RefPtr preloader = new FontPreloader(); dom::ReferrerPolicy referrerPolicy = PreloadReferrerPolicy(aReferrerPolicy); preloader->OpenChannel(key, aURI, cors, referrerPolicy, mDocument, aEarlyHintPreloaderId, supportsPriorityValue); } void PreloadService::PreloadFetch(nsIURI* aURI, const nsAString& aCrossOrigin, const nsAString& aReferrerPolicy, uint64_t aEarlyHintPreloaderId, const nsAString& aFetchPriority) { CORSMode cors = dom::Element::StringToCORSMode(aCrossOrigin); auto key = PreloadHashKey::CreateAsFetch(aURI, cors); if (PreloadExists(key)) { return; } RefPtr preloader = new FetchPreloader(); dom::ReferrerPolicy referrerPolicy = PreloadReferrerPolicy(aReferrerPolicy); const auto fetchPriority = nsGenericHTMLElement::ToFetchPriority(aFetchPriority); const int32_t supportsPriorityValue = SupportsPriorityValueFor::LinkRelPreloadFetch(fetchPriority); if (supportsPriorityValue) { LogPriorityMapping(sPreloadServiceLog, fetchPriority, supportsPriorityValue); } preloader->OpenChannel(key, aURI, cors, referrerPolicy, mDocument, aEarlyHintPreloaderId, supportsPriorityValue); } // 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 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