| /* |
| * Copyright (C) 2017 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "DOMCache.h" |
| |
| #include "CacheQueryOptions.h" |
| #include "FetchResponse.h" |
| #include "HTTPParsers.h" |
| #include "JSFetchRequest.h" |
| #include "JSFetchResponse.h" |
| #include "ReadableStreamChunk.h" |
| #include "ScriptExecutionContext.h" |
| #include "URL.h" |
| |
| |
| namespace WebCore { |
| using namespace WebCore::DOMCacheEngine; |
| |
| DOMCache::DOMCache(ScriptExecutionContext& context, String&& name, uint64_t identifier, Ref<CacheStorageConnection>&& connection) |
| : ActiveDOMObject(&context) |
| , m_name(WTFMove(name)) |
| , m_identifier(identifier) |
| , m_connection(WTFMove(connection)) |
| { |
| suspendIfNeeded(); |
| m_connection->reference(m_identifier); |
| } |
| |
| DOMCache::~DOMCache() |
| { |
| m_connection->dereference(m_identifier); |
| } |
| |
| void DOMCache::match(RequestInfo&& info, CacheQueryOptions&& options, Ref<DeferredPromise>&& promise) |
| { |
| doMatch(WTFMove(info), WTFMove(options), [promise = WTFMove(promise)](ExceptionOr<FetchResponse*>&& result) mutable { |
| if (result.hasException()) { |
| promise->reject(result.releaseException()); |
| return; |
| } |
| if (!result.returnValue()) { |
| promise->resolve(); |
| return; |
| } |
| promise->resolve<IDLInterface<FetchResponse>>(*result.returnValue()); |
| }); |
| } |
| |
| void DOMCache::doMatch(RequestInfo&& info, CacheQueryOptions&& options, MatchCallback&& callback) |
| { |
| if (UNLIKELY(!scriptExecutionContext())) |
| return; |
| |
| auto requestOrException = requestFromInfo(WTFMove(info), options.ignoreMethod); |
| if (requestOrException.hasException()) { |
| callback(nullptr); |
| return; |
| } |
| auto request = requestOrException.releaseReturnValue(); |
| |
| queryCache(request.get(), WTFMove(options), [this, callback = WTFMove(callback)](ExceptionOr<Vector<CacheStorageRecord>>&& result) mutable { |
| if (result.hasException()) { |
| callback(result.releaseException()); |
| return; |
| } |
| if (result.returnValue().isEmpty()) { |
| callback(nullptr); |
| return; |
| } |
| callback(result.returnValue()[0].response->clone(*scriptExecutionContext()).releaseReturnValue().ptr()); |
| }); |
| } |
| |
| Vector<Ref<FetchResponse>> DOMCache::cloneResponses(const Vector<CacheStorageRecord>& records) |
| { |
| auto& context = *scriptExecutionContext(); |
| return WTF::map(records, [&context] (const auto& record) { |
| return record.response->clone(context).releaseReturnValue(); |
| }); |
| } |
| |
| void DOMCache::matchAll(std::optional<RequestInfo>&& info, CacheQueryOptions&& options, MatchAllPromise&& promise) |
| { |
| if (UNLIKELY(!scriptExecutionContext())) |
| return; |
| |
| RefPtr<FetchRequest> request; |
| if (info) { |
| auto requestOrException = requestFromInfo(WTFMove(info.value()), options.ignoreMethod); |
| if (requestOrException.hasException()) { |
| promise.resolve({ }); |
| return; |
| } |
| request = requestOrException.releaseReturnValue(); |
| } |
| |
| if (!request) { |
| retrieveRecords(URL { }, [this, promise = WTFMove(promise)](std::optional<Exception>&& exception) mutable { |
| if (exception) { |
| promise.reject(WTFMove(exception.value())); |
| return; |
| } |
| promise.resolve(cloneResponses(m_records)); |
| }); |
| return; |
| } |
| queryCache(request.releaseNonNull(), WTFMove(options), [this, promise = WTFMove(promise)](ExceptionOr<Vector<CacheStorageRecord>>&& result) mutable { |
| if (result.hasException()) { |
| promise.reject(result.releaseException()); |
| return; |
| } |
| promise.resolve(cloneResponses(result.releaseReturnValue())); |
| }); |
| } |
| |
| void DOMCache::add(RequestInfo&& info, DOMPromiseDeferred<void>&& promise) |
| { |
| addAll(Vector<RequestInfo> { WTFMove(info) }, WTFMove(promise)); |
| } |
| |
| static inline bool hasResponseVaryStarHeaderValue(const FetchResponse& response) |
| { |
| auto varyValue = response.headers().internalHeaders().get(WebCore::HTTPHeaderName::Vary); |
| bool hasStar = false; |
| varyValue.split(',', false, [&](StringView view) { |
| if (!hasStar && stripLeadingAndTrailingHTTPSpaces(view) == "*") |
| hasStar = true; |
| }); |
| return hasStar; |
| } |
| |
| class FetchTasksHandler : public RefCounted<FetchTasksHandler> { |
| public: |
| explicit FetchTasksHandler(Function<void(ExceptionOr<Vector<Record>>&&)>&& callback) |
| : m_callback(WTFMove(callback)) |
| { |
| } |
| |
| ~FetchTasksHandler() |
| { |
| if (m_callback) |
| m_callback(WTFMove(m_records)); |
| } |
| |
| const Vector<Record>& records() const { return m_records; } |
| |
| size_t addRecord(Record&& record) |
| { |
| ASSERT(!isDone()); |
| m_records.append(WTFMove(record)); |
| return m_records.size() - 1; |
| } |
| |
| void addResponseBody(size_t position, Ref<SharedBuffer>&& data) |
| { |
| ASSERT(!isDone()); |
| m_records[position].responseBody = WTFMove(data); |
| } |
| |
| bool isDone() const { return !m_callback; } |
| |
| void error(Exception&& exception) |
| { |
| if (auto callback = WTFMove(m_callback)) |
| callback(WTFMove(exception)); |
| } |
| |
| private: |
| Vector<Record> m_records; |
| Function<void(ExceptionOr<Vector<Record>>&&)> m_callback; |
| }; |
| |
| ExceptionOr<Ref<FetchRequest>> DOMCache::requestFromInfo(RequestInfo&& info, bool ignoreMethod) |
| { |
| RefPtr<FetchRequest> request; |
| if (WTF::holds_alternative<RefPtr<FetchRequest>>(info)) { |
| request = WTF::get<RefPtr<FetchRequest>>(info).releaseNonNull(); |
| if (request->method() != "GET" && !ignoreMethod) |
| return Exception { TypeError, ASCIILiteral("Request method is not GET") }; |
| } else |
| request = FetchRequest::create(*scriptExecutionContext(), WTFMove(info), { }).releaseReturnValue(); |
| |
| if (!protocolIsInHTTPFamily(request->url())) |
| return Exception { TypeError, ASCIILiteral("Request url is not HTTP/HTTPS") }; |
| |
| return request.releaseNonNull(); |
| } |
| |
| void DOMCache::addAll(Vector<RequestInfo>&& infos, DOMPromiseDeferred<void>&& promise) |
| { |
| if (UNLIKELY(!scriptExecutionContext())) |
| return; |
| |
| Vector<Ref<FetchRequest>> requests; |
| requests.reserveInitialCapacity(infos.size()); |
| for (auto& info : infos) { |
| bool ignoreMethod = false; |
| auto requestOrException = requestFromInfo(WTFMove(info), ignoreMethod); |
| if (requestOrException.hasException()) { |
| promise.reject(requestOrException.releaseException()); |
| return; |
| } |
| requests.uncheckedAppend(requestOrException.releaseReturnValue()); |
| } |
| |
| auto taskHandler = adoptRef(*new FetchTasksHandler([protectedThis = makeRef(*this), this, promise = WTFMove(promise)](ExceptionOr<Vector<Record>>&& result) mutable { |
| if (result.hasException()) { |
| promise.reject(result.releaseException()); |
| return; |
| } |
| batchPutOperation(result.releaseReturnValue(), [promise = WTFMove(promise)](ExceptionOr<void>&& result) mutable { |
| promise.settle(WTFMove(result)); |
| }); |
| })); |
| |
| for (auto& request : requests) { |
| auto& requestReference = request.get(); |
| FetchResponse::fetch(*scriptExecutionContext(), requestReference, [this, request = WTFMove(request), taskHandler = taskHandler.copyRef()](ExceptionOr<FetchResponse&>&& result) mutable { |
| |
| if (taskHandler->isDone()) |
| return; |
| |
| if (result.hasException()) { |
| taskHandler->error(result.releaseException()); |
| return; |
| } |
| |
| auto& response = result.releaseReturnValue(); |
| |
| if (!response.ok()) { |
| taskHandler->error(Exception { TypeError, ASCIILiteral("Response is not OK") }); |
| return; |
| } |
| |
| if (hasResponseVaryStarHeaderValue(response)) { |
| taskHandler->error(Exception { TypeError, ASCIILiteral("Response has a '*' Vary header value") }); |
| return; |
| } |
| |
| if (response.status() == 206) { |
| taskHandler->error(Exception { TypeError, ASCIILiteral("Response is a 206 partial") }); |
| return; |
| } |
| |
| CacheQueryOptions options; |
| for (const auto& record : taskHandler->records()) { |
| if (DOMCacheEngine::queryCacheMatch(request->resourceRequest(), record.request, record.response, options)) { |
| taskHandler->error(Exception { InvalidStateError, ASCIILiteral("addAll cannot store several matching requests")}); |
| return; |
| } |
| } |
| size_t recordPosition = taskHandler->addRecord(toConnectionRecord(request.get(), response, nullptr)); |
| |
| response.consumeBodyReceivedByChunk([taskHandler = WTFMove(taskHandler), recordPosition, data = SharedBuffer::create()] (ExceptionOr<ReadableStreamChunk*>&& result) mutable { |
| if (taskHandler->isDone()) |
| return; |
| |
| if (result.hasException()) { |
| taskHandler->error(result.releaseException()); |
| return; |
| } |
| |
| if (auto chunk = result.returnValue()) |
| data->append(reinterpret_cast<const char*>(chunk->data), chunk->size); |
| else |
| taskHandler->addResponseBody(recordPosition, WTFMove(data)); |
| }); |
| }); |
| } |
| } |
| |
| void DOMCache::putWithResponseData(DOMPromiseDeferred<void>&& promise, Ref<FetchRequest>&& request, Ref<FetchResponse>&& response, ExceptionOr<RefPtr<SharedBuffer>>&& responseBody) |
| { |
| if (responseBody.hasException()) { |
| promise.reject(responseBody.releaseException()); |
| return; |
| } |
| |
| DOMCacheEngine::ResponseBody body; |
| if (auto buffer = responseBody.releaseReturnValue()) |
| body = buffer.releaseNonNull(); |
| batchPutOperation(request.get(), response.get(), WTFMove(body), [promise = WTFMove(promise)](ExceptionOr<void>&& result) mutable { |
| promise.settle(WTFMove(result)); |
| }); |
| } |
| |
| void DOMCache::put(RequestInfo&& info, Ref<FetchResponse>&& response, DOMPromiseDeferred<void>&& promise) |
| { |
| if (UNLIKELY(!scriptExecutionContext())) |
| return; |
| |
| bool ignoreMethod = false; |
| auto requestOrException = requestFromInfo(WTFMove(info), ignoreMethod); |
| if (requestOrException.hasException()) { |
| promise.reject(requestOrException.releaseException()); |
| return; |
| } |
| auto request = requestOrException.releaseReturnValue(); |
| |
| if (response->loadingError()) { |
| promise.reject(Exception { TypeError, response->loadingError()->localizedDescription() }); |
| return; |
| } |
| |
| if (hasResponseVaryStarHeaderValue(response.get())) { |
| promise.reject(Exception { TypeError, ASCIILiteral("Response has a '*' Vary header value") }); |
| return; |
| } |
| |
| if (response->status() == 206) { |
| promise.reject(Exception { TypeError, ASCIILiteral("Response is a 206 partial") }); |
| return; |
| } |
| |
| if (response->isDisturbedOrLocked()) { |
| promise.reject(Exception { TypeError, ASCIILiteral("Response is disturbed or locked") }); |
| return; |
| } |
| |
| if (response->isBlobFormData()) { |
| promise.reject(Exception { NotSupportedError, ASCIILiteral("Not implemented") }); |
| return; |
| } |
| |
| // FIXME: for efficiency, we should load blobs directly instead of going through the readableStream path. |
| if (response->isBlobBody()) |
| response->readableStream(*scriptExecutionContext()->execState()); |
| |
| if (response->isBodyReceivedByChunk()) { |
| response->consumeBodyReceivedByChunk([promise = WTFMove(promise), request = WTFMove(request), response = WTFMove(response), data = SharedBuffer::create(), pendingActivity = makePendingActivity(*this), this](auto&& result) mutable { |
| |
| if (result.hasException()) { |
| this->putWithResponseData(WTFMove(promise), WTFMove(request), WTFMove(response), result.releaseException().isolatedCopy()); |
| return; |
| } |
| |
| if (auto chunk = result.returnValue()) |
| data->append(reinterpret_cast<const char*>(chunk->data), chunk->size); |
| else |
| this->putWithResponseData(WTFMove(promise), WTFMove(request), WTFMove(response), RefPtr<SharedBuffer> { WTFMove(data) }); |
| }); |
| return; |
| } |
| |
| batchPutOperation(request.get(), response.get(), response->consumeBody(), [promise = WTFMove(promise)](ExceptionOr<void>&& result) mutable { |
| promise.settle(WTFMove(result)); |
| }); |
| } |
| |
| void DOMCache::remove(RequestInfo&& info, CacheQueryOptions&& options, DOMPromiseDeferred<IDLBoolean>&& promise) |
| { |
| if (UNLIKELY(!scriptExecutionContext())) |
| return; |
| |
| auto requestOrException = requestFromInfo(WTFMove(info), options.ignoreMethod); |
| if (requestOrException.hasException()) { |
| promise.resolve(false); |
| return; |
| } |
| |
| batchDeleteOperation(requestOrException.releaseReturnValue(), WTFMove(options), [promise = WTFMove(promise)](ExceptionOr<bool>&& result) mutable { |
| promise.settle(WTFMove(result)); |
| }); |
| } |
| |
| static inline Ref<FetchRequest> copyRequestRef(const CacheStorageRecord& record) |
| { |
| return record.request.copyRef(); |
| } |
| |
| void DOMCache::keys(std::optional<RequestInfo>&& info, CacheQueryOptions&& options, KeysPromise&& promise) |
| { |
| if (UNLIKELY(!scriptExecutionContext())) |
| return; |
| |
| RefPtr<FetchRequest> request; |
| if (info) { |
| auto requestOrException = requestFromInfo(WTFMove(info.value()), options.ignoreMethod); |
| if (requestOrException.hasException()) { |
| promise.resolve(Vector<Ref<FetchRequest>> { }); |
| return; |
| } |
| request = requestOrException.releaseReturnValue(); |
| } |
| |
| if (!request) { |
| retrieveRecords(URL { }, [this, promise = WTFMove(promise)](std::optional<Exception>&& exception) mutable { |
| if (exception) { |
| promise.reject(WTFMove(exception.value())); |
| return; |
| } |
| promise.resolve(WTF::map(m_records, copyRequestRef)); |
| }); |
| return; |
| } |
| |
| queryCache(request.releaseNonNull(), WTFMove(options), [promise = WTFMove(promise)](ExceptionOr<Vector<CacheStorageRecord>>&& result) mutable { |
| if (result.hasException()) { |
| promise.reject(result.releaseException()); |
| return; |
| } |
| |
| promise.resolve(WTF::map(result.releaseReturnValue(), copyRequestRef)); |
| }); |
| } |
| |
| void DOMCache::retrieveRecords(const URL& url, WTF::Function<void(std::optional<Exception>&&)>&& callback) |
| { |
| setPendingActivity(this); |
| |
| URL retrieveURL = url; |
| retrieveURL.removeQueryAndFragmentIdentifier(); |
| |
| m_connection->retrieveRecords(m_identifier, retrieveURL, [this, callback = WTFMove(callback)](RecordsOrError&& result) { |
| if (!m_isStopped) { |
| if (!result.has_value()) { |
| callback(DOMCacheEngine::convertToExceptionAndLog(scriptExecutionContext(), result.error())); |
| return; |
| } |
| |
| if (result.has_value()) |
| updateRecords(WTFMove(result.value())); |
| callback(std::nullopt); |
| } |
| unsetPendingActivity(this); |
| }); |
| } |
| |
| void DOMCache::queryCache(Ref<FetchRequest>&& request, CacheQueryOptions&& options, WTF::Function<void(ExceptionOr<Vector<CacheStorageRecord>>&&)>&& callback) |
| { |
| auto url = request->url(); |
| retrieveRecords(url, [this, request = WTFMove(request), options = WTFMove(options), callback = WTFMove(callback)](std::optional<Exception>&& exception) mutable { |
| if (exception) { |
| callback(WTFMove(exception.value())); |
| return; |
| } |
| callback(queryCacheWithTargetStorage(request.get(), options, m_records)); |
| }); |
| } |
| |
| static inline bool queryCacheMatch(const FetchRequest& request, const FetchRequest& cachedRequest, const ResourceResponse& cachedResponse, const CacheQueryOptions& options) |
| { |
| // We need to pass the resource request with all correct headers hence why we call resourceRequest(). |
| return DOMCacheEngine::queryCacheMatch(request.resourceRequest(), cachedRequest.resourceRequest(), cachedResponse, options); |
| } |
| |
| Vector<CacheStorageRecord> DOMCache::queryCacheWithTargetStorage(const FetchRequest& request, const CacheQueryOptions& options, const Vector<CacheStorageRecord>& targetStorage) |
| { |
| if (!options.ignoreMethod && request.method() != "GET") |
| return { }; |
| |
| Vector<CacheStorageRecord> records; |
| for (auto& record : targetStorage) { |
| if (queryCacheMatch(request, record.request.get(), record.response->resourceResponse(), options)) |
| records.append({ record.identifier, record.updateResponseCounter, record.request.copyRef(), record.response.copyRef() }); |
| } |
| return records; |
| } |
| |
| void DOMCache::batchDeleteOperation(const FetchRequest& request, CacheQueryOptions&& options, WTF::Function<void(ExceptionOr<bool>&&)>&& callback) |
| { |
| setPendingActivity(this); |
| m_connection->batchDeleteOperation(m_identifier, request.internalRequest(), WTFMove(options), [this, callback = WTFMove(callback)](RecordIdentifiersOrError&& result) { |
| if (!m_isStopped) { |
| if (!result.has_value()) |
| callback(DOMCacheEngine::convertToExceptionAndLog(scriptExecutionContext(), result.error())); |
| else |
| callback(!result.value().isEmpty()); |
| } |
| unsetPendingActivity(this); |
| }); |
| } |
| |
| Record DOMCache::toConnectionRecord(const FetchRequest& request, FetchResponse& response, DOMCacheEngine::ResponseBody&& responseBody) |
| { |
| auto cachedResponse = response.resourceResponse(); |
| ResourceRequest cachedRequest = request.internalRequest(); |
| cachedRequest.setHTTPHeaderFields(request.headers().internalHeaders()); |
| |
| ASSERT(!cachedRequest.isNull()); |
| ASSERT(!cachedResponse.isNull()); |
| |
| auto sizeWithPadding = response.bodySizeWithPadding(); |
| if (!sizeWithPadding) { |
| sizeWithPadding = m_connection->computeRecordBodySize(response, responseBody, cachedResponse.tainting()); |
| response.setBodySizeWithPadding(sizeWithPadding); |
| } |
| |
| return { 0, 0, |
| request.headers().guard(), WTFMove(cachedRequest), request.fetchOptions(), request.internalRequestReferrer(), |
| response.headers().guard(), WTFMove(cachedResponse), WTFMove(responseBody), sizeWithPadding |
| }; |
| } |
| |
| void DOMCache::batchPutOperation(const FetchRequest& request, FetchResponse& response, DOMCacheEngine::ResponseBody&& responseBody, WTF::Function<void(ExceptionOr<void>&&)>&& callback) |
| { |
| Vector<Record> records; |
| records.append(toConnectionRecord(request, response, WTFMove(responseBody))); |
| |
| batchPutOperation(WTFMove(records), WTFMove(callback)); |
| } |
| |
| void DOMCache::batchPutOperation(Vector<Record>&& records, WTF::Function<void(ExceptionOr<void>&&)>&& callback) |
| { |
| setPendingActivity(this); |
| m_connection->batchPutOperation(m_identifier, WTFMove(records), [this, callback = WTFMove(callback)](RecordIdentifiersOrError&& result) { |
| if (!m_isStopped) { |
| if (!result.has_value()) |
| callback(DOMCacheEngine::convertToExceptionAndLog(scriptExecutionContext(), result.error())); |
| else |
| callback({ }); |
| } |
| unsetPendingActivity(this); |
| }); |
| } |
| |
| void DOMCache::updateRecords(Vector<Record>&& records) |
| { |
| ASSERT(scriptExecutionContext()); |
| Vector<CacheStorageRecord> newRecords; |
| |
| for (auto& record : records) { |
| size_t index = m_records.findMatching([&](const auto& item) { return item.identifier == record.identifier; }); |
| if (index != notFound) { |
| auto& current = m_records[index]; |
| if (current.updateResponseCounter != record.updateResponseCounter) { |
| auto response = FetchResponse::create(*scriptExecutionContext(), std::nullopt, record.responseHeadersGuard, WTFMove(record.response)); |
| response->setBodyData(WTFMove(record.responseBody), record.responseBodySize); |
| |
| current.response = WTFMove(response); |
| current.updateResponseCounter = record.updateResponseCounter; |
| } |
| newRecords.append(WTFMove(current)); |
| } else { |
| auto requestHeaders = FetchHeaders::create(record.requestHeadersGuard, HTTPHeaderMap { record.request.httpHeaderFields() }); |
| auto request = FetchRequest::create(*scriptExecutionContext(), std::nullopt, WTFMove(requestHeaders), WTFMove(record.request), WTFMove(record.options), WTFMove(record.referrer)); |
| |
| auto response = FetchResponse::create(*scriptExecutionContext(), std::nullopt, record.responseHeadersGuard, WTFMove(record.response)); |
| response->setBodyData(WTFMove(record.responseBody), record.responseBodySize); |
| |
| newRecords.append(CacheStorageRecord { record.identifier, record.updateResponseCounter, WTFMove(request), WTFMove(response) }); |
| } |
| } |
| m_records = WTFMove(newRecords); |
| } |
| |
| void DOMCache::stop() |
| { |
| m_isStopped = true; |
| } |
| |
| const char* DOMCache::activeDOMObjectName() const |
| { |
| return "Cache"; |
| } |
| |
| bool DOMCache::canSuspendForDocumentSuspension() const |
| { |
| return m_records.isEmpty() && !hasPendingActivity(); |
| } |
| |
| |
| } // namespace WebCore |