Unreviewed restoration of non-unified build.
[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/CompletionHandler.h>
37 #include <wtf/URL.h>
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(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
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     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
191 private:
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
203 ExceptionOr<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
219 void 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
302 void 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
317 void 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
381 void 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
397 static inline Ref<FetchRequest> copyRequestRef(const CacheStorageRecord& record)
398 {
399     return record.request.copyRef();
400 }
401
402 void 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
438 void 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
457 void 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
469 static 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
475 Vector<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
488 void 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
502 Record 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
523 void 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
531 void 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
544 void 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
574 void DOMCache::stop()
575 {
576     if (m_isStopped)
577         return;
578     m_isStopped = true;
579     m_connection->dereference(m_identifier);
580 }
581
582 const char* DOMCache::activeDOMObjectName() const
583 {
584     return "Cache";
585 }
586
587 bool DOMCache::canSuspendForDocumentSuspension() const
588 {
589     return m_records.isEmpty() && !hasPendingActivity();
590 }
591
592
593 } // namespace WebCore