blob: 3fd0fd5e18b8a1f250f135a950ae76f5cc411975 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/loader/preload_helper.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/core/css/media_list.h"
#include "third_party/blink/renderer/core/css/media_query_evaluator.h"
#include "third_party/blink/renderer/core/css/parser/sizes_attribute_parser.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/frame/frame_console.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/frame/use_counter.h"
#include "third_party/blink/renderer/core/frame/viewport_data.h"
#include "third_party/blink/renderer/core/html/parser/html_preload_scanner.h"
#include "third_party/blink/renderer/core/html/parser/html_srcset_parser.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/loader/importance_attribute.h"
#include "third_party/blink/renderer/core/loader/link_load_parameters.h"
#include "third_party/blink/renderer/core/loader/modulescript/module_script_fetch_request.h"
#include "third_party/blink/renderer/core/loader/network_hints_interface.h"
#include "third_party/blink/renderer/core/loader/resource/css_style_sheet_resource.h"
#include "third_party/blink/renderer/core/loader/resource/font_resource.h"
#include "third_party/blink/renderer/core/loader/resource/image_resource.h"
#include "third_party/blink/renderer/core/loader/resource/link_fetch_resource.h"
#include "third_party/blink/renderer/core/loader/resource/script_resource.h"
#include "third_party/blink/renderer/core/loader/subresource_integrity_helper.h"
#include "third_party/blink/renderer/core/script/modulator.h"
#include "third_party/blink/renderer/core/script/script_loader.h"
#include "third_party/blink/renderer/platform/loader/fetch/fetch_initiator_type_names.h"
#include "third_party/blink/renderer/platform/loader/fetch/raw_resource.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_loader_options.h"
#include "third_party/blink/renderer/platform/loader/link_header.h"
#include "third_party/blink/renderer/platform/loader/subresource_integrity.h"
#include "third_party/blink/renderer/platform/network/mime/mime_type_registry.h"
namespace blink {
namespace {
void SendMessageToConsoleForPossiblyNullDocument(
ConsoleMessage* console_message,
Document* document,
LocalFrame* frame) {
DCHECK(document || frame);
DCHECK(!document || document->GetFrame() == frame);
// Route the console message through Document if possible, so that script line
// numbers can be included. Otherwise, route directly to the FrameConsole, to
// ensure we never drop a message.
if (document)
document->AddConsoleMessage(console_message);
else
frame->Console().AddMessage(console_message);
}
bool IsSupportedType(ResourceType resource_type, const String& mime_type) {
if (mime_type.IsEmpty())
return true;
switch (resource_type) {
case ResourceType::kImage:
return MIMETypeRegistry::IsSupportedImagePrefixedMIMEType(mime_type);
case ResourceType::kScript:
return MIMETypeRegistry::IsSupportedJavaScriptMIMEType(mime_type);
case ResourceType::kCSSStyleSheet:
return MIMETypeRegistry::IsSupportedStyleSheetMIMEType(mime_type);
case ResourceType::kFont:
return MIMETypeRegistry::IsSupportedFontMIMEType(mime_type);
case ResourceType::kAudio:
case ResourceType::kVideo:
return MIMETypeRegistry::IsSupportedMediaMIMEType(mime_type, String());
case ResourceType::kTextTrack:
return MIMETypeRegistry::IsSupportedTextTrackMIMEType(mime_type);
case ResourceType::kRaw:
return true;
default:
NOTREACHED();
}
return false;
}
MediaValues* CreateMediaValues(Document& document,
ViewportDescription* viewport_description) {
MediaValues* media_values =
MediaValues::CreateDynamicIfFrameExists(document.GetFrame());
if (viewport_description) {
FloatSize initial_viewport(media_values->DeviceWidth(),
media_values->DeviceHeight());
PageScaleConstraints constraints = viewport_description->Resolve(
initial_viewport, document.GetViewportData().ViewportDefaultMinWidth());
media_values->OverrideViewportDimensions(constraints.layout_size.Width(),
constraints.layout_size.Height());
}
return media_values;
}
} // namespace
void PreloadHelper::DnsPrefetchIfNeeded(
const LinkLoadParameters& params,
Document* document,
LocalFrame* frame,
const NetworkHintsInterface& network_hints_interface,
LinkCaller caller) {
if (params.rel.IsDNSPrefetch()) {
UseCounter::Count(frame, WebFeature::kLinkRelDnsPrefetch);
if (caller == kLinkCalledFromHeader)
UseCounter::Count(frame, WebFeature::kLinkHeaderDnsPrefetch);
Settings* settings = frame ? frame->GetSettings() : nullptr;
// FIXME: The href attribute of the link element can be in "//hostname"
// form, and we shouldn't attempt to complete that as URL
// <https://bugs.webkit.org/show_bug.cgi?id=48857>.
if (settings && settings->GetDNSPrefetchingEnabled() &&
params.href.IsValid() && !params.href.IsEmpty()) {
if (settings->GetLogDnsPrefetchAndPreconnect()) {
SendMessageToConsoleForPossiblyNullDocument(
ConsoleMessage::Create(
kOtherMessageSource, kVerboseMessageLevel,
String("DNS prefetch triggered for " + params.href.Host())),
document, frame);
}
network_hints_interface.DnsPrefetchHost(params.href.Host());
}
}
}
void PreloadHelper::PreconnectIfNeeded(
const LinkLoadParameters& params,
Document* document,
LocalFrame* frame,
const NetworkHintsInterface& network_hints_interface,
LinkCaller caller) {
if (params.rel.IsPreconnect() && params.href.IsValid() &&
params.href.ProtocolIsInHTTPFamily()) {
UseCounter::Count(frame, WebFeature::kLinkRelPreconnect);
if (caller == kLinkCalledFromHeader)
UseCounter::Count(frame, WebFeature::kLinkHeaderPreconnect);
Settings* settings = frame ? frame->GetSettings() : nullptr;
if (settings && settings->GetLogDnsPrefetchAndPreconnect()) {
SendMessageToConsoleForPossiblyNullDocument(
ConsoleMessage::Create(
kOtherMessageSource, kVerboseMessageLevel,
String("Preconnect triggered for ") + params.href.GetString()),
document, frame);
if (params.cross_origin != kCrossOriginAttributeNotSet) {
SendMessageToConsoleForPossiblyNullDocument(
ConsoleMessage::Create(kOtherMessageSource, kVerboseMessageLevel,
String("Preconnect CORS setting is ") +
String((params.cross_origin ==
kCrossOriginAttributeAnonymous)
? "anonymous"
: "use-credentials")),
document, frame);
}
}
network_hints_interface.PreconnectHost(params.href, params.cross_origin);
}
}
base::Optional<ResourceType> PreloadHelper::GetResourceTypeFromAsAttribute(
const String& as) {
DCHECK_EQ(as.DeprecatedLower(), as);
if (as == "image")
return ResourceType::kImage;
if (as == "script")
return ResourceType::kScript;
if (as == "style")
return ResourceType::kCSSStyleSheet;
if (as == "video")
return ResourceType::kVideo;
if (as == "audio")
return ResourceType::kAudio;
if (as == "track")
return ResourceType::kTextTrack;
if (as == "font")
return ResourceType::kFont;
if (as == "fetch")
return ResourceType::kRaw;
return base::nullopt;
}
static bool MediaMatches(const String& media, MediaValues* media_values) {
scoped_refptr<MediaQuerySet> media_queries = MediaQuerySet::Create(media);
MediaQueryEvaluator evaluator(*media_values);
return evaluator.Eval(*media_queries);
}
// |base_url| is used in Link HTTP Header based preloads to resolve relative
// URLs in srcset, which should be based on the resource's URL, not the
// document's base URL. If |base_url| is a null URL, relative URLs are resolved
// using |document.CompleteURL()|.
Resource* PreloadHelper::PreloadIfNeeded(
const LinkLoadParameters& params,
Document& document,
const KURL& base_url,
LinkCaller caller,
ViewportDescription* viewport_description,
ParserDisposition parser_disposition) {
if (!document.Loader() || !params.rel.IsLinkPreload())
return nullptr;
base::Optional<ResourceType> resource_type =
PreloadHelper::GetResourceTypeFromAsAttribute(params.as);
MediaValues* media_values = nullptr;
KURL url;
if (resource_type == ResourceType::kImage && !params.image_srcset.IsEmpty() &&
RuntimeEnabledFeatures::PreloadImageSrcSetEnabled()) {
media_values = CreateMediaValues(document, viewport_description);
float source_size =
SizesAttributeParser(media_values, params.image_sizes).length();
ImageCandidate candidate = BestFitSourceForImageAttributes(
media_values->DevicePixelRatio(), source_size, params.href,
params.image_srcset);
url = base_url.IsNull() ? document.CompleteURL(candidate.ToString())
: KURL(base_url, candidate.ToString());
} else {
url = params.href;
}
UseCounter::Count(document, WebFeature::kLinkRelPreload);
if (!url.IsValid() || url.IsEmpty()) {
document.AddConsoleMessage(ConsoleMessage::Create(
kOtherMessageSource, kWarningMessageLevel,
String("<link rel=preload> has an invalid `href` value")));
return nullptr;
}
// Preload only if media matches
if (!params.media.IsEmpty()) {
if (!media_values)
media_values = CreateMediaValues(document, viewport_description);
if (!MediaMatches(params.media, media_values))
return nullptr;
}
if (caller == kLinkCalledFromHeader)
UseCounter::Count(document, WebFeature::kLinkHeaderPreload);
if (resource_type == base::nullopt) {
document.AddConsoleMessage(ConsoleMessage::Create(
kOtherMessageSource, kWarningMessageLevel,
String("<link rel=preload> must have a valid `as` value")));
return nullptr;
}
if (!IsSupportedType(resource_type.value(), params.type)) {
document.AddConsoleMessage(ConsoleMessage::Create(
kOtherMessageSource, kWarningMessageLevel,
String("<link rel=preload> has an unsupported `type` value")));
return nullptr;
}
ResourceRequest resource_request(url);
resource_request.SetRequestContext(ResourceFetcher::DetermineRequestContext(
resource_type.value(), ResourceFetcher::kImageNotImageSet));
resource_request.SetReferrerPolicy(params.referrer_policy);
resource_request.SetFetchImportanceMode(
GetFetchImportanceAttributeValue(params.importance));
ResourceLoaderOptions options;
options.initiator_info.name = fetch_initiator_type_names::kLink;
options.parser_disposition = parser_disposition;
FetchParameters link_fetch_params(resource_request, options);
link_fetch_params.SetCharset(document.Encoding());
if (params.cross_origin != kCrossOriginAttributeNotSet) {
link_fetch_params.SetCrossOriginAccessControl(document.GetSecurityOrigin(),
params.cross_origin);
}
link_fetch_params.SetContentSecurityPolicyNonce(params.nonce);
Settings* settings = document.GetSettings();
if (settings && settings->GetLogPreload()) {
document.AddConsoleMessage(ConsoleMessage::Create(
kOtherMessageSource, kVerboseMessageLevel,
String("Preload triggered for " + url.Host() + url.GetPath())));
}
link_fetch_params.SetLinkPreload(true);
return PreloadHelper::StartPreload(resource_type.value(), link_fetch_params,
document.Fetcher());
}
// https://html.spec.whatwg.org/multipage/links.html#link-type-modulepreload
void PreloadHelper::ModulePreloadIfNeeded(
const LinkLoadParameters& params,
Document& document,
ViewportDescription* viewport_description,
SingleModuleClient* client) {
if (!document.Loader() || !params.rel.IsModulePreload())
return;
UseCounter::Count(document, WebFeature::kLinkRelModulePreload);
// Step 1. "If the href attribute's value is the empty string, then return."
// [spec text]
if (params.href.IsEmpty()) {
document.AddConsoleMessage(
ConsoleMessage::Create(kOtherMessageSource, kWarningMessageLevel,
"<link rel=modulepreload> has no `href` value"));
return;
}
// Step 5. "Let settings object be the link element's node document's relevant
// settings object." [spec text]
// |document| is the node document here, and its context document is the
// relevant settings object.
Document* context_document = document.ContextDocument();
Modulator* modulator =
Modulator::From(ToScriptStateForMainWorld(context_document->GetFrame()));
DCHECK(modulator);
if (!modulator)
return;
// Step 2. "Let destination be the current state of the as attribute (a
// destination), or "script" if it is in no state." [spec text]
// Step 3. "If destination is not script-like, then queue a task on the
// networking task source to fire an event named error at the link element,
// and return." [spec text]
// Currently we only support as="script".
if (!params.as.IsEmpty() && params.as != "script") {
document.AddConsoleMessage(ConsoleMessage::Create(
kOtherMessageSource, kWarningMessageLevel,
String("<link rel=modulepreload> has an invalid `as` value " +
params.as)));
// This triggers the same logic as Step 11 asynchronously, which will fire
// the error event.
if (client) {
modulator->TaskRunner()->PostTask(
FROM_HERE, WTF::Bind(&SingleModuleClient::NotifyModuleLoadFinished,
WrapPersistent(client), nullptr));
}
return;
}
mojom::RequestContextType destination = mojom::RequestContextType::SCRIPT;
// Step 4. "Parse the URL given by the href attribute, relative to the
// element's node document. If that fails, then return. Otherwise, let url be
// the resulting URL record." [spec text]
// |href| is already resolved in caller side.
if (!params.href.IsValid()) {
document.AddConsoleMessage(ConsoleMessage::Create(
kOtherMessageSource, kWarningMessageLevel,
"<link rel=modulepreload> has an invalid `href` value " +
params.href.GetString()));
return;
}
// Preload only if media matches.
// https://html.spec.whatwg.org/#processing-the-media-attribute
if (!params.media.IsEmpty()) {
MediaValues* media_values =
CreateMediaValues(document, viewport_description);
if (!MediaMatches(params.media, media_values))
return;
}
// Step 6. "Let credentials mode be the module script credentials mode for the
// crossorigin attribute." [spec text]
network::mojom::FetchCredentialsMode credentials_mode =
ScriptLoader::ModuleScriptCredentialsMode(params.cross_origin);
// Step 7. "Let cryptographic nonce be the value of the nonce attribute, if it
// is specified, or the empty string otherwise." [spec text]
// |nonce| parameter is the value of the nonce attribute.
// Step 8. "Let integrity metadata be the value of the integrity attribute, if
// it is specified, or the empty string otherwise." [spec text]
IntegrityMetadataSet integrity_metadata;
if (!params.integrity.IsEmpty()) {
SubresourceIntegrity::IntegrityFeatures integrity_features =
SubresourceIntegrityHelper::GetFeatures(&document);
SubresourceIntegrity::ReportInfo report_info;
SubresourceIntegrity::ParseIntegrityAttribute(
params.integrity, integrity_features, integrity_metadata, &report_info);
SubresourceIntegrityHelper::DoReport(document, report_info);
}
// Step 9. "Let referrer policy be the current state of the element's
// referrerpolicy attribute." [spec text]
// |referrer_policy| parameter is the value of the referrerpolicy attribute.
// Step 10. "Let options be a script fetch options whose cryptographic nonce
// is cryptographic nonce, integrity metadata is integrity metadata, parser
// metadata is "not-parser-inserted", credentials mode is credentials mode,
// and referrer policy is referrer policy." [spec text]
ModuleScriptFetchRequest request(
params.href, destination,
ScriptFetchOptions(params.nonce, integrity_metadata, params.integrity,
kNotParserInserted, credentials_mode,
params.referrer_policy,
mojom::FetchImportanceMode::kImportanceAuto),
Referrer::NoReferrer(), TextPosition::MinimumPosition());
// Step 11. "Fetch a single module script given url, settings object,
// destination, options, settings object, "client", and with the top-level
// module fetch flag set. Wait until algorithm asynchronously completes with
// result." [spec text]
modulator->FetchSingle(request, context_document->Fetcher(),
ModuleGraphLevel::kDependentModuleFetch,
ModuleScriptCustomFetchType::kNone, client);
Settings* settings = document.GetSettings();
if (settings && settings->GetLogPreload()) {
document.AddConsoleMessage(
ConsoleMessage::Create(kOtherMessageSource, kVerboseMessageLevel,
"Module preload triggered for " +
params.href.Host() + params.href.GetPath()));
}
// Asynchronously continue processing after
// client->NotifyModuleLoadFinished() is called.
}
Resource* PreloadHelper::PrefetchIfNeeded(const LinkLoadParameters& params,
Document& document) {
if (params.rel.IsLinkPrefetch() && params.href.IsValid() &&
document.GetFrame()) {
UseCounter::Count(document, WebFeature::kLinkRelPrefetch);
ResourceRequest resource_request(params.href);
resource_request.SetReferrerPolicy(params.referrer_policy);
resource_request.SetFetchImportanceMode(
GetFetchImportanceAttributeValue(params.importance));
ResourceLoaderOptions options;
options.initiator_info.name = fetch_initiator_type_names::kLink;
FetchParameters link_fetch_params(resource_request, options);
if (params.cross_origin != kCrossOriginAttributeNotSet) {
link_fetch_params.SetCrossOriginAccessControl(
document.GetSecurityOrigin(), params.cross_origin);
}
return LinkFetchResource::Fetch(ResourceType::kLinkPrefetch,
link_fetch_params, document.Fetcher());
}
return nullptr;
}
void PreloadHelper::LoadLinksFromHeader(
const String& header_value,
const KURL& base_url,
LocalFrame& frame,
Document* document,
const NetworkHintsInterface& network_hints_interface,
CanLoadResources can_load_resources,
MediaPreloadPolicy media_policy,
ViewportDescriptionWrapper* viewport_description_wrapper) {
if (header_value.IsEmpty())
return;
LinkHeaderSet header_set(header_value);
for (auto& header : header_set) {
if (!header.Valid() || header.Url().IsEmpty() || header.Rel().IsEmpty())
continue;
if (media_policy == kOnlyLoadMedia && !header.IsViewportDependent())
continue;
if (media_policy == kOnlyLoadNonMedia && header.IsViewportDependent())
continue;
const LinkLoadParameters params(header, base_url);
// Sanity check to avoid re-entrancy here.
if (params.href == base_url)
continue;
if (can_load_resources != kOnlyLoadResources) {
DnsPrefetchIfNeeded(params, document, &frame, network_hints_interface,
kLinkCalledFromHeader);
PreconnectIfNeeded(params, document, &frame, network_hints_interface,
kLinkCalledFromHeader);
}
if (can_load_resources != kDoNotLoadResources) {
DCHECK(document);
ViewportDescription* viewport_description =
(viewport_description_wrapper && viewport_description_wrapper->set)
? &(viewport_description_wrapper->description)
: nullptr;
PreloadIfNeeded(params, *document, base_url, kLinkCalledFromHeader,
viewport_description, kNotParserInserted);
PrefetchIfNeeded(params, *document);
ModulePreloadIfNeeded(params, *document, viewport_description, nullptr);
}
if (params.rel.IsServiceWorker()) {
UseCounter::Count(&frame, WebFeature::kLinkHeaderServiceWorker);
}
// TODO(yoav): Add more supported headers as needed.
}
}
Resource* PreloadHelper::StartPreload(ResourceType type,
FetchParameters& params,
ResourceFetcher* resource_fetcher) {
Resource* resource = nullptr;
switch (type) {
case ResourceType::kImage:
resource = ImageResource::Fetch(params, resource_fetcher);
break;
case ResourceType::kScript:
params.SetRequestContext(mojom::RequestContextType::SCRIPT);
resource = ScriptResource::Fetch(params, resource_fetcher, nullptr,
ScriptResource::kAllowStreaming);
break;
case ResourceType::kCSSStyleSheet:
resource =
CSSStyleSheetResource::Fetch(params, resource_fetcher, nullptr);
break;
case ResourceType::kFont:
resource = FontResource::Fetch(params, resource_fetcher, nullptr);
break;
case ResourceType::kAudio:
case ResourceType::kVideo:
resource = RawResource::FetchMedia(params, resource_fetcher, nullptr);
break;
case ResourceType::kTextTrack:
resource = RawResource::FetchTextTrack(params, resource_fetcher, nullptr);
break;
case ResourceType::kImportResource:
resource = RawResource::FetchImport(params, resource_fetcher, nullptr);
break;
case ResourceType::kRaw:
resource = RawResource::Fetch(params, resource_fetcher, nullptr);
break;
default:
NOTREACHED();
}
return resource;
}
} // namespace blink