blob: 8a92a60c654dd088e8a8db36d38e12b6e9687673 [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/frame/local_frame.h"
#include "third_party/blink/renderer/core/loader/allowed_by_nosniff.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/histogram.h"
#include "third_party/blink/renderer/platform/loader/fetch/cached_metadata.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/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/#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) {
FetchParameters params = options.CreateFetchParameters(
url, element_document.GetSecurityOrigin(), cross_origin, encoding, defer);
ClassicPendingScript* pending_script = new ClassicPendingScript(
element, TextPosition(), 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,
ScriptSourceLocationType source_location_type,
const ScriptFetchOptions& options) {
ClassicPendingScript* pending_script =
new ClassicPendingScript(element, starting_position, source_location_type,
options, false /* is_external */);
pending_script->CheckState();
return pending_script;
}
ClassicPendingScript::ClassicPendingScript(
ScriptElementBase* element,
const TextPosition& starting_position,
ScriptSourceLocationType source_location_type,
const ScriptFetchOptions& options,
bool is_external)
: PendingScript(element, starting_position),
options_(options),
base_url_for_inline_script_(
is_external ? KURL() : element->GetDocument().BaseURL()),
source_text_for_inline_script_(is_external ? String()
: element->TextFromChildren()),
source_location_type_(source_location_type),
is_external_(is_external),
ready_state_(is_external ? kWaitingForResource : kReady),
integrity_failure_(false),
is_currently_streaming_(false) {
CHECK(GetElement());
MemoryCoordinator::Instance().RegisterClient(this);
}
ClassicPendingScript::~ClassicPendingScript() {}
NOINLINE void ClassicPendingScript::CheckState() const {
// TODO(hiroshige): Turn these CHECK()s into DCHECK() before going to beta.
CHECK(GetElement());
CHECK_EQ(is_external_, !!GetResource());
}
namespace {
enum class StreamedBoolean {
// Must match BooleanStreamed in enums.xml.
kNotStreamed = 0,
kStreamed = 1,
kMaxValue = kStreamed
};
void RecordStartedStreamingHistogram(ScriptSchedulingType type,
bool did_use_streamer) {
StreamedBoolean streamed = did_use_streamer ? StreamedBoolean::kStreamed
: StreamedBoolean::kNotStreamed;
switch (type) {
case ScriptSchedulingType::kParserBlocking: {
UMA_HISTOGRAM_ENUMERATION(
"WebCore.Scripts.ParsingBlocking.StartedStreaming", streamed);
break;
}
case ScriptSchedulingType::kDefer: {
UMA_HISTOGRAM_ENUMERATION("WebCore.Scripts.Deferred.StartedStreaming",
streamed);
break;
}
case ScriptSchedulingType::kAsync: {
UMA_HISTOGRAM_ENUMERATION("WebCore.Scripts.Async.StartedStreaming",
streamed);
break;
}
default: {
UMA_HISTOGRAM_ENUMERATION("WebCore.Scripts.Other.StartedStreaming",
streamed);
break;
}
}
}
void RecordNotStreamingReasonHistogram(
ScriptSchedulingType type,
ScriptStreamer::NotStreamingReason reason) {
switch (type) {
case ScriptSchedulingType::kParserBlocking: {
UMA_HISTOGRAM_ENUMERATION(
"WebCore.Scripts.ParsingBlocking.NotStreamingReason", reason,
ScriptStreamer::NotStreamingReason::kCount);
break;
}
case ScriptSchedulingType::kDefer: {
UMA_HISTOGRAM_ENUMERATION("WebCore.Scripts.Deferred.NotStreamingReason",
reason,
ScriptStreamer::NotStreamingReason::kCount);
break;
}
case ScriptSchedulingType::kAsync: {
UMA_HISTOGRAM_ENUMERATION("WebCore.Scripts.Async.NotStreamingReason",
reason,
ScriptStreamer::NotStreamingReason::kCount);
break;
}
default: {
UMA_HISTOGRAM_ENUMERATION("WebCore.Scripts.Other.NotStreamingReason",
reason,
ScriptStreamer::NotStreamingReason::kCount);
break;
}
}
}
} // namespace
void ClassicPendingScript::RecordStreamingHistogram(
ScriptSchedulingType type,
bool can_use_streamer,
ScriptStreamer::NotStreamingReason reason) {
RecordStartedStreamingHistogram(type, can_use_streamer);
if (!can_use_streamer) {
DCHECK_NE(ScriptStreamer::kInvalid, reason);
RecordNotStreamingReasonHistogram(type, reason);
}
}
void ClassicPendingScript::DisposeInternal() {
MemoryCoordinator::Instance().UnregisterClient(this);
ClearResource();
integrity_failure_ = false;
}
void ClassicPendingScript::WatchForLoad(PendingScriptClient* client) {
if (is_external_) {
// Once we are watching the ClassicPendingScript for load, we won't ever
// try to start streaming this resource via this ClassicPendingScript, so
// we mark the resource to instead get a finished notification when loading
// (rather than streaming) completes.
//
// Do this in a task rather than directly to make sure the IsReady state
// of PendingScript does not change during this call.
GetElement()
->GetDocument()
.GetTaskRunner(TaskType::kNetworking)
->PostTask(FROM_HERE,
WTF::Bind(&ScriptResource::SetClientIsWaitingForFinished,
WrapPersistent(ToScriptResource(GetResource()))));
}
PendingScript::WatchForLoad(client);
}
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->GetDocument(),
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, so this check ensures that the
// integrity attribute isn't empty in addition to checking if the
// resource has empty integrity metadata.
if (!element->IntegrityAttributeValue().IsEmpty()) {
integrity_failure_ = GetResource()->IntegrityDisposition() !=
ResourceIntegrityDisposition::kPassed;
}
}
if (intervened_) {
CrossOriginAttributeValue cross_origin =
GetCrossOriginAttributeValue(element->CrossOriginAttributeValue());
PossiblyFetchBlockedDocWriteScript(resource, element->GetDocument(),
options_, cross_origin);
}
bool error_occurred = GetResource()->ErrorOccurred() || integrity_failure_;
AdvanceReadyState(error_occurred ? kErrorOccurred : kReady);
}
void ClassicPendingScript::Trace(blink::Visitor* visitor) {
ResourceClient::Trace(visitor);
MemoryCoordinatorClient::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;
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());
RecordStreamingHistogram(GetSchedulingType(), false,
ScriptStreamer::kInlineScript);
ScriptSourceCode source_code(source_text_for_inline_script_,
source_location_type_, cache_handler,
document_url, StartingPosition());
return ClassicScript::Create(source_code, base_url_for_inline_script_,
options_,
SanitizeScriptErrors::kDoNotSanitize);
}
DCHECK(GetResource()->IsLoaded());
ScriptResource* resource = ToScriptResource(GetResource());
// If the MIME check fails, which is considered as load failure.
if (!AllowedByNosniff::MimeTypeAsScript(
GetElement()->GetDocument().ContextDocument(),
resource->GetResponse(), AllowedByNosniff::MimeTypeCheck::kLax)) {
return nullptr;
}
// Check if we can use the script streamer.
bool streamer_ready = false;
ScriptStreamer::NotStreamingReason not_streamed_reason =
resource->NoStreamerReason();
ScriptStreamer* streamer = resource->TakeStreamer();
if (streamer) {
DCHECK_EQ(not_streamed_reason, ScriptStreamer::kInvalid);
if (streamer->StreamingSuppressed()) {
not_streamed_reason = streamer->StreamingSuppressedReason();
} else if (ready_state_ == kErrorOccurred) {
not_streamed_reason = ScriptStreamer::kErrorOccurred;
} else {
// Streamer can be used to compile script.
CHECK_EQ(ready_state_, kReady);
not_streamed_reason = ScriptStreamer::kInvalid;
streamer_ready = true;
}
}
RecordStreamingHistogram(GetSchedulingType(), streamer_ready,
not_streamed_reason);
ScriptSourceCode source_code(streamer_ready ? streamer : nullptr, resource,
not_streamed_reason);
// The base URL for external classic script is
//
// <spec href="https://html.spec.whatwg.org/#concept-script-base-url">
// ... the URL from which the script was obtained, ...</spec>
const KURL& base_url = source_code.Url();
return ClassicScript::Create(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;
ScriptResource* resource = ToScriptResource(GetResource());
// Did we transition into a 'ready' state?
if (IsReady() && !old_is_ready && IsWatchingForLoad())
PendingScriptFinished();
// Did we finish streaming?
if (IsCurrentlyStreaming()) {
if (ready_state_ == kReady || ready_state_ == kErrorOccurred) {
// Call the streamer_done_ callback. Ensure that is_currently_streaming_
// is reset only after the callback returns, to prevent accidentally
// start streaming by work done within the callback. (crbug.com/754360)
base::OnceClosure done = std::move(streamer_done_);
if (done)
std::move(done).Run();
is_currently_streaming_ = false;
}
}
// Streaming-related post conditions:
// To help diagnose crbug.com/78426, we'll temporarily add some DCHECKs
// that are a subset of the DCHECKs below:
if (IsCurrentlyStreaming()) {
DCHECK(resource->HasStreamer());
DCHECK(!resource->HasFinishedStreamer());
}
// IsCurrentlyStreaming should match what streamer_ thinks.
DCHECK_EQ(IsCurrentlyStreaming(),
resource->HasStreamer() && !resource->HasFinishedStreamer());
// IsCurrentlyStreaming should match the ready_state_.
DCHECK_EQ(IsCurrentlyStreaming(),
resource->HasStreamer() && ready_state_ == kWaitingForResource);
// We can only have a streamer_done_ callback if we are actually streaming.
DCHECK(IsCurrentlyStreaming() || !streamer_done_);
}
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::StartStreamingIfPossible(
base::OnceClosure done) {
if (IsCurrentlyStreaming())
return false;
// We can start streaming in two states: While still loading
// (kWaitingForResource), or after having loaded (kReady).
if (ready_state_ != kWaitingForResource && ready_state_ != kReady)
return false;
Document* document = &GetElement()->GetDocument();
if (!document || !document->GetFrame())
return false;
// Parser blocking scripts tend to do a lot of work in the 'finished'
// callbacks, while async + in-order scripts all do control-like activities
// (like posting new tasks). Use the 'control' queue only for control tasks.
// (More details in discussion for cl 500147.)
auto task_type = GetSchedulingType() == ScriptSchedulingType::kParserBlocking
? TaskType::kNetworking
: TaskType::kNetworkingControl;
DCHECK(!IsCurrentlyStreaming());
DCHECK(!streamer_done_);
ReadyState ready_state_before_stream = ready_state_;
bool success = ToScriptResource(GetResource())
->StartStreaming(document->GetTaskRunner(task_type));
// We have to make sure that the ready state is not changed by starting
// streaming, just in case we're relying on IsReady being false.
CHECK_EQ(ready_state_before_stream, ready_state_);
// If we have successfully started streaming, we are required to call the
// callback.
is_currently_streaming_ = success;
if (success)
streamer_done_ = std::move(done);
return success;
}
bool ClassicPendingScript::IsCurrentlyStreaming() const {
return is_currently_streaming_;
}
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