blob: 1115f90f6da340c4e3963dcec7aa6c7f2a88d9e2 [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 "modules/cachestorage/Cache.h"
#include <memory>
#include <utility>
#include "bindings/core/v8/CallbackPromiseAdapter.h"
#include "bindings/core/v8/ExceptionState.h"
#include "bindings/core/v8/IDLTypes.h"
#include "bindings/core/v8/NativeValueTraitsImpl.h"
#include "bindings/core/v8/ScriptPromiseResolver.h"
#include "bindings/core/v8/V8BindingForCore.h"
#include "bindings/modules/v8/V8Response.h"
#include "core/dom/DOMException.h"
#include "core/inspector/ConsoleMessage.h"
#include "modules/cachestorage/CacheStorageError.h"
#include "modules/fetch/BodyStreamBuffer.h"
#include "modules/fetch/FetchDataLoader.h"
#include "modules/fetch/GlobalFetch.h"
#include "modules/fetch/Request.h"
#include "modules/fetch/Response.h"
#include "platform/HTTPNames.h"
#include "platform/Histogram.h"
#include "platform/bindings/ScriptState.h"
#include "platform/bindings/V8ThrowException.h"
#include "public/platform/modules/serviceworker/WebServiceWorkerCache.h"
namespace blink {
namespace {
// FIXME: Consider using CallbackPromiseAdapter.
class CacheMatchCallbacks : public WebServiceWorkerCache::CacheMatchCallbacks {
WTF_MAKE_NONCOPYABLE(CacheMatchCallbacks);
public:
explicit CacheMatchCallbacks(ScriptPromiseResolver* resolver)
: resolver_(resolver) {}
void OnSuccess(const WebServiceWorkerResponse& web_response) override {
if (!resolver_->GetExecutionContext() ||
resolver_->GetExecutionContext()->IsContextDestroyed())
return;
ScriptState::Scope scope(resolver_->GetScriptState());
resolver_->Resolve(
Response::Create(resolver_->GetScriptState(), web_response));
resolver_.Clear();
}
void OnError(WebServiceWorkerCacheError reason) override {
if (!resolver_->GetExecutionContext() ||
resolver_->GetExecutionContext()->IsContextDestroyed())
return;
if (reason == kWebServiceWorkerCacheErrorNotFound)
resolver_->Resolve();
else
resolver_->Reject(CacheStorageError::CreateException(reason));
resolver_.Clear();
}
private:
Persistent<ScriptPromiseResolver> resolver_;
};
// FIXME: Consider using CallbackPromiseAdapter.
class CacheWithResponsesCallbacks
: public WebServiceWorkerCache::CacheWithResponsesCallbacks {
WTF_MAKE_NONCOPYABLE(CacheWithResponsesCallbacks);
public:
explicit CacheWithResponsesCallbacks(ScriptPromiseResolver* resolver)
: resolver_(resolver) {}
void OnSuccess(
const WebVector<WebServiceWorkerResponse>& web_responses) override {
if (!resolver_->GetExecutionContext() ||
resolver_->GetExecutionContext()->IsContextDestroyed())
return;
ScriptState::Scope scope(resolver_->GetScriptState());
HeapVector<Member<Response>> responses;
for (size_t i = 0; i < web_responses.size(); ++i)
responses.push_back(
Response::Create(resolver_->GetScriptState(), web_responses[i]));
resolver_->Resolve(responses);
resolver_.Clear();
}
void OnError(WebServiceWorkerCacheError reason) override {
if (!resolver_->GetExecutionContext() ||
resolver_->GetExecutionContext()->IsContextDestroyed())
return;
resolver_->Reject(CacheStorageError::CreateException(reason));
resolver_.Clear();
}
protected:
Persistent<ScriptPromiseResolver> resolver_;
};
// FIXME: Consider using CallbackPromiseAdapter.
class CacheDeleteCallback : public WebServiceWorkerCache::CacheBatchCallbacks {
WTF_MAKE_NONCOPYABLE(CacheDeleteCallback);
public:
explicit CacheDeleteCallback(ScriptPromiseResolver* resolver)
: resolver_(resolver) {}
void OnSuccess() override {
if (!resolver_->GetExecutionContext() ||
resolver_->GetExecutionContext()->IsContextDestroyed())
return;
resolver_->Resolve(true);
resolver_.Clear();
}
void OnError(WebServiceWorkerCacheError reason) override {
if (!resolver_->GetExecutionContext() ||
resolver_->GetExecutionContext()->IsContextDestroyed())
return;
if (reason == kWebServiceWorkerCacheErrorNotFound)
resolver_->Resolve(false);
else
resolver_->Reject(CacheStorageError::CreateException(reason));
resolver_.Clear();
}
private:
Persistent<ScriptPromiseResolver> resolver_;
};
// FIXME: Consider using CallbackPromiseAdapter.
class CacheWithRequestsCallbacks
: public WebServiceWorkerCache::CacheWithRequestsCallbacks {
WTF_MAKE_NONCOPYABLE(CacheWithRequestsCallbacks);
public:
explicit CacheWithRequestsCallbacks(ScriptPromiseResolver* resolver)
: resolver_(resolver) {}
void OnSuccess(
const WebVector<WebServiceWorkerRequest>& web_requests) override {
if (!resolver_->GetExecutionContext() ||
resolver_->GetExecutionContext()->IsContextDestroyed())
return;
ScriptState::Scope scope(resolver_->GetScriptState());
HeapVector<Member<Request>> requests;
for (size_t i = 0; i < web_requests.size(); ++i)
requests.push_back(
Request::Create(resolver_->GetScriptState(), web_requests[i]));
resolver_->Resolve(requests);
resolver_.Clear();
}
void OnError(WebServiceWorkerCacheError reason) override {
if (!resolver_->GetExecutionContext() ||
resolver_->GetExecutionContext()->IsContextDestroyed())
return;
resolver_->Reject(CacheStorageError::CreateException(reason));
resolver_.Clear();
}
private:
Persistent<ScriptPromiseResolver> resolver_;
};
// Used for UMA. Append only.
enum class ResponseType {
kBasicType,
kCORSType,
kDefaultType,
kErrorType,
kOpaqueType,
kOpaqueRedirectType,
kEnumMax,
};
void RecordResponseTypeForAdd(const Member<Response>& response) {
ResponseType type = ResponseType::kEnumMax;
switch (response->GetResponse()->GetType()) {
case FetchResponseData::kBasicType:
type = ResponseType::kBasicType;
break;
case FetchResponseData::kCORSType:
type = ResponseType::kCORSType;
break;
case FetchResponseData::kDefaultType:
type = ResponseType::kDefaultType;
break;
case FetchResponseData::kErrorType:
type = ResponseType::kErrorType;
break;
case FetchResponseData::kOpaqueType:
type = ResponseType::kOpaqueType;
break;
case FetchResponseData::kOpaqueRedirectType:
type = ResponseType::kOpaqueRedirectType;
break;
}
DEFINE_THREAD_SAFE_STATIC_LOCAL(EnumerationHistogram, response_type_histogram,
("ServiceWorkerCache.Cache.AddResponseType",
static_cast<int>(ResponseType::kEnumMax)));
response_type_histogram.Count(static_cast<int>(type));
};
bool VaryHeaderContainsAsterisk(const Response* response) {
const FetchHeaderList* headers = response->headers()->HeaderList();
String varyHeader;
if (headers->Get("vary", varyHeader)) {
Vector<String> fields;
varyHeader.Split(',', fields);
return std::any_of(fields.begin(), fields.end(), [](const String& field) {
return field.StripWhiteSpace() == "*";
});
}
return false;
}
} // namespace
// TODO(nhiroki): Unfortunately, we have to go through V8 to wait for the fetch
// promise. It should be better to achieve this only within C++ world.
class Cache::FetchResolvedForAdd final : public ScriptFunction {
public:
static v8::Local<v8::Function> Create(
ScriptState* script_state,
Cache* cache,
const HeapVector<Member<Request>>& requests) {
FetchResolvedForAdd* self =
new FetchResolvedForAdd(script_state, cache, requests);
return self->BindToV8Function();
}
ScriptValue Call(ScriptValue value) override {
NonThrowableExceptionState exception_state;
HeapVector<Member<Response>> responses =
NativeValueTraits<IDLSequence<Response>>::NativeValue(
GetScriptState()->GetIsolate(), value.V8Value(), exception_state);
for (const auto& response : responses) {
if (!response->ok()) {
ScriptPromise rejection = ScriptPromise::Reject(
GetScriptState(),
V8ThrowException::CreateTypeError(GetScriptState()->GetIsolate(),
"Request failed"));
return ScriptValue(GetScriptState(), rejection.V8Value());
}
if (VaryHeaderContainsAsterisk(response)) {
ScriptPromise rejection = ScriptPromise::Reject(
GetScriptState(),
V8ThrowException::CreateTypeError(GetScriptState()->GetIsolate(),
"Vary header contains *"));
return ScriptValue(GetScriptState(), rejection.V8Value());
}
}
for (const auto& response : responses)
RecordResponseTypeForAdd(response);
ScriptPromise put_promise =
cache_->PutImpl(GetScriptState(), requests_, responses);
return ScriptValue(GetScriptState(), put_promise.V8Value());
}
DEFINE_INLINE_VIRTUAL_TRACE() {
visitor->Trace(cache_);
visitor->Trace(requests_);
ScriptFunction::Trace(visitor);
}
private:
FetchResolvedForAdd(ScriptState* script_state,
Cache* cache,
const HeapVector<Member<Request>>& requests)
: ScriptFunction(script_state), cache_(cache), requests_(requests) {}
Member<Cache> cache_;
HeapVector<Member<Request>> requests_;
};
class Cache::BarrierCallbackForPut final
: public GarbageCollectedFinalized<BarrierCallbackForPut> {
public:
BarrierCallbackForPut(int number_of_operations,
Cache* cache,
ScriptPromiseResolver* resolver)
: number_of_remaining_operations_(number_of_operations),
cache_(cache),
resolver_(resolver) {
DCHECK_LT(0, number_of_remaining_operations_);
batch_operations_.resize(number_of_operations);
}
void OnSuccess(size_t index,
const WebServiceWorkerCache::BatchOperation& batch_operation) {
DCHECK_LT(index, batch_operations_.size());
if (completed_)
return;
if (!resolver_->GetExecutionContext() ||
resolver_->GetExecutionContext()->IsContextDestroyed())
return;
batch_operations_[index] = batch_operation;
if (--number_of_remaining_operations_ != 0)
return;
cache_->WebCache()->DispatchBatch(
WTF::MakeUnique<CallbackPromiseAdapter<void, CacheStorageError>>(
resolver_),
batch_operations_);
}
void OnError(const String& error_message) {
if (completed_)
return;
completed_ = true;
if (!resolver_->GetExecutionContext() ||
resolver_->GetExecutionContext()->IsContextDestroyed())
return;
ScriptState* state = resolver_->GetScriptState();
ScriptState::Scope scope(state);
resolver_->Reject(
V8ThrowException::CreateTypeError(state->GetIsolate(), error_message));
}
DEFINE_INLINE_VIRTUAL_TRACE() {
visitor->Trace(cache_);
visitor->Trace(resolver_);
}
private:
bool completed_ = false;
int number_of_remaining_operations_;
Member<Cache> cache_;
Member<ScriptPromiseResolver> resolver_;
Vector<WebServiceWorkerCache::BatchOperation> batch_operations_;
};
class Cache::BlobHandleCallbackForPut final
: public GarbageCollectedFinalized<BlobHandleCallbackForPut>,
public FetchDataLoader::Client {
USING_GARBAGE_COLLECTED_MIXIN(BlobHandleCallbackForPut);
public:
BlobHandleCallbackForPut(size_t index,
BarrierCallbackForPut* barrier_callback,
Request* request,
Response* response)
: index_(index), barrier_callback_(barrier_callback) {
request->PopulateWebServiceWorkerRequest(web_request_);
response->PopulateWebServiceWorkerResponse(web_response_);
}
~BlobHandleCallbackForPut() override {}
void DidFetchDataLoadedBlobHandle(
PassRefPtr<BlobDataHandle> handle) override {
WebServiceWorkerCache::BatchOperation batch_operation;
batch_operation.operation_type = WebServiceWorkerCache::kOperationTypePut;
batch_operation.request = web_request_;
batch_operation.response = web_response_;
batch_operation.response.SetBlobDataHandle(std::move(handle));
barrier_callback_->OnSuccess(index_, batch_operation);
}
void DidFetchDataLoadFailed() override {
barrier_callback_->OnError("network error");
}
DEFINE_INLINE_VIRTUAL_TRACE() {
visitor->Trace(barrier_callback_);
FetchDataLoader::Client::Trace(visitor);
}
private:
const size_t index_;
Member<BarrierCallbackForPut> barrier_callback_;
WebServiceWorkerRequest web_request_;
WebServiceWorkerResponse web_response_;
};
Cache* Cache::Create(GlobalFetch::ScopedFetcher* fetcher,
std::unique_ptr<WebServiceWorkerCache> web_cache) {
return new Cache(fetcher, std::move(web_cache));
}
ScriptPromise Cache::match(ScriptState* script_state,
const RequestInfo& request,
const CacheQueryOptions& 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 Cache::matchAll(ScriptState* script_state,
ExceptionState& exception_state) {
return MatchAllImpl(script_state);
}
ScriptPromise Cache::matchAll(ScriptState* script_state,
const RequestInfo& request,
const CacheQueryOptions& options,
ExceptionState& exception_state) {
DCHECK(!request.isNull());
if (request.isRequest())
return MatchAllImpl(script_state, request.getAsRequest(), options);
Request* new_request =
Request::Create(script_state, request.getAsUSVString(), exception_state);
if (exception_state.HadException())
return ScriptPromise();
return MatchAllImpl(script_state, new_request, options);
}
ScriptPromise Cache::add(ScriptState* script_state,
const RequestInfo& request,
ExceptionState& exception_state) {
DCHECK(!request.isNull());
HeapVector<Member<Request>> requests;
if (request.isRequest()) {
requests.push_back(request.getAsRequest());
} else {
requests.push_back(Request::Create(script_state, request.getAsUSVString(),
exception_state));
if (exception_state.HadException())
return ScriptPromise();
}
return AddAllImpl(script_state, requests, exception_state);
}
ScriptPromise Cache::addAll(ScriptState* script_state,
const HeapVector<RequestInfo>& raw_requests,
ExceptionState& exception_state) {
HeapVector<Member<Request>> requests;
for (RequestInfo request : raw_requests) {
if (request.isRequest()) {
requests.push_back(request.getAsRequest());
} else {
requests.push_back(Request::Create(script_state, request.getAsUSVString(),
exception_state));
if (exception_state.HadException())
return ScriptPromise();
}
}
return AddAllImpl(script_state, requests, exception_state);
}
ScriptPromise Cache::deleteFunction(ScriptState* script_state,
const RequestInfo& request,
const CacheQueryOptions& options,
ExceptionState& exception_state) {
DCHECK(!request.isNull());
if (request.isRequest())
return DeleteImpl(script_state, request.getAsRequest(), options);
Request* new_request =
Request::Create(script_state, request.getAsUSVString(), exception_state);
if (exception_state.HadException())
return ScriptPromise();
return DeleteImpl(script_state, new_request, options);
}
ScriptPromise Cache::put(ScriptState* script_state,
const RequestInfo& request,
Response* response,
ExceptionState& exception_state) {
DCHECK(!request.isNull());
if (request.isRequest())
return PutImpl(script_state,
HeapVector<Member<Request>>(1, request.getAsRequest()),
HeapVector<Member<Response>>(1, response));
Request* new_request =
Request::Create(script_state, request.getAsUSVString(), exception_state);
if (exception_state.HadException())
return ScriptPromise();
return PutImpl(script_state, HeapVector<Member<Request>>(1, new_request),
HeapVector<Member<Response>>(1, response));
}
ScriptPromise Cache::keys(ScriptState* script_state, ExceptionState&) {
return KeysImpl(script_state);
}
ScriptPromise Cache::keys(ScriptState* script_state,
const RequestInfo& request,
const CacheQueryOptions& options,
ExceptionState& exception_state) {
DCHECK(!request.isNull());
if (request.isRequest())
return KeysImpl(script_state, request.getAsRequest(), options);
Request* new_request =
Request::Create(script_state, request.getAsUSVString(), exception_state);
if (exception_state.HadException())
return ScriptPromise();
return KeysImpl(script_state, new_request, options);
}
// static
WebServiceWorkerCache::QueryParams Cache::ToWebQueryParams(
const CacheQueryOptions& options) {
WebServiceWorkerCache::QueryParams web_query_params;
web_query_params.ignore_search = options.ignoreSearch();
web_query_params.ignore_method = options.ignoreMethod();
web_query_params.ignore_vary = options.ignoreVary();
web_query_params.cache_name = options.cacheName();
return web_query_params;
}
Cache::Cache(GlobalFetch::ScopedFetcher* fetcher,
std::unique_ptr<WebServiceWorkerCache> web_cache)
: scoped_fetcher_(fetcher), web_cache_(std::move(web_cache)) {}
DEFINE_TRACE(Cache) {
visitor->Trace(scoped_fetcher_);
}
ScriptPromise Cache::MatchImpl(ScriptState* script_state,
const Request* request,
const CacheQueryOptions& options) {
WebServiceWorkerRequest web_request;
request->PopulateWebServiceWorkerRequest(web_request);
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
const ScriptPromise promise = resolver->Promise();
if (request->method() != HTTPNames::GET && !options.ignoreMethod()) {
resolver->Resolve();
return promise;
}
web_cache_->DispatchMatch(WTF::MakeUnique<CacheMatchCallbacks>(resolver),
web_request, ToWebQueryParams(options));
return promise;
}
ScriptPromise Cache::MatchAllImpl(ScriptState* script_state) {
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
const ScriptPromise promise = resolver->Promise();
web_cache_->DispatchMatchAll(
WTF::MakeUnique<CacheWithResponsesCallbacks>(resolver),
WebServiceWorkerRequest(), WebServiceWorkerCache::QueryParams());
return promise;
}
ScriptPromise Cache::MatchAllImpl(ScriptState* script_state,
const Request* request,
const CacheQueryOptions& options) {
WebServiceWorkerRequest web_request;
request->PopulateWebServiceWorkerRequest(web_request);
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
const ScriptPromise promise = resolver->Promise();
if (request->method() != HTTPNames::GET && !options.ignoreMethod()) {
resolver->Resolve(HeapVector<Member<Response>>());
return promise;
}
web_cache_->DispatchMatchAll(
WTF::MakeUnique<CacheWithResponsesCallbacks>(resolver), web_request,
ToWebQueryParams(options));
return promise;
}
ScriptPromise Cache::AddAllImpl(ScriptState* script_state,
const HeapVector<Member<Request>>& requests,
ExceptionState& exception_state) {
if (requests.IsEmpty())
return ScriptPromise::CastUndefined(script_state);
HeapVector<RequestInfo> request_infos;
request_infos.resize(requests.size());
Vector<ScriptPromise> promises;
promises.resize(requests.size());
for (size_t i = 0; i < requests.size(); ++i) {
if (!requests[i]->url().ProtocolIsInHTTPFamily())
return ScriptPromise::Reject(script_state,
V8ThrowException::CreateTypeError(
script_state->GetIsolate(),
"Add/AddAll does not support schemes "
"other than \"http\" or \"https\""));
if (requests[i]->method() != HTTPNames::GET)
return ScriptPromise::Reject(
script_state,
V8ThrowException::CreateTypeError(
script_state->GetIsolate(),
"Add/AddAll only supports the GET request method."));
request_infos[i].setRequest(requests[i]);
promises[i] = scoped_fetcher_->Fetch(script_state, request_infos[i],
Dictionary(), exception_state);
}
return ScriptPromise::All(script_state, promises)
.Then(FetchResolvedForAdd::Create(script_state, this, requests));
}
ScriptPromise Cache::DeleteImpl(ScriptState* script_state,
const Request* request,
const CacheQueryOptions& options) {
WebVector<WebServiceWorkerCache::BatchOperation> batch_operations(size_t(1));
batch_operations[0].operation_type =
WebServiceWorkerCache::kOperationTypeDelete;
request->PopulateWebServiceWorkerRequest(batch_operations[0].request);
batch_operations[0].match_params = ToWebQueryParams(options);
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
const ScriptPromise promise = resolver->Promise();
if (request->method() != HTTPNames::GET && !options.ignoreMethod()) {
resolver->Resolve(false);
return promise;
}
web_cache_->DispatchBatch(WTF::MakeUnique<CacheDeleteCallback>(resolver),
batch_operations);
return promise;
}
ScriptPromise Cache::PutImpl(ScriptState* script_state,
const HeapVector<Member<Request>>& requests,
const HeapVector<Member<Response>>& responses) {
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
const ScriptPromise promise = resolver->Promise();
BarrierCallbackForPut* barrier_callback =
new BarrierCallbackForPut(requests.size(), this, resolver);
for (size_t i = 0; i < requests.size(); ++i) {
KURL url(KURL(), requests[i]->url());
if (!url.ProtocolIsInHTTPFamily()) {
barrier_callback->OnError("Request scheme '" + url.Protocol() +
"' is unsupported");
return promise;
}
if (requests[i]->method() != HTTPNames::GET) {
barrier_callback->OnError("Request method '" + requests[i]->method() +
"' is unsupported");
return promise;
}
DCHECK(!requests[i]->HasBody());
if (VaryHeaderContainsAsterisk(responses[i])) {
barrier_callback->OnError("Vary header contains *");
return promise;
}
if (responses[i]->status() == 206) {
barrier_callback->OnError(
"Partial response (status code 206) is unsupported");
return promise;
}
if (responses[i]->IsBodyLocked() || responses[i]->bodyUsed()) {
barrier_callback->OnError("Response body is already used");
return promise;
}
BodyStreamBuffer* buffer = responses[i]->InternalBodyBuffer();
if (buffer) {
// If the response has body, read the all data and create
// the blob handle and dispatch the put batch asynchronously.
FetchDataLoader* loader = FetchDataLoader::CreateLoaderAsBlobHandle(
responses[i]->InternalMIMEType());
buffer->StartLoading(
loader, new BlobHandleCallbackForPut(i, barrier_callback, requests[i],
responses[i]));
continue;
}
WebServiceWorkerCache::BatchOperation batch_operation;
batch_operation.operation_type = WebServiceWorkerCache::kOperationTypePut;
requests[i]->PopulateWebServiceWorkerRequest(batch_operation.request);
responses[i]->PopulateWebServiceWorkerResponse(batch_operation.response);
barrier_callback->OnSuccess(i, batch_operation);
}
return promise;
}
ScriptPromise Cache::KeysImpl(ScriptState* script_state) {
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
const ScriptPromise promise = resolver->Promise();
web_cache_->DispatchKeys(
WTF::MakeUnique<CacheWithRequestsCallbacks>(resolver),
WebServiceWorkerRequest(), WebServiceWorkerCache::QueryParams());
return promise;
}
ScriptPromise Cache::KeysImpl(ScriptState* script_state,
const Request* request,
const CacheQueryOptions& options) {
WebServiceWorkerRequest web_request;
request->PopulateWebServiceWorkerRequest(web_request);
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
const ScriptPromise promise = resolver->Promise();
if (request->method() != HTTPNames::GET && !options.ignoreMethod()) {
resolver->Resolve(HeapVector<Member<Request>>());
return promise;
}
web_cache_->DispatchKeys(
WTF::MakeUnique<CacheWithRequestsCallbacks>(resolver), web_request,
ToWebQueryParams(options));
return promise;
}
WebServiceWorkerCache* Cache::WebCache() const {
return web_cache_.get();
}
} // namespace blink