blob: a507399e3bd993ba316cdcc33633b1b7788d2984 [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 "core/script/ClassicPendingScript.h"
#include "bindings/core/v8/ScriptSourceCode.h"
#include "bindings/core/v8/ScriptStreamer.h"
#include "bindings/core/v8/V8BindingForCore.h"
#include "core/dom/Document.h"
#include "core/frame/LocalFrame.h"
#include "core/loader/AllowedByNosniff.h"
#include "core/loader/SubresourceIntegrityHelper.h"
#include "core/loader/resource/ScriptResource.h"
#include "core/script/DocumentWriteIntervention.h"
#include "core/script/ScriptLoader.h"
#include "platform/bindings/ScriptState.h"
#include "platform/loader/fetch/MemoryCache.h"
#include "public/platform/TaskType.h"
namespace blink {
ClassicPendingScript* ClassicPendingScript::Fetch(
const KURL& url,
Document& element_document,
const ScriptFetchOptions& options,
const WTF::TextEncoding& encoding,
ScriptElementBase* element,
FetchParameters::DeferOption defer) {
FetchParameters params = options.CreateFetchParameters(
url, element_document.GetSecurityOrigin(), 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);
// https://html.spec.whatwg.org/#fetch-a-classic-script
// Step 2. Set request's client to settings object. [spec text]
//
// Note: |element_document| corresponds to the settings object.
ScriptResource* resource =
ScriptResource::Fetch(params, element_document.Fetcher(), pending_script);
if (!resource)
return nullptr;
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_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(!prefinalizer_called_);
CHECK(GetElement());
CHECK_EQ(is_external_, !!GetResource());
CHECK(GetResource() || !streamer_);
}
void ClassicPendingScript::Prefinalize() {
// TODO(hiroshige): Consider moving this to ScriptStreamer's prefinalizer.
// https://crbug.com/715309
CancelStreaming();
prefinalizer_called_ = true;
}
void ClassicPendingScript::DisposeInternal() {
MemoryCoordinator::Instance().UnregisterClient(this);
ClearResource();
integrity_failure_ = false;
CancelStreaming();
}
void ClassicPendingScript::StreamingFinished() {
CheckState();
DCHECK(streamer_); // Should only be called by ScriptStreamer.
DCHECK(IsCurrentlyStreaming());
if (ready_state_ == kWaitingForStreaming) {
FinishWaitingForStreaming();
} else if (ready_state_ == kReadyStreaming) {
FinishReadyStreaming();
} else {
NOTREACHED();
}
DCHECK(!IsCurrentlyStreaming());
}
void ClassicPendingScript::FinishWaitingForStreaming() {
CheckState();
DCHECK(GetResource());
DCHECK_EQ(ready_state_, kWaitingForStreaming);
bool error_occurred = GetResource()->ErrorOccurred() || integrity_failure_;
AdvanceReadyState(error_occurred ? kErrorOccurred : kReady);
}
void ClassicPendingScript::FinishReadyStreaming() {
CheckState();
DCHECK(GetResource());
DCHECK_EQ(ready_state_, kReadyStreaming);
AdvanceReadyState(kReady);
}
void ClassicPendingScript::CancelStreaming() {
if (!streamer_)
return;
streamer_->Cancel();
streamer_ = nullptr;
streamer_done_.Reset();
}
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();
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_) {
PossiblyFetchBlockedDocWriteScript(resource, element->GetDocument(),
options_);
}
// We are now waiting for script streaming to finish.
// If there is no script streamer, this step completes immediately.
AdvanceReadyState(kWaitingForStreaming);
if (streamer_)
streamer_->NotifyFinished();
else
FinishWaitingForStreaming();
}
void ClassicPendingScript::DataReceived(Resource* resource,
const char*,
size_t) {
if (streamer_)
streamer_->NotifyAppendData(ToScriptResource(resource));
}
void ClassicPendingScript::Trace(blink::Visitor* visitor) {
visitor->Trace(streamer_);
ResourceClient::Trace(visitor);
MemoryCoordinatorClient::Trace(visitor);
PendingScript::Trace(visitor);
}
bool ClassicPendingScript::CheckMIMETypeBeforeRunScript(
Document* context_document) const {
if (!is_external_)
return true;
return AllowedByNosniff::MimeTypeAsScript(context_document,
GetResource()->GetResponse());
}
ClassicScript* ClassicPendingScript::GetSource(const KURL& document_url,
bool& error_occurred) const {
CheckState();
DCHECK(IsReady());
error_occurred = ErrorOccurred();
if (!is_external_) {
ScriptSourceCode source_code(
GetElement()->TextFromChildren(), source_location_type_,
nullptr /* cache_handler */, document_url, StartingPosition());
return ClassicScript::Create(source_code, base_url_for_inline_script_,
options_, kSharableCrossOrigin);
}
DCHECK(GetResource()->IsLoaded());
ScriptResource* resource = ToScriptResource(GetResource());
bool streamer_ready = (ready_state_ == kReady) && streamer_ &&
!streamer_->StreamingSuppressed();
ScriptSourceCode source_code(streamer_ready ? streamer_ : nullptr, resource);
// The base URL for external classic script is
// "the URL from which the script was obtained" [spec text]
// https://html.spec.whatwg.org/multipage/webappapis.html#concept-script-base-url
const KURL& base_url = source_code.Url();
return ClassicScript::Create(
source_code, base_url, options_,
resource->CalculateAccessControlStatus(
GetElement()->GetDocument().GetSecurityOrigin()));
}
void ClassicPendingScript::SetStreamer(ScriptStreamer* streamer) {
DCHECK(streamer);
DCHECK(!streamer_);
DCHECK(!IsWatchingForLoad() || ready_state_ != kWaitingForResource);
DCHECK(!streamer->IsFinished());
DCHECK(ready_state_ == kWaitingForResource || ready_state_ == kReady);
streamer_ = streamer;
is_currently_streaming_ = true;
if (streamer && ready_state_ == kReady)
AdvanceReadyState(kReadyStreaming);
CheckState();
}
bool ClassicPendingScript::IsReady() const {
CheckState();
return ready_state_ >= kReady;
}
bool ClassicPendingScript::ErrorOccurred() const {
CheckState();
return ready_state_ == kErrorOccurred;
}
void ClassicPendingScript::AdvanceReadyState(ReadyState new_ready_state) {
// We will allow exactly these state transitions:
//
// kWaitingForResource -> kWaitingForStreaming -> [kReady, kErrorOccurred]
// kReady -> kReadyStreaming -> kReady
switch (ready_state_) {
case kWaitingForResource:
CHECK_EQ(new_ready_state, kWaitingForStreaming);
break;
case kWaitingForStreaming:
CHECK(new_ready_state == kReady || new_ready_state == kErrorOccurred);
break;
case kReady:
CHECK_EQ(new_ready_state, kReadyStreaming);
break;
case kReadyStreaming:
CHECK_EQ(new_ready_state, kReady);
break;
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())
Client()->PendingScriptFinished(this);
// 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:
// IsCurrentlyStreaming should match what streamer_ thinks.
DCHECK_EQ(IsCurrentlyStreaming(), streamer_ && !streamer_->IsFinished());
// IsCurrentlyStreaming should match the ready_state_.
DCHECK_EQ(IsCurrentlyStreaming(),
ready_state_ == kReadyStreaming ||
(streamer_ && (ready_state_ == kWaitingForResource ||
ready_state_ == kWaitingForStreaming)));
// We can only have a streamer_done_ callback if we are actually streaming.
DCHECK(IsCurrentlyStreaming() || !streamer_done_);
}
void ClassicPendingScript::OnPurgeMemory() {
CheckState();
CancelStreaming();
}
bool ClassicPendingScript::StartStreamingIfPossible(
ScriptStreamer::Type streamer_type,
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;
ScriptState* script_state = ToScriptStateForMainWorld(document->GetFrame());
if (!script_state)
return false;
// To support streaming re-try, we'll clear the existing streamer if
// it exists; it claims to be finished; but it's finished because streaming
// has been suppressed.
if (streamer_ && streamer_->StreamingSuppressed() &&
streamer_->IsFinished()) {
DCHECK_EQ(ready_state_, kReady);
DCHECK(!streamer_done_);
DCHECK(!IsCurrentlyStreaming());
streamer_.Clear();
}
if (streamer_)
return false;
// The two checks above should imply that we're not presently streaming.
DCHECK(!IsCurrentlyStreaming());
// 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 = streamer_type == ScriptStreamer::kParsingBlocking
? TaskType::kNetworking
: TaskType::kNetworkingControl;
DCHECK(!streamer_);
DCHECK(!IsCurrentlyStreaming());
DCHECK(!streamer_done_);
ScriptStreamer::StartStreaming(
this, streamer_type, document->GetFrame()->GetSettings(), script_state,
document->GetTaskRunner(task_type));
bool success = streamer_ && !streamer_->IsStreamingFinished();
// If we have successfully started streaming, we are required to call the
// callback.
DCHECK_EQ(success, IsCurrentlyStreaming());
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