blob: 460936df8cbd59edcf27df0e9d50dfa65b23b36a [file] [log] [blame]
// Copyright 2017 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/script/classic_pending_script.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/renderer/bindings/core/v8/script_source_code.h"
#include "third_party/blink/renderer/bindings/core/v8/script_streamer.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/scriptable_document_parser.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/inspector/inspector_trace_events.h"
#include "third_party/blink/renderer/core/loader/document_loader.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/document_write_intervention.h"
#include "third_party/blink/renderer/core/script/script_loader.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/instrumentation/histogram.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
#include "third_party/blink/renderer/platform/loader/allowed_by_nosniff.h"
#include "third_party/blink/renderer/platform/loader/fetch/cached_metadata.h"
#include "third_party/blink/renderer/platform/loader/fetch/detachable_use_counter.h"
#include "third_party/blink/renderer/platform/loader/fetch/memory_cache.h"
#include "third_party/blink/renderer/platform/loader/fetch/raw_resource.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_client.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h"
#include "third_party/blink/renderer/platform/loader/fetch/source_keyed_cached_metadata_handler.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
namespace blink {
// <specdef href="https://html.spec.whatwg.org/C/#fetch-a-classic-script">
ClassicPendingScript* ClassicPendingScript::Fetch(
const KURL& url,
Document& element_document,
const ScriptFetchOptions& options,
CrossOriginAttributeValue cross_origin,
const WTF::TextEncoding& encoding,
ScriptElementBase* element,
FetchParameters::DeferOption defer) {
ExecutionContext* context = element_document.GetExecutionContext();
FetchParameters params(options.CreateFetchParameters(
url, context->GetSecurityOrigin(), context->GetCurrentWorld(),
cross_origin, encoding, defer));
ClassicPendingScript* pending_script =
MakeGarbageCollected<ClassicPendingScript>(
element, TextPosition(), KURL(), String(),
ScriptSourceLocationType::kExternalFile, options,
true /* is_external */);
// [Intervention]
// For users on slow connections, we want to avoid blocking the parser in
// the main frame on script loads inserted via document.write, since it
// can add significant delays before page content is displayed on the
// screen.
pending_script->intervened_ =
MaybeDisallowFetchForDocWrittenScript(params, element_document);
// <spec step="2">Set request's client to settings object.</spec>
//
// Note: |element_document| corresponds to the settings object.
//
// We allow streaming, as WatchForLoad() is always called when the script
// needs to execute and the ScriptResource is not finished, so
// SetClientIsWaitingForFinished is always set on the resource.
ScriptResource::Fetch(params, element_document.Fetcher(), pending_script,
ScriptResource::kAllowStreaming);
pending_script->CheckState();
return pending_script;
}
ClassicPendingScript* ClassicPendingScript::CreateInline(
ScriptElementBase* element,
const TextPosition& starting_position,
const KURL& base_url,
const String& source_text,
ScriptSourceLocationType source_location_type,
const ScriptFetchOptions& options) {
ClassicPendingScript* pending_script =
MakeGarbageCollected<ClassicPendingScript>(
element, starting_position, base_url, source_text,
source_location_type, options, false /* is_external */);
pending_script->CheckState();
return pending_script;
}
ClassicPendingScript::ClassicPendingScript(
ScriptElementBase* element,
const TextPosition& starting_position,
const KURL& base_url_for_inline_script,
const String& source_text_for_inline_script,
ScriptSourceLocationType source_location_type,
const ScriptFetchOptions& options,
bool is_external)
: PendingScript(element, starting_position),
options_(options),
base_url_for_inline_script_(base_url_for_inline_script),
source_text_for_inline_script_(source_text_for_inline_script),
source_location_type_(source_location_type),
is_external_(is_external),
ready_state_(is_external ? kWaitingForResource : kReady),
integrity_failure_(false) {
CHECK(GetElement());
if (is_external_) {
DCHECK(base_url_for_inline_script_.IsNull());
DCHECK(source_text_for_inline_script_.IsNull());
} else {
DCHECK(!base_url_for_inline_script_.IsNull());
DCHECK(!source_text_for_inline_script_.IsNull());
}
MemoryPressureListenerRegistry::Instance().RegisterClient(this);
}
ClassicPendingScript::~ClassicPendingScript() {}
NOINLINE void ClassicPendingScript::CheckState() const {
DCHECK(GetElement());
DCHECK_EQ(is_external_, !!GetResource());
}
void ClassicPendingScript::RecordThirdPartyRequestWithCookieIfNeeded(
const ResourceResponse& response) const {
// Can be null in some cases where loading failed.
if (response.IsNull())
return;
scoped_refptr<SecurityOrigin> script_origin =
SecurityOrigin::Create(response.ResponseUrl());
const SecurityOrigin* doc_origin =
GetElement()->GetExecutionContext()->GetSecurityOrigin();
scoped_refptr<const SecurityOrigin> top_frame_origin =
GetElement()->GetDocument().TopFrameOrigin();
// The use counter is meant to gather data for prerendering: how often do
// pages make credentialed requests to third parties from first-party frames,
// that cannot be delayed during prerendering until the page is navigated to.
// Therefore...
// Ignore third-party frames.
if (!top_frame_origin || top_frame_origin->RegistrableDomain() !=
doc_origin->RegistrableDomain()) {
return;
}
// Ignore first-party requests.
if (doc_origin->RegistrableDomain() == script_origin->RegistrableDomain())
return;
// Ignore cookie-less requests.
if (!response.WasCookieInRequest())
return;
// Ignore scripts that can be delayed. This is only async scripts currently.
// kDefer and kForceDefer don't count as delayable since delaying them
// artificially further while prerendering would prevent the page from making
// progress.
if (GetSchedulingType() == ScriptSchedulingType::kAsync)
return;
GetElement()->GetExecutionContext()->CountUse(
mojom::blink::WebFeature::
kUndeferrableThirdPartySubresourceRequestWithCookie);
}
void ClassicPendingScript::DisposeInternal() {
MemoryPressureListenerRegistry::Instance().UnregisterClient(this);
ClearResource();
integrity_failure_ = false;
}
bool ClassicPendingScript::IsEligibleForDelay() const {
DCHECK_EQ(GetSchedulingType(), ScriptSchedulingType::kAsync);
// We don't delay async scripts that have matched a resource in the preload
// cache, because we're using <link rel=preload> as a signal that the script
// is higher-than-usual priority, and therefore should be executed earlier
// rather than later.
return !GetResource()->IsLinkPreload();
}
void ClassicPendingScript::NotifyFinished(Resource* resource) {
// The following SRI checks need to be here because, unfortunately, fetches
// are not done purely according to the Fetch spec. In particular,
// different requests for the same resource do not have different
// responses; the memory cache can (and will) return the exact same
// Resource object.
//
// For different requests, the same Resource object will be returned and
// will not be associated with the particular request. Therefore, when the
// body of the response comes in, there's no way to validate the integrity
// of the Resource object against a particular request (since there may be
// several pending requests all tied to the identical object, and the
// actual requests are not stored).
//
// In order to simulate the correct behavior, Blink explicitly does the SRI
// checks here, when a PendingScript tied to a particular request is
// finished (and in the case of a StyleSheet, at the point of execution),
// while having proper Fetch checks in the fetch module for use in the
// fetch JavaScript API. In a future world where the ResourceFetcher uses
// the Fetch algorithm, this should be fixed by having separate Response
// objects (perhaps attached to identical Resource objects) per request.
//
// See https://crbug.com/500701 for more information.
CheckState();
DCHECK(GetResource());
ScriptElementBase* element = GetElement();
if (element) {
SubresourceIntegrityHelper::DoReport(*element->GetExecutionContext(),
GetResource()->IntegrityReportInfo());
// It is possible to get back a script resource with integrity metadata
// for a request with an empty integrity attribute. In that case, the
// integrity check should be skipped, as the integrity may not have been
// "meant" for this specific request. If the resource is being served from
// the preload cache however, we know any associated integrity metadata and
// checks were destined for this request, so we cannot skip the integrity
// check.
if (!element->IntegrityAttributeValue().IsEmpty() ||
GetResource()->IsLinkPreload()) {
integrity_failure_ = GetResource()->IntegrityDisposition() !=
ResourceIntegrityDisposition::kPassed;
}
}
if (intervened_) {
CrossOriginAttributeValue cross_origin =
GetCrossOriginAttributeValue(element->CrossOriginAttributeValue());
PossiblyFetchBlockedDocWriteScript(resource, element->GetDocument(),
options_, cross_origin);
}
TRACE_EVENT_WITH_FLOW1(
TRACE_DISABLED_BY_DEFAULT("v8.compile"),
"ClassicPendingScript::NotifyFinished", this, TRACE_EVENT_FLAG_FLOW_OUT,
"data",
inspector_parse_script_event::Data(GetResource()->InspectorId(),
GetResource()->Url().GetString()));
bool error_occurred = GetResource()->ErrorOccurred() || integrity_failure_;
AdvanceReadyState(error_occurred ? kErrorOccurred : kReady);
}
void ClassicPendingScript::Trace(Visitor* visitor) const {
ResourceClient::Trace(visitor);
MemoryPressureListener::Trace(visitor);
PendingScript::Trace(visitor);
}
static SingleCachedMetadataHandler* GetInlineCacheHandler(const String& source,
Document& document) {
if (!RuntimeEnabledFeatures::CacheInlineScriptCodeEnabled())
return nullptr;
ScriptableDocumentParser* scriptable_parser =
document.GetScriptableDocumentParser();
if (!scriptable_parser)
return nullptr;
SourceKeyedCachedMetadataHandler* document_cache_handler =
scriptable_parser->GetInlineScriptCacheHandler();
if (!document_cache_handler)
return nullptr;
return document_cache_handler->HandlerForSource(source);
}
ClassicScript* ClassicPendingScript::GetSource(const KURL& document_url) const {
CheckState();
DCHECK(IsReady());
if (ready_state_ == kErrorOccurred)
return nullptr;
TRACE_EVENT0("blink", "ClassicPendingScript::GetSource");
if (!is_external_) {
SingleCachedMetadataHandler* cache_handler = nullptr;
// We only create an inline cache handler for html-embedded scripts, not
// for scripts produced by document.write, or not parser-inserted. This is
// because we expect those to be too dynamic to benefit from caching.
// TODO(leszeks): ScriptSourceLocationType was previously only used for UMA,
// so it's a bit of a layer violation to use it for affecting cache
// behaviour. We should decide whether it is ok for this parameter to be
// used for behavioural changes (and if yes, update its documentation), or
// otherwise trigger this behaviour differently.
if (source_location_type_ == ScriptSourceLocationType::kInline) {
cache_handler = GetInlineCacheHandler(source_text_for_inline_script_,
GetElement()->GetDocument());
}
DCHECK(!GetResource());
ScriptStreamer::RecordStreamingHistogram(
GetSchedulingType(), false,
ScriptStreamer::NotStreamingReason::kInlineScript);
ScriptSourceCode source_code(source_text_for_inline_script_,
source_location_type_, cache_handler,
document_url, StartingPosition());
return MakeGarbageCollected<ClassicScript>(
source_code, base_url_for_inline_script_, options_,
SanitizeScriptErrors::kDoNotSanitize);
}
DCHECK(GetResource()->IsLoaded());
auto* resource = To<ScriptResource>(GetResource());
RecordThirdPartyRequestWithCookieIfNeeded(resource->GetResponse());
auto* fetcher = GetElement()->GetExecutionContext()->Fetcher();
// If the MIME check fails, which is considered as load failure.
if (!AllowedByNosniff::MimeTypeAsScript(
fetcher->GetUseCounter(), &fetcher->GetConsoleLogger(),
resource->GetResponse(),
AllowedByNosniff::MimeTypeCheck::kLaxForElement)) {
return nullptr;
}
// Check if we can use the script streamer.
ScriptStreamer* streamer;
ScriptStreamer::NotStreamingReason not_streamed_reason;
std::tie(streamer, not_streamed_reason) = ScriptStreamer::TakeFrom(resource);
if (ready_state_ == kErrorOccurred) {
not_streamed_reason = ScriptStreamer::NotStreamingReason::kErrorOccurred;
streamer = nullptr;
}
if (streamer)
CHECK_EQ(ready_state_, kReady);
ScriptStreamer::RecordStreamingHistogram(GetSchedulingType(), streamer,
not_streamed_reason);
TRACE_EVENT_WITH_FLOW1(TRACE_DISABLED_BY_DEFAULT("v8.compile"),
"ClassicPendingScript::GetSource", this,
TRACE_EVENT_FLAG_FLOW_IN, "not_streamed_reason",
not_streamed_reason);
ScriptSourceCode source_code(streamer, resource, not_streamed_reason);
// The base URL for external classic script is
//
// <spec href="https://html.spec.whatwg.org/C/#concept-script-base-url">
// ... the URL from which the script was obtained, ...</spec>
const KURL& base_url = source_code.Url();
return MakeGarbageCollected<ClassicScript>(
source_code, base_url, options_,
resource->GetResponse().IsCorsSameOrigin()
? SanitizeScriptErrors::kDoNotSanitize
: SanitizeScriptErrors::kSanitize);
}
bool ClassicPendingScript::IsReady() const {
CheckState();
return ready_state_ >= kReady;
}
void ClassicPendingScript::AdvanceReadyState(ReadyState new_ready_state) {
// We will allow exactly these state transitions:
//
// kWaitingForResource -> [kReady, kErrorOccurred]
switch (ready_state_) {
case kWaitingForResource:
CHECK(new_ready_state == kReady || new_ready_state == kErrorOccurred);
break;
case kReady:
case kErrorOccurred:
NOTREACHED();
break;
}
bool old_is_ready = IsReady();
ready_state_ = new_ready_state;
// Did we transition into a 'ready' state?
if (IsReady() && !old_is_ready && IsWatchingForLoad())
PendingScriptFinished();
}
void ClassicPendingScript::OnPurgeMemory() {
CheckState();
// TODO(crbug.com/846951): the implementation of CancelStreaming() is
// currently incorrect and consequently a call to this method was removed from
// here.
}
bool ClassicPendingScript::WasCanceled() const {
if (!is_external_)
return false;
return GetResource()->WasCanceled();
}
KURL ClassicPendingScript::UrlForTracing() const {
if (!is_external_ || !GetResource())
return NullURL();
return GetResource()->Url();
}
} // namespace blink