blob: 2122c8c17b9ca531b99f05646dec2b1b48a8b5fd [file] [log] [blame]
// Copyright 2014 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/modules/cache_storage/cache_storage.h"
#include <utility>
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "services/service_manager/public/cpp/interface_provider.h"
#include "third_party/blink/public/common/cache_storage/cache_storage_utils.h"
#include "third_party/blink/public/mojom/cache_storage/cache_storage.mojom-blink.h"
#include "third_party/blink/public/platform/web_content_settings_client.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/fetch/request.h"
#include "third_party/blink/renderer/core/fetch/response.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/modules/cache_storage/cache_storage_blob_client_list.h"
#include "third_party/blink/renderer/modules/cache_storage/cache_storage_error.h"
#include "third_party/blink/renderer/modules/cache_storage/cache_storage_trace_utils.h"
#include "third_party/blink/renderer/modules/cache_storage/cache_utils.h"
#include "third_party/blink/renderer/modules/service_worker/service_worker_global_scope.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/traced_value.h"
#include "third_party/blink/renderer/platform/network/http_names.h"
namespace mojo {
using blink::mojom::blink::CacheQueryOptions;
using blink::mojom::blink::CacheQueryOptionsPtr;
using blink::mojom::blink::MultiCacheQueryOptions;
using blink::mojom::blink::MultiCacheQueryOptionsPtr;
template <>
struct TypeConverter<MultiCacheQueryOptionsPtr,
const blink::MultiCacheQueryOptions*> {
static MultiCacheQueryOptionsPtr Convert(
const blink::MultiCacheQueryOptions* input) {
CacheQueryOptionsPtr query_options = CacheQueryOptions::New();
query_options->ignore_search = input->ignoreSearch();
query_options->ignore_method = input->ignoreMethod();
query_options->ignore_vary = input->ignoreVary();
MultiCacheQueryOptionsPtr output = MultiCacheQueryOptions::New();
output->query_options = std::move(query_options);
output->cache_name = input->cacheName();
return output;
}
};
} // namespace mojo
namespace blink {
namespace {
bool IsCacheStorageAllowed(ScriptState* script_state) {
ExecutionContext* context = ExecutionContext::From(script_state);
if (auto* document = DynamicTo<Document>(context)) {
LocalFrame* frame = document->GetFrame();
if (!frame)
return false;
if (auto* settings_client = frame->GetContentSettingsClient()) {
// This triggers a sync IPC.
return settings_client->AllowCacheStorage(
WebSecurityOrigin(context->GetSecurityOrigin()));
}
return true;
}
WebContentSettingsClient* content_settings_client =
To<WorkerGlobalScope>(context)->ContentSettingsClient();
if (!content_settings_client)
return true;
// This triggers a sync IPC.
return content_settings_client->AllowCacheStorage(WebSecurityOrigin());
}
} // namespace
ScriptPromise CacheStorage::open(ScriptState* script_state,
const String& cache_name) {
int64_t trace_id = blink::cache_storage::CreateTraceId();
TRACE_EVENT_WITH_FLOW1("CacheStorage", "CacheStorage::Open",
TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_OUT,
"name", CacheStorageTracedValue(cache_name));
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise promise = resolver->Promise();
if (!IsAllowed(script_state)) {
resolver->Reject(
MakeGarbageCollected<DOMException>(DOMExceptionCode::kSecurityError));
return promise;
}
// The context may be destroyed and the mojo connection unbound. However the
// object may live on, reject any requests after the context is destroyed.
if (!cache_storage_remote_) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInvalidStateError));
return promise;
}
ever_used_ = true;
// Make sure to bind the CacheStorage object to keep the mojo interface
// pointer alive during the operation. Otherwise GC might prevent the
// callback from ever being executed.
cache_storage_remote_->Open(
cache_name, trace_id,
WTF::Bind(
[](ScriptPromiseResolver* resolver,
GlobalFetch::ScopedFetcher* fetcher, base::TimeTicks start_time,
int64_t trace_id, mojom::blink::OpenResultPtr result) {
UMA_HISTOGRAM_TIMES("ServiceWorkerCache.CacheStorage.Renderer.Open",
base::TimeTicks::Now() - start_time);
if (!resolver->GetExecutionContext() ||
resolver->GetExecutionContext()->IsContextDestroyed()) {
return;
}
if (result->is_status()) {
TRACE_EVENT_WITH_FLOW1(
"CacheStorage", "CacheStorage::Open::Callback",
TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_IN, "status",
CacheStorageTracedValue(result->get_status()));
switch (result->get_status()) {
case mojom::blink::CacheStorageError::kErrorNotFound:
case mojom::blink::CacheStorageError::kErrorStorage:
resolver->Resolve();
break;
default:
resolver->Reject(
CacheStorageError::CreateException(result->get_status()));
break;
}
} else {
TRACE_EVENT_WITH_FLOW1(
"CacheStorage", "CacheStorage::Open::Callback",
TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_IN, "status",
"success");
// See https://bit.ly/2S0zRAS for task types.
resolver->Resolve(MakeGarbageCollected<Cache>(
fetcher, std::move(result->get_cache()),
resolver->GetExecutionContext()->GetTaskRunner(
blink::TaskType::kMiscPlatformAPI)));
}
},
WrapPersistent(resolver), WrapPersistent(scoped_fetcher_.Get()),
base::TimeTicks::Now(), trace_id));
return promise;
}
ScriptPromise CacheStorage::has(ScriptState* script_state,
const String& cache_name) {
int64_t trace_id = blink::cache_storage::CreateTraceId();
TRACE_EVENT_WITH_FLOW1("CacheStorage", "CacheStorage::Has",
TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_OUT,
"name", CacheStorageTracedValue(cache_name));
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise promise = resolver->Promise();
if (!IsAllowed(script_state)) {
resolver->Reject(
MakeGarbageCollected<DOMException>(DOMExceptionCode::kSecurityError));
return promise;
}
// The context may be destroyed and the mojo connection unbound. However the
// object may live on, reject any requests after the context is destroyed.
if (!cache_storage_remote_) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInvalidStateError));
return promise;
}
ever_used_ = true;
// Make sure to bind the CacheStorage object to keep the mojo interface
// pointer alive during the operation. Otherwise GC might prevent the
// callback from ever being executed.
cache_storage_remote_->Has(
cache_name, trace_id,
WTF::Bind(
[](ScriptPromiseResolver* resolver, base::TimeTicks start_time,
int64_t trace_id, mojom::blink::CacheStorageError result) {
UMA_HISTOGRAM_TIMES("ServiceWorkerCache.CacheStorage.Renderer.Has",
base::TimeTicks::Now() - start_time);
TRACE_EVENT_WITH_FLOW1(
"CacheStorage", "CacheStorage::Has::Callback",
TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_IN, "status",
CacheStorageTracedValue(result));
if (!resolver->GetExecutionContext() ||
resolver->GetExecutionContext()->IsContextDestroyed())
return;
switch (result) {
case mojom::blink::CacheStorageError::kSuccess:
resolver->Resolve(true);
break;
case mojom::blink::CacheStorageError::kErrorNotFound:
resolver->Resolve(false);
break;
default:
resolver->Reject(CacheStorageError::CreateException(result));
break;
}
},
WrapPersistent(resolver), base::TimeTicks::Now(), trace_id));
return promise;
}
ScriptPromise CacheStorage::Delete(ScriptState* script_state,
const String& cache_name) {
int64_t trace_id = blink::cache_storage::CreateTraceId();
TRACE_EVENT_WITH_FLOW1("CacheStorage", "CacheStorage::Delete",
TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_OUT,
"name", CacheStorageTracedValue(cache_name));
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise promise = resolver->Promise();
if (!IsAllowed(script_state)) {
resolver->Reject(
MakeGarbageCollected<DOMException>(DOMExceptionCode::kSecurityError));
return promise;
}
// The context may be destroyed and the mojo connection unbound. However the
// object may live on, reject any requests after the context is destroyed.
if (!cache_storage_remote_) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInvalidStateError));
return promise;
}
ever_used_ = true;
// Make sure to bind the CacheStorage object to keep the mojo interface
// pointer alive during the operation. Otherwise GC might prevent the
// callback from ever being executed.
cache_storage_remote_->Delete(
cache_name, trace_id,
WTF::Bind(
[](ScriptPromiseResolver* resolver, base::TimeTicks start_time,
int64_t trace_id, mojom::blink::CacheStorageError result) {
UMA_HISTOGRAM_TIMES(
"ServiceWorkerCache.CacheStorage.Renderer.Delete",
base::TimeTicks::Now() - start_time);
TRACE_EVENT_WITH_FLOW1(
"CacheStorage", "CacheStorage::Delete::Callback",
TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_IN, "status",
CacheStorageTracedValue(result));
if (!resolver->GetExecutionContext() ||
resolver->GetExecutionContext()->IsContextDestroyed())
return;
switch (result) {
case mojom::blink::CacheStorageError::kSuccess:
resolver->Resolve(true);
break;
case mojom::blink::CacheStorageError::kErrorStorage:
case mojom::blink::CacheStorageError::kErrorNotFound:
resolver->Resolve(false);
break;
default:
resolver->Reject(CacheStorageError::CreateException(result));
break;
}
},
WrapPersistent(resolver), base::TimeTicks::Now(), trace_id));
return promise;
}
ScriptPromise CacheStorage::keys(ScriptState* script_state) {
int64_t trace_id = blink::cache_storage::CreateTraceId();
TRACE_EVENT_WITH_FLOW0("CacheStorage", "CacheStorage::Keys",
TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_OUT);
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise promise = resolver->Promise();
if (!IsAllowed(script_state)) {
resolver->Reject(
MakeGarbageCollected<DOMException>(DOMExceptionCode::kSecurityError));
return promise;
}
// The context may be destroyed and the mojo connection unbound. However the
// object may live on, reject any requests after the context is destroyed.
if (!cache_storage_remote_) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInvalidStateError));
return promise;
}
ever_used_ = true;
// Make sure to bind the CacheStorage object to keep the mojo interface
// pointer alive during the operation. Otherwise GC might prevent the
// callback from ever being executed.
cache_storage_remote_->Keys(
trace_id,
WTF::Bind(
[](ScriptPromiseResolver* resolver, base::TimeTicks start_time,
int64_t trace_id, const Vector<String>& keys) {
UMA_HISTOGRAM_TIMES("ServiceWorkerCache.CacheStorage.Renderer.Keys",
base::TimeTicks::Now() - start_time);
TRACE_EVENT_WITH_FLOW1(
"CacheStorage", "CacheStorage::Keys::Callback",
TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_IN, "key_list",
CacheStorageTracedValue(keys));
if (!resolver->GetExecutionContext() ||
resolver->GetExecutionContext()->IsContextDestroyed())
return;
resolver->Resolve(keys);
},
WrapPersistent(resolver), base::TimeTicks::Now(), trace_id));
return promise;
}
ScriptPromise CacheStorage::match(ScriptState* script_state,
const RequestInfo& request,
const MultiCacheQueryOptions* options,
ExceptionState& exception_state) {
DCHECK(!request.IsNull());
if (request.IsRequest())
return MatchImpl(script_state, request.GetAsRequest(), options);
Request* new_request =
Request::Create(script_state, request.GetAsUSVString(), exception_state);
if (exception_state.HadException())
return ScriptPromise();
return MatchImpl(script_state, new_request, options);
}
ScriptPromise CacheStorage::MatchImpl(ScriptState* script_state,
const Request* request,
const MultiCacheQueryOptions* options) {
int64_t trace_id = blink::cache_storage::CreateTraceId();
mojom::blink::FetchAPIRequestPtr mojo_request =
request->CreateFetchAPIRequest();
mojom::blink::MultiCacheQueryOptionsPtr mojo_options =
mojom::blink::MultiCacheQueryOptions::From(options);
ExecutionContext* context = ExecutionContext::From(script_state);
bool in_related_fetch_event = false;
if (auto* global_scope = DynamicTo<ServiceWorkerGlobalScope>(context))
in_related_fetch_event = global_scope->HasRelatedFetchEvent(request->url());
TRACE_EVENT_WITH_FLOW2("CacheStorage", "CacheStorage::MatchImpl",
TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_OUT,
"request", CacheStorageTracedValue(mojo_request),
"options", CacheStorageTracedValue(mojo_options));
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
const ScriptPromise promise = resolver->Promise();
if (!IsAllowed(script_state)) {
resolver->Reject(
MakeGarbageCollected<DOMException>(DOMExceptionCode::kSecurityError));
return promise;
}
// The context may be destroyed and the mojo connection unbound. However the
// object may live on, reject any requests after the context is destroyed.
if (!cache_storage_remote_) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInvalidStateError));
return promise;
}
if (request->method() != http_names::kGET && !options->ignoreMethod()) {
resolver->Resolve();
return promise;
}
ever_used_ = true;
// Make sure to bind the CacheStorage object to keep the mojo interface
// pointer alive during the operation. Otherwise GC might prevent the
// callback from ever being executed.
cache_storage_remote_->Match(
std::move(mojo_request), std::move(mojo_options), in_related_fetch_event,
trace_id,
WTF::Bind(
[](ScriptPromiseResolver* resolver, base::TimeTicks start_time,
const MultiCacheQueryOptions* options, int64_t trace_id,
CacheStorage* self, mojom::blink::MatchResultPtr result) {
base::TimeDelta elapsed = base::TimeTicks::Now() - start_time;
if (!options->hasCacheName() || options->cacheName().IsEmpty()) {
UMA_HISTOGRAM_LONG_TIMES(
"ServiceWorkerCache.CacheStorage.Renderer.MatchAllCaches",
elapsed);
} else {
UMA_HISTOGRAM_LONG_TIMES(
"ServiceWorkerCache.CacheStorage.Renderer.MatchOneCache",
elapsed);
}
if (!resolver->GetExecutionContext() ||
resolver->GetExecutionContext()->IsContextDestroyed())
return;
if (result->is_status()) {
TRACE_EVENT_WITH_FLOW1(
"CacheStorage", "CacheStorage::MatchImpl::Callback",
TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_IN, "status",
CacheStorageTracedValue(result->get_status()));
switch (result->get_status()) {
case mojom::CacheStorageError::kErrorNotFound:
case mojom::CacheStorageError::kErrorStorage:
case mojom::CacheStorageError::kErrorCacheNameNotFound:
resolver->Resolve();
break;
default:
resolver->Reject(
CacheStorageError::CreateException(result->get_status()));
break;
}
} else {
ScriptState::Scope scope(resolver->GetScriptState());
if (result->is_eager_response()) {
TRACE_EVENT_WITH_FLOW1(
"CacheStorage", "CacheStorage::MatchImpl::Callback",
TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_IN,
"eager_response",
CacheStorageTracedValue(
result->get_eager_response()->response));
resolver->Resolve(
CreateEagerResponse(resolver->GetScriptState(),
std::move(result->get_eager_response()),
self->blob_client_list_));
} else {
TRACE_EVENT_WITH_FLOW1(
"CacheStorage", "CacheStorage::MatchImpl::Callback",
TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_IN,
"response",
CacheStorageTracedValue(result->get_response()));
resolver->Resolve(Response::Create(resolver->GetScriptState(),
*result->get_response()));
}
}
},
WrapPersistent(resolver), base::TimeTicks::Now(),
WrapPersistent(options), trace_id, WrapPersistent(this)));
return promise;
}
CacheStorage::CacheStorage(ExecutionContext* context,
GlobalFetch::ScopedFetcher* fetcher)
: ContextLifecycleObserver(context),
scoped_fetcher_(fetcher),
blob_client_list_(MakeGarbageCollected<CacheStorageBlobClientList>()),
ever_used_(false) {
// See https://bit.ly/2S0zRAS for task types.
scoped_refptr<base::SingleThreadTaskRunner> task_runner =
context->GetTaskRunner(blink::TaskType::kMiscPlatformAPI);
// Service workers may already have a CacheStoragePtr provided as an
// optimization.
if (auto* service_worker = DynamicTo<ServiceWorkerGlobalScope>(context)) {
mojo::PendingRemote<mojom::blink::CacheStorage> info =
service_worker->TakeCacheStorage();
if (info) {
cache_storage_remote_ = mojo::Remote<mojom::blink::CacheStorage>(
std::move(info), task_runner);
return;
}
}
context->GetInterfaceProvider()->GetInterface(
cache_storage_remote_.BindNewPipeAndPassReceiver(task_runner));
}
CacheStorage::~CacheStorage() = default;
bool CacheStorage::HasPendingActivity() const {
// Once the CacheStorage has been used once we keep it alive until the
// context goes away. This allows us to use the existence of this
// context as a hint to optimizations such as keeping backend disk_caches
// open in the browser process.
//
// Note, this also keeps the CacheStorage alive during active Cache and
// CacheStorage operations.
return ever_used_;
}
void CacheStorage::Trace(blink::Visitor* visitor) {
visitor->Trace(scoped_fetcher_);
visitor->Trace(blob_client_list_);
ScriptWrappable::Trace(visitor);
ContextLifecycleObserver::Trace(visitor);
}
bool CacheStorage::IsAllowed(ScriptState* script_state) {
if (!allowed_.has_value()) {
// Cache the IsCacheStorageAllowed() because it triggers a sync IPC.
allowed_.emplace(IsCacheStorageAllowed(script_state));
}
return allowed_.value();
}
void CacheStorage::ContextDestroyed(ExecutionContext*) {
cache_storage_remote_.reset();
}
} // namespace blink