[Cache API] Unify WebCore and WebKit error handling
[WebKit-https.git] / Source / WebCore / Modules / cache / Cache.cpp
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 "Cache.h"
28
29 #include "CacheQueryOptions.h"
30 #include "FetchResponse.h"
31 #include "HTTPParsers.h"
32 #include "JSFetchRequest.h"
33 #include "JSFetchResponse.h"
34 #include "ScriptExecutionContext.h"
35 #include "URL.h"
36
37 using namespace WebCore::DOMCache;
38
39 namespace WebCore {
40
41 static Record toConnectionRecord(const FetchRequest&, FetchResponse&, ResponseBody&&);
42
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))
48 {
49     suspendIfNeeded();
50 }
51
52 Cache::~Cache()
53 {
54 }
55
56 void Cache::match(RequestInfo&& info, CacheQueryOptions&& options, Ref<DeferredPromise>&& promise)
57 {
58     doMatch(WTFMove(info), WTFMove(options), [promise = WTFMove(promise)](FetchResponse* result) mutable {
59         if (!result) {
60             promise->resolve();
61             return;
62         }
63         promise->resolve<IDLInterface<FetchResponse>>(*result);
64     });
65 }
66
67 void Cache::doMatch(RequestInfo&& info, CacheQueryOptions&& options, MatchCallback&& callback)
68 {
69     if (UNLIKELY(!scriptExecutionContext()))
70         return;
71
72     auto requestOrException = requestFromInfo(WTFMove(info), options.ignoreMethod);
73     if (requestOrException.hasException()) {
74         callback(nullptr);
75         return;
76     }
77     auto request = requestOrException.releaseReturnValue();
78
79     queryCache(request.get(), WTFMove(options), [callback = WTFMove(callback)](const Vector<CacheStorageRecord>& records) mutable {
80         if (records.isEmpty()) {
81             callback(nullptr);
82             return;
83         }
84         callback(records[0].response->cloneForJS().ptr());
85     });
86 }
87
88 void Cache::matchAll(std::optional<RequestInfo>&& info, CacheQueryOptions&& options, MatchAllPromise&& promise)
89 {
90     if (UNLIKELY(!scriptExecutionContext()))
91         return;
92
93     RefPtr<FetchRequest> request;
94     if (info) {
95         auto requestOrException = requestFromInfo(WTFMove(info.value()), options.ignoreMethod);
96         if (requestOrException.hasException()) {
97             promise.resolve({ });
98             return;
99         }
100         request = requestOrException.releaseReturnValue();
101     }
102
103     if (!request) {
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);
110         });
111         return;
112     }
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);
119     });
120 }
121
122 void Cache::add(RequestInfo&& info, DOMPromiseDeferred<void>&& promise)
123 {
124     addAll(Vector<RequestInfo> { WTFMove(info) }, WTFMove(promise));
125 }
126
127 static inline bool hasResponseVaryStarHeaderValue(const FetchResponse& response)
128 {
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) == "*")
133             hasStar = true;
134     });
135     return hasStar;
136 }
137
138 class FetchTasksHandler : public RefCounted<FetchTasksHandler> {
139 public:
140     explicit FetchTasksHandler(Function<void(ExceptionOr<Vector<Record>>&&)>&& callback)
141         : m_callback(WTFMove(callback))
142     {
143     }
144
145     ~FetchTasksHandler()
146     {
147         if (m_callback)
148             m_callback(WTFMove(m_records));
149     }
150
151     const Vector<Record>& records() const { return m_records; }
152
153     size_t addRecord(Record&& record)
154     {
155         ASSERT(!isDone());
156         m_records.append(WTFMove(record));
157         return m_records.size() - 1;
158     }
159
160     void addResponseBody(size_t position, Ref<SharedBuffer>&& data)
161     {
162         ASSERT(!isDone());
163         m_records[position].responseBody = WTFMove(data);
164     }
165
166     bool isDone() const { return !m_callback; }
167
168     void error(Exception&& exception)
169     {
170         if (auto callback = WTFMove(m_callback))
171             callback(WTFMove(exception));
172     }
173
174 private:
175     Vector<Record> m_records;
176     Function<void(ExceptionOr<Vector<Record>>&&)> m_callback;
177 };
178
179 ExceptionOr<Ref<FetchRequest>> Cache::requestFromInfo(RequestInfo&& info, bool ignoreMethod)
180 {
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") };
186     } else
187         request = FetchRequest::create(*scriptExecutionContext(), WTFMove(info), { }).releaseReturnValue();
188
189     if (!protocolIsInHTTPFamily(request->url()))
190         return Exception { TypeError, ASCIILiteral("Request url is not HTTP/HTTPS") };
191
192     return request.releaseNonNull();
193 }
194
195 void Cache::addAll(Vector<RequestInfo>&& infos, DOMPromiseDeferred<void>&& promise)
196 {
197     if (UNLIKELY(!scriptExecutionContext()))
198         return;
199
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());
207             return;
208         }
209         requests.uncheckedAppend(requestOrException.releaseReturnValue());
210     }
211
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());
215             return;
216         }
217         batchPutOperation(result.releaseReturnValue(), [promise = WTFMove(promise)](ExceptionOr<void>&& result) mutable {
218             promise.settle(WTFMove(result));
219         });
220     }));
221
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 {
225
226             if (taskHandler->isDone())
227                 return;
228
229             if (result.hasException()) {
230                 taskHandler->error(result.releaseException());
231                 return;
232             }
233
234             auto& response = result.releaseReturnValue();
235
236             if (!response.ok()) {
237                 taskHandler->error(Exception { TypeError, ASCIILiteral("Response is not OK") });
238                 return;
239             }
240
241             if (hasResponseVaryStarHeaderValue(response)) {
242                 taskHandler->error(Exception { TypeError, ASCIILiteral("Response has a '*' Vary header value") });
243                 return;
244             }
245
246             if (response.status() == 206) {
247                 taskHandler->error(Exception { TypeError, ASCIILiteral("Response is a 206 partial") });
248                 return;
249             }
250
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")});
255                     return;
256                 }
257             }
258             size_t recordPosition = taskHandler->addRecord(toConnectionRecord(request.get(), response, nullptr));
259
260             response.consumeBodyWhenLoaded([taskHandler = WTFMove(taskHandler), recordPosition](ExceptionOr<RefPtr<SharedBuffer>>&& result) mutable {
261                 if (taskHandler->isDone())
262                     return;
263
264                 if (result.hasException()) {
265                     taskHandler->error(result.releaseException());
266                     return;
267                 }
268                 if (auto value = result.releaseReturnValue())
269                     taskHandler->addResponseBody(recordPosition, value.releaseNonNull());
270             });
271         });
272     }
273 }
274
275 void Cache::put(RequestInfo&& info, Ref<FetchResponse>&& response, DOMPromiseDeferred<void>&& promise)
276 {
277     if (UNLIKELY(!scriptExecutionContext()))
278         return;
279
280     bool ignoreMethod = false;
281     auto requestOrException = requestFromInfo(WTFMove(info), ignoreMethod);
282     if (requestOrException.hasException()) {
283         promise.reject(requestOrException.releaseException());
284         return;
285     }
286     auto request = requestOrException.releaseReturnValue();
287
288     if (hasResponseVaryStarHeaderValue(response.get())) {
289         promise.reject(Exception { TypeError, ASCIILiteral("Response has a '*' Vary header value") });
290         return;
291     }
292
293     if (response->status() == 206) {
294         promise.reject(Exception { TypeError, ASCIILiteral("Response is a 206 partial") });
295         return;
296     }
297
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") });
301         return;
302     }
303
304     if (response->isDisturbed()) {
305         promise.reject(Exception { TypeError, ASCIILiteral("Response is disturbed or locked") });
306         return;
307     }
308
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());
314             else {
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));
320                 });
321             }
322             unsetPendingActivity(this);
323         });
324         return;
325     }
326
327     batchPutOperation(request.get(), response.get(), response->consumeBody(), [promise = WTFMove(promise)](ExceptionOr<void>&& result) mutable {
328         promise.settle(WTFMove(result));
329     });
330 }
331
332 void Cache::remove(RequestInfo&& info, CacheQueryOptions&& options, DOMPromiseDeferred<IDLBoolean>&& promise)
333 {
334     if (UNLIKELY(!scriptExecutionContext()))
335         return;
336
337     auto requestOrException = requestFromInfo(WTFMove(info), options.ignoreMethod);
338     if (requestOrException.hasException()) {
339         promise.resolve(false);
340         return;
341     }
342
343     batchDeleteOperation(requestOrException.releaseReturnValue(), WTFMove(options), [promise = WTFMove(promise)](ExceptionOr<bool>&& result) mutable {
344         promise.settle(WTFMove(result));
345     });
346 }
347
348 void Cache::keys(std::optional<RequestInfo>&& info, CacheQueryOptions&& options, KeysPromise&& promise)
349 {
350     if (UNLIKELY(!scriptExecutionContext()))
351         return;
352
353     RefPtr<FetchRequest> request;
354     if (info) {
355         auto requestOrException = requestFromInfo(WTFMove(info.value()), options.ignoreMethod);
356         if (requestOrException.hasException()) {
357             promise.resolve(Vector<Ref<FetchRequest>> { });
358             return;
359         }
360         request = requestOrException.releaseReturnValue();
361     }
362
363     if (!request) {
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);
370         });
371         return;
372     }
373
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);
380     });
381 }
382
383 void Cache::retrieveRecords(WTF::Function<void()>&& callback)
384 {
385     setPendingActivity(this);
386     m_connection->retrieveRecords(m_identifier, [this, callback = WTFMove(callback)](RecordsOrError&& result) {
387         if (!m_isStopped) {
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()));
392             callback();
393         }
394         unsetPendingActivity(this);
395     });
396 }
397
398 void Cache::queryCache(Ref<FetchRequest>&& request, CacheQueryOptions&& options, WTF::Function<void(const Vector<CacheStorageRecord>&)>&& callback)
399 {
400     retrieveRecords([this, request = WTFMove(request), options = WTFMove(options), callback = WTFMove(callback)]() mutable {
401         callback(queryCacheWithTargetStorage(request.get(), options, m_records));
402     });
403 }
404
405 static inline bool queryCacheMatch(const FetchRequest& request, const FetchRequest& cachedRequest, const ResourceResponse& cachedResponse, const CacheQueryOptions& options)
406 {
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);
409 }
410
411 Vector<CacheStorageRecord> Cache::queryCacheWithTargetStorage(const FetchRequest& request, const CacheQueryOptions& options, const Vector<CacheStorageRecord>& targetStorage)
412 {
413     if (!options.ignoreMethod && request.method() != "GET")
414         return { };
415
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() });
420     }
421     return records;
422 }
423
424 void Cache::batchDeleteOperation(const FetchRequest& request, CacheQueryOptions&& options, WTF::Function<void(ExceptionOr<bool>&&)>&& callback)
425 {
426     setPendingActivity(this);
427     m_connection->batchDeleteOperation(m_identifier, request.internalRequest(), WTFMove(options), [this, callback = WTFMove(callback)](RecordIdentifiersOrError&& result) {
428         if (!m_isStopped) {
429             if (!result.hasValue())
430                 callback(DOMCache::errorToException(result.error()));
431             else
432                 callback(!result.value().isEmpty());
433         }
434         unsetPendingActivity(this);
435     });
436 }
437
438 Record toConnectionRecord(const FetchRequest& request, FetchResponse& response, DOMCache::ResponseBody&& responseBody)
439 {
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);
444
445     ResourceRequest cachedRequest = request.internalRequest();
446     cachedRequest.setHTTPHeaderFields(request.headers().internalHeaders());
447
448     ASSERT(!cachedRequest.isNull());
449     ASSERT(!cachedResponse.isNull());
450
451     return { 0, 0,
452         request.headers().guard(), WTFMove(cachedRequest), request.fetchOptions(), request.internalRequestReferrer(),
453         response.headers().guard(), WTFMove(cachedResponse), WTFMove(responseBody)
454     };
455 }
456
457 void Cache::batchPutOperation(const FetchRequest& request, FetchResponse& response, DOMCache::ResponseBody&& responseBody, WTF::Function<void(ExceptionOr<void>&&)>&& callback)
458 {
459     Vector<Record> records;
460     records.append(toConnectionRecord(request, response, WTFMove(responseBody)));
461
462     batchPutOperation(WTFMove(records), WTFMove(callback));
463 }
464
465 void Cache::batchPutOperation(Vector<Record>&& records, WTF::Function<void(ExceptionOr<void>&&)>&& callback)
466 {
467     setPendingActivity(this);
468     m_connection->batchPutOperation(m_identifier, WTFMove(records), [this, callback = WTFMove(callback)](RecordIdentifiersOrError&& result) {
469         if (!m_isStopped) {
470             if (!result.hasValue())
471                 callback(DOMCache::errorToException(result.error()));
472             else
473                 callback({ });
474         }
475         unsetPendingActivity(this);
476     });
477 }
478
479 void Cache::updateRecords(Vector<Record>&& records)
480 {
481     ASSERT(scriptExecutionContext());
482     Vector<CacheStorageRecord> newRecords;
483
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));
492
493                 current.response = WTFMove(response);
494                 current.updateResponseCounter = record.updateResponseCounter;
495             }
496             newRecords.append(WTFMove(current));
497         } else {
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));
501
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));
505
506             newRecords.append(CacheStorageRecord { record.identifier, record.updateResponseCounter, WTFMove(request), WTFMove(response) });
507         }
508     }
509     m_records = WTFMove(newRecords);
510 }
511
512 void Cache::stop()
513 {
514     m_isStopped = true;
515 }
516
517 const char* Cache::activeDOMObjectName() const
518 {
519     return "Cache";
520 }
521
522 bool Cache::canSuspendForDocumentSuspension() const
523 {
524     return m_records.isEmpty() && !hasPendingActivity();
525 }
526
527
528 } // namespace WebCore