2 * Copyright (C) 2017 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
29 #include "CacheQueryOptions.h"
30 #include "FetchResponse.h"
31 #include "HTTPParsers.h"
32 #include "JSFetchRequest.h"
33 #include "JSFetchResponse.h"
34 #include "ScriptExecutionContext.h"
37 using namespace WebCore::DOMCache;
41 static Record toConnectionRecord(const FetchRequest&, FetchResponse&, ResponseBody&&);
43 Cache::Cache(ScriptExecutionContext& context, String&& name, uint64_t identifier, Ref<CacheStorageConnection>&& connection)
44 : ActiveDOMObject(&context)
45 , m_name(WTFMove(name))
46 , m_identifier(identifier)
47 , m_connection(WTFMove(connection))
56 void Cache::match(RequestInfo&& info, CacheQueryOptions&& options, Ref<DeferredPromise>&& promise)
58 doMatch(WTFMove(info), WTFMove(options), [promise = WTFMove(promise)](FetchResponse* result) mutable {
63 promise->resolve<IDLInterface<FetchResponse>>(*result);
67 void Cache::doMatch(RequestInfo&& info, CacheQueryOptions&& options, MatchCallback&& callback)
69 if (UNLIKELY(!scriptExecutionContext()))
72 auto requestOrException = requestFromInfo(WTFMove(info), options.ignoreMethod);
73 if (requestOrException.hasException()) {
77 auto request = requestOrException.releaseReturnValue();
79 queryCache(request.get(), WTFMove(options), [callback = WTFMove(callback)](const Vector<CacheStorageRecord>& records) mutable {
80 if (records.isEmpty()) {
84 callback(records[0].response->cloneForJS().ptr());
88 void Cache::matchAll(std::optional<RequestInfo>&& info, CacheQueryOptions&& options, MatchAllPromise&& promise)
90 if (UNLIKELY(!scriptExecutionContext()))
93 RefPtr<FetchRequest> request;
95 auto requestOrException = requestFromInfo(WTFMove(info.value()), options.ignoreMethod);
96 if (requestOrException.hasException()) {
100 request = requestOrException.releaseReturnValue();
104 retrieveRecords([this, promise = WTFMove(promise)]() mutable {
105 Vector<Ref<FetchResponse>> responses;
106 responses.reserveInitialCapacity(m_records.size());
107 for (auto& record : m_records)
108 responses.uncheckedAppend(record.response->cloneForJS());
109 promise.resolve(responses);
113 queryCache(request.releaseNonNull(), WTFMove(options), [promise = WTFMove(promise)](const Vector<CacheStorageRecord>& records) mutable {
114 Vector<Ref<FetchResponse>> responses;
115 responses.reserveInitialCapacity(records.size());
116 for (auto& record : records)
117 responses.uncheckedAppend(record.response->cloneForJS());
118 promise.resolve(responses);
122 void Cache::add(RequestInfo&& info, DOMPromiseDeferred<void>&& promise)
124 addAll(Vector<RequestInfo> { WTFMove(info) }, WTFMove(promise));
127 static inline bool hasResponseVaryStarHeaderValue(const FetchResponse& response)
129 auto varyValue = response.headers().internalHeaders().get(WebCore::HTTPHeaderName::Vary);
130 bool hasStar = false;
131 varyValue.split(',', false, [&](StringView view) {
132 if (!hasStar && stripLeadingAndTrailingHTTPSpaces(view) == "*")
138 class FetchTasksHandler : public RefCounted<FetchTasksHandler> {
140 explicit FetchTasksHandler(Function<void(ExceptionOr<Vector<Record>>&&)>&& callback)
141 : m_callback(WTFMove(callback))
148 m_callback(WTFMove(m_records));
151 const Vector<Record>& records() const { return m_records; }
153 size_t addRecord(Record&& record)
156 m_records.append(WTFMove(record));
157 return m_records.size() - 1;
160 void addResponseBody(size_t position, Ref<SharedBuffer>&& data)
163 m_records[position].responseBody = WTFMove(data);
166 bool isDone() const { return !m_callback; }
168 void error(Exception&& exception)
170 if (auto callback = WTFMove(m_callback))
171 callback(WTFMove(exception));
175 Vector<Record> m_records;
176 Function<void(ExceptionOr<Vector<Record>>&&)> m_callback;
179 ExceptionOr<Ref<FetchRequest>> Cache::requestFromInfo(RequestInfo&& info, bool ignoreMethod)
181 RefPtr<FetchRequest> request;
182 if (WTF::holds_alternative<RefPtr<FetchRequest>>(info)) {
183 request = WTF::get<RefPtr<FetchRequest>>(info).releaseNonNull();
184 if (request->method() != "GET" && !ignoreMethod)
185 return Exception { TypeError, ASCIILiteral("Request method is not GET") };
187 request = FetchRequest::create(*scriptExecutionContext(), WTFMove(info), { }).releaseReturnValue();
189 if (!protocolIsInHTTPFamily(request->url()))
190 return Exception { TypeError, ASCIILiteral("Request url is not HTTP/HTTPS") };
192 return request.releaseNonNull();
195 void Cache::addAll(Vector<RequestInfo>&& infos, DOMPromiseDeferred<void>&& promise)
197 if (UNLIKELY(!scriptExecutionContext()))
200 Vector<Ref<FetchRequest>> requests;
201 requests.reserveInitialCapacity(infos.size());
202 for (auto& info : infos) {
203 bool ignoreMethod = false;
204 auto requestOrException = requestFromInfo(WTFMove(info), ignoreMethod);
205 if (requestOrException.hasException()) {
206 promise.reject(requestOrException.releaseException());
209 requests.uncheckedAppend(requestOrException.releaseReturnValue());
212 auto taskHandler = adoptRef(*new FetchTasksHandler([protectedThis = makeRef(*this), this, promise = WTFMove(promise)](ExceptionOr<Vector<Record>>&& result) mutable {
213 if (result.hasException()) {
214 promise.reject(result.releaseException());
217 batchPutOperation(result.releaseReturnValue(), [promise = WTFMove(promise)](ExceptionOr<void>&& result) mutable {
218 promise.settle(WTFMove(result));
222 for (auto& request : requests) {
223 auto& requestReference = request.get();
224 FetchResponse::fetch(*scriptExecutionContext(), requestReference, [this, request = WTFMove(request), taskHandler = taskHandler.copyRef()](ExceptionOr<FetchResponse&>&& result) mutable {
226 if (taskHandler->isDone())
229 if (result.hasException()) {
230 taskHandler->error(result.releaseException());
234 auto& response = result.releaseReturnValue();
236 if (!response.ok()) {
237 taskHandler->error(Exception { TypeError, ASCIILiteral("Response is not OK") });
241 if (hasResponseVaryStarHeaderValue(response)) {
242 taskHandler->error(Exception { TypeError, ASCIILiteral("Response has a '*' Vary header value") });
246 if (response.status() == 206) {
247 taskHandler->error(Exception { TypeError, ASCIILiteral("Response is a 206 partial") });
251 CacheQueryOptions options;
252 for (const auto& record : taskHandler->records()) {
253 if (DOMCache::queryCacheMatch(request->resourceRequest(), record.request, record.response, options)) {
254 taskHandler->error(Exception { InvalidStateError, ASCIILiteral("addAll cannot store several matching requests")});
258 size_t recordPosition = taskHandler->addRecord(toConnectionRecord(request.get(), response, nullptr));
260 response.consumeBodyWhenLoaded([taskHandler = WTFMove(taskHandler), recordPosition](ExceptionOr<RefPtr<SharedBuffer>>&& result) mutable {
261 if (taskHandler->isDone())
264 if (result.hasException()) {
265 taskHandler->error(result.releaseException());
268 if (auto value = result.releaseReturnValue())
269 taskHandler->addResponseBody(recordPosition, value.releaseNonNull());
275 void Cache::put(RequestInfo&& info, Ref<FetchResponse>&& response, DOMPromiseDeferred<void>&& promise)
277 if (UNLIKELY(!scriptExecutionContext()))
280 bool ignoreMethod = false;
281 auto requestOrException = requestFromInfo(WTFMove(info), ignoreMethod);
282 if (requestOrException.hasException()) {
283 promise.reject(requestOrException.releaseException());
286 auto request = requestOrException.releaseReturnValue();
288 if (hasResponseVaryStarHeaderValue(response.get())) {
289 promise.reject(Exception { TypeError, ASCIILiteral("Response has a '*' Vary header value") });
293 if (response->status() == 206) {
294 promise.reject(Exception { TypeError, ASCIILiteral("Response is a 206 partial") });
298 // FIXME: Add support for ReadableStream.
299 if (response->isReadableStreamBody()) {
300 promise.reject(Exception { NotSupportedError, ASCIILiteral("Caching a Response with data stored in a ReadableStream is not yet supported") });
304 if (response->isDisturbed()) {
305 promise.reject(Exception { TypeError, ASCIILiteral("Response is disturbed or locked") });
309 if (response->isLoading()) {
310 setPendingActivity(this);
311 response->consumeBodyWhenLoaded([promise = WTFMove(promise), request = WTFMove(request), response = WTFMove(response), this](ExceptionOr<RefPtr<SharedBuffer>>&& result) mutable {
312 if (result.hasException())
313 promise.reject(result.releaseException());
315 DOMCache::ResponseBody body;
316 if (auto buffer = result.releaseReturnValue())
317 body = buffer.releaseNonNull();
318 batchPutOperation(request.get(), response.get(), WTFMove(body), [promise = WTFMove(promise)](ExceptionOr<void>&& result) mutable {
319 promise.settle(WTFMove(result));
322 unsetPendingActivity(this);
327 batchPutOperation(request.get(), response.get(), response->consumeBody(), [promise = WTFMove(promise)](ExceptionOr<void>&& result) mutable {
328 promise.settle(WTFMove(result));
332 void Cache::remove(RequestInfo&& info, CacheQueryOptions&& options, DOMPromiseDeferred<IDLBoolean>&& promise)
334 if (UNLIKELY(!scriptExecutionContext()))
337 auto requestOrException = requestFromInfo(WTFMove(info), options.ignoreMethod);
338 if (requestOrException.hasException()) {
339 promise.resolve(false);
343 batchDeleteOperation(requestOrException.releaseReturnValue(), WTFMove(options), [promise = WTFMove(promise)](ExceptionOr<bool>&& result) mutable {
344 promise.settle(WTFMove(result));
348 void Cache::keys(std::optional<RequestInfo>&& info, CacheQueryOptions&& options, KeysPromise&& promise)
350 if (UNLIKELY(!scriptExecutionContext()))
353 RefPtr<FetchRequest> request;
355 auto requestOrException = requestFromInfo(WTFMove(info.value()), options.ignoreMethod);
356 if (requestOrException.hasException()) {
357 promise.resolve(Vector<Ref<FetchRequest>> { });
360 request = requestOrException.releaseReturnValue();
364 retrieveRecords([this, promise = WTFMove(promise)]() mutable {
365 Vector<Ref<FetchRequest>> requests;
366 requests.reserveInitialCapacity(m_records.size());
367 for (auto& record : m_records)
368 requests.uncheckedAppend(record.request.copyRef());
369 promise.resolve(requests);
374 queryCache(request.releaseNonNull(), WTFMove(options), [promise = WTFMove(promise)](const Vector<CacheStorageRecord>& records) mutable {
375 Vector<Ref<FetchRequest>> requests;
376 requests.reserveInitialCapacity(records.size());
377 for (auto& record : records)
378 requests.uncheckedAppend(record.request.copyRef());
379 promise.resolve(requests);
383 void Cache::retrieveRecords(WTF::Function<void()>&& callback)
385 setPendingActivity(this);
386 m_connection->retrieveRecords(m_identifier, [this, callback = WTFMove(callback)](RecordsOrError&& result) {
388 // FIXME: We should probably propagate that error up to the promise based operation.
389 ASSERT(result.hasValue());
390 if (result.hasValue())
391 updateRecords(WTFMove(result.value()));
394 unsetPendingActivity(this);
398 void Cache::queryCache(Ref<FetchRequest>&& request, CacheQueryOptions&& options, WTF::Function<void(const Vector<CacheStorageRecord>&)>&& callback)
400 retrieveRecords([this, request = WTFMove(request), options = WTFMove(options), callback = WTFMove(callback)]() mutable {
401 callback(queryCacheWithTargetStorage(request.get(), options, m_records));
405 static inline bool queryCacheMatch(const FetchRequest& request, const FetchRequest& cachedRequest, const ResourceResponse& cachedResponse, const CacheQueryOptions& options)
407 // We need to pass the resource request with all correct headers hence why we call resourceRequest().
408 return DOMCache::queryCacheMatch(request.resourceRequest(), cachedRequest.resourceRequest(), cachedResponse, options);
411 Vector<CacheStorageRecord> Cache::queryCacheWithTargetStorage(const FetchRequest& request, const CacheQueryOptions& options, const Vector<CacheStorageRecord>& targetStorage)
413 if (!options.ignoreMethod && request.method() != "GET")
416 Vector<CacheStorageRecord> records;
417 for (auto& record : targetStorage) {
418 if (queryCacheMatch(request, record.request.get(), record.response->resourceResponse(), options))
419 records.append({ record.identifier, record.updateResponseCounter, record.request.copyRef(), record.response.copyRef() });
424 void Cache::batchDeleteOperation(const FetchRequest& request, CacheQueryOptions&& options, WTF::Function<void(ExceptionOr<bool>&&)>&& callback)
426 setPendingActivity(this);
427 m_connection->batchDeleteOperation(m_identifier, request.internalRequest(), WTFMove(options), [this, callback = WTFMove(callback)](RecordIdentifiersOrError&& result) {
429 if (!result.hasValue())
430 callback(DOMCache::errorToException(result.error()));
432 callback(!result.value().isEmpty());
434 unsetPendingActivity(this);
438 Record toConnectionRecord(const FetchRequest& request, FetchResponse& response, DOMCache::ResponseBody&& responseBody)
440 // FIXME: Add a setHTTPHeaderFields on ResourceResponseBase.
441 ResourceResponse cachedResponse = response.resourceResponse();
442 for (auto& header : response.headers().internalHeaders())
443 cachedResponse.setHTTPHeaderField(header.key, header.value);
445 ResourceRequest cachedRequest = request.internalRequest();
446 cachedRequest.setHTTPHeaderFields(request.headers().internalHeaders());
448 ASSERT(!cachedRequest.isNull());
449 ASSERT(!cachedResponse.isNull());
452 request.headers().guard(), WTFMove(cachedRequest), request.fetchOptions(), request.internalRequestReferrer(),
453 response.headers().guard(), WTFMove(cachedResponse), WTFMove(responseBody)
457 void Cache::batchPutOperation(const FetchRequest& request, FetchResponse& response, DOMCache::ResponseBody&& responseBody, WTF::Function<void(ExceptionOr<void>&&)>&& callback)
459 Vector<Record> records;
460 records.append(toConnectionRecord(request, response, WTFMove(responseBody)));
462 batchPutOperation(WTFMove(records), WTFMove(callback));
465 void Cache::batchPutOperation(Vector<Record>&& records, WTF::Function<void(ExceptionOr<void>&&)>&& callback)
467 setPendingActivity(this);
468 m_connection->batchPutOperation(m_identifier, WTFMove(records), [this, callback = WTFMove(callback)](RecordIdentifiersOrError&& result) {
470 if (!result.hasValue())
471 callback(DOMCache::errorToException(result.error()));
475 unsetPendingActivity(this);
479 void Cache::updateRecords(Vector<Record>&& records)
481 ASSERT(scriptExecutionContext());
482 Vector<CacheStorageRecord> newRecords;
484 for (auto& record : records) {
485 size_t index = m_records.findMatching([&](const auto& item) { return item.identifier == record.identifier; });
486 if (index != notFound) {
487 auto& current = m_records[index];
488 if (current.updateResponseCounter != record.updateResponseCounter) {
489 auto responseHeaders = FetchHeaders::create(record.responseHeadersGuard, HTTPHeaderMap { record.response.httpHeaderFields() });
490 auto response = FetchResponse::create(*scriptExecutionContext(), std::nullopt, WTFMove(responseHeaders), WTFMove(record.response));
491 response->setBodyData(WTFMove(record.responseBody));
493 current.response = WTFMove(response);
494 current.updateResponseCounter = record.updateResponseCounter;
496 newRecords.append(WTFMove(current));
498 auto requestHeaders = FetchHeaders::create(record.requestHeadersGuard, HTTPHeaderMap { record.request.httpHeaderFields() });
499 FetchRequest::InternalRequest internalRequest = { WTFMove(record.request), WTFMove(record.options), WTFMove(record.referrer) };
500 auto request = FetchRequest::create(*scriptExecutionContext(), std::nullopt, WTFMove(requestHeaders), WTFMove(internalRequest));
502 auto responseHeaders = FetchHeaders::create(record.responseHeadersGuard, HTTPHeaderMap { record.response.httpHeaderFields() });
503 auto response = FetchResponse::create(*scriptExecutionContext(), std::nullopt, WTFMove(responseHeaders), WTFMove(record.response));
504 response->setBodyData(WTFMove(record.responseBody));
506 newRecords.append(CacheStorageRecord { record.identifier, record.updateResponseCounter, WTFMove(request), WTFMove(response) });
509 m_records = WTFMove(newRecords);
517 const char* Cache::activeDOMObjectName() const
522 bool Cache::canSuspendForDocumentSuspension() const
524 return m_records.isEmpty() && !hasPendingActivity();
528 } // namespace WebCore