1 | /* |
2 | * Copyright (C) 2014-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 "NetworkCache.h" |
28 | |
29 | #include "Logging.h" |
30 | #include "NetworkCacheSpeculativeLoadManager.h" |
31 | #include "NetworkCacheStorage.h" |
32 | #include "NetworkProcess.h" |
33 | #include <WebCore/CacheValidation.h> |
34 | #include <WebCore/HTTPHeaderNames.h> |
35 | #include <WebCore/LowPowerModeNotifier.h> |
36 | #include <WebCore/NetworkStorageSession.h> |
37 | #include <WebCore/ResourceRequest.h> |
38 | #include <WebCore/ResourceResponse.h> |
39 | #include <WebCore/SharedBuffer.h> |
40 | #include <wtf/FileSystem.h> |
41 | #include <wtf/MainThread.h> |
42 | #include <wtf/NeverDestroyed.h> |
43 | #include <wtf/RunLoop.h> |
44 | #include <wtf/text/StringBuilder.h> |
45 | |
46 | #if PLATFORM(COCOA) |
47 | #include <notify.h> |
48 | #endif |
49 | |
50 | namespace WebKit { |
51 | namespace NetworkCache { |
52 | |
53 | using namespace FileSystem; |
54 | |
55 | static const AtomString& resourceType() |
56 | { |
57 | ASSERT(WTF::RunLoop::isMain()); |
58 | static NeverDestroyed<const AtomString> resource("Resource" , AtomString::ConstructFromLiteral); |
59 | return resource; |
60 | } |
61 | |
62 | RefPtr<Cache> Cache::open(NetworkProcess& networkProcess, const String& cachePath, OptionSet<Option> options) |
63 | { |
64 | auto storage = Storage::open(cachePath, options.contains(Option::TestingMode) ? Storage::Mode::AvoidRandomness : Storage::Mode::Normal); |
65 | |
66 | LOG(NetworkCache, "(NetworkProcess) opened cache storage, success %d" , !!storage); |
67 | |
68 | if (!storage) |
69 | return nullptr; |
70 | |
71 | return adoptRef(*new Cache(networkProcess, storage.releaseNonNull(), options)); |
72 | } |
73 | |
74 | #if PLATFORM(GTK) |
75 | static void dumpFileChanged(Cache* cache) |
76 | { |
77 | cache->dumpContentsToFile(); |
78 | } |
79 | #endif |
80 | |
81 | Cache::Cache(NetworkProcess& networkProcess, Ref<Storage>&& storage, OptionSet<Option> options) |
82 | : m_storage(WTFMove(storage)) |
83 | , m_networkProcess(networkProcess) |
84 | { |
85 | #if ENABLE(NETWORK_CACHE_SPECULATIVE_REVALIDATION) |
86 | if (options.contains(Option::SpeculativeRevalidation)) { |
87 | m_lowPowerModeNotifier = std::make_unique<WebCore::LowPowerModeNotifier>([this](bool isLowPowerModeEnabled) { |
88 | ASSERT(WTF::RunLoop::isMain()); |
89 | if (isLowPowerModeEnabled) |
90 | m_speculativeLoadManager = nullptr; |
91 | else { |
92 | ASSERT(!m_speculativeLoadManager); |
93 | m_speculativeLoadManager = std::make_unique<SpeculativeLoadManager>(*this, m_storage.get()); |
94 | } |
95 | }); |
96 | if (!m_lowPowerModeNotifier->isLowPowerModeEnabled()) |
97 | m_speculativeLoadManager = std::make_unique<SpeculativeLoadManager>(*this, m_storage.get()); |
98 | } |
99 | #endif |
100 | |
101 | if (options.contains(Option::RegisterNotify)) { |
102 | #if PLATFORM(COCOA) |
103 | // Triggers with "notifyutil -p com.apple.WebKit.Cache.dump". |
104 | int token; |
105 | notify_register_dispatch("com.apple.WebKit.Cache.dump" , &token, dispatch_get_main_queue(), ^(int) { |
106 | dumpContentsToFile(); |
107 | }); |
108 | #endif |
109 | #if PLATFORM(GTK) |
110 | // Triggers with "touch $cachePath/dump". |
111 | CString dumpFilePath = fileSystemRepresentation(pathByAppendingComponent(m_storage->basePath(), "dump" )); |
112 | GRefPtr<GFile> dumpFile = adoptGRef(g_file_new_for_path(dumpFilePath.data())); |
113 | GFileMonitor* monitor = g_file_monitor_file(dumpFile.get(), G_FILE_MONITOR_NONE, nullptr, nullptr); |
114 | g_signal_connect_swapped(monitor, "changed" , G_CALLBACK(dumpFileChanged), this); |
115 | #endif |
116 | } |
117 | } |
118 | |
119 | Cache::~Cache() |
120 | { |
121 | } |
122 | |
123 | void Cache::setCapacity(size_t maximumSize) |
124 | { |
125 | m_storage->setCapacity(maximumSize); |
126 | } |
127 | |
128 | Key Cache::makeCacheKey(const WebCore::ResourceRequest& request) |
129 | { |
130 | // FIXME: This implements minimal Range header disk cache support. We don't parse |
131 | // ranges so only the same exact range request will be served from the cache. |
132 | String range = request.httpHeaderField(WebCore::HTTPHeaderName::Range); |
133 | return { request.cachePartition(), resourceType(), range, request.url().string(), m_storage->salt() }; |
134 | } |
135 | |
136 | static bool cachePolicyAllowsExpired(WebCore::ResourceRequestCachePolicy policy) |
137 | { |
138 | switch (policy) { |
139 | case WebCore::ResourceRequestCachePolicy::ReturnCacheDataElseLoad: |
140 | case WebCore::ResourceRequestCachePolicy::ReturnCacheDataDontLoad: |
141 | return true; |
142 | case WebCore::ResourceRequestCachePolicy::UseProtocolCachePolicy: |
143 | case WebCore::ResourceRequestCachePolicy::ReloadIgnoringCacheData: |
144 | case WebCore::ResourceRequestCachePolicy::RefreshAnyCacheData: |
145 | return false; |
146 | case WebCore::ResourceRequestCachePolicy::DoNotUseAnyCache: |
147 | ASSERT_NOT_REACHED(); |
148 | return false; |
149 | } |
150 | return false; |
151 | } |
152 | |
153 | static bool responseHasExpired(const WebCore::ResourceResponse& response, WallTime timestamp, Optional<Seconds> maxStale) |
154 | { |
155 | if (response.cacheControlContainsNoCache()) |
156 | return true; |
157 | |
158 | auto age = WebCore::computeCurrentAge(response, timestamp); |
159 | auto lifetime = WebCore::computeFreshnessLifetimeForHTTPFamily(response, timestamp); |
160 | |
161 | auto maximumStaleness = maxStale ? maxStale.value() : 0_ms; |
162 | bool hasExpired = age - lifetime > maximumStaleness; |
163 | |
164 | #ifndef LOG_DISABLED |
165 | if (hasExpired) |
166 | LOG(NetworkCache, "(NetworkProcess) needsRevalidation hasExpired age=%f lifetime=%f max-stale=%g" , age, lifetime, maxStale); |
167 | #endif |
168 | |
169 | return hasExpired; |
170 | } |
171 | |
172 | static bool responseNeedsRevalidation(const WebCore::ResourceResponse& response, const WebCore::ResourceRequest& request, WallTime timestamp) |
173 | { |
174 | auto requestDirectives = WebCore::parseCacheControlDirectives(request.httpHeaderFields()); |
175 | if (requestDirectives.noCache) |
176 | return true; |
177 | // For requests we ignore max-age values other than zero. |
178 | if (requestDirectives.maxAge && requestDirectives.maxAge.value() == 0_ms) |
179 | return true; |
180 | |
181 | return responseHasExpired(response, timestamp, requestDirectives.maxStale); |
182 | } |
183 | |
184 | static UseDecision makeUseDecision(NetworkProcess& networkProcess, const Entry& entry, const WebCore::ResourceRequest& request) |
185 | { |
186 | // The request is conditional so we force revalidation from the network. We merely check the disk cache |
187 | // so we can update the cache entry. |
188 | if (request.isConditional() && !entry.redirectRequest()) |
189 | return UseDecision::Validate; |
190 | |
191 | if (!WebCore::verifyVaryingRequestHeaders(networkProcess.defaultStorageSession(), entry.varyingRequestHeaders(), request)) |
192 | return UseDecision::NoDueToVaryingHeaderMismatch; |
193 | |
194 | // We never revalidate in the case of a history navigation. |
195 | if (cachePolicyAllowsExpired(request.cachePolicy())) |
196 | return UseDecision::Use; |
197 | |
198 | if (!responseNeedsRevalidation(entry.response(), request, entry.timeStamp())) |
199 | return UseDecision::Use; |
200 | |
201 | if (!entry.response().hasCacheValidatorFields()) |
202 | return UseDecision::NoDueToMissingValidatorFields; |
203 | |
204 | return entry.redirectRequest() ? UseDecision::NoDueToExpiredRedirect : UseDecision::Validate; |
205 | } |
206 | |
207 | static RetrieveDecision makeRetrieveDecision(const WebCore::ResourceRequest& request) |
208 | { |
209 | ASSERT(request.cachePolicy() != WebCore::ResourceRequestCachePolicy::DoNotUseAnyCache); |
210 | |
211 | // FIXME: Support HEAD requests. |
212 | if (request.httpMethod() != "GET" ) |
213 | return RetrieveDecision::NoDueToHTTPMethod; |
214 | if (request.cachePolicy() == WebCore::ResourceRequestCachePolicy::ReloadIgnoringCacheData && !request.isConditional()) |
215 | return RetrieveDecision::NoDueToReloadIgnoringCache; |
216 | |
217 | return RetrieveDecision::Yes; |
218 | } |
219 | |
220 | static bool isMediaMIMEType(const String& type) |
221 | { |
222 | return startsWithLettersIgnoringASCIICase(type, "video/" ) || startsWithLettersIgnoringASCIICase(type, "audio/" ); |
223 | } |
224 | |
225 | static StoreDecision makeStoreDecision(const WebCore::ResourceRequest& originalRequest, const WebCore::ResourceResponse& response, size_t bodySize) |
226 | { |
227 | if (!originalRequest.url().protocolIsInHTTPFamily() || !response.isHTTP()) |
228 | return StoreDecision::NoDueToProtocol; |
229 | |
230 | if (originalRequest.httpMethod() != "GET" ) |
231 | return StoreDecision::NoDueToHTTPMethod; |
232 | |
233 | auto requestDirectives = WebCore::parseCacheControlDirectives(originalRequest.httpHeaderFields()); |
234 | if (requestDirectives.noStore) |
235 | return StoreDecision::NoDueToNoStoreRequest; |
236 | |
237 | if (response.cacheControlContainsNoStore()) |
238 | return StoreDecision::NoDueToNoStoreResponse; |
239 | |
240 | if (!WebCore::isStatusCodeCacheableByDefault(response.httpStatusCode())) { |
241 | // http://tools.ietf.org/html/rfc7234#section-4.3.2 |
242 | bool = response.expires() || response.cacheControlMaxAge(); |
243 | bool = WebCore::isStatusCodePotentiallyCacheable(response.httpStatusCode()) && hasExpirationHeaders; |
244 | if (!expirationHeadersAllowCaching) |
245 | return StoreDecision::NoDueToHTTPStatusCode; |
246 | } |
247 | |
248 | bool isMainResource = originalRequest.requester() == WebCore::ResourceRequest::Requester::Main; |
249 | bool storeUnconditionallyForHistoryNavigation = isMainResource || originalRequest.priority() == WebCore::ResourceLoadPriority::VeryHigh; |
250 | if (!storeUnconditionallyForHistoryNavigation) { |
251 | auto now = WallTime::now(); |
252 | bool hasNonZeroLifetime = !response.cacheControlContainsNoCache() && WebCore::computeFreshnessLifetimeForHTTPFamily(response, now) > 0_ms; |
253 | |
254 | bool possiblyReusable = response.hasCacheValidatorFields() || hasNonZeroLifetime; |
255 | if (!possiblyReusable) |
256 | return StoreDecision::NoDueToUnlikelyToReuse; |
257 | } |
258 | |
259 | // Media loaded via XHR is likely being used for MSE streaming (YouTube and Netflix for example). |
260 | // Streaming media fills the cache quickly and is unlikely to be reused. |
261 | // FIXME: We should introduce a separate media cache partition that doesn't affect other resources. |
262 | // FIXME: We should also make sure make the MSE paths are copy-free so we can use mapped buffers from disk effectively. |
263 | auto requester = originalRequest.requester(); |
264 | bool isDefinitelyStreamingMedia = requester == WebCore::ResourceRequest::Requester::Media; |
265 | bool isLikelyStreamingMedia = requester == WebCore::ResourceRequest::Requester::XHR && isMediaMIMEType(response.mimeType()); |
266 | if (isLikelyStreamingMedia || isDefinitelyStreamingMedia) |
267 | return StoreDecision::NoDueToStreamingMedia; |
268 | |
269 | return StoreDecision::Yes; |
270 | } |
271 | |
272 | void Cache::retrieve(const WebCore::ResourceRequest& request, const GlobalFrameID& frameID, RetrieveCompletionHandler&& completionHandler) |
273 | { |
274 | ASSERT(request.url().protocolIsInHTTPFamily()); |
275 | |
276 | LOG(NetworkCache, "(NetworkProcess) retrieving %s priority %d" , request.url().string().ascii().data(), static_cast<int>(request.priority())); |
277 | |
278 | Key storageKey = makeCacheKey(request); |
279 | auto priority = static_cast<unsigned>(request.priority()); |
280 | |
281 | RetrieveInfo info; |
282 | info.startTime = MonotonicTime::now(); |
283 | info.priority = priority; |
284 | |
285 | #if ENABLE(NETWORK_CACHE_SPECULATIVE_REVALIDATION) |
286 | bool canUseSpeculativeRevalidation = m_speculativeLoadManager && !request.isConditional() && !cachePolicyAllowsExpired(request.cachePolicy()); |
287 | if (canUseSpeculativeRevalidation) |
288 | m_speculativeLoadManager->registerLoad(frameID, request, storageKey); |
289 | #endif |
290 | |
291 | auto retrieveDecision = makeRetrieveDecision(request); |
292 | if (retrieveDecision != RetrieveDecision::Yes) { |
293 | completeRetrieve(WTFMove(completionHandler), nullptr, info); |
294 | return; |
295 | } |
296 | |
297 | #if ENABLE(NETWORK_CACHE_SPECULATIVE_REVALIDATION) |
298 | if (canUseSpeculativeRevalidation && m_speculativeLoadManager->canRetrieve(storageKey, request, frameID)) { |
299 | m_speculativeLoadManager->retrieve(storageKey, [networkProcess = makeRef(networkProcess()), request, completionHandler = WTFMove(completionHandler), info = WTFMove(info)](std::unique_ptr<Entry> entry) mutable { |
300 | info.wasSpeculativeLoad = true; |
301 | if (entry && WebCore::verifyVaryingRequestHeaders(networkProcess->defaultStorageSession(), entry->varyingRequestHeaders(), request)) |
302 | completeRetrieve(WTFMove(completionHandler), WTFMove(entry), info); |
303 | else |
304 | completeRetrieve(WTFMove(completionHandler), nullptr, info); |
305 | }); |
306 | return; |
307 | } |
308 | #endif |
309 | |
310 | m_storage->retrieve(storageKey, priority, [request, completionHandler = WTFMove(completionHandler), info = WTFMove(info), storageKey, networkProcess = makeRef(networkProcess())](auto record, auto timings) mutable { |
311 | info.storageTimings = timings; |
312 | |
313 | if (!record) { |
314 | LOG(NetworkCache, "(NetworkProcess) not found in storage" ); |
315 | |
316 | completeRetrieve(WTFMove(completionHandler), nullptr, info); |
317 | return false; |
318 | } |
319 | |
320 | ASSERT(record->key == storageKey); |
321 | |
322 | auto entry = Entry::decodeStorageRecord(*record); |
323 | |
324 | auto useDecision = entry ? makeUseDecision(networkProcess, *entry, request) : UseDecision::NoDueToDecodeFailure; |
325 | switch (useDecision) { |
326 | case UseDecision::Use: |
327 | break; |
328 | case UseDecision::Validate: |
329 | entry->setNeedsValidation(true); |
330 | break; |
331 | default: |
332 | entry = nullptr; |
333 | }; |
334 | |
335 | #if !LOG_DISABLED |
336 | auto elapsed = MonotonicTime::now() - info.startTime; |
337 | LOG(NetworkCache, "(NetworkProcess) retrieve complete useDecision=%d priority=%d time=%" PRIi64 "ms" , static_cast<int>(useDecision), static_cast<int>(request.priority()), elapsed.millisecondsAs<int64_t>()); |
338 | #endif |
339 | completeRetrieve(WTFMove(completionHandler), WTFMove(entry), info); |
340 | |
341 | return useDecision != UseDecision::NoDueToDecodeFailure; |
342 | }); |
343 | } |
344 | |
345 | void Cache::completeRetrieve(RetrieveCompletionHandler&& handler, std::unique_ptr<Entry> entry, RetrieveInfo& info) |
346 | { |
347 | info.completionTime = MonotonicTime::now(); |
348 | handler(WTFMove(entry), info); |
349 | } |
350 | |
351 | std::unique_ptr<Entry> Cache::makeEntry(const WebCore::ResourceRequest& request, const WebCore::ResourceResponse& response, RefPtr<WebCore::SharedBuffer>&& responseData) |
352 | { |
353 | return std::make_unique<Entry>(makeCacheKey(request), response, WTFMove(responseData), WebCore::collectVaryingRequestHeaders(networkProcess().defaultStorageSession(), request, response)); |
354 | } |
355 | |
356 | std::unique_ptr<Entry> Cache::makeRedirectEntry(const WebCore::ResourceRequest& request, const WebCore::ResourceResponse& response, const WebCore::ResourceRequest& redirectRequest) |
357 | { |
358 | return std::make_unique<Entry>(makeCacheKey(request), response, redirectRequest, WebCore::collectVaryingRequestHeaders(networkProcess().defaultStorageSession(), request, response)); |
359 | } |
360 | |
361 | std::unique_ptr<Entry> Cache::store(const WebCore::ResourceRequest& request, const WebCore::ResourceResponse& response, RefPtr<WebCore::SharedBuffer>&& responseData, Function<void(MappedBody&)>&& completionHandler) |
362 | { |
363 | ASSERT(responseData); |
364 | |
365 | LOG(NetworkCache, "(NetworkProcess) storing %s, partition %s" , request.url().string().latin1().data(), makeCacheKey(request).partition().latin1().data()); |
366 | |
367 | StoreDecision storeDecision = makeStoreDecision(request, response, responseData ? responseData->size() : 0); |
368 | if (storeDecision != StoreDecision::Yes) { |
369 | LOG(NetworkCache, "(NetworkProcess) didn't store, storeDecision=%d" , static_cast<int>(storeDecision)); |
370 | auto key = makeCacheKey(request); |
371 | |
372 | auto isSuccessfulRevalidation = response.httpStatusCode() == 304; |
373 | if (!isSuccessfulRevalidation) { |
374 | // Make sure we don't keep a stale entry in the cache. |
375 | remove(key); |
376 | } |
377 | |
378 | return nullptr; |
379 | } |
380 | |
381 | auto cacheEntry = makeEntry(request, response, WTFMove(responseData)); |
382 | auto record = cacheEntry->encodeAsStorageRecord(); |
383 | |
384 | m_storage->store(record, [protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](const Data& bodyData) mutable { |
385 | MappedBody mappedBody; |
386 | #if ENABLE(SHAREABLE_RESOURCE) |
387 | if (auto sharedMemory = bodyData.tryCreateSharedMemory()) { |
388 | mappedBody.shareableResource = ShareableResource::create(sharedMemory.releaseNonNull(), 0, bodyData.size()); |
389 | ASSERT(mappedBody.shareableResource); |
390 | mappedBody.shareableResource->createHandle(mappedBody.shareableResourceHandle); |
391 | } |
392 | #endif |
393 | completionHandler(mappedBody); |
394 | LOG(NetworkCache, "(NetworkProcess) stored" ); |
395 | }); |
396 | |
397 | return cacheEntry; |
398 | } |
399 | |
400 | std::unique_ptr<Entry> Cache::storeRedirect(const WebCore::ResourceRequest& request, const WebCore::ResourceResponse& response, const WebCore::ResourceRequest& redirectRequest, Optional<Seconds> maxAgeCap) |
401 | { |
402 | LOG(NetworkCache, "(NetworkProcess) storing redirect %s -> %s" , request.url().string().latin1().data(), redirectRequest.url().string().latin1().data()); |
403 | |
404 | StoreDecision storeDecision = makeStoreDecision(request, response, 0); |
405 | if (storeDecision != StoreDecision::Yes) { |
406 | LOG(NetworkCache, "(NetworkProcess) didn't store redirect, storeDecision=%d" , static_cast<int>(storeDecision)); |
407 | return nullptr; |
408 | } |
409 | |
410 | auto cacheEntry = makeRedirectEntry(request, response, redirectRequest); |
411 | |
412 | #if ENABLE(RESOURCE_LOAD_STATISTICS) |
413 | if (maxAgeCap) { |
414 | LOG(NetworkCache, "(NetworkProcess) capping max age for redirect %s -> %s" , request.url().string().latin1().data(), redirectRequest.url().string().latin1().data()); |
415 | cacheEntry->capMaxAge(maxAgeCap.value()); |
416 | } |
417 | #else |
418 | UNUSED_PARAM(maxAgeCap); |
419 | #endif |
420 | |
421 | auto record = cacheEntry->encodeAsStorageRecord(); |
422 | |
423 | m_storage->store(record, nullptr); |
424 | |
425 | return cacheEntry; |
426 | } |
427 | |
428 | std::unique_ptr<Entry> Cache::update(const WebCore::ResourceRequest& originalRequest, const GlobalFrameID& frameID, const Entry& existingEntry, const WebCore::ResourceResponse& validatingResponse) |
429 | { |
430 | LOG(NetworkCache, "(NetworkProcess) updating %s" , originalRequest.url().string().latin1().data()); |
431 | |
432 | WebCore::ResourceResponse response = existingEntry.response(); |
433 | WebCore::updateResponseHeadersAfterRevalidation(response, validatingResponse); |
434 | |
435 | auto updateEntry = std::make_unique<Entry>(existingEntry.key(), response, existingEntry.buffer(), WebCore::collectVaryingRequestHeaders(networkProcess().defaultStorageSession(), originalRequest, response)); |
436 | auto updateRecord = updateEntry->encodeAsStorageRecord(); |
437 | |
438 | m_storage->store(updateRecord, { }); |
439 | |
440 | return updateEntry; |
441 | } |
442 | |
443 | void Cache::remove(const Key& key) |
444 | { |
445 | m_storage->remove(key); |
446 | } |
447 | |
448 | void Cache::remove(const WebCore::ResourceRequest& request) |
449 | { |
450 | remove(makeCacheKey(request)); |
451 | } |
452 | |
453 | void Cache::remove(const Vector<Key>& keys, Function<void()>&& completionHandler) |
454 | { |
455 | m_storage->remove(keys, WTFMove(completionHandler)); |
456 | } |
457 | |
458 | void Cache::traverse(Function<void(const TraversalEntry*)>&& traverseHandler) |
459 | { |
460 | // Protect against clients making excessive traversal requests. |
461 | const unsigned maximumTraverseCount = 3; |
462 | if (m_traverseCount >= maximumTraverseCount) { |
463 | WTFLogAlways("Maximum parallel cache traverse count exceeded. Ignoring traversal request." ); |
464 | |
465 | RunLoop::main().dispatch([traverseHandler = WTFMove(traverseHandler)] () mutable { |
466 | traverseHandler(nullptr); |
467 | }); |
468 | return; |
469 | } |
470 | |
471 | ++m_traverseCount; |
472 | |
473 | m_storage->traverse(resourceType(), { }, [this, protectedThis = makeRef(*this), traverseHandler = WTFMove(traverseHandler)] (const Storage::Record* record, const Storage::RecordInfo& recordInfo) mutable { |
474 | if (!record) { |
475 | --m_traverseCount; |
476 | traverseHandler(nullptr); |
477 | return; |
478 | } |
479 | |
480 | auto entry = Entry::decodeStorageRecord(*record); |
481 | if (!entry) |
482 | return; |
483 | |
484 | TraversalEntry traversalEntry { *entry, recordInfo }; |
485 | traverseHandler(&traversalEntry); |
486 | }); |
487 | } |
488 | |
489 | String Cache::dumpFilePath() const |
490 | { |
491 | return pathByAppendingComponent(m_storage->versionPath(), "dump.json" ); |
492 | } |
493 | |
494 | void Cache::dumpContentsToFile() |
495 | { |
496 | auto fd = openFile(dumpFilePath(), FileOpenMode::Write); |
497 | if (!isHandleValid(fd)) |
498 | return; |
499 | auto prologue = String("{\n\"entries\": [\n" ).utf8(); |
500 | writeToFile(fd, prologue.data(), prologue.length()); |
501 | |
502 | struct Totals { |
503 | unsigned count { 0 }; |
504 | double worth { 0 }; |
505 | size_t bodySize { 0 }; |
506 | }; |
507 | Totals totals; |
508 | auto flags = { Storage::TraverseFlag::ComputeWorth, Storage::TraverseFlag::ShareCount }; |
509 | size_t capacity = m_storage->capacity(); |
510 | m_storage->traverse(resourceType(), flags, [fd, totals, capacity](const Storage::Record* record, const Storage::RecordInfo& info) mutable { |
511 | if (!record) { |
512 | StringBuilder epilogue; |
513 | epilogue.appendLiteral("{}\n],\n" ); |
514 | epilogue.appendLiteral("\"totals\": {\n" ); |
515 | epilogue.appendLiteral("\"capacity\": " ); |
516 | epilogue.appendNumber(capacity); |
517 | epilogue.appendLiteral(",\n" ); |
518 | epilogue.appendLiteral("\"count\": " ); |
519 | epilogue.appendNumber(totals.count); |
520 | epilogue.appendLiteral(",\n" ); |
521 | epilogue.appendLiteral("\"bodySize\": " ); |
522 | epilogue.appendNumber(totals.bodySize); |
523 | epilogue.appendLiteral(",\n" ); |
524 | epilogue.appendLiteral("\"averageWorth\": " ); |
525 | epilogue.appendFixedPrecisionNumber(totals.count ? totals.worth / totals.count : 0); |
526 | epilogue.appendLiteral("\n" ); |
527 | epilogue.appendLiteral("}\n}\n" ); |
528 | auto writeData = epilogue.toString().utf8(); |
529 | writeToFile(fd, writeData.data(), writeData.length()); |
530 | closeFile(fd); |
531 | return; |
532 | } |
533 | auto entry = Entry::decodeStorageRecord(*record); |
534 | if (!entry) |
535 | return; |
536 | ++totals.count; |
537 | totals.worth += info.worth; |
538 | totals.bodySize += info.bodySize; |
539 | |
540 | StringBuilder json; |
541 | entry->asJSON(json, info); |
542 | json.appendLiteral(",\n" ); |
543 | auto writeData = json.toString().utf8(); |
544 | writeToFile(fd, writeData.data(), writeData.length()); |
545 | }); |
546 | } |
547 | |
548 | void Cache::deleteDumpFile() |
549 | { |
550 | WorkQueue::create("com.apple.WebKit.Cache.delete" )->dispatch([path = dumpFilePath().isolatedCopy()] { |
551 | deleteFile(path); |
552 | }); |
553 | } |
554 | |
555 | void Cache::clear(WallTime modifiedSince, Function<void()>&& completionHandler) |
556 | { |
557 | LOG(NetworkCache, "(NetworkProcess) clearing cache" ); |
558 | |
559 | String anyType; |
560 | m_storage->clear(anyType, modifiedSince, WTFMove(completionHandler)); |
561 | |
562 | deleteDumpFile(); |
563 | } |
564 | |
565 | void Cache::clear() |
566 | { |
567 | clear(-WallTime::infinity(), nullptr); |
568 | } |
569 | |
570 | String Cache::recordsPath() const |
571 | { |
572 | return m_storage->recordsPath(); |
573 | } |
574 | |
575 | void Cache::retrieveData(const DataKey& dataKey, Function<void(const uint8_t*, size_t)> completionHandler) |
576 | { |
577 | Key key { dataKey, m_storage->salt() }; |
578 | m_storage->retrieve(key, 4, [completionHandler = WTFMove(completionHandler)] (auto record, auto) mutable { |
579 | if (!record || !record->body.size()) { |
580 | completionHandler(nullptr, 0); |
581 | return true; |
582 | } |
583 | completionHandler(record->body.data(), record->body.size()); |
584 | return true; |
585 | }); |
586 | } |
587 | |
588 | void Cache::storeData(const DataKey& dataKey, const uint8_t* data, size_t size) |
589 | { |
590 | Key key { dataKey, m_storage->salt() }; |
591 | Storage::Record record { key, WallTime::now(), { }, Data { data, size }, { } }; |
592 | m_storage->store(record, { }); |
593 | } |
594 | |
595 | } |
596 | } |
597 | |