1/*
2 * Copyright (C) 2017 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
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.
12 *
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.
24 */
25
26#include "config.h"
27#include "DOMCache.h"
28
29#include "CacheQueryOptions.h"
30#include "FetchResponse.h"
31#include "HTTPParsers.h"
32#include "JSFetchRequest.h"
33#include "JSFetchResponse.h"
34#include "ReadableStreamChunk.h"
35#include "ScriptExecutionContext.h"
36#include <wtf/CompletionHandler.h>
37#include <wtf/URL.h>
38
39namespace WebCore {
40using namespace WebCore::DOMCacheEngine;
41
42DOMCache::DOMCache(ScriptExecutionContext& context, String&& name, uint64_t identifier, Ref<CacheStorageConnection>&& connection)
43 : ActiveDOMObject(&context)
44 , m_name(WTFMove(name))
45 , m_identifier(identifier)
46 , m_connection(WTFMove(connection))
47{
48 suspendIfNeeded();
49 m_connection->reference(m_identifier);
50}
51
52DOMCache::~DOMCache()
53{
54 if (!m_isStopped)
55 m_connection->dereference(m_identifier);
56}
57
58void DOMCache::match(RequestInfo&& info, CacheQueryOptions&& options, Ref<DeferredPromise>&& promise)
59{
60 doMatch(WTFMove(info), WTFMove(options), [promise = WTFMove(promise)](ExceptionOr<FetchResponse*>&& result) mutable {
61 if (result.hasException()) {
62 promise->reject(result.releaseException());
63 return;
64 }
65 if (!result.returnValue()) {
66 promise->resolve();
67 return;
68 }
69 promise->resolve<IDLInterface<FetchResponse>>(*result.returnValue());
70 });
71}
72
73void DOMCache::doMatch(RequestInfo&& info, CacheQueryOptions&& options, MatchCallback&& callback)
74{
75 if (UNLIKELY(!scriptExecutionContext()))
76 return;
77
78 auto requestOrException = requestFromInfo(WTFMove(info), options.ignoreMethod);
79 if (requestOrException.hasException()) {
80 callback(nullptr);
81 return;
82 }
83 auto request = requestOrException.releaseReturnValue();
84
85 queryCache(request.get(), WTFMove(options), [this, callback = WTFMove(callback)](ExceptionOr<Vector<CacheStorageRecord>>&& result) mutable {
86 if (result.hasException()) {
87 callback(result.releaseException());
88 return;
89 }
90 if (result.returnValue().isEmpty()) {
91 callback(nullptr);
92 return;
93 }
94 callback(result.returnValue()[0].response->clone(*scriptExecutionContext()).releaseReturnValue().ptr());
95 });
96}
97
98Vector<Ref<FetchResponse>> DOMCache::cloneResponses(const Vector<CacheStorageRecord>& records)
99{
100 auto& context = *scriptExecutionContext();
101 return WTF::map(records, [&context] (const auto& record) {
102 return record.response->clone(context).releaseReturnValue();
103 });
104}
105
106void DOMCache::matchAll(Optional<RequestInfo>&& info, CacheQueryOptions&& options, MatchAllPromise&& promise)
107{
108 if (UNLIKELY(!scriptExecutionContext()))
109 return;
110
111 RefPtr<FetchRequest> request;
112 if (info) {
113 auto requestOrException = requestFromInfo(WTFMove(info.value()), options.ignoreMethod);
114 if (requestOrException.hasException()) {
115 promise.resolve({ });
116 return;
117 }
118 request = requestOrException.releaseReturnValue();
119 }
120
121 if (!request) {
122 retrieveRecords(URL { }, [this, promise = WTFMove(promise)](Optional<Exception>&& exception) mutable {
123 if (exception) {
124 promise.reject(WTFMove(exception.value()));
125 return;
126 }
127 promise.resolve(cloneResponses(m_records));
128 });
129 return;
130 }
131 queryCache(request.releaseNonNull(), WTFMove(options), [this, promise = WTFMove(promise)](ExceptionOr<Vector<CacheStorageRecord>>&& result) mutable {
132 if (result.hasException()) {
133 promise.reject(result.releaseException());
134 return;
135 }
136 promise.resolve(cloneResponses(result.releaseReturnValue()));
137 });
138}
139
140void DOMCache::add(RequestInfo&& info, DOMPromiseDeferred<void>&& promise)
141{
142 addAll(Vector<RequestInfo> { WTFMove(info) }, WTFMove(promise));
143}
144
145static inline bool hasResponseVaryStarHeaderValue(const FetchResponse& response)
146{
147 auto varyValue = response.headers().internalHeaders().get(WebCore::HTTPHeaderName::Vary);
148 bool hasStar = false;
149 varyValue.split(',', [&](StringView view) {
150 if (!hasStar && stripLeadingAndTrailingHTTPSpaces(view) == "*")
151 hasStar = true;
152 });
153 return hasStar;
154}
155
156class FetchTasksHandler : public RefCounted<FetchTasksHandler> {
157public:
158 static Ref<FetchTasksHandler> create(Ref<DOMCache>&& domCache, CompletionHandler<void(ExceptionOr<Vector<Record>>&&)>&& callback) { return adoptRef(*new FetchTasksHandler(WTFMove(domCache), WTFMove(callback))); }
159
160 ~FetchTasksHandler()
161 {
162 if (m_callback)
163 m_callback(WTFMove(m_records));
164 }
165
166 const Vector<Record>& records() const { return m_records; }
167
168 size_t addRecord(Record&& record)
169 {
170 ASSERT(!isDone());
171 m_records.append(WTFMove(record));
172 return m_records.size() - 1;
173 }
174
175 void addResponseBody(size_t position, FetchResponse& response, DOMCacheEngine::ResponseBody&& data)
176 {
177 ASSERT(!isDone());
178 auto& record = m_records[position];
179 record.responseBodySize = m_domCache->connection().computeRecordBodySize(response, data);
180 record.responseBody = WTFMove(data);
181 }
182
183 bool isDone() const { return !m_callback; }
184
185 void error(Exception&& exception)
186 {
187 if (auto callback = WTFMove(m_callback))
188 callback(WTFMove(exception));
189 }
190
191private:
192 FetchTasksHandler(Ref<DOMCache>&& domCache, CompletionHandler<void(ExceptionOr<Vector<Record>>&&)>&& callback)
193 : m_domCache(WTFMove(domCache))
194 , m_callback(WTFMove(callback))
195 {
196 }
197
198 Ref<DOMCache> m_domCache;
199 Vector<Record> m_records;
200 CompletionHandler<void(ExceptionOr<Vector<Record>>&&)> m_callback;
201};
202
203ExceptionOr<Ref<FetchRequest>> DOMCache::requestFromInfo(RequestInfo&& info, bool ignoreMethod)
204{
205 RefPtr<FetchRequest> request;
206 if (WTF::holds_alternative<RefPtr<FetchRequest>>(info)) {
207 request = WTF::get<RefPtr<FetchRequest>>(info).releaseNonNull();
208 if (request->method() != "GET" && !ignoreMethod)
209 return Exception { TypeError, "Request method is not GET"_s };
210 } else
211 request = FetchRequest::create(*scriptExecutionContext(), WTFMove(info), { }).releaseReturnValue();
212
213 if (!protocolIsInHTTPFamily(request->url()))
214 return Exception { TypeError, "Request url is not HTTP/HTTPS"_s };
215
216 return request.releaseNonNull();
217}
218
219void DOMCache::addAll(Vector<RequestInfo>&& infos, DOMPromiseDeferred<void>&& promise)
220{
221 if (UNLIKELY(!scriptExecutionContext()))
222 return;
223
224 Vector<Ref<FetchRequest>> requests;
225 requests.reserveInitialCapacity(infos.size());
226 for (auto& info : infos) {
227 bool ignoreMethod = false;
228 auto requestOrException = requestFromInfo(WTFMove(info), ignoreMethod);
229 if (requestOrException.hasException()) {
230 promise.reject(requestOrException.releaseException());
231 return;
232 }
233 requests.uncheckedAppend(requestOrException.releaseReturnValue());
234 }
235
236 auto taskHandler = FetchTasksHandler::create(*this, [this, promise = WTFMove(promise)](ExceptionOr<Vector<Record>>&& result) mutable {
237 if (result.hasException()) {
238 promise.reject(result.releaseException());
239 return;
240 }
241 batchPutOperation(result.releaseReturnValue(), [promise = WTFMove(promise)](ExceptionOr<void>&& result) mutable {
242 promise.settle(WTFMove(result));
243 });
244 });
245
246 for (auto& request : requests) {
247 auto& requestReference = request.get();
248 FetchResponse::fetch(*scriptExecutionContext(), requestReference, [this, request = WTFMove(request), taskHandler = taskHandler.copyRef()](ExceptionOr<FetchResponse&>&& result) mutable {
249
250 if (taskHandler->isDone())
251 return;
252
253 if (result.hasException()) {
254 taskHandler->error(result.releaseException());
255 return;
256 }
257
258 auto& response = result.releaseReturnValue();
259
260 if (!response.ok()) {
261 taskHandler->error(Exception { TypeError, "Response is not OK"_s });
262 return;
263 }
264
265 if (hasResponseVaryStarHeaderValue(response)) {
266 taskHandler->error(Exception { TypeError, "Response has a '*' Vary header value"_s });
267 return;
268 }
269
270 if (response.status() == 206) {
271 taskHandler->error(Exception { TypeError, "Response is a 206 partial"_s });
272 return;
273 }
274
275 CacheQueryOptions options;
276 for (const auto& record : taskHandler->records()) {
277 if (DOMCacheEngine::queryCacheMatch(request->resourceRequest(), record.request, record.response, options)) {
278 taskHandler->error(Exception { InvalidStateError, "addAll cannot store several matching requests"_s});
279 return;
280 }
281 }
282 size_t recordPosition = taskHandler->addRecord(toConnectionRecord(request.get(), response, nullptr));
283
284 response.consumeBodyReceivedByChunk([taskHandler = WTFMove(taskHandler), recordPosition, data = SharedBuffer::create(), response = makeRef(response)] (ExceptionOr<ReadableStreamChunk*>&& result) mutable {
285 if (taskHandler->isDone())
286 return;
287
288 if (result.hasException()) {
289 taskHandler->error(result.releaseException());
290 return;
291 }
292
293 if (auto chunk = result.returnValue())
294 data->append(reinterpret_cast<const char*>(chunk->data), chunk->size);
295 else
296 taskHandler->addResponseBody(recordPosition, response, WTFMove(data));
297 });
298 });
299 }
300}
301
302void DOMCache::putWithResponseData(DOMPromiseDeferred<void>&& promise, Ref<FetchRequest>&& request, Ref<FetchResponse>&& response, ExceptionOr<RefPtr<SharedBuffer>>&& responseBody)
303{
304 if (responseBody.hasException()) {
305 promise.reject(responseBody.releaseException());
306 return;
307 }
308
309 DOMCacheEngine::ResponseBody body;
310 if (auto buffer = responseBody.releaseReturnValue())
311 body = buffer.releaseNonNull();
312 batchPutOperation(request.get(), response.get(), WTFMove(body), [promise = WTFMove(promise)](ExceptionOr<void>&& result) mutable {
313 promise.settle(WTFMove(result));
314 });
315}
316
317void DOMCache::put(RequestInfo&& info, Ref<FetchResponse>&& response, DOMPromiseDeferred<void>&& promise)
318{
319 if (UNLIKELY(!scriptExecutionContext()))
320 return;
321
322 bool ignoreMethod = false;
323 auto requestOrException = requestFromInfo(WTFMove(info), ignoreMethod);
324 if (requestOrException.hasException()) {
325 promise.reject(requestOrException.releaseException());
326 return;
327 }
328 auto request = requestOrException.releaseReturnValue();
329
330 if (auto exception = response->loadingException()) {
331 promise.reject(*exception);
332 return;
333 }
334
335 if (hasResponseVaryStarHeaderValue(response.get())) {
336 promise.reject(Exception { TypeError, "Response has a '*' Vary header value"_s });
337 return;
338 }
339
340 if (response->status() == 206) {
341 promise.reject(Exception { TypeError, "Response is a 206 partial"_s });
342 return;
343 }
344
345 if (response->isDisturbedOrLocked()) {
346 promise.reject(Exception { TypeError, "Response is disturbed or locked"_s });
347 return;
348 }
349
350 if (response->isBlobFormData()) {
351 promise.reject(Exception { NotSupportedError, "Not implemented"_s });
352 return;
353 }
354
355 // FIXME: for efficiency, we should load blobs directly instead of going through the readableStream path.
356 if (response->isBlobBody())
357 response->readableStream(*scriptExecutionContext()->execState());
358
359 if (response->isBodyReceivedByChunk()) {
360 auto& responseRef = response.get();
361 responseRef.consumeBodyReceivedByChunk([promise = WTFMove(promise), request = WTFMove(request), response = WTFMove(response), data = SharedBuffer::create(), pendingActivity = makePendingActivity(*this), this](auto&& result) mutable {
362
363 if (result.hasException()) {
364 this->putWithResponseData(WTFMove(promise), WTFMove(request), WTFMove(response), result.releaseException().isolatedCopy());
365 return;
366 }
367
368 if (auto chunk = result.returnValue())
369 data->append(reinterpret_cast<const char*>(chunk->data), chunk->size);
370 else
371 this->putWithResponseData(WTFMove(promise), WTFMove(request), WTFMove(response), RefPtr<SharedBuffer> { WTFMove(data) });
372 });
373 return;
374 }
375
376 batchPutOperation(request.get(), response.get(), response->consumeBody(), [promise = WTFMove(promise)](ExceptionOr<void>&& result) mutable {
377 promise.settle(WTFMove(result));
378 });
379}
380
381void DOMCache::remove(RequestInfo&& info, CacheQueryOptions&& options, DOMPromiseDeferred<IDLBoolean>&& promise)
382{
383 if (UNLIKELY(!scriptExecutionContext()))
384 return;
385
386 auto requestOrException = requestFromInfo(WTFMove(info), options.ignoreMethod);
387 if (requestOrException.hasException()) {
388 promise.resolve(false);
389 return;
390 }
391
392 batchDeleteOperation(requestOrException.releaseReturnValue(), WTFMove(options), [promise = WTFMove(promise)](ExceptionOr<bool>&& result) mutable {
393 promise.settle(WTFMove(result));
394 });
395}
396
397static inline Ref<FetchRequest> copyRequestRef(const CacheStorageRecord& record)
398{
399 return record.request.copyRef();
400}
401
402void DOMCache::keys(Optional<RequestInfo>&& info, CacheQueryOptions&& options, KeysPromise&& promise)
403{
404 if (UNLIKELY(!scriptExecutionContext()))
405 return;
406
407 RefPtr<FetchRequest> request;
408 if (info) {
409 auto requestOrException = requestFromInfo(WTFMove(info.value()), options.ignoreMethod);
410 if (requestOrException.hasException()) {
411 promise.resolve(Vector<Ref<FetchRequest>> { });
412 return;
413 }
414 request = requestOrException.releaseReturnValue();
415 }
416
417 if (!request) {
418 retrieveRecords(URL { }, [this, promise = WTFMove(promise)](Optional<Exception>&& exception) mutable {
419 if (exception) {
420 promise.reject(WTFMove(exception.value()));
421 return;
422 }
423 promise.resolve(WTF::map(m_records, copyRequestRef));
424 });
425 return;
426 }
427
428 queryCache(request.releaseNonNull(), WTFMove(options), [promise = WTFMove(promise)](ExceptionOr<Vector<CacheStorageRecord>>&& result) mutable {
429 if (result.hasException()) {
430 promise.reject(result.releaseException());
431 return;
432 }
433
434 promise.resolve(WTF::map(result.releaseReturnValue(), copyRequestRef));
435 });
436}
437
438void DOMCache::retrieveRecords(const URL& url, WTF::Function<void(Optional<Exception>&&)>&& callback)
439{
440 URL retrieveURL = url;
441 retrieveURL.removeQueryAndFragmentIdentifier();
442
443 m_connection->retrieveRecords(m_identifier, retrieveURL, [this, pendingActivity = makePendingActivity(*this), callback = WTFMove(callback)](RecordsOrError&& result) {
444 if (m_isStopped)
445 return;
446
447 if (!result.has_value()) {
448 callback(DOMCacheEngine::convertToExceptionAndLog(scriptExecutionContext(), result.error()));
449 return;
450 }
451
452 updateRecords(WTFMove(result.value()));
453 callback(WTF::nullopt);
454 });
455}
456
457void DOMCache::queryCache(Ref<FetchRequest>&& request, CacheQueryOptions&& options, WTF::Function<void(ExceptionOr<Vector<CacheStorageRecord>>&&)>&& callback)
458{
459 auto url = request->url();
460 retrieveRecords(url, [this, request = WTFMove(request), options = WTFMove(options), callback = WTFMove(callback)](Optional<Exception>&& exception) mutable {
461 if (exception) {
462 callback(WTFMove(exception.value()));
463 return;
464 }
465 callback(queryCacheWithTargetStorage(request.get(), options, m_records));
466 });
467}
468
469static inline bool queryCacheMatch(const FetchRequest& request, const FetchRequest& cachedRequest, const ResourceResponse& cachedResponse, const CacheQueryOptions& options)
470{
471 // We need to pass the resource request with all correct headers hence why we call resourceRequest().
472 return DOMCacheEngine::queryCacheMatch(request.resourceRequest(), cachedRequest.resourceRequest(), cachedResponse, options);
473}
474
475Vector<CacheStorageRecord> DOMCache::queryCacheWithTargetStorage(const FetchRequest& request, const CacheQueryOptions& options, const Vector<CacheStorageRecord>& targetStorage)
476{
477 if (!options.ignoreMethod && request.method() != "GET")
478 return { };
479
480 Vector<CacheStorageRecord> records;
481 for (auto& record : targetStorage) {
482 if (queryCacheMatch(request, record.request.get(), record.response->resourceResponse(), options))
483 records.append({ record.identifier, record.updateResponseCounter, record.request.copyRef(), record.response.copyRef() });
484 }
485 return records;
486}
487
488void DOMCache::batchDeleteOperation(const FetchRequest& request, CacheQueryOptions&& options, WTF::Function<void(ExceptionOr<bool>&&)>&& callback)
489{
490 m_connection->batchDeleteOperation(m_identifier, request.internalRequest(), WTFMove(options), [this, pendingActivity = makePendingActivity(*this), callback = WTFMove(callback)](RecordIdentifiersOrError&& result) {
491 if (m_isStopped)
492 return;
493
494 if (!result.has_value()) {
495 callback(DOMCacheEngine::convertToExceptionAndLog(scriptExecutionContext(), result.error()));
496 return;
497 }
498 callback(!result.value().isEmpty());
499 });
500}
501
502Record DOMCache::toConnectionRecord(const FetchRequest& request, FetchResponse& response, DOMCacheEngine::ResponseBody&& responseBody)
503{
504 auto cachedResponse = response.resourceResponse();
505 ResourceRequest cachedRequest = request.internalRequest();
506 cachedRequest.setHTTPHeaderFields(request.headers().internalHeaders());
507
508 ASSERT(!cachedRequest.isNull());
509 ASSERT(!cachedResponse.isNull());
510
511 auto sizeWithPadding = response.bodySizeWithPadding();
512 if (!sizeWithPadding) {
513 sizeWithPadding = m_connection->computeRecordBodySize(response, responseBody);
514 response.setBodySizeWithPadding(sizeWithPadding);
515 }
516
517 return { 0, 0,
518 request.headers().guard(), WTFMove(cachedRequest), request.fetchOptions(), request.internalRequestReferrer(),
519 response.headers().guard(), WTFMove(cachedResponse), WTFMove(responseBody), sizeWithPadding
520 };
521}
522
523void DOMCache::batchPutOperation(const FetchRequest& request, FetchResponse& response, DOMCacheEngine::ResponseBody&& responseBody, WTF::Function<void(ExceptionOr<void>&&)>&& callback)
524{
525 Vector<Record> records;
526 records.append(toConnectionRecord(request, response, WTFMove(responseBody)));
527
528 batchPutOperation(WTFMove(records), WTFMove(callback));
529}
530
531void DOMCache::batchPutOperation(Vector<Record>&& records, WTF::Function<void(ExceptionOr<void>&&)>&& callback)
532{
533 m_connection->batchPutOperation(m_identifier, WTFMove(records), [this, pendingActivity = makePendingActivity(*this), callback = WTFMove(callback)](RecordIdentifiersOrError&& result) {
534 if (m_isStopped)
535 return;
536 if (!result.has_value()) {
537 callback(DOMCacheEngine::convertToExceptionAndLog(scriptExecutionContext(), result.error()));
538 return;
539 }
540 callback({ });
541 });
542}
543
544void DOMCache::updateRecords(Vector<Record>&& records)
545{
546 ASSERT(scriptExecutionContext());
547 Vector<CacheStorageRecord> newRecords;
548
549 for (auto& record : records) {
550 size_t index = m_records.findMatching([&](const auto& item) { return item.identifier == record.identifier; });
551 if (index != notFound) {
552 auto& current = m_records[index];
553 if (current.updateResponseCounter != record.updateResponseCounter) {
554 auto response = FetchResponse::create(*scriptExecutionContext(), WTF::nullopt, record.responseHeadersGuard, WTFMove(record.response));
555 response->setBodyData(WTFMove(record.responseBody), record.responseBodySize);
556
557 current.response = WTFMove(response);
558 current.updateResponseCounter = record.updateResponseCounter;
559 }
560 newRecords.append(WTFMove(current));
561 } else {
562 auto requestHeaders = FetchHeaders::create(record.requestHeadersGuard, HTTPHeaderMap { record.request.httpHeaderFields() });
563 auto request = FetchRequest::create(*scriptExecutionContext(), WTF::nullopt, WTFMove(requestHeaders), WTFMove(record.request), WTFMove(record.options), WTFMove(record.referrer));
564
565 auto response = FetchResponse::create(*scriptExecutionContext(), WTF::nullopt, record.responseHeadersGuard, WTFMove(record.response));
566 response->setBodyData(WTFMove(record.responseBody), record.responseBodySize);
567
568 newRecords.append(CacheStorageRecord { record.identifier, record.updateResponseCounter, WTFMove(request), WTFMove(response) });
569 }
570 }
571 m_records = WTFMove(newRecords);
572}
573
574void DOMCache::stop()
575{
576 if (m_isStopped)
577 return;
578 m_isStopped = true;
579 m_connection->dereference(m_identifier);
580}
581
582const char* DOMCache::activeDOMObjectName() const
583{
584 return "Cache";
585}
586
587bool DOMCache::canSuspendForDocumentSuspension() const
588{
589 return m_records.isEmpty() && !hasPendingActivity();
590}
591
592
593} // namespace WebCore
594