[Cache API] Add support for overwriting responses with put on an existing record
[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 namespace WebCore {
38
39 static CacheStorageConnection::Record toConnectionRecord(const FetchRequest&, FetchResponse&, CacheStorageConnection::ResponseBody&&);
40
41 Cache::Cache(ScriptExecutionContext& context, String&& name, uint64_t identifier, Ref<CacheStorageConnection>&& connection)
42     : ActiveDOMObject(&context)
43     , m_name(WTFMove(name))
44     , m_identifier(identifier)
45     , m_connection(WTFMove(connection))
46 {
47     suspendIfNeeded();
48 }
49
50 Cache::~Cache()
51 {
52 }
53
54 void Cache::match(RequestInfo&& info, CacheQueryOptions&& options, Ref<DeferredPromise>&& promise)
55 {
56     doMatch(WTFMove(info), WTFMove(options), [promise = WTFMove(promise)](FetchResponse* result) mutable {
57         if (!result) {
58             promise->resolve();
59             return;
60         }
61         promise->resolve<IDLInterface<FetchResponse>>(*result);
62     });
63 }
64
65 void Cache::doMatch(RequestInfo&& info, CacheQueryOptions&& options, MatchCallback&& callback)
66 {
67     if (UNLIKELY(!scriptExecutionContext()))
68         return;
69
70     auto requestOrException = requestFromInfo(WTFMove(info), options.ignoreMethod);
71     if (requestOrException.hasException()) {
72         callback(nullptr);
73         return;
74     }
75     auto request = requestOrException.releaseReturnValue();
76
77     queryCache(request.get(), WTFMove(options), [callback = WTFMove(callback)](const Vector<CacheStorageRecord>& records) mutable {
78         if (records.isEmpty()) {
79             callback(nullptr);
80             return;
81         }
82         callback(records[0].response->cloneForJS().ptr());
83     });
84 }
85
86 void Cache::matchAll(std::optional<RequestInfo>&& info, CacheQueryOptions&& options, MatchAllPromise&& promise)
87 {
88     if (UNLIKELY(!scriptExecutionContext()))
89         return;
90
91     RefPtr<FetchRequest> request;
92     if (info) {
93         auto requestOrException = requestFromInfo(WTFMove(info.value()), options.ignoreMethod);
94         if (requestOrException.hasException()) {
95             promise.resolve({ });
96             return;
97         }
98         request = requestOrException.releaseReturnValue();
99     }
100
101     if (!request) {
102         retrieveRecords([this, promise = WTFMove(promise)]() mutable {
103             Vector<Ref<FetchResponse>> responses;
104             responses.reserveInitialCapacity(m_records.size());
105             for (auto& record : m_records)
106                 responses.uncheckedAppend(record.response->cloneForJS());
107             promise.resolve(responses);
108         });
109         return;
110     }
111     queryCache(request.releaseNonNull(), WTFMove(options), [promise = WTFMove(promise)](const Vector<CacheStorageRecord>& records) mutable {
112         Vector<Ref<FetchResponse>> responses;
113         responses.reserveInitialCapacity(records.size());
114         for (auto& record : records)
115             responses.uncheckedAppend(record.response->cloneForJS());
116         promise.resolve(responses);
117     });
118 }
119
120 void Cache::add(RequestInfo&& info, DOMPromiseDeferred<void>&& promise)
121 {
122     addAll(Vector<RequestInfo> { WTFMove(info) }, WTFMove(promise));
123 }
124
125 static inline bool hasResponseVaryStarHeaderValue(const FetchResponse& response)
126 {
127     auto varyValue = response.headers().internalHeaders().get(WebCore::HTTPHeaderName::Vary);
128     bool hasStar = false;
129     varyValue.split(',', false, [&](StringView view) {
130         if (!hasStar && stripLeadingAndTrailingHTTPSpaces(view.toStringWithoutCopying()) == "*")
131             hasStar = true;
132     });
133     return hasStar;
134 }
135
136 class FetchTasksHandler : public RefCounted<FetchTasksHandler> {
137 public:
138     explicit FetchTasksHandler(Function<void(ExceptionOr<Vector<CacheStorageConnection::Record>>&&)>&& callback)
139         : m_callback(WTFMove(callback))
140     {
141     }
142
143     ~FetchTasksHandler()
144     {
145         if (m_callback)
146             m_callback(WTFMove(m_records));
147     }
148
149     const Vector<CacheStorageConnection::Record>& records() const { return m_records; }
150
151     size_t addRecord(CacheStorageConnection::Record&& record)
152     {
153         ASSERT(!isDone());
154         m_records.append(WTFMove(record));
155         return m_records.size() - 1;
156     }
157
158     void addResponseBody(size_t position, Ref<SharedBuffer>&& data)
159     {
160         ASSERT(!isDone());
161         m_records[position].responseBody = WTFMove(data);
162     }
163
164     bool isDone() const { return !m_callback; }
165
166     void error(Exception&& exception)
167     {
168         if (auto callback = WTFMove(m_callback))
169             callback(WTFMove(exception));
170     }
171
172 private:
173     Vector<CacheStorageConnection::Record> m_records;
174     Function<void(ExceptionOr<Vector<CacheStorageConnection::Record>>&&)> m_callback;
175 };
176
177 ExceptionOr<Ref<FetchRequest>> Cache::requestFromInfo(RequestInfo&& info, bool ignoreMethod)
178 {
179     RefPtr<FetchRequest> request;
180     if (WTF::holds_alternative<RefPtr<FetchRequest>>(info)) {
181         request = WTF::get<RefPtr<FetchRequest>>(info).releaseNonNull();
182         if (request->method() != "GET" && !ignoreMethod)
183             return Exception { TypeError, ASCIILiteral("Request method is not GET") };
184     } else
185         request = FetchRequest::create(*scriptExecutionContext(), WTFMove(info), { }).releaseReturnValue();
186
187     if (!protocolIsInHTTPFamily(request->url()))
188         return Exception { TypeError, ASCIILiteral("Request url is not HTTP/HTTPS") };
189
190     return request.releaseNonNull();
191 }
192
193 void Cache::addAll(Vector<RequestInfo>&& infos, DOMPromiseDeferred<void>&& promise)
194 {
195     if (UNLIKELY(!scriptExecutionContext()))
196         return;
197
198     Vector<Ref<FetchRequest>> requests;
199     requests.reserveInitialCapacity(infos.size());
200     for (auto& info : infos) {
201         bool ignoreMethod = false;
202         auto requestOrException = requestFromInfo(WTFMove(info), ignoreMethod);
203         if (requestOrException.hasException()) {
204             promise.reject(requestOrException.releaseException());
205             return;
206         }
207         requests.uncheckedAppend(requestOrException.releaseReturnValue());
208     }
209
210     auto taskHandler = adoptRef(*new FetchTasksHandler([protectedThis = makeRef(*this), this, promise = WTFMove(promise)](ExceptionOr<Vector<CacheStorageConnection::Record>>&& result) mutable {
211         if (result.hasException()) {
212             promise.reject(result.releaseException());
213             return;
214         }
215         batchPutOperation(result.releaseReturnValue(), [promise = WTFMove(promise)](ExceptionOr<void>&& result) mutable {
216             promise.settle(WTFMove(result));
217         });
218     }));
219
220     for (auto& request : requests) {
221         auto& requestReference = request.get();
222         FetchResponse::fetch(*scriptExecutionContext(), requestReference, [this, request = WTFMove(request), taskHandler = taskHandler.copyRef()](ExceptionOr<FetchResponse&>&& result) mutable {
223
224             if (taskHandler->isDone())
225                 return;
226
227             if (result.hasException()) {
228                 taskHandler->error(result.releaseException());
229                 return;
230             }
231
232             auto& response = result.releaseReturnValue();
233
234             if (!response.ok()) {
235                 taskHandler->error(Exception { TypeError, ASCIILiteral("Response is not OK") });
236                 return;
237             }
238
239             if (hasResponseVaryStarHeaderValue(response)) {
240                 taskHandler->error(Exception { TypeError, ASCIILiteral("Response has a '*' Vary header value") });
241                 return;
242             }
243
244             if (response.status() == 206) {
245                 taskHandler->error(Exception { TypeError, ASCIILiteral("Response is a 206 partial") });
246                 return;
247             }
248
249             CacheQueryOptions options;
250             for (const auto& record : taskHandler->records()) {
251                 if (CacheStorageConnection::queryCacheMatch(request->resourceRequest(), record.request, record.response, options)) {
252                     taskHandler->error(Exception { InvalidStateError, ASCIILiteral("addAll cannot store several matching requests")});
253                     return;
254                 }
255             }
256             size_t recordPosition = taskHandler->addRecord(toConnectionRecord(request.get(), response, nullptr));
257
258             response.consumeBodyWhenLoaded([taskHandler = WTFMove(taskHandler), recordPosition](ExceptionOr<RefPtr<SharedBuffer>>&& result) mutable {
259                 if (taskHandler->isDone())
260                     return;
261
262                 if (result.hasException()) {
263                     taskHandler->error(result.releaseException());
264                     return;
265                 }
266                 if (auto value = result.releaseReturnValue())
267                     taskHandler->addResponseBody(recordPosition, value.releaseNonNull());
268             });
269         });
270     }
271 }
272
273 void Cache::put(RequestInfo&& info, Ref<FetchResponse>&& response, DOMPromiseDeferred<void>&& promise)
274 {
275     if (UNLIKELY(!scriptExecutionContext()))
276         return;
277
278     bool ignoreMethod = false;
279     auto requestOrException = requestFromInfo(WTFMove(info), ignoreMethod);
280     if (requestOrException.hasException()) {
281         promise.reject(requestOrException.releaseException());
282         return;
283     }
284     auto request = requestOrException.releaseReturnValue();
285
286     if (hasResponseVaryStarHeaderValue(response.get())) {
287         promise.reject(Exception { TypeError, ASCIILiteral("Response has a '*' Vary header value") });
288         return;
289     }
290
291     if (response->status() == 206) {
292         promise.reject(Exception { TypeError, ASCIILiteral("Response is a 206 partial") });
293         return;
294     }
295
296     // FIXME: Add support for ReadableStream.
297     if (response->isReadableStreamBody()) {
298         promise.reject(Exception { NotSupportedError, ASCIILiteral("Caching a Response with data stored in a ReadableStream is not yet supported") });
299         return;
300     }
301
302     if (response->isDisturbed()) {
303         promise.reject(Exception { TypeError, ASCIILiteral("Response is disturbed or locked") });
304         return;
305     }
306
307     if (response->isLoading()) {
308         setPendingActivity(this);
309         response->consumeBodyWhenLoaded([promise = WTFMove(promise), request = WTFMove(request), response = WTFMove(response), this](ExceptionOr<RefPtr<SharedBuffer>>&& result) mutable {
310             if (result.hasException())
311                 promise.reject(result.releaseException());
312             else {
313                 CacheStorageConnection::ResponseBody body;
314                 if (auto buffer = result.releaseReturnValue())
315                     body = buffer.releaseNonNull();
316                 batchPutOperation(request.get(), response.get(), WTFMove(body), [promise = WTFMove(promise)](ExceptionOr<void>&& result) mutable {
317                     promise.settle(WTFMove(result));
318                 });
319             }
320             unsetPendingActivity(this);
321         });
322         return;
323     }
324
325     batchPutOperation(request.get(), response.get(), response->consumeBody(), [promise = WTFMove(promise)](ExceptionOr<void>&& result) mutable {
326         promise.settle(WTFMove(result));
327     });
328 }
329
330 void Cache::remove(RequestInfo&& info, CacheQueryOptions&& options, DOMPromiseDeferred<IDLBoolean>&& promise)
331 {
332     if (UNLIKELY(!scriptExecutionContext()))
333         return;
334
335     auto requestOrException = requestFromInfo(WTFMove(info), options.ignoreMethod);
336     if (requestOrException.hasException()) {
337         promise.resolve(false);
338         return;
339     }
340
341     batchDeleteOperation(requestOrException.releaseReturnValue(), WTFMove(options), [promise = WTFMove(promise)](ExceptionOr<bool>&& result) mutable {
342         promise.settle(WTFMove(result));
343     });
344 }
345
346 void Cache::keys(std::optional<RequestInfo>&& info, CacheQueryOptions&& options, KeysPromise&& promise)
347 {
348     if (UNLIKELY(!scriptExecutionContext()))
349         return;
350
351     RefPtr<FetchRequest> request;
352     if (info) {
353         auto requestOrException = requestFromInfo(WTFMove(info.value()), options.ignoreMethod);
354         if (requestOrException.hasException()) {
355             promise.resolve(Vector<Ref<FetchRequest>> { });
356             return;
357         }
358         request = requestOrException.releaseReturnValue();
359     }
360
361     if (!request) {
362         retrieveRecords([this, promise = WTFMove(promise)]() mutable {
363             Vector<Ref<FetchRequest>> requests;
364             requests.reserveInitialCapacity(m_records.size());
365             for (auto& record : m_records)
366                 requests.uncheckedAppend(record.request.copyRef());
367             promise.resolve(requests);
368         });
369         return;
370     }
371
372     queryCache(request.releaseNonNull(), WTFMove(options), [promise = WTFMove(promise)](const Vector<CacheStorageRecord>& records) mutable {
373         Vector<Ref<FetchRequest>> requests;
374         requests.reserveInitialCapacity(records.size());
375         for (auto& record : records)
376             requests.uncheckedAppend(record.request.copyRef());
377         promise.resolve(requests);
378     });
379 }
380
381 void Cache::retrieveRecords(WTF::Function<void()>&& callback)
382 {
383     setPendingActivity(this);
384     m_connection->retrieveRecords(m_identifier, [this, callback = WTFMove(callback)](Vector<CacheStorageConnection::Record>&& records) {
385         if (!m_isStopped) {
386             updateRecords(WTFMove(records));
387             callback();
388         }
389         unsetPendingActivity(this);
390     });
391 }
392
393 void Cache::queryCache(Ref<FetchRequest>&& request, CacheQueryOptions&& options, WTF::Function<void(const Vector<CacheStorageRecord>&)>&& callback)
394 {
395     retrieveRecords([this, request = WTFMove(request), options = WTFMove(options), callback = WTFMove(callback)]() mutable {
396         callback(queryCacheWithTargetStorage(request.get(), options, m_records));
397     });
398 }
399
400 static inline bool queryCacheMatch(const FetchRequest& request, const FetchRequest& cachedRequest, const ResourceResponse& cachedResponse, const CacheQueryOptions& options)
401 {
402     // We need to pass the resource request with all correct headers hence why we call resourceRequest().
403     return CacheStorageConnection::queryCacheMatch(request.resourceRequest(), cachedRequest.resourceRequest(), cachedResponse, options);
404 }
405
406 Vector<CacheStorageRecord> Cache::queryCacheWithTargetStorage(const FetchRequest& request, const CacheQueryOptions& options, const Vector<CacheStorageRecord>& targetStorage)
407 {
408     if (!options.ignoreMethod && request.method() != "GET")
409         return { };
410
411     Vector<CacheStorageRecord> records;
412     for (auto& record : targetStorage) {
413         if (queryCacheMatch(request, record.request.get(), record.response->resourceResponse(), options))
414             records.append({ record.identifier, record.updateResponseCounter, record.request.copyRef(), record.response.copyRef() });
415     }
416     return records;
417 }
418
419 void Cache::batchDeleteOperation(const FetchRequest& request, CacheQueryOptions&& options, WTF::Function<void(ExceptionOr<bool>&&)>&& callback)
420 {
421     setPendingActivity(this);
422     m_connection->batchDeleteOperation(m_identifier, request.internalRequest(), WTFMove(options), [this, callback = WTFMove(callback)](Vector<uint64_t>&& records, CacheStorageConnection::Error error) {
423         if (!m_isStopped)
424             callback(CacheStorageConnection::exceptionOrResult(!records.isEmpty(), error));
425
426         unsetPendingActivity(this);
427     });
428 }
429
430 CacheStorageConnection::Record toConnectionRecord(const FetchRequest& request, FetchResponse& response, CacheStorageConnection::ResponseBody&& responseBody)
431 {
432     // FIXME: Add a setHTTPHeaderFields on ResourceResponseBase.
433     ResourceResponse cachedResponse = response.resourceResponse();
434     for (auto& header : response.headers().internalHeaders())
435         cachedResponse.setHTTPHeaderField(header.key, header.value);
436
437     ResourceRequest cachedRequest = request.internalRequest();
438     cachedRequest.setHTTPHeaderFields(request.headers().internalHeaders());
439
440     ASSERT(!cachedRequest.isNull());
441     ASSERT(!cachedResponse.isNull());
442
443     return { 0, 0,
444         request.headers().guard(), WTFMove(cachedRequest), request.fetchOptions(), request.internalRequestReferrer(),
445         response.headers().guard(), WTFMove(cachedResponse), WTFMove(responseBody)
446     };
447 }
448
449 void Cache::batchPutOperation(const FetchRequest& request, FetchResponse& response, CacheStorageConnection::ResponseBody&& responseBody, WTF::Function<void(ExceptionOr<void>&&)>&& callback)
450 {
451     Vector<CacheStorageConnection::Record> records;
452     records.append(toConnectionRecord(request, response, WTFMove(responseBody)));
453
454     batchPutOperation(WTFMove(records), WTFMove(callback));
455 }
456
457 void Cache::batchPutOperation(Vector<CacheStorageConnection::Record>&& records, WTF::Function<void(ExceptionOr<void>&&)>&& callback)
458 {
459     setPendingActivity(this);
460     m_connection->batchPutOperation(m_identifier, WTFMove(records), [this, callback = WTFMove(callback)](Vector<uint64_t>&&, CacheStorageConnection::Error error) {
461         if (!m_isStopped)
462             callback(CacheStorageConnection::errorToException(error));
463
464         unsetPendingActivity(this);
465     });
466 }
467
468 void Cache::updateRecords(Vector<CacheStorageConnection::Record>&& records)
469 {
470     ASSERT(scriptExecutionContext());
471     Vector<CacheStorageRecord> newRecords;
472
473     for (auto& record : records) {
474         size_t index = m_records.findMatching([&](const auto& item) { return item.identifier == record.identifier; });
475         if (index != notFound) {
476             auto& current = m_records[index];
477             if (current.updateResponseCounter != record.updateResponseCounter) {
478                 auto responseHeaders = FetchHeaders::create(record.responseHeadersGuard, HTTPHeaderMap { record.response.httpHeaderFields() });
479                 auto response = FetchResponse::create(*scriptExecutionContext(), std::nullopt, WTFMove(responseHeaders), WTFMove(record.response));
480                 response->setBodyData(WTFMove(record.responseBody));
481
482                 current.response = WTFMove(response);
483                 current.updateResponseCounter = record.updateResponseCounter;
484             }
485             newRecords.append(WTFMove(current));
486         } else {
487             auto requestHeaders = FetchHeaders::create(record.requestHeadersGuard, HTTPHeaderMap { record.request.httpHeaderFields() });
488             FetchRequest::InternalRequest internalRequest = { WTFMove(record.request), WTFMove(record.options), WTFMove(record.referrer) };
489             auto request = FetchRequest::create(*scriptExecutionContext(), std::nullopt, WTFMove(requestHeaders), WTFMove(internalRequest));
490
491             auto responseHeaders = FetchHeaders::create(record.responseHeadersGuard, HTTPHeaderMap { record.response.httpHeaderFields() });
492             auto response = FetchResponse::create(*scriptExecutionContext(), std::nullopt, WTFMove(responseHeaders), WTFMove(record.response));
493             response->setBodyData(WTFMove(record.responseBody));
494
495             newRecords.append(CacheStorageRecord { record.identifier, record.updateResponseCounter, WTFMove(request), WTFMove(response) });
496         }
497     }
498     m_records = WTFMove(newRecords);
499 }
500
501 void Cache::stop()
502 {
503     m_isStopped = true;
504 }
505
506 const char* Cache::activeDOMObjectName() const
507 {
508     return "Cache";
509 }
510
511 bool Cache::canSuspendForDocumentSuspension() const
512 {
513     return m_records.isEmpty() && !hasPendingActivity();
514 }
515
516
517 } // namespace WebCore