Move URL from WebCore to WTF
[WebKit-https.git] / Source / WebCore / Modules / cache / DOMCache.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 "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/URL.h>
37
38
39 namespace WebCore {
40 using namespace WebCore::DOMCacheEngine;
41
42 DOMCache::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
52 DOMCache::~DOMCache()
53 {
54     if (!m_isStopped)
55         m_connection->dereference(m_identifier);
56 }
57
58 void 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
73 void 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
98 Vector<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
106 void DOMCache::matchAll(std::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)](std::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
140 void DOMCache::add(RequestInfo&& info, DOMPromiseDeferred<void>&& promise)
141 {
142     addAll(Vector<RequestInfo> { WTFMove(info) }, WTFMove(promise));
143 }
144
145 static 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
156 class FetchTasksHandler : public RefCounted<FetchTasksHandler> {
157 public:
158     explicit FetchTasksHandler(Function<void(ExceptionOr<Vector<Record>>&&)>&& callback)
159         : m_callback(WTFMove(callback))
160     {
161     }
162
163     ~FetchTasksHandler()
164     {
165         if (m_callback)
166             m_callback(WTFMove(m_records));
167     }
168
169     const Vector<Record>& records() const { return m_records; }
170
171     size_t addRecord(Record&& record)
172     {
173         ASSERT(!isDone());
174         m_records.append(WTFMove(record));
175         return m_records.size() - 1;
176     }
177
178     void addResponseBody(size_t position, Ref<SharedBuffer>&& data)
179     {
180         ASSERT(!isDone());
181         m_records[position].responseBody = WTFMove(data);
182     }
183
184     bool isDone() const { return !m_callback; }
185
186     void error(Exception&& exception)
187     {
188         if (auto callback = WTFMove(m_callback))
189             callback(WTFMove(exception));
190     }
191
192 private:
193     Vector<Record> m_records;
194     Function<void(ExceptionOr<Vector<Record>>&&)> m_callback;
195 };
196
197 ExceptionOr<Ref<FetchRequest>> DOMCache::requestFromInfo(RequestInfo&& info, bool ignoreMethod)
198 {
199     RefPtr<FetchRequest> request;
200     if (WTF::holds_alternative<RefPtr<FetchRequest>>(info)) {
201         request = WTF::get<RefPtr<FetchRequest>>(info).releaseNonNull();
202         if (request->method() != "GET" && !ignoreMethod)
203             return Exception { TypeError, "Request method is not GET"_s };
204     } else
205         request = FetchRequest::create(*scriptExecutionContext(), WTFMove(info), { }).releaseReturnValue();
206
207     if (!protocolIsInHTTPFamily(request->url()))
208         return Exception { TypeError, "Request url is not HTTP/HTTPS"_s };
209
210     return request.releaseNonNull();
211 }
212
213 void DOMCache::addAll(Vector<RequestInfo>&& infos, DOMPromiseDeferred<void>&& promise)
214 {
215     if (UNLIKELY(!scriptExecutionContext()))
216         return;
217
218     Vector<Ref<FetchRequest>> requests;
219     requests.reserveInitialCapacity(infos.size());
220     for (auto& info : infos) {
221         bool ignoreMethod = false;
222         auto requestOrException = requestFromInfo(WTFMove(info), ignoreMethod);
223         if (requestOrException.hasException()) {
224             promise.reject(requestOrException.releaseException());
225             return;
226         }
227         requests.uncheckedAppend(requestOrException.releaseReturnValue());
228     }
229
230     auto taskHandler = adoptRef(*new FetchTasksHandler([protectedThis = makeRef(*this), this, promise = WTFMove(promise)](ExceptionOr<Vector<Record>>&& result) mutable {
231         if (result.hasException()) {
232             promise.reject(result.releaseException());
233             return;
234         }
235         batchPutOperation(result.releaseReturnValue(), [promise = WTFMove(promise)](ExceptionOr<void>&& result) mutable {
236             promise.settle(WTFMove(result));
237         });
238     }));
239
240     for (auto& request : requests) {
241         auto& requestReference = request.get();
242         FetchResponse::fetch(*scriptExecutionContext(), requestReference, [this, request = WTFMove(request), taskHandler = taskHandler.copyRef()](ExceptionOr<FetchResponse&>&& result) mutable {
243
244             if (taskHandler->isDone())
245                 return;
246
247             if (result.hasException()) {
248                 taskHandler->error(result.releaseException());
249                 return;
250             }
251
252             auto& response = result.releaseReturnValue();
253
254             if (!response.ok()) {
255                 taskHandler->error(Exception { TypeError, "Response is not OK"_s });
256                 return;
257             }
258
259             if (hasResponseVaryStarHeaderValue(response)) {
260                 taskHandler->error(Exception { TypeError, "Response has a '*' Vary header value"_s });
261                 return;
262             }
263
264             if (response.status() == 206) {
265                 taskHandler->error(Exception { TypeError, "Response is a 206 partial"_s });
266                 return;
267             }
268
269             CacheQueryOptions options;
270             for (const auto& record : taskHandler->records()) {
271                 if (DOMCacheEngine::queryCacheMatch(request->resourceRequest(), record.request, record.response, options)) {
272                     taskHandler->error(Exception { InvalidStateError, "addAll cannot store several matching requests"_s});
273                     return;
274                 }
275             }
276             size_t recordPosition = taskHandler->addRecord(toConnectionRecord(request.get(), response, nullptr));
277
278             response.consumeBodyReceivedByChunk([taskHandler = WTFMove(taskHandler), recordPosition, data = SharedBuffer::create()] (ExceptionOr<ReadableStreamChunk*>&& result) mutable {
279                 if (taskHandler->isDone())
280                     return;
281
282                 if (result.hasException()) {
283                     taskHandler->error(result.releaseException());
284                     return;
285                 }
286
287                 if (auto chunk = result.returnValue())
288                     data->append(reinterpret_cast<const char*>(chunk->data), chunk->size);
289                 else
290                     taskHandler->addResponseBody(recordPosition, WTFMove(data));
291             });
292         });
293     }
294 }
295
296 void DOMCache::putWithResponseData(DOMPromiseDeferred<void>&& promise, Ref<FetchRequest>&& request, Ref<FetchResponse>&& response, ExceptionOr<RefPtr<SharedBuffer>>&& responseBody)
297 {
298     if (responseBody.hasException()) {
299         promise.reject(responseBody.releaseException());
300         return;
301     }
302
303     DOMCacheEngine::ResponseBody body;
304     if (auto buffer = responseBody.releaseReturnValue())
305         body = buffer.releaseNonNull();
306     batchPutOperation(request.get(), response.get(), WTFMove(body), [promise = WTFMove(promise)](ExceptionOr<void>&& result) mutable {
307         promise.settle(WTFMove(result));
308     });
309 }
310
311 void DOMCache::put(RequestInfo&& info, Ref<FetchResponse>&& response, DOMPromiseDeferred<void>&& promise)
312 {
313     if (UNLIKELY(!scriptExecutionContext()))
314         return;
315
316     bool ignoreMethod = false;
317     auto requestOrException = requestFromInfo(WTFMove(info), ignoreMethod);
318     if (requestOrException.hasException()) {
319         promise.reject(requestOrException.releaseException());
320         return;
321     }
322     auto request = requestOrException.releaseReturnValue();
323
324     if (response->loadingError()) {
325         promise.reject(Exception { TypeError, response->loadingError()->localizedDescription() });
326         return;
327     }
328
329     if (hasResponseVaryStarHeaderValue(response.get())) {
330         promise.reject(Exception { TypeError, "Response has a '*' Vary header value"_s });
331         return;
332     }
333
334     if (response->status() == 206) {
335         promise.reject(Exception { TypeError, "Response is a 206 partial"_s });
336         return;
337     }
338
339     if (response->isDisturbedOrLocked()) {
340         promise.reject(Exception { TypeError, "Response is disturbed or locked"_s });
341         return;
342     }
343
344     if (response->isBlobFormData()) {
345         promise.reject(Exception { NotSupportedError, "Not implemented"_s });
346         return;
347     }
348
349     // FIXME: for efficiency, we should load blobs directly instead of going through the readableStream path.
350     if (response->isBlobBody())
351         response->readableStream(*scriptExecutionContext()->execState());
352
353     if (response->isBodyReceivedByChunk()) {
354         auto& responseRef = response.get();
355         responseRef.consumeBodyReceivedByChunk([promise = WTFMove(promise), request = WTFMove(request), response = WTFMove(response), data = SharedBuffer::create(), pendingActivity = makePendingActivity(*this), this](auto&& result) mutable {
356
357             if (result.hasException()) {
358                 this->putWithResponseData(WTFMove(promise), WTFMove(request), WTFMove(response), result.releaseException().isolatedCopy());
359                 return;
360             }
361
362             if (auto chunk = result.returnValue())
363                 data->append(reinterpret_cast<const char*>(chunk->data), chunk->size);
364             else
365                 this->putWithResponseData(WTFMove(promise), WTFMove(request), WTFMove(response), RefPtr<SharedBuffer> { WTFMove(data) });
366         });
367         return;
368     }
369
370     batchPutOperation(request.get(), response.get(), response->consumeBody(), [promise = WTFMove(promise)](ExceptionOr<void>&& result) mutable {
371         promise.settle(WTFMove(result));
372     });
373 }
374
375 void DOMCache::remove(RequestInfo&& info, CacheQueryOptions&& options, DOMPromiseDeferred<IDLBoolean>&& promise)
376 {
377     if (UNLIKELY(!scriptExecutionContext()))
378         return;
379
380     auto requestOrException = requestFromInfo(WTFMove(info), options.ignoreMethod);
381     if (requestOrException.hasException()) {
382         promise.resolve(false);
383         return;
384     }
385
386     batchDeleteOperation(requestOrException.releaseReturnValue(), WTFMove(options), [promise = WTFMove(promise)](ExceptionOr<bool>&& result) mutable {
387         promise.settle(WTFMove(result));
388     });
389 }
390
391 static inline Ref<FetchRequest> copyRequestRef(const CacheStorageRecord& record)
392 {
393     return record.request.copyRef();
394 }
395
396 void DOMCache::keys(std::optional<RequestInfo>&& info, CacheQueryOptions&& options, KeysPromise&& promise)
397 {
398     if (UNLIKELY(!scriptExecutionContext()))
399         return;
400
401     RefPtr<FetchRequest> request;
402     if (info) {
403         auto requestOrException = requestFromInfo(WTFMove(info.value()), options.ignoreMethod);
404         if (requestOrException.hasException()) {
405             promise.resolve(Vector<Ref<FetchRequest>> { });
406             return;
407         }
408         request = requestOrException.releaseReturnValue();
409     }
410
411     if (!request) {
412         retrieveRecords(URL { }, [this, promise = WTFMove(promise)](std::optional<Exception>&& exception) mutable {
413             if (exception) {
414                 promise.reject(WTFMove(exception.value()));
415                 return;
416             }
417             promise.resolve(WTF::map(m_records, copyRequestRef));
418         });
419         return;
420     }
421
422     queryCache(request.releaseNonNull(), WTFMove(options), [promise = WTFMove(promise)](ExceptionOr<Vector<CacheStorageRecord>>&& result) mutable {
423         if (result.hasException()) {
424             promise.reject(result.releaseException());
425             return;
426         }
427
428         promise.resolve(WTF::map(result.releaseReturnValue(), copyRequestRef));
429     });
430 }
431
432 void DOMCache::retrieveRecords(const URL& url, WTF::Function<void(std::optional<Exception>&&)>&& callback)
433 {
434     setPendingActivity(this);
435
436     URL retrieveURL = url;
437     retrieveURL.removeQueryAndFragmentIdentifier();
438
439     m_connection->retrieveRecords(m_identifier, retrieveURL, [this, callback = WTFMove(callback)](RecordsOrError&& result) {
440         if (!m_isStopped) {
441             if (!result.has_value()) {
442                 callback(DOMCacheEngine::convertToExceptionAndLog(scriptExecutionContext(), result.error()));
443                 return;
444             }
445
446             if (result.has_value())
447                 updateRecords(WTFMove(result.value()));
448             callback(std::nullopt);
449         }
450         unsetPendingActivity(this);
451     });
452 }
453
454 void DOMCache::queryCache(Ref<FetchRequest>&& request, CacheQueryOptions&& options, WTF::Function<void(ExceptionOr<Vector<CacheStorageRecord>>&&)>&& callback)
455 {
456     auto url = request->url();
457     retrieveRecords(url, [this, request = WTFMove(request), options = WTFMove(options), callback = WTFMove(callback)](std::optional<Exception>&& exception) mutable {
458         if (exception) {
459             callback(WTFMove(exception.value()));
460             return;
461         }
462         callback(queryCacheWithTargetStorage(request.get(), options, m_records));
463     });
464 }
465
466 static inline bool queryCacheMatch(const FetchRequest& request, const FetchRequest& cachedRequest, const ResourceResponse& cachedResponse, const CacheQueryOptions& options)
467 {
468     // We need to pass the resource request with all correct headers hence why we call resourceRequest().
469     return DOMCacheEngine::queryCacheMatch(request.resourceRequest(), cachedRequest.resourceRequest(), cachedResponse, options);
470 }
471
472 Vector<CacheStorageRecord> DOMCache::queryCacheWithTargetStorage(const FetchRequest& request, const CacheQueryOptions& options, const Vector<CacheStorageRecord>& targetStorage)
473 {
474     if (!options.ignoreMethod && request.method() != "GET")
475         return { };
476
477     Vector<CacheStorageRecord> records;
478     for (auto& record : targetStorage) {
479         if (queryCacheMatch(request, record.request.get(), record.response->resourceResponse(), options))
480             records.append({ record.identifier, record.updateResponseCounter, record.request.copyRef(), record.response.copyRef() });
481     }
482     return records;
483 }
484
485 void DOMCache::batchDeleteOperation(const FetchRequest& request, CacheQueryOptions&& options, WTF::Function<void(ExceptionOr<bool>&&)>&& callback)
486 {
487     setPendingActivity(this);
488     m_connection->batchDeleteOperation(m_identifier, request.internalRequest(), WTFMove(options), [this, callback = WTFMove(callback)](RecordIdentifiersOrError&& result) {
489         if (!m_isStopped) {
490             if (!result.has_value())
491                 callback(DOMCacheEngine::convertToExceptionAndLog(scriptExecutionContext(), result.error()));
492             else
493                 callback(!result.value().isEmpty());
494         }
495         unsetPendingActivity(this);
496     });
497 }
498
499 Record DOMCache::toConnectionRecord(const FetchRequest& request, FetchResponse& response, DOMCacheEngine::ResponseBody&& responseBody)
500 {
501     auto cachedResponse = response.resourceResponse();
502     ResourceRequest cachedRequest = request.internalRequest();
503     cachedRequest.setHTTPHeaderFields(request.headers().internalHeaders());
504
505     ASSERT(!cachedRequest.isNull());
506     ASSERT(!cachedResponse.isNull());
507
508     auto sizeWithPadding = response.bodySizeWithPadding();
509     if (!sizeWithPadding) {
510         sizeWithPadding = m_connection->computeRecordBodySize(response, responseBody, cachedResponse.tainting());
511         response.setBodySizeWithPadding(sizeWithPadding);
512     }
513
514     return { 0, 0,
515         request.headers().guard(), WTFMove(cachedRequest), request.fetchOptions(), request.internalRequestReferrer(),
516         response.headers().guard(), WTFMove(cachedResponse), WTFMove(responseBody), sizeWithPadding
517     };
518 }
519
520 void DOMCache::batchPutOperation(const FetchRequest& request, FetchResponse& response, DOMCacheEngine::ResponseBody&& responseBody, WTF::Function<void(ExceptionOr<void>&&)>&& callback)
521 {
522     Vector<Record> records;
523     records.append(toConnectionRecord(request, response, WTFMove(responseBody)));
524
525     batchPutOperation(WTFMove(records), WTFMove(callback));
526 }
527
528 void DOMCache::batchPutOperation(Vector<Record>&& records, WTF::Function<void(ExceptionOr<void>&&)>&& callback)
529 {
530     setPendingActivity(this);
531     m_connection->batchPutOperation(m_identifier, WTFMove(records), [this, callback = WTFMove(callback)](RecordIdentifiersOrError&& result) {
532         if (!m_isStopped) {
533             if (!result.has_value())
534                 callback(DOMCacheEngine::convertToExceptionAndLog(scriptExecutionContext(), result.error()));
535             else
536                 callback({ });
537         }
538         unsetPendingActivity(this);
539     });
540 }
541
542 void DOMCache::updateRecords(Vector<Record>&& records)
543 {
544     ASSERT(scriptExecutionContext());
545     Vector<CacheStorageRecord> newRecords;
546
547     for (auto& record : records) {
548         size_t index = m_records.findMatching([&](const auto& item) { return item.identifier == record.identifier; });
549         if (index != notFound) {
550             auto& current = m_records[index];
551             if (current.updateResponseCounter != record.updateResponseCounter) {
552                 auto response = FetchResponse::create(*scriptExecutionContext(), std::nullopt, record.responseHeadersGuard, WTFMove(record.response));
553                 response->setBodyData(WTFMove(record.responseBody), record.responseBodySize);
554
555                 current.response = WTFMove(response);
556                 current.updateResponseCounter = record.updateResponseCounter;
557             }
558             newRecords.append(WTFMove(current));
559         } else {
560             auto requestHeaders = FetchHeaders::create(record.requestHeadersGuard, HTTPHeaderMap { record.request.httpHeaderFields() });
561             auto request = FetchRequest::create(*scriptExecutionContext(), std::nullopt, WTFMove(requestHeaders),  WTFMove(record.request), WTFMove(record.options), WTFMove(record.referrer));
562
563             auto response = FetchResponse::create(*scriptExecutionContext(), std::nullopt, record.responseHeadersGuard, WTFMove(record.response));
564             response->setBodyData(WTFMove(record.responseBody), record.responseBodySize);
565
566             newRecords.append(CacheStorageRecord { record.identifier, record.updateResponseCounter, WTFMove(request), WTFMove(response) });
567         }
568     }
569     m_records = WTFMove(newRecords);
570 }
571
572 void DOMCache::stop()
573 {
574     if (m_isStopped)
575         return;
576     m_isStopped = true;
577     m_connection->dereference(m_identifier);
578 }
579
580 const char* DOMCache::activeDOMObjectName() const
581 {
582     return "Cache";
583 }
584
585 bool DOMCache::canSuspendForDocumentSuspension() const
586 {
587     return m_records.isEmpty() && !hasPendingActivity();
588 }
589
590
591 } // namespace WebCore