blob: 02a7e4ebad1df84f4b7724947950dc41e5ead5b6 [file] [log] [blame]
/*
* Copyright (C) 2011 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
#include "core/loader/LinkLoader.h"
#include "bindings/core/v8/V8BindingForCore.h"
#include "core/css/MediaList.h"
#include "core/css/MediaQueryEvaluator.h"
#include "core/dom/Document.h"
#include "core/dom/ModuleScript.h"
#include "core/dom/ScriptLoader.h"
#include "core/frame/FrameConsole.h"
#include "core/frame/LocalFrame.h"
#include "core/frame/Settings.h"
#include "core/frame/UseCounter.h"
#include "core/html/CrossOriginAttribute.h"
#include "core/html/LinkRelAttribute.h"
#include "core/html/parser/HTMLPreloadScanner.h"
#include "core/inspector/ConsoleMessage.h"
#include "core/loader/DocumentLoader.h"
#include "core/loader/NetworkHintsInterface.h"
#include "core/loader/modulescript/ModuleScriptFetchRequest.h"
#include "core/loader/private/PrerenderHandle.h"
#include "core/loader/resource/LinkFetchResource.h"
#include "platform/Prerender.h"
#include "platform/loader/LinkHeader.h"
#include "platform/loader/fetch/FetchParameters.h"
#include "platform/loader/fetch/ResourceClient.h"
#include "platform/loader/fetch/ResourceFetcher.h"
#include "platform/loader/fetch/ResourceFinishObserver.h"
#include "platform/loader/fetch/ResourceLoaderOptions.h"
#include "platform/loader/fetch/fetch_initiator_type_names.h"
#include "platform/network/mime/MIMETypeRegistry.h"
#include "public/platform/WebPrerender.h"
namespace blink {
static unsigned PrerenderRelTypesFromRelAttribute(
const LinkRelAttribute& rel_attribute,
Document& document) {
unsigned result = 0;
if (rel_attribute.IsLinkPrerender()) {
result |= kPrerenderRelTypePrerender;
UseCounter::Count(document, WebFeature::kLinkRelPrerender);
}
if (rel_attribute.IsLinkNext()) {
result |= kPrerenderRelTypeNext;
UseCounter::Count(document, WebFeature::kLinkRelNext);
}
return result;
}
class LinkLoader::FinishObserver final
: public GarbageCollectedFinalized<ResourceFinishObserver>,
public ResourceFinishObserver {
USING_GARBAGE_COLLECTED_MIXIN(FinishObserver);
USING_PRE_FINALIZER(FinishObserver, ClearResource);
public:
FinishObserver(LinkLoader* loader, Resource* resource)
: loader_(loader), resource_(resource) {
resource_->AddFinishObserver(
this, loader_->client_->GetLoadingTaskRunner().get());
}
// ResourceFinishObserver implementation
void NotifyFinished() override {
if (!resource_)
return;
loader_->NotifyFinished();
ClearResource();
}
String DebugName() const override {
return "LinkLoader::ResourceFinishObserver";
}
Resource* GetResource() { return resource_; }
void ClearResource() {
if (!resource_)
return;
resource_->RemoveFinishObserver(this);
resource_ = nullptr;
}
void Trace(blink::Visitor* visitor) override {
visitor->Trace(loader_);
visitor->Trace(resource_);
blink::ResourceFinishObserver::Trace(visitor);
}
private:
Member<LinkLoader> loader_;
Member<Resource> resource_;
};
LinkLoader::LinkLoader(LinkLoaderClient* client,
scoped_refptr<WebTaskRunner> task_runner)
: client_(client) {
DCHECK(client_);
}
LinkLoader::~LinkLoader() {}
void LinkLoader::NotifyFinished() {
DCHECK(finish_observer_);
Resource* resource = finish_observer_->GetResource();
if (resource->ErrorOccurred())
client_->LinkLoadingErrored();
else
client_->LinkLoaded();
}
// https://html.spec.whatwg.org/#link-type-modulepreload
void LinkLoader::NotifyModuleLoadFinished(ModuleScript* module) {
// Step 11. "If result is null, fire an event named error at the link element,
// and return." [spec text]
// Step 12. "Fire an event named load at the link element." [spec text]
if (!module || module->IsErrored())
client_->LinkLoadingErrored();
else
client_->LinkLoaded();
}
void LinkLoader::DidStartPrerender() {
client_->DidStartLinkPrerender();
}
void LinkLoader::DidStopPrerender() {
client_->DidStopLinkPrerender();
}
void LinkLoader::DidSendLoadForPrerender() {
client_->DidSendLoadForLinkPrerender();
}
void LinkLoader::DidSendDOMContentLoadedForPrerender() {
client_->DidSendDOMContentLoadedForLinkPrerender();
}
enum LinkCaller {
kLinkCalledFromHeader,
kLinkCalledFromMarkup,
};
static 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);
}
static void DnsPrefetchIfNeeded(
const LinkRelAttribute& rel_attribute,
const KURL& href,
Document* document,
LocalFrame* frame,
const NetworkHintsInterface& network_hints_interface,
LinkCaller caller) {
if (rel_attribute.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() && href.IsValid() &&
!href.IsEmpty()) {
if (settings->GetLogDnsPrefetchAndPreconnect()) {
SendMessageToConsoleForPossiblyNullDocument(
ConsoleMessage::Create(
kOtherMessageSource, kVerboseMessageLevel,
String("DNS prefetch triggered for " + href.Host())),
document, frame);
}
network_hints_interface.DnsPrefetchHost(href.Host());
}
}
}
static void PreconnectIfNeeded(
const LinkRelAttribute& rel_attribute,
const KURL& href,
Document* document,
LocalFrame* frame,
const CrossOriginAttributeValue cross_origin,
const NetworkHintsInterface& network_hints_interface,
LinkCaller caller) {
if (rel_attribute.IsPreconnect() && href.IsValid() &&
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 ") + href.GetString()),
document, frame);
if (cross_origin != kCrossOriginAttributeNotSet) {
SendMessageToConsoleForPossiblyNullDocument(
ConsoleMessage::Create(
kOtherMessageSource, kVerboseMessageLevel,
String("Preconnect CORS setting is ") +
String((cross_origin == kCrossOriginAttributeAnonymous)
? "anonymous"
: "use-credentials")),
document, frame);
}
}
network_hints_interface.PreconnectHost(href, cross_origin);
}
}
WTF::Optional<Resource::Type> LinkLoader::GetResourceTypeFromAsAttribute(
const String& as) {
DCHECK_EQ(as.DeprecatedLower(), as);
if (as == "image") {
return Resource::kImage;
} else if (as == "script") {
return Resource::kScript;
} else if (as == "style") {
return Resource::kCSSStyleSheet;
} else if (as == "video") {
return Resource::kMedia;
} else if (as == "audio") {
return Resource::kMedia;
} else if (as == "track") {
return Resource::kTextTrack;
} else if (as == "font") {
return Resource::kFont;
} else if (as == "fetch") {
return Resource::kRaw;
}
return WTF::nullopt;
}
Resource* LinkLoader::GetResourceForTesting() {
return finish_observer_ ? finish_observer_->GetResource() : nullptr;
}
static bool IsSupportedType(Resource::Type resource_type,
const String& mime_type) {
if (mime_type.IsEmpty())
return true;
switch (resource_type) {
case Resource::kImage:
return MIMETypeRegistry::IsSupportedImagePrefixedMIMEType(mime_type);
case Resource::kScript:
return MIMETypeRegistry::IsSupportedJavaScriptMIMEType(mime_type);
case Resource::kCSSStyleSheet:
return MIMETypeRegistry::IsSupportedStyleSheetMIMEType(mime_type);
case Resource::kFont:
return MIMETypeRegistry::IsSupportedFontMIMEType(mime_type);
case Resource::kMedia:
return MIMETypeRegistry::IsSupportedMediaMIMEType(mime_type, String());
case Resource::kTextTrack:
return MIMETypeRegistry::IsSupportedTextTrackMIMEType(mime_type);
case Resource::kRaw:
return true;
default:
NOTREACHED();
}
return false;
}
static bool MediaMatches(Document& document,
const String& media,
ViewportDescription* viewport_description) {
if (media.IsEmpty())
return true;
MediaValues* media_values =
MediaValues::CreateDynamicIfFrameExists(document.GetFrame());
if (viewport_description) {
media_values->OverrideViewportDimensions(
viewport_description->max_width.GetFloatValue(),
viewport_description->max_height.GetFloatValue());
}
scoped_refptr<MediaQuerySet> media_queries = MediaQuerySet::Create(media);
MediaQueryEvaluator evaluator(*media_values);
return evaluator.Eval(*media_queries);
}
static Resource* PreloadIfNeeded(const LinkRelAttribute& rel_attribute,
const KURL& href,
Document& document,
const String& as,
const String& mime_type,
const String& media,
const String& nonce,
CrossOriginAttributeValue cross_origin,
LinkCaller caller,
ViewportDescription* viewport_description,
ReferrerPolicy referrer_policy) {
if (!document.Loader() || !rel_attribute.IsLinkPreload())
return nullptr;
UseCounter::Count(document, WebFeature::kLinkRelPreload);
if (!href.IsValid() || href.IsEmpty()) {
document.AddConsoleMessage(ConsoleMessage::Create(
kOtherMessageSource, kWarningMessageLevel,
String("<link rel=preload> has an invalid `href` value")));
return nullptr;
}
// Preload only if media matches
if (!MediaMatches(document, media, viewport_description))
return nullptr;
if (caller == kLinkCalledFromHeader)
UseCounter::Count(document, WebFeature::kLinkHeaderPreload);
Optional<Resource::Type> resource_type =
LinkLoader::GetResourceTypeFromAsAttribute(as);
if (resource_type == WTF::nullopt) {
document.AddConsoleMessage(ConsoleMessage::Create(
kOtherMessageSource, kWarningMessageLevel,
String("<link rel=preload> must have a valid `as` value")));
return nullptr;
}
if (!IsSupportedType(resource_type.value(), mime_type)) {
document.AddConsoleMessage(ConsoleMessage::Create(
kOtherMessageSource, kWarningMessageLevel,
String("<link rel=preload> has an unsupported `type` value")));
return nullptr;
}
ResourceRequest resource_request(href);
resource_request.SetRequestContext(ResourceFetcher::DetermineRequestContext(
resource_type.value(), ResourceFetcher::kImageNotImageSet, false));
if (referrer_policy != kReferrerPolicyDefault) {
resource_request.SetHTTPReferrer(SecurityPolicy::GenerateReferrer(
referrer_policy, href, document.OutgoingReferrer()));
}
ResourceLoaderOptions options;
options.initiator_info.name = FetchInitiatorTypeNames::link;
FetchParameters link_fetch_params(resource_request, options);
link_fetch_params.SetCharset(document.Encoding());
if (cross_origin != kCrossOriginAttributeNotSet) {
link_fetch_params.SetCrossOriginAccessControl(document.GetSecurityOrigin(),
cross_origin);
}
link_fetch_params.SetContentSecurityPolicyNonce(nonce);
Settings* settings = document.GetSettings();
if (settings && settings->GetLogPreload()) {
document.AddConsoleMessage(ConsoleMessage::Create(
kOtherMessageSource, kVerboseMessageLevel,
String("Preload triggered for " + href.Host() + href.GetPath())));
}
link_fetch_params.SetLinkPreload(true);
return document.Loader()->StartPreload(resource_type.value(),
link_fetch_params);
}
// https://html.spec.whatwg.org/#link-type-modulepreload
static void ModulePreloadIfNeeded(const LinkRelAttribute& rel_attribute,
const KURL& href,
Document& document,
const String& as,
const String& media,
const String& nonce,
CrossOriginAttributeValue cross_origin,
ViewportDescription* viewport_description,
ReferrerPolicy referrer_policy,
LinkLoader* link_loader) {
if (!document.Loader() || !rel_attribute.IsModulePreload())
return;
// TODO(ksakamoto): add UseCounter
// Step 1. "If the href attribute's value is the empty string, then return."
// [spec text]
if (href.IsEmpty()) {
document.AddConsoleMessage(
ConsoleMessage::Create(kOtherMessageSource, kWarningMessageLevel,
"<link rel=modulepreload> has no `href` value"));
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 (!as.IsEmpty() && as != "script") {
document.AddConsoleMessage(ConsoleMessage::Create(
kOtherMessageSource, kWarningMessageLevel,
String("<link rel=modulepreload> has an invalid `as` value " + as)));
if (link_loader)
link_loader->DispatchLinkLoadingErroredAsync();
return;
}
// 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 (!href.IsValid()) {
document.AddConsoleMessage(ConsoleMessage::Create(
kOtherMessageSource, kWarningMessageLevel,
"<link rel=modulepreload> has an invalid `href` value " +
href.GetString()));
return;
}
// Preload only if media matches.
// https://html.spec.whatwg.org/#processing-the-media-attribute
if (!MediaMatches(document, media, viewport_description))
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 6. "Let credentials mode be the module script credentials mode for the
// crossorigin attribute." [spec text]
network::mojom::FetchCredentialsMode credentials_mode =
ScriptLoader::ModuleScriptCredentialsMode(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]
// TODO(ksakamoto): Support integrity attribute.
// Step 9. "Let options be a script fetch options whose cryptographic nonce is
// cryptographic nonce, integrity metadata is integrity metadata, parser
// metadata is "not-parser-inserted", and credentials mode is credentials
// mode." [spec text]
ModuleScriptFetchRequest request(
href, referrer_policy,
ScriptFetchOptions(nonce, IntegrityMetadataSet(), String(),
kNotParserInserted, credentials_mode));
// Step 10. "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, ModuleGraphLevel::kDependentModuleFetch,
link_loader);
Settings* settings = document.GetSettings();
if (settings && settings->GetLogPreload()) {
document.AddConsoleMessage(ConsoleMessage::Create(
kOtherMessageSource, kVerboseMessageLevel,
"Module preload triggered for " + href.Host() + href.GetPath()));
}
// Asynchronously continue processing after
// LinkLoader::NotifyModuleLoadFinished() is called.
}
static Resource* PrefetchIfNeeded(Document& document,
const KURL& href,
const LinkRelAttribute& rel_attribute,
CrossOriginAttributeValue cross_origin,
ReferrerPolicy referrer_policy) {
if (rel_attribute.IsLinkPrefetch() && href.IsValid() && document.GetFrame()) {
UseCounter::Count(document, WebFeature::kLinkRelPrefetch);
ResourceRequest resource_request(href);
if (referrer_policy != kReferrerPolicyDefault) {
resource_request.SetHTTPReferrer(SecurityPolicy::GenerateReferrer(
referrer_policy, href, document.OutgoingReferrer()));
}
ResourceLoaderOptions options;
options.initiator_info.name = FetchInitiatorTypeNames::link;
FetchParameters link_fetch_params(resource_request, options);
if (cross_origin != kCrossOriginAttributeNotSet) {
link_fetch_params.SetCrossOriginAccessControl(
document.GetSecurityOrigin(), cross_origin);
}
return LinkFetchResource::Fetch(Resource::kLinkPrefetch, link_fetch_params,
document.Fetcher());
}
return nullptr;
}
void LinkLoader::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.Media().IsEmpty())
continue;
if (media_policy == kOnlyLoadNonMedia && !header.Media().IsEmpty())
continue;
LinkRelAttribute rel_attribute(header.Rel());
KURL url(base_url, header.Url());
// Sanity check to avoid re-entrancy here.
if (url == base_url)
continue;
if (can_load_resources != kOnlyLoadResources) {
DnsPrefetchIfNeeded(rel_attribute, url, document, &frame,
network_hints_interface, kLinkCalledFromHeader);
PreconnectIfNeeded(rel_attribute, url, document, &frame,
GetCrossOriginAttributeValue(header.CrossOrigin()),
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;
CrossOriginAttributeValue cross_origin =
GetCrossOriginAttributeValue(header.CrossOrigin());
PreloadIfNeeded(rel_attribute, url, *document, header.As(),
header.MimeType(), header.Media(), header.Nonce(),
cross_origin, kLinkCalledFromHeader, viewport_description,
kReferrerPolicyDefault);
PrefetchIfNeeded(*document, url, rel_attribute, cross_origin,
kReferrerPolicyDefault);
ModulePreloadIfNeeded(rel_attribute, url, *document, header.As(),
header.Media(), header.Nonce(), cross_origin,
viewport_description, kReferrerPolicyDefault,
nullptr);
}
if (rel_attribute.IsServiceWorker()) {
UseCounter::Count(&frame, WebFeature::kLinkHeaderServiceWorker);
}
// TODO(yoav): Add more supported headers as needed.
}
}
bool LinkLoader::LoadLink(
const LinkRelAttribute& rel_attribute,
CrossOriginAttributeValue cross_origin,
const String& type,
const String& as,
const String& media,
const String& nonce,
ReferrerPolicy referrer_policy,
const KURL& href,
Document& document,
const NetworkHintsInterface& network_hints_interface) {
// If any loading process is in progress, abort it.
Abort();
if (!client_->ShouldLoadLink())
return false;
DnsPrefetchIfNeeded(rel_attribute, href, &document, document.GetFrame(),
network_hints_interface, kLinkCalledFromMarkup);
PreconnectIfNeeded(rel_attribute, href, &document, document.GetFrame(),
cross_origin, network_hints_interface,
kLinkCalledFromMarkup);
Resource* resource = PreloadIfNeeded(
rel_attribute, href, document, as, type, media, nonce, cross_origin,
kLinkCalledFromMarkup, nullptr, referrer_policy);
if (!resource) {
resource = PrefetchIfNeeded(document, href, rel_attribute, cross_origin,
referrer_policy);
}
if (resource)
finish_observer_ = new FinishObserver(this, resource);
ModulePreloadIfNeeded(rel_attribute, href, document, as, media, nonce,
cross_origin, nullptr, referrer_policy, this);
if (const unsigned prerender_rel_types =
PrerenderRelTypesFromRelAttribute(rel_attribute, document)) {
if (!prerender_) {
prerender_ =
PrerenderHandle::Create(document, this, href, prerender_rel_types);
} else if (prerender_->Url() != href) {
prerender_->Cancel();
prerender_ =
PrerenderHandle::Create(document, this, href, prerender_rel_types);
}
// TODO(gavinp): Handle changes to rel types of existing prerenders.
} else if (prerender_) {
prerender_->Cancel();
prerender_.Clear();
}
return true;
}
void LinkLoader::DispatchLinkLoadingErroredAsync() {
client_->GetLoadingTaskRunner()->PostTask(
BLINK_FROM_HERE, WTF::Bind(&LinkLoaderClient::LinkLoadingErrored,
WrapPersistent(client_.Get())));
}
void LinkLoader::Abort() {
if (prerender_) {
prerender_->Cancel();
prerender_.Clear();
}
if (finish_observer_) {
finish_observer_->ClearResource();
finish_observer_ = nullptr;
}
}
void LinkLoader::Trace(blink::Visitor* visitor) {
visitor->Trace(finish_observer_);
visitor->Trace(client_);
visitor->Trace(prerender_);
SingleModuleClient::Trace(visitor);
PrerenderClient::Trace(visitor);
}
} // namespace blink