blob: e38c594abad197eb61122b9f9a5389174e73807b [file] [log] [blame]
/*
Copyright (C) 1998 Lars Knoll (knoll@mpi-hd.mpg.de)
Copyright (C) 2001 Dirk Mueller (mueller@kde.org)
Copyright (C) 2002 Waldo Bastian (bastian@kde.org)
Copyright (C) 2006 Samuel Weinig (sam.weinig@gmail.com)
Copyright (C) 2004, 2005, 2006, 2007, 2008 Apple Inc. All rights reserved.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
This class provides all functionality needed for loading images, style
sheets and html pages from the web. It has a memory cache for these objects.
*/
#include "third_party/blink/renderer/core/loader/resource/script_resource.h"
#include <utility>
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-blink.h"
#include "third_party/blink/public/mojom/loader/code_cache.mojom-blink.h"
#include "third_party/blink/public/mojom/loader/request_context_frame_type.mojom-blink.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/loader/subresource_integrity_helper.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/web_memory_allocator_dump.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/web_process_memory_dump.h"
#include "third_party/blink/renderer/platform/loader/fetch/cached_metadata.h"
#include "third_party/blink/renderer/platform/loader/fetch/data_pipe_bytes_consumer.h"
#include "third_party/blink/renderer/platform/loader/fetch/fetch_parameters.h"
#include "third_party/blink/renderer/platform/loader/fetch/integrity_metadata.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_client_walker.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_loader.h"
#include "third_party/blink/renderer/platform/loader/fetch/response_body_loader.h"
#include "third_party/blink/renderer/platform/loader/fetch/script_cached_metadata_handler.h"
#include "third_party/blink/renderer/platform/loader/fetch/text_resource_decoder_options.h"
#include "third_party/blink/renderer/platform/loader/subresource_integrity.h"
#include "third_party/blink/renderer/platform/network/mime/mime_type_registry.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
#include "third_party/blink/renderer/platform/wtf/shared_buffer.h"
namespace blink {
namespace {
// Returns true if the given request context is a script-like destination
// defined in the Fetch spec:
// https://fetch.spec.whatwg.org/#request-destination-script-like
bool IsRequestContextSupported(
mojom::blink::RequestContextType request_context) {
// TODO(nhiroki): Support "audioworklet" and "paintworklet" destinations.
switch (request_context) {
case mojom::blink::RequestContextType::SCRIPT:
case mojom::blink::RequestContextType::WORKER:
case mojom::blink::RequestContextType::SERVICE_WORKER:
case mojom::blink::RequestContextType::SHARED_WORKER:
return true;
default:
break;
}
NOTREACHED() << "Incompatible request context type: " << request_context;
return false;
}
} // namespace
ScriptResource* ScriptResource::Fetch(FetchParameters& params,
ResourceFetcher* fetcher,
ResourceClient* client,
StreamingAllowed streaming_allowed) {
DCHECK(IsRequestContextSupported(
params.GetResourceRequest().GetRequestContext()));
auto* resource = To<ScriptResource>(fetcher->RequestResource(
params, ScriptResourceFactory(streaming_allowed, params.GetScriptType()),
client));
return resource;
}
ScriptResource* ScriptResource::CreateForTest(
const KURL& url,
const WTF::TextEncoding& encoding,
mojom::blink::ScriptType script_type) {
ResourceRequest request(url);
request.SetCredentialsMode(network::mojom::CredentialsMode::kOmit);
ResourceLoaderOptions options(nullptr /* world */);
TextResourceDecoderOptions decoder_options(
TextResourceDecoderOptions::kPlainTextContent, encoding);
return MakeGarbageCollected<ScriptResource>(request, options, decoder_options,
kNoStreaming, script_type);
}
ScriptResource::ScriptResource(
const ResourceRequest& resource_request,
const ResourceLoaderOptions& options,
const TextResourceDecoderOptions& decoder_options,
StreamingAllowed streaming_allowed,
mojom::blink::ScriptType script_type)
: TextResource(resource_request,
ResourceType::kScript,
options,
decoder_options),
script_type_(script_type) {
static bool script_streaming_enabled =
base::FeatureList::IsEnabled(features::kScriptStreaming);
if (!script_streaming_enabled) {
DisableStreaming(
ScriptStreamer::NotStreamingReason::kDisabledByFeatureList);
} else if (streaming_allowed == kNoStreaming) {
DisableStreaming(ScriptStreamer::NotStreamingReason::kStreamingDisabled);
} else if (!Url().ProtocolIsInHTTPFamily()) {
DisableStreaming(ScriptStreamer::NotStreamingReason::kNotHTTP);
}
}
ScriptResource::~ScriptResource() = default;
void ScriptResource::Trace(Visitor* visitor) const {
visitor->Trace(streamer_);
visitor->Trace(cached_metadata_handler_);
TextResource::Trace(visitor);
}
Resource::MatchStatus ScriptResource::CanReuse(
const FetchParameters& params) const {
if (script_type_ != params.GetScriptType())
return Resource::MatchStatus::kScriptTypeDoesNotMatch;
return Resource::CanReuse(params);
}
void ScriptResource::OnMemoryDump(WebMemoryDumpLevelOfDetail level_of_detail,
WebProcessMemoryDump* memory_dump) const {
Resource::OnMemoryDump(level_of_detail, memory_dump);
{
const String name = GetMemoryDumpName() + "/decoded_script";
source_text_.OnMemoryDump(memory_dump, name);
}
if (cached_metadata_handler_) {
const String name = GetMemoryDumpName() + "/code_cache";
cached_metadata_handler_->OnMemoryDump(memory_dump, name);
}
}
const ParkableString& ScriptResource::SourceText() {
CHECK(IsLoaded());
if (source_text_.IsNull() && Data()) {
String source_text = DecodedText();
ClearData();
SetDecodedSize(source_text.CharactersSizeInBytes());
source_text_ = ParkableString(source_text.ReleaseImpl());
}
return source_text_;
}
String ScriptResource::TextForInspector() const {
// If the resource buffer exists, we can safely return the decoded text.
if (ResourceBuffer())
return DecodedText();
// If there is no resource buffer, then we have three cases.
// TODO(crbug.com/865098): Simplify the below code and remove the CHECKs once
// the assumptions are confirmed.
if (IsLoaded()) {
if (!source_text_.IsNull()) {
// 1. We have finished loading, and have already decoded the buffer into
// the source text and cleared the resource buffer to save space.
return source_text_.ToString();
}
// 2. We have finished loading with no data received, so no streaming ever
// happened or streaming was suppressed.
DCHECK(!streamer_ ||
streamer_->StreamingSuppressedReason() ==
ScriptStreamer::NotStreamingReason::kScriptTooSmall);
return "";
}
// 3. We haven't started loading, and actually haven't received any data yet
// at all to initialise the resource buffer, so the resource is empty.
return "";
}
SingleCachedMetadataHandler* ScriptResource::CacheHandler() {
return cached_metadata_handler_;
}
void ScriptResource::SetSerializedCachedMetadata(mojo_base::BigBuffer data) {
// Resource ignores the cached metadata.
Resource::SetSerializedCachedMetadata(mojo_base::BigBuffer());
if (cached_metadata_handler_) {
cached_metadata_handler_->SetSerializedCachedMetadata(std::move(data));
}
}
void ScriptResource::DestroyDecodedDataIfPossible() {
if (cached_metadata_handler_) {
cached_metadata_handler_->ClearCachedMetadata(
CachedMetadataHandler::kClearLocally);
}
}
void ScriptResource::DestroyDecodedDataForFailedRevalidation() {
source_text_ = ParkableString();
// Make sure there's no streaming.
DCHECK(!streamer_);
DCHECK_EQ(streaming_state_, StreamingState::kStreamingDisabled);
SetDecodedSize(0);
cached_metadata_handler_ = nullptr;
}
void ScriptResource::SetRevalidatingRequest(
const ResourceRequestHead& request) {
CHECK(IsLoaded());
if (streamer_) {
CHECK(streamer_->IsFinished());
streamer_ = nullptr;
}
// Revalidation requests don't actually load the current Resource, so disable
// streaming.
DisableStreaming(ScriptStreamer::NotStreamingReason::kRevalidate);
TextResource::SetRevalidatingRequest(request);
}
bool ScriptResource::CanUseCacheValidator() const {
// Do not revalidate until ClassicPendingScript is removed, i.e. the script
// content is retrieved in ScriptLoader::ExecuteScriptBlock().
// crbug.com/692856
if (HasClientsOrObservers())
return false;
// Do not revalidate until streaming is complete.
if (!IsLoaded())
return false;
return Resource::CanUseCacheValidator();
}
size_t ScriptResource::CodeCacheSize() const {
return cached_metadata_handler_ ? cached_metadata_handler_->GetCodeCacheSize()
: 0;
}
void ScriptResource::ResponseReceived(const ResourceResponse& response) {
const bool is_successful_revalidation =
IsSuccessfulRevalidationResponse(response);
Resource::ResponseReceived(response);
if (is_successful_revalidation) {
return;
}
cached_metadata_handler_ = nullptr;
// Currently we support the metadata caching only for HTTP family.
if (GetResourceRequest().Url().ProtocolIsInHTTPFamily() &&
response.CurrentRequestUrl().ProtocolIsInHTTPFamily()) {
cached_metadata_handler_ =
MakeGarbageCollected<ScriptCachedMetadataHandler>(
Encoding(), CachedMetadataSender::Create(
response, mojom::blink::CodeCacheType::kJavascript,
GetResourceRequest().RequestorOrigin()));
}
} // namespace blink
void ScriptResource::ResponseBodyReceived(
ResponseBodyLoaderDrainableInterface& body_loader,
scoped_refptr<base::SingleThreadTaskRunner> loader_task_runner) {
if (streaming_state_ == StreamingState::kStreamingDisabled)
return;
CHECK_EQ(streaming_state_, StreamingState::kWaitingForDataPipe);
// Checked in the constructor.
CHECK(Url().ProtocolIsInHTTPFamily());
CHECK(base::FeatureList::IsEnabled(features::kScriptStreaming));
ResponseBodyLoaderClient* response_body_loader_client;
mojo::ScopedDataPipeConsumerHandle data_pipe =
body_loader.DrainAsDataPipe(&response_body_loader_client);
if (!data_pipe) {
DisableStreaming(ScriptStreamer::NotStreamingReason::kNoDataPipe);
return;
}
CheckStreamingState();
CHECK(!ErrorOccurred());
streamer_ = MakeGarbageCollected<ScriptStreamer>(this, std::move(data_pipe),
response_body_loader_client,
loader_task_runner);
CHECK_EQ(no_streamer_reason_, ScriptStreamer::NotStreamingReason::kInvalid);
AdvanceStreamingState(StreamingState::kStreaming);
}
void ScriptResource::NotifyFinished() {
DCHECK(IsLoaded());
switch (streaming_state_) {
case StreamingState::kWaitingForDataPipe:
// We never received a response body, otherwise the state would be
// one of kStreaming or kNoStreaming. So, either there was an error, or
// there was no response body loader (thus no data pipe) at all. Either
// way, we want to disable streaming.
if (ErrorOccurred()) {
DisableStreaming(ScriptStreamer::NotStreamingReason::kErrorOccurred);
} else {
DisableStreaming(ScriptStreamer::NotStreamingReason::kNoDataPipe);
}
break;
case StreamingState::kStreaming:
DCHECK(streamer_);
if (!streamer_->IsFinished()) {
// This notification didn't come from the streaming finishing, so it
// must be an external error (e.g. cancelling the resource).
CHECK(ErrorOccurred());
streamer_->Cancel();
streamer_.Release();
DisableStreaming(ScriptStreamer::NotStreamingReason::kErrorOccurred);
}
break;
case StreamingState::kStreamingDisabled:
// If streaming is already disabled, we can just continue as before.
break;
}
CheckStreamingState();
TextResource::NotifyFinished();
}
ScriptStreamer* ScriptResource::TakeStreamer() {
CHECK(IsLoaded());
if (!streamer_)
return nullptr;
ScriptStreamer* streamer = streamer_;
// A second use of the streamer is not possible, so we null it out and disable
// streaming for subsequent uses.
streamer_ = nullptr;
DisableStreaming(
ScriptStreamer::NotStreamingReason::kSecondScriptResourceUse);
return streamer;
}
void ScriptResource::DisableStreaming(
ScriptStreamer::NotStreamingReason no_streamer_reason) {
CHECK_NE(no_streamer_reason, ScriptStreamer::NotStreamingReason::kInvalid);
if (no_streamer_reason_ != ScriptStreamer::NotStreamingReason::kInvalid) {
// Streaming is already disabled, no need to disable it again.
return;
}
no_streamer_reason_ = no_streamer_reason;
AdvanceStreamingState(StreamingState::kStreamingDisabled);
}
void ScriptResource::AdvanceStreamingState(StreamingState new_state) {
switch (streaming_state_) {
case StreamingState::kWaitingForDataPipe:
CHECK(new_state == StreamingState::kStreaming ||
new_state == StreamingState::kStreamingDisabled);
break;
case StreamingState::kStreaming:
CHECK_EQ(new_state, StreamingState::kStreamingDisabled);
break;
case StreamingState::kStreamingDisabled:
CHECK(false);
break;
}
streaming_state_ = new_state;
CheckStreamingState();
}
void ScriptResource::CheckStreamingState() const {
// TODO(leszeks): Eventually convert these CHECKs into DCHECKs once the logic
// is a bit more baked in.
switch (streaming_state_) {
case StreamingState::kWaitingForDataPipe:
CHECK(!streamer_);
CHECK_EQ(no_streamer_reason_,
ScriptStreamer::NotStreamingReason::kInvalid);
break;
case StreamingState::kStreaming:
CHECK(streamer_);
CHECK(streamer_->CanStartStreaming() || streamer_->IsStreamingStarted() ||
streamer_->IsStreamingSuppressed());
CHECK(IsLoading() || streamer_->IsFinished());
break;
case StreamingState::kStreamingDisabled:
CHECK(!streamer_);
CHECK_NE(no_streamer_reason_,
ScriptStreamer::NotStreamingReason::kInvalid);
break;
}
}
} // namespace blink