blob: 26f8c7fc7a221da20925f58d54792a2b6370e3a6 [file] [log] [blame]
/*
* Copyright (C) 2017-2020 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "DOMCache.h"
#include "CacheQueryOptions.h"
#include "CachedResourceRequestInitiators.h"
#include "EventLoop.h"
#include "FetchResponse.h"
#include "HTTPParsers.h"
#include "JSFetchRequest.h"
#include "JSFetchResponse.h"
#include "ScriptExecutionContext.h"
#include <wtf/CompletionHandler.h>
#include <wtf/URL.h>
namespace WebCore {
using namespace WebCore::DOMCacheEngine;
Ref<DOMCache> DOMCache::create(ScriptExecutionContext& context, String&& name, uint64_t identifier, Ref<CacheStorageConnection>&& connection)
{
auto cache = adoptRef(*new DOMCache(context, WTFMove(name), identifier, WTFMove(connection)));
cache->suspendIfNeeded();
return cache;
}
DOMCache::DOMCache(ScriptExecutionContext& context, String&& name, uint64_t identifier, Ref<CacheStorageConnection>&& connection)
: ActiveDOMObject(&context)
, m_name(WTFMove(name))
, m_identifier(identifier)
, m_connection(WTFMove(connection))
{
m_connection->reference(m_identifier);
}
DOMCache::~DOMCache()
{
if (!m_isStopped)
m_connection->dereference(m_identifier);
}
void DOMCache::match(RequestInfo&& info, CacheQueryOptions&& options, Ref<DeferredPromise>&& promise)
{
doMatch(WTFMove(info), WTFMove(options), [this, protectedThis = Ref { *this }, promise = WTFMove(promise)](ExceptionOr<RefPtr<FetchResponse>>&& result) mutable {
queueTaskKeepingObjectAlive(*this, TaskSource::DOMManipulation, [promise = WTFMove(promise), result = WTFMove(result)]() mutable {
if (result.hasException()) {
promise->reject(result.releaseException());
return;
}
if (!result.returnValue()) {
promise->resolve();
return;
}
promise->resolve<IDLInterface<FetchResponse>>(*result.returnValue());
});
});
}
static Ref<FetchResponse> createResponse(ScriptExecutionContext& context, const DOMCacheEngine::Record& record, MonotonicTime requestStart)
{
auto resourceResponse = record.response;
resourceResponse.setSource(ResourceResponse::Source::DOMCache);
auto metrics = Box<NetworkLoadMetrics>::create();
metrics->requestStart = requestStart;
metrics->responseStart = MonotonicTime::now();
resourceResponse.setDeprecatedNetworkLoadMetrics(WTFMove(metrics));
auto response = FetchResponse::create(&context, std::nullopt, record.responseHeadersGuard, WTFMove(resourceResponse));
response->setBodyData(copyResponseBody(record.responseBody), record.responseBodySize);
return response;
}
void DOMCache::doMatch(RequestInfo&& info, CacheQueryOptions&& options, MatchCallback&& callback)
{
if (UNLIKELY(!scriptExecutionContext()))
return;
auto requestOrException = requestFromInfo(WTFMove(info), options.ignoreMethod);
if (requestOrException.hasException()) {
callback(nullptr);
return;
}
auto request = requestOrException.releaseReturnValue()->resourceRequest();
auto requestStart = MonotonicTime::now();
queryCache(WTFMove(request), options, ShouldRetrieveResponses::Yes, [this, callback = WTFMove(callback), requestStart](auto&& result) mutable {
if (result.hasException()) {
callback(result.releaseException());
return;
}
RefPtr<FetchResponse> response;
if (!result.returnValue().isEmpty())
response = createResponse(*scriptExecutionContext(), result.returnValue()[0], requestStart);
callback(WTFMove(response));
});
}
Vector<Ref<FetchResponse>> DOMCache::cloneResponses(const Vector<DOMCacheEngine::Record>& records, MonotonicTime requestStart)
{
return WTF::map(records, [this, requestStart] (const auto& record) {
return createResponse(*scriptExecutionContext(), record, requestStart);
});
}
void DOMCache::matchAll(std::optional<RequestInfo>&& info, CacheQueryOptions&& options, MatchAllPromise&& promise)
{
if (UNLIKELY(!scriptExecutionContext()))
return;
ResourceRequest resourceRequest;
if (info) {
auto requestOrException = requestFromInfo(WTFMove(info.value()), options.ignoreMethod);
if (requestOrException.hasException()) {
promise.resolve({ });
return;
}
resourceRequest = requestOrException.releaseReturnValue()->resourceRequest();
}
auto requestStart = MonotonicTime::now();
queryCache(WTFMove(resourceRequest), options, ShouldRetrieveResponses::Yes, [this, promise = WTFMove(promise), requestStart](auto&& result) mutable {
queueTaskKeepingObjectAlive(*this, TaskSource::DOMManipulation, [this, promise = WTFMove(promise), result = WTFMove(result), requestStart]() mutable {
if (result.hasException()) {
promise.reject(result.releaseException());
return;
}
promise.resolve(cloneResponses(result.releaseReturnValue(), requestStart));
});
});
}
void DOMCache::add(RequestInfo&& info, DOMPromiseDeferred<void>&& promise)
{
addAll(Vector<RequestInfo> { WTFMove(info) }, WTFMove(promise));
}
static inline bool hasResponseVaryStarHeaderValue(const FetchResponse& response)
{
auto varyValue = response.headers().internalHeaders().get(WebCore::HTTPHeaderName::Vary);
bool hasStar = false;
varyValue.split(',', [&](StringView view) {
if (!hasStar && stripLeadingAndTrailingHTTPSpaces(view) == "*"_s)
hasStar = true;
});
return hasStar;
}
class FetchTasksHandler : public RefCounted<FetchTasksHandler> {
public:
static Ref<FetchTasksHandler> create(Ref<DOMCache>&& domCache, CompletionHandler<void(ExceptionOr<Vector<Record>>&&)>&& callback) { return adoptRef(*new FetchTasksHandler(WTFMove(domCache), WTFMove(callback))); }
~FetchTasksHandler()
{
if (m_callback)
m_callback(WTFMove(m_records));
}
const Vector<Record>& records() const { return m_records; }
size_t addRecord(Record&& record)
{
ASSERT(!isDone());
m_records.append(WTFMove(record));
return m_records.size() - 1;
}
void addResponseBody(size_t position, FetchResponse& response, DOMCacheEngine::ResponseBody&& data)
{
ASSERT(!isDone());
auto& record = m_records[position];
record.responseBodySize = m_domCache->connection().computeRecordBodySize(response, data);
record.responseBody = WTFMove(data);
}
bool isDone() const { return !m_callback; }
void error(Exception&& exception)
{
if (auto callback = WTFMove(m_callback))
callback(WTFMove(exception));
}
private:
FetchTasksHandler(Ref<DOMCache>&& domCache, CompletionHandler<void(ExceptionOr<Vector<Record>>&&)>&& callback)
: m_domCache(WTFMove(domCache))
, m_callback(WTFMove(callback))
{
}
Ref<DOMCache> m_domCache;
Vector<Record> m_records;
CompletionHandler<void(ExceptionOr<Vector<Record>>&&)> m_callback;
};
ExceptionOr<Ref<FetchRequest>> DOMCache::requestFromInfo(RequestInfo&& info, bool ignoreMethod)
{
RefPtr<FetchRequest> request;
if (std::holds_alternative<RefPtr<FetchRequest>>(info)) {
request = std::get<RefPtr<FetchRequest>>(info).releaseNonNull();
if (request->method() != "GET"_s && !ignoreMethod)
return Exception { TypeError, "Request method is not GET"_s };
} else
request = FetchRequest::create(*scriptExecutionContext(), WTFMove(info), { }).releaseReturnValue();
if (!request->url().protocolIsInHTTPFamily())
return Exception { TypeError, "Request url is not HTTP/HTTPS"_s };
return request.releaseNonNull();
}
void DOMCache::addAll(Vector<RequestInfo>&& infos, DOMPromiseDeferred<void>&& promise)
{
if (UNLIKELY(!scriptExecutionContext()))
return;
Vector<Ref<FetchRequest>> requests;
requests.reserveInitialCapacity(infos.size());
for (auto& info : infos) {
bool ignoreMethod = false;
auto requestOrException = requestFromInfo(WTFMove(info), ignoreMethod);
if (requestOrException.hasException()) {
promise.reject(requestOrException.releaseException());
return;
}
requests.uncheckedAppend(requestOrException.releaseReturnValue());
}
auto taskHandler = FetchTasksHandler::create(*this, [this, protectedThis = Ref { *this }, promise = WTFMove(promise)](ExceptionOr<Vector<Record>>&& result) mutable {
if (result.hasException()) {
queueTaskKeepingObjectAlive(*this, TaskSource::DOMManipulation, [promise = WTFMove(promise), exception = result.releaseException()]() mutable {
promise.reject(WTFMove(exception));
});
return;
}
batchPutOperation(result.releaseReturnValue(), [this, protectedThis = WTFMove(protectedThis), promise = WTFMove(promise)](ExceptionOr<void>&& result) mutable {
queueTaskKeepingObjectAlive(*this, TaskSource::DOMManipulation, [promise = WTFMove(promise), result = WTFMove(result)]() mutable {
promise.settle(WTFMove(result));
});
});
});
for (auto& request : requests) {
auto& requestReference = request.get();
if (requestReference.signal().aborted()) {
taskHandler->error(Exception { AbortError, "Request signal is aborted"_s });
return;
}
FetchResponse::fetch(*scriptExecutionContext(), requestReference, [this, request = WTFMove(request), taskHandler](auto&& result) mutable {
if (taskHandler->isDone())
return;
if (result.hasException()) {
taskHandler->error(result.releaseException());
return;
}
auto protectedResponse = result.releaseReturnValue();
auto& response = protectedResponse.get();
if (!response.ok()) {
taskHandler->error(Exception { TypeError, "Response is not OK"_s });
return;
}
if (hasResponseVaryStarHeaderValue(response)) {
taskHandler->error(Exception { TypeError, "Response has a '*' Vary header value"_s });
return;
}
if (response.status() == 206) {
taskHandler->error(Exception { TypeError, "Response is a 206 partial"_s });
return;
}
CacheQueryOptions options;
for (const auto& record : taskHandler->records()) {
if (DOMCacheEngine::queryCacheMatch(request->resourceRequest(), record.request, record.response, options)) {
taskHandler->error(Exception { InvalidStateError, "addAll cannot store several matching requests"_s});
return;
}
}
size_t recordPosition = taskHandler->addRecord(toConnectionRecord(request.get(), response, nullptr));
response.consumeBodyReceivedByChunk([taskHandler = WTFMove(taskHandler), recordPosition, data = SharedBufferBuilder(), response = WTFMove(protectedResponse)] (auto&& result) mutable {
if (taskHandler->isDone())
return;
if (result.hasException()) {
taskHandler->error(result.releaseException());
return;
}
if (auto* chunk = result.returnValue())
data.append(chunk->data(), chunk->size());
else
taskHandler->addResponseBody(recordPosition, response, data.takeAsContiguous());
});
}, cachedResourceRequestInitiators().fetch);
}
}
void DOMCache::putWithResponseData(DOMPromiseDeferred<void>&& promise, Ref<FetchRequest>&& request, Ref<FetchResponse>&& response, ExceptionOr<RefPtr<SharedBuffer>>&& responseBody)
{
if (responseBody.hasException()) {
queueTaskKeepingObjectAlive(*this, TaskSource::DOMManipulation, [promise = WTFMove(promise), exception = responseBody.releaseException()]() mutable {
promise.reject(WTFMove(exception));
});
return;
}
DOMCacheEngine::ResponseBody body;
if (auto buffer = responseBody.releaseReturnValue())
body = buffer->makeContiguous();
batchPutOperation(request.get(), response.get(), WTFMove(body), [this, protectedThis = Ref { *this }, promise = WTFMove(promise)](ExceptionOr<void>&& result) mutable {
queueTaskKeepingObjectAlive(*this, TaskSource::DOMManipulation, [promise = WTFMove(promise), result = WTFMove(result)]() mutable {
promise.settle(WTFMove(result));
});
});
}
void DOMCache::put(RequestInfo&& info, Ref<FetchResponse>&& response, DOMPromiseDeferred<void>&& promise)
{
if (UNLIKELY(!scriptExecutionContext()))
return;
bool ignoreMethod = false;
auto requestOrException = requestFromInfo(WTFMove(info), ignoreMethod);
if (requestOrException.hasException()) {
promise.reject(requestOrException.releaseException());
return;
}
auto request = requestOrException.releaseReturnValue();
if (auto exception = response->loadingException()) {
promise.reject(*exception);
return;
}
if (hasResponseVaryStarHeaderValue(response.get())) {
promise.reject(Exception { TypeError, "Response has a '*' Vary header value"_s });
return;
}
if (response->status() == 206) {
promise.reject(Exception { TypeError, "Response is a 206 partial"_s });
return;
}
if (response->isDisturbedOrLocked()) {
promise.reject(Exception { TypeError, "Response is disturbed or locked"_s });
return;
}
// FIXME: for efficiency, we should load blobs/form data directly instead of going through the readableStream path.
if (response->isBlobBody() || response->isBlobFormData()) {
auto streamOrException = response->readableStream(*scriptExecutionContext()->globalObject());
if (UNLIKELY(streamOrException.hasException())) {
promise.reject(streamOrException.releaseException());
return;
}
}
if (response->isBodyReceivedByChunk()) {
auto& responseRef = response.get();
responseRef.consumeBodyReceivedByChunk([promise = WTFMove(promise), request = WTFMove(request), response = WTFMove(response), data = SharedBufferBuilder(), pendingActivity = makePendingActivity(*this), this](auto&& result) mutable {
if (result.hasException()) {
this->putWithResponseData(WTFMove(promise), WTFMove(request), WTFMove(response), result.releaseException().isolatedCopy());
return;
}
if (auto* chunk = result.returnValue())
data.append(chunk->data(), chunk->size());
else
this->putWithResponseData(WTFMove(promise), WTFMove(request), WTFMove(response), RefPtr<SharedBuffer> { data.takeAsContiguous() });
});
return;
}
batchPutOperation(request.get(), response.get(), response->consumeBody(), [this, protectedThis = Ref { *this }, promise = WTFMove(promise)](ExceptionOr<void>&& result) mutable {
queueTaskKeepingObjectAlive(*this, TaskSource::DOMManipulation, [promise = WTFMove(promise), result = WTFMove(result)]() mutable {
promise.settle(WTFMove(result));
});
});
}
void DOMCache::remove(RequestInfo&& info, CacheQueryOptions&& options, DOMPromiseDeferred<IDLBoolean>&& promise)
{
if (UNLIKELY(!scriptExecutionContext()))
return;
auto requestOrException = requestFromInfo(WTFMove(info), options.ignoreMethod);
if (requestOrException.hasException()) {
promise.resolve(false);
return;
}
batchDeleteOperation(requestOrException.releaseReturnValue(), WTFMove(options), [this, protectedThis = Ref { *this }, promise = WTFMove(promise)](ExceptionOr<bool>&& result) mutable {
queueTaskKeepingObjectAlive(*this, TaskSource::DOMManipulation, [promise = WTFMove(promise), result = WTFMove(result)]() mutable {
promise.settle(WTFMove(result));
});
});
}
static Ref<FetchRequest> createRequest(ScriptExecutionContext& context, const DOMCacheEngine::Record& record)
{
auto requestHeaders = FetchHeaders::create(record.requestHeadersGuard, HTTPHeaderMap { record.request.httpHeaderFields() });
return FetchRequest::create(context, std::nullopt, WTFMove(requestHeaders), ResourceRequest { record.request }, FetchOptions { record.options }, String { record.referrer });
}
void DOMCache::keys(std::optional<RequestInfo>&& info, CacheQueryOptions&& options, KeysPromise&& promise)
{
if (UNLIKELY(!scriptExecutionContext()))
return;
ResourceRequest resourceRequest;
if (info) {
auto requestOrException = requestFromInfo(WTFMove(info.value()), options.ignoreMethod);
if (requestOrException.hasException()) {
promise.resolve(Vector<Ref<FetchRequest>> { });
return;
}
resourceRequest = requestOrException.releaseReturnValue()->resourceRequest();
}
queryCache(WTFMove(resourceRequest), options, ShouldRetrieveResponses::No, [this, promise = WTFMove(promise)](auto&& result) mutable {
queueTaskKeepingObjectAlive(*this, TaskSource::DOMManipulation, [this, promise = WTFMove(promise), result = WTFMove(result)]() mutable {
if (result.hasException()) {
promise.reject(result.releaseException());
return;
}
auto records = result.releaseReturnValue();
promise.resolve(WTF::map(records, [this](auto& record) {
return createRequest(*scriptExecutionContext(), record);
}));
});
});
}
void DOMCache::queryCache(ResourceRequest&& request, const CacheQueryOptions& options, ShouldRetrieveResponses shouldRetrieveResponses, RecordsCallback&& callback)
{
RetrieveRecordsOptions retrieveOptions { WTFMove(request), scriptExecutionContext()->crossOriginEmbedderPolicy(), *scriptExecutionContext()->securityOrigin(), options.ignoreSearch, options.ignoreMethod, options.ignoreVary, shouldRetrieveResponses == ShouldRetrieveResponses::Yes };
m_connection->retrieveRecords(m_identifier, WTFMove(retrieveOptions), [this, pendingActivity = makePendingActivity(*this), callback = WTFMove(callback)](auto&& result) mutable {
if (m_isStopped) {
callback(DOMCacheEngine::convertToExceptionAndLog(scriptExecutionContext(), DOMCacheEngine::Error::Stopped));
return;
}
if (!result.has_value()) {
callback(DOMCacheEngine::convertToExceptionAndLog(scriptExecutionContext(), result.error()));
return;
}
callback(WTFMove(result.value()));
});
}
void DOMCache::batchDeleteOperation(const FetchRequest& request, CacheQueryOptions&& options, CompletionHandler<void(ExceptionOr<bool>&&)>&& callback)
{
m_connection->batchDeleteOperation(m_identifier, request.internalRequest(), WTFMove(options), [this, pendingActivity = makePendingActivity(*this), callback = WTFMove(callback)](RecordIdentifiersOrError&& result) mutable {
if (m_isStopped) {
callback(DOMCacheEngine::convertToExceptionAndLog(scriptExecutionContext(), DOMCacheEngine::Error::Stopped));
return;
}
if (!result.has_value()) {
callback(DOMCacheEngine::convertToExceptionAndLog(scriptExecutionContext(), result.error()));
return;
}
callback(!result.value().isEmpty());
});
}
Record DOMCache::toConnectionRecord(const FetchRequest& request, FetchResponse& response, DOMCacheEngine::ResponseBody&& responseBody)
{
auto cachedResponse = response.resourceResponse();
ResourceRequest cachedRequest = request.internalRequest();
cachedRequest.setHTTPHeaderFields(request.headers().internalHeaders());
ASSERT(!cachedRequest.isNull());
ASSERT(!cachedResponse.isNull());
auto sizeWithPadding = response.bodySizeWithPadding();
if (!sizeWithPadding) {
sizeWithPadding = m_connection->computeRecordBodySize(response, responseBody);
response.setBodySizeWithPadding(sizeWithPadding);
}
return { 0, 0,
request.headers().guard(), WTFMove(cachedRequest), request.fetchOptions(), request.internalRequestReferrer(),
response.headers().guard(), WTFMove(cachedResponse), WTFMove(responseBody), sizeWithPadding
};
}
void DOMCache::batchPutOperation(const FetchRequest& request, FetchResponse& response, DOMCacheEngine::ResponseBody&& responseBody, CompletionHandler<void(ExceptionOr<void>&&)>&& callback)
{
auto record = toConnectionRecord(request, response, WTFMove(responseBody));
batchPutOperation({ WTFMove(record) }, WTFMove(callback));
}
void DOMCache::batchPutOperation(Vector<Record>&& records, CompletionHandler<void(ExceptionOr<void>&&)>&& callback)
{
m_connection->batchPutOperation(m_identifier, WTFMove(records), [this, pendingActivity = makePendingActivity(*this), callback = WTFMove(callback)](auto&& result) mutable {
if (m_isStopped) {
callback(DOMCacheEngine::convertToExceptionAndLog(scriptExecutionContext(), DOMCacheEngine::Error::Stopped));
return;
}
if (!result.has_value()) {
callback(DOMCacheEngine::convertToExceptionAndLog(scriptExecutionContext(), result.error()));
return;
}
callback({ });
});
}
void DOMCache::stop()
{
if (m_isStopped)
return;
m_isStopped = true;
m_connection->dereference(m_identifier);
}
const char* DOMCache::activeDOMObjectName() const
{
return "Cache";
}
} // namespace WebCore