diff --git a/dom/base/Link.cpp b/dom/base/Link.cpp index 2a0b926086..e00a166c11 100644 --- a/dom/base/Link.cpp +++ b/dom/base/Link.cpp @@ -23,6 +23,13 @@ #include "mozilla/Services.h" +#include "nsIContentPolicy.h" +#include "nsCSSParser.h" +#include "nsMimeTypes.h" +#include "nsIMediaList.h" +#include "DecoderTraits.h" +#include "imgLoader.h" + namespace mozilla { namespace dom { @@ -116,7 +123,7 @@ Link::CancelDNSPrefetch(nsWrapperCache::FlagsType aDeferredFlag, } void -Link::TryDNSPrefetchPreconnectOrPrefetch() +Link::TrySpeculativeLoadFeature() { MOZ_ASSERT(mElement->IsInComposedDoc()); if (!ElementHasHref()) { @@ -136,15 +143,28 @@ Link::TryDNSPrefetchPreconnectOrPrefetch() mElement->NodePrincipal()); if ((linkTypes & nsStyleLinkElement::ePREFETCH) || - (linkTypes & nsStyleLinkElement::eNEXT)){ + (linkTypes & nsStyleLinkElement::eNEXT) || + (linkTypes & nsStyleLinkElement::ePRELOAD)) { nsCOMPtr prefetchService(do_GetService(NS_PREFETCHSERVICE_CONTRACTID)); if (prefetchService) { nsCOMPtr uri(GetURI()); if (uri) { nsCOMPtr domNode = GetAsDOMNode(mElement); - prefetchService->PrefetchURI(uri, - mElement->OwnerDoc()->GetDocumentURI(), - domNode, linkTypes & nsStyleLinkElement::ePREFETCH); + if (linkTypes & nsStyleLinkElement::ePRELOAD) { + nsContentPolicyType policyType; + bool isPreloadValid = CheckPreloadAttrs(policyType, mElement); + if (!isPreloadValid) { + // XXX: WPTs expect that we timeout on invalid preloads including + // those with valid destinations instead of firing an error event. + return; + } + nsContentUtils::DispatchEventForPreloadURI(domNode, policyType); + } else { + prefetchService->PrefetchURI(uri, + mElement->OwnerDoc()->GetDocumentURI(), + domNode, + linkTypes & nsStyleLinkElement::ePREFETCH); + } return; } } @@ -166,6 +186,95 @@ Link::TryDNSPrefetchPreconnectOrPrefetch() } } +void +Link::UpdatePreload(nsIAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue) +{ + MOZ_ASSERT(mElement->IsInComposedDoc()); + + if (!ElementHasHref()) { + return; + } + + nsAutoString rel; + if (!mElement->GetAttr(kNameSpaceID_None, nsGkAtoms::rel, rel)) { + return; + } + + if (!nsContentUtils::PrefetchEnabled(mElement->OwnerDoc()->GetDocShell()) || + !nsContentUtils::IsPreloadEnabled()) { + return; + } + + uint32_t linkTypes = nsStyleLinkElement::ParseLinkTypes(rel, + mElement->NodePrincipal()); + + if (!(linkTypes & nsStyleLinkElement::ePRELOAD)) { + return; + } + + nsCOMPtr uri(GetURI()); + if (!uri) { + return; + } + + nsCOMPtr domNode = GetAsDOMNode(mElement); + + nsContentPolicyType policyType; + bool isPreloadValid = CheckPreloadAttrs(policyType, mElement); + if (!isPreloadValid) { + // XXX: WPTs expect that we timeout on invalid preloads including + // those with valid destinations instead of firing an error event. + return; + } + + if (aName == nsGkAtoms::crossorigin) { + CORSMode corsMode = Element::AttrValueToCORSMode(aValue); + CORSMode oldCorsMode = Element::AttrValueToCORSMode(aOldValue); + if (corsMode != oldCorsMode) { + nsContentUtils::DispatchEventForPreloadURI(domNode, policyType); + } + return; + } + + nsAutoString oldValue; + if (aOldValue) { + aOldValue->ToString(oldValue); + } else { + oldValue = EmptyString(); + } + nsContentPolicyType oldPolicyType = nsIContentPolicy::TYPE_INVALID; + + if (aName == nsGkAtoms::as) { + if (aOldValue) { + CheckPreloadAttrs(oldPolicyType, aName, oldValue, mElement); + } + } else if (aName == nsGkAtoms::type) { + if (CheckPreloadAttrs(oldPolicyType, aName, oldValue, mElement)) { + oldPolicyType = policyType; + } else { + oldPolicyType = nsIContentPolicy::TYPE_INVALID; + } + } else if (aName == nsGkAtoms::media) { + if (CheckPreloadAttrs(oldPolicyType, aName, oldValue, mElement)) { + oldPolicyType = policyType; + } else { + oldPolicyType = nsIContentPolicy::TYPE_INVALID; + } + } + + // Return early for invalid preloads since we shouldn't trigger a new fetch. + if (policyType == nsIContentPolicy::TYPE_INVALID) { + return; + } + + // Fire the associated event if the policy type has changed. + if (policyType != oldPolicyType) { + nsContentUtils::DispatchEventForPreloadURI(domNode, policyType); + } +} + void Link::CancelPrefetch() { @@ -676,6 +785,120 @@ Link::SetHrefAttribute(nsIURI *aURI) NS_ConvertUTF8toUTF16(href), true); } +bool +Link::CheckPreloadAttrs(nsContentPolicyType& aPolicyType, + const nsAString& aDestination, + const nsAString& aType, + const nsAString& aMedia, + nsIDocument* aDocument) +{ + nsString mimeType; + nsString params; + nsContentUtils::SplitMimeType(aType, mimeType, params); + ToLowerCase(mimeType); + + nsAttrValue destinationAttr; + ParseDestinationValue(aDestination, destinationAttr); + + aPolicyType = DestinationToContentPolicy(destinationAttr); + if (aPolicyType == nsIContentPolicy::TYPE_INVALID) { + return false; + } + + // Check if media attribute is valid. + if (!aMedia.IsEmpty()) { + nsCSSParser cssParser; + RefPtr mediaList = new nsMediaList(); + cssParser.ParseMediaList(aMedia, nullptr, 0, mediaList, false); + + nsIPresShell* shell = aDocument->GetShell(); + if (!shell) { + return false; + } + + nsPresContext* presContext = shell->GetPresContext(); + if (!presContext) { + return false; + } + if (!mediaList->Matches(presContext, nullptr)) { + return false; + } + } + + if (mimeType.IsEmpty()) { + return true; + } + + switch (aPolicyType) { + case nsIContentPolicy::TYPE_OTHER: + if (destinationAttr.GetEnumValue() == DESTINATION_JSON) { + return nsContentUtils::IsJSONMIMEType(mimeType); + } + return true; + case nsIContentPolicy::TYPE_MEDIA: + if (destinationAttr.GetEnumValue() == DESTINATION_TRACK) { + return mimeType.EqualsASCII(TEXT_VTT); + } + return DecoderTraits::IsSupportedInVideoDocument(NS_ConvertUTF16toUTF8(mimeType)); + case nsIContentPolicy::TYPE_FONT: + return nsContentUtils::IsFontMIMEType(mimeType); + case nsIContentPolicy::TYPE_IMAGE: + return imgLoader::SupportImageWithMimeType(NS_ConvertUTF16toUTF8(mimeType).get(), + AcceptedMimeTypes::IMAGES_AND_DOCUMENTS); + case nsIContentPolicy::TYPE_SCRIPT: + return nsContentUtils::IsJavascriptMIMEType(mimeType); + case nsIContentPolicy::TYPE_STYLESHEET: + return mimeType.EqualsASCII(TEXT_CSS); + default: + return false; + } +} + +bool +Link::CheckPreloadAttrs(nsContentPolicyType& aPolicyType, + nsIAtom* aName, + const nsAString& aValue, + Element* aElement) +{ + nsAutoString destination; + if (aName == nsGkAtoms::as) { + destination = aValue; + } else { + aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::as, destination); + } + + nsAutoString type; + if (aName == nsGkAtoms::type) { + type = aValue; + } else { + aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::type, type); + } + + nsAutoString media; + if (aName == nsGkAtoms::media) { + media = aValue; + } else { + aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::media, media); + } + + return CheckPreloadAttrs(aPolicyType, + destination, + type, + media, + aElement->OwnerDoc()); +} + +bool +Link::CheckPreloadAttrs(nsContentPolicyType& aPolicyType, + Element* aElement) +{ + nsAutoString unused; + return CheckPreloadAttrs(aPolicyType, + nullptr, + unused, + aElement); +} + /* static */ void Link::ParseDestinationValue(const nsAString& aValue, nsAttrValue& aResult) @@ -687,6 +910,31 @@ Link::ParseDestinationValue(const nsAString& aValue, &kDestinationAttributeTable[0]); } +/* static */ nsContentPolicyType +Link::DestinationToContentPolicy(const nsAttrValue& aValue) +{ + switch (aValue.GetEnumValue()) { + case DESTINATION_AUDIO: + case DESTINATION_TRACK: + case DESTINATION_VIDEO: + return nsIContentPolicy::TYPE_MEDIA; + case DESTINATION_FONT: + return nsIContentPolicy::TYPE_FONT; + case DESTINATION_IMAGE: + return nsIContentPolicy::TYPE_IMAGE; + case DESTINATION_SCRIPT: + return nsIContentPolicy::TYPE_SCRIPT; + case DESTINATION_STYLE: + return nsIContentPolicy::TYPE_STYLESHEET; + case DESTINATION_JSON: + case DESTINATION_FETCH: + return nsIContentPolicy::TYPE_OTHER; + case DESTINATION_INVALID: + default: + return nsIContentPolicy::TYPE_INVALID; + } +} + size_t Link::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { diff --git a/dom/base/Link.h b/dom/base/Link.h index cb4beeadb7..0974d6108c 100644 --- a/dom/base/Link.h +++ b/dom/base/Link.h @@ -116,10 +116,25 @@ public: nsWrapperCache::FlagsType aRequestedFlag); // This is called by HTMLLinkElement. - void TryDNSPrefetchPreconnectOrPrefetch(); + void TrySpeculativeLoadFeature(); + void UpdatePreload(nsIAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue); void CancelPrefetch(); static void ParseDestinationValue(const nsAString& aValue, nsAttrValue& aResult); + static bool CheckPreloadAttrs(nsContentPolicyType& aPolicyType, + Element* aElement); + static bool CheckPreloadAttrs(nsContentPolicyType& aPolicyType, + nsIAtom* aName, + const nsAString& aValue, + Element* aElement); + static bool CheckPreloadAttrs(nsContentPolicyType& aPolicyType, + const nsAString& aAs, + const nsAString& aType, + const nsAString& aMedia, + nsIDocument* aDocument); + static nsContentPolicyType DestinationToContentPolicy(const nsAttrValue& aValue); protected: virtual ~Link(); diff --git a/dom/base/nsContentSink.cpp b/dom/base/nsContentSink.cpp index 299c5a5c56..26e1ea5f89 100644 --- a/dom/base/nsContentSink.cpp +++ b/dom/base/nsContentSink.cpp @@ -48,6 +48,7 @@ #include "nsIObserverService.h" #include "mozilla/Preferences.h" #include "mozilla/dom/ScriptLoader.h" +#include "mozilla/dom/Link.h" #include "nsParserConstants.h" #include "nsSandboxFlags.h" @@ -724,18 +725,21 @@ nsContentSink::ProcessLink(const nsSubstring& aAnchor, const nsSubstring& aHref, return NS_OK; } - bool hasPrefetch = linkTypes & nsStyleLinkElement::ePREFETCH; // prefetch href if relation is "next" or "prefetch" - if (hasPrefetch || (linkTypes & nsStyleLinkElement::eNEXT)) { - PrefetchHref(aHref, mDocument, hasPrefetch); + if ((linkTypes & nsStyleLinkElement::eNEXT) || + (linkTypes & nsStyleLinkElement::ePREFETCH) || + (linkTypes & nsStyleLinkElement::ePRELOAD)) { + PrefetchOrPreloadHref(aHref, mDocument, linkTypes, aDestination, aType, aMedia); } - if (!aHref.IsEmpty() && (linkTypes & nsStyleLinkElement::eDNS_PREFETCH)) { - PrefetchDNS(aHref); - } + if (!aHref.IsEmpty()) { + if (linkTypes & nsStyleLinkElement::eDNS_PREFETCH) { + PrefetchDNS(aHref); + } - if (!aHref.IsEmpty() && (linkTypes & nsStyleLinkElement::ePRECONNECT)) { - Preconnect(aHref, aCrossOrigin); + if (linkTypes & nsStyleLinkElement::ePRECONNECT) { + Preconnect(aHref, aCrossOrigin); + } } // is it a stylesheet link? @@ -855,9 +859,12 @@ nsContentSink::ProcessMETATag(nsIContent* aContent) void -nsContentSink::PrefetchHref(const nsAString &aHref, - nsINode *aSource, - bool aExplicit) +nsContentSink::PrefetchOrPreloadHref(const nsAString &aHref, + nsINode *aSource, + uint32_t aLinkTypes, + const nsAString& aDestination, + const nsAString& aType, + const nsAString& aMedia) { nsCOMPtr prefetchService(do_GetService(NS_PREFETCHSERVICE_CONTRACTID)); if (prefetchService) { @@ -869,7 +876,25 @@ nsContentSink::PrefetchHref(const nsAString &aHref, mDocument->GetDocBaseURI()); if (uri) { nsCOMPtr domNode = do_QueryInterface(aSource); - prefetchService->PrefetchURI(uri, mDocumentURI, domNode, aExplicit); + if (aLinkTypes & nsStyleLinkElement::ePRELOAD) { + nsContentPolicyType policyType; + bool isPreloadValid = Link::CheckPreloadAttrs(policyType, + aDestination, + aType, + aMedia, + mDocument); + if (!isPreloadValid) { + // XXX: WPTs expect that we timeout on invalid preloads including + // those with valid destinations instead of firing an error event. + return; + } + nsContentUtils::DispatchEventForPreloadURI(domNode, policyType); + } else { + prefetchService->PrefetchURI(uri, + mDocumentURI, + domNode, + aLinkTypes & nsStyleLinkElement::ePREFETCH); + } } } } diff --git a/dom/base/nsContentSink.h b/dom/base/nsContentSink.h index 61f72dfc5c..bed7392ca3 100644 --- a/dom/base/nsContentSink.h +++ b/dom/base/nsContentSink.h @@ -163,8 +163,12 @@ protected: const nsSubstring& aType, const nsSubstring& aMedia); - void PrefetchHref(const nsAString &aHref, nsINode *aSource, - bool aExplicit); + void PrefetchOrPreloadHref(const nsAString &aHref, + nsINode *aSource, + uint32_t aLinkTypes, + const nsAString& aDestination, + const nsAString& aType, + const nsAString& aMedia); // For PrefetchDNS() aHref can either be the usual // URI format or of the form "//www.hostname.com" without a scheme. diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp index 029748c40a..d86d3fb1a3 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp @@ -216,6 +216,7 @@ #include "TabChild.h" #include "mozilla/dom/DocGroup.h" #include "mozilla/dom/TabGroup.h" +#include "mozilla/AsyncEventDispatcher.h" #include "nsIBidiKeyboard.h" @@ -4120,6 +4121,24 @@ nsresult GetEventAndTarget(nsIDocument* aDoc, nsISupports* aTarget, return NS_OK; } +void +nsContentUtils::DispatchEventForPreloadURI(nsIDOMNode* aNode, + nsContentPolicyType& aPolicyType) +{ + nsCOMPtr domNode = do_QueryInterface(aNode); + if (domNode && domNode->IsInComposedDoc()) { + bool success = aPolicyType != nsIContentPolicy::TYPE_INVALID; + RefPtr asyncDispatcher = + new AsyncEventDispatcher(domNode, + success ? + NS_LITERAL_STRING("load") : + NS_LITERAL_STRING("error"), + /* aCanBubble = */ false, + /* aCancelable = */ false); + asyncDispatcher->RunDOMEventWhenSafe(); + } +} + // static nsresult nsContentUtils::DispatchTrustedEvent(nsIDocument* aDoc, nsISupports* aTarget, @@ -7441,6 +7460,48 @@ nsContentUtils::IsJavascriptMIMEType(const nsAString& aMIMEType) return false; } +bool +nsContentUtils::IsJSONMIMEType(const nsAString& aMIMEType) +{ + static const char* jsonTypes[] = { + "text/json", + "application/json", + "application/geo+json", + nullptr + }; + + for (uint32_t i = 0; jsonTypes[i]; ++i) { + if (aMIMEType.LowerCaseEqualsASCII(jsonTypes[i])) { + return true; + } + } + + return false; +} + +bool +nsContentUtils::IsFontMIMEType(const nsAString& aMIMEType) +{ + // The following list was taken from IANA and excludes deprecated mime-types: + // https://www.iana.org/assignments/media-types/media-types.xhtml#font + static const char* fontTypes[] = { + "font/otf", + "font/sfnt", + "font/ttf", + "font/woff", + "font/woff2", + nullptr + }; + + for (uint32_t i = 0; fontTypes[i]; ++i) { + if (aMIMEType.LowerCaseEqualsASCII(fontTypes[i])) { + return true; + } + } + + return false; +} + nsresult nsContentUtils::GenerateUUIDInPlace(nsID& aUUID) { diff --git a/dom/base/nsContentUtils.h b/dom/base/nsContentUtils.h index bce0085c61..0073c4953f 100644 --- a/dom/base/nsContentUtils.h +++ b/dom/base/nsContentUtils.h @@ -1178,6 +1178,16 @@ public: static void MaybeFireNodeRemoved(nsINode* aChild, nsINode* aParent, nsIDocument* aOwnerDoc); + + /** + * This method creates and dispatches either a "load" or "error" event + * to the provided node (a preload link). + * @param aNode The node to fire the event at. + * @param aPolicyType The resulting content policy from the preload. + */ + static void DispatchEventForPreloadURI(nsIDOMNode* aNode, + nsContentPolicyType& aPolicyType); + /** * This method creates and dispatches a trusted event. * Works only with events which can be created by calling @@ -2383,6 +2393,10 @@ public: static bool IsJavascriptMIMEType(const nsAString& aMIMEType); + static bool IsJSONMIMEType(const nsAString& aMIMEType); + + static bool IsFontMIMEType(const nsAString& aMIMEType); + static void SplitMimeType(const nsAString& aValue, nsString& aType, nsString& aParams); diff --git a/dom/html/HTMLLinkElement.cpp b/dom/html/HTMLLinkElement.cpp index 9778922196..8fe317cec0 100644 --- a/dom/html/HTMLLinkElement.cpp +++ b/dom/html/HTMLLinkElement.cpp @@ -176,7 +176,7 @@ HTMLLinkElement::BindToTree(nsIDocument* aDocument, if (nsIDocument* doc = GetComposedDoc()) { doc->RegisterPendingLinkUpdate(this); - TryDNSPrefetchPreconnectOrPrefetch(); + TrySpeculativeLoadFeature(); } void (HTMLLinkElement::*update)() = &HTMLLinkElement::UpdateStyleSheetInternal; @@ -399,9 +399,17 @@ HTMLLinkElement::AfterSetAttr(int32_t aNameSpaceID, nsIAtom* aName, UpdateImport(); } - if ((aName == nsGkAtoms::rel || aName == nsGkAtoms::href) && - IsInComposedDoc()) { - TryDNSPrefetchPreconnectOrPrefetch(); + if (IsInComposedDoc()) { + if (aName == nsGkAtoms::rel || aName == nsGkAtoms::href) { + TrySpeculativeLoadFeature(); + } + + if (aName == nsGkAtoms::as || + aName == nsGkAtoms::type || + aName == nsGkAtoms::crossorigin || + aName == nsGkAtoms::media) { + UpdatePreload(aName, aValue, aOldValue); + } } UpdateStyleSheetInternal(nullptr, nullptr, @@ -428,6 +436,13 @@ HTMLLinkElement::AfterSetAttr(int32_t aNameSpaceID, nsIAtom* aName, (LINK_DISABLED && aName == nsGkAtoms::disabled)) { UpdateStyleSheetInternal(nullptr, nullptr, true); } + if ((aName == nsGkAtoms::as || + aName == nsGkAtoms::type || + aName == nsGkAtoms::crossorigin || + aName == nsGkAtoms::media) && + IsInComposedDoc()) { + UpdatePreload(aName, aValue, aOldValue); + } if (aName == nsGkAtoms::href || aName == nsGkAtoms::rel) { UpdateImport(); @@ -531,7 +546,8 @@ HTMLLinkElement::GetStyleSheetInfo(nsAString& aTitle, nsAutoString rel; GetAttr(kNameSpaceID_None, nsGkAtoms::rel, rel); - uint32_t linkTypes = nsStyleLinkElement::ParseLinkTypes(rel, NodePrincipal()); + uint32_t linkTypes = + nsStyleLinkElement::ParseLinkTypes(rel, NodePrincipal()); // Is it a stylesheet link? if (!(linkTypes & nsStyleLinkElement::eSTYLESHEET)) { return;