1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "third_party/blink/renderer/modules/cache_storage/cache_storage.h"
6 
7 #include <utility>
8 
9 #include "base/memory/ptr_util.h"
10 #include "base/metrics/histogram_macros.h"
11 #include "third_party/blink/public/common/browser_interface_broker_proxy.h"
12 #include "third_party/blink/public/common/cache_storage/cache_storage_utils.h"
13 #include "third_party/blink/public/mojom/cache_storage/cache_storage.mojom-blink.h"
14 #include "third_party/blink/public/platform/web_content_settings_client.h"
15 #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
16 #include "third_party/blink/renderer/bindings/modules/v8/v8_multi_cache_query_options.h"
17 #include "third_party/blink/renderer/core/dom/document.h"
18 #include "third_party/blink/renderer/core/dom/dom_exception.h"
19 #include "third_party/blink/renderer/core/execution_context/execution_context.h"
20 #include "third_party/blink/renderer/core/fetch/request.h"
21 #include "third_party/blink/renderer/core/fetch/response.h"
22 #include "third_party/blink/renderer/core/frame/local_frame.h"
23 #include "third_party/blink/renderer/core/inspector/console_message.h"
24 #include "third_party/blink/renderer/modules/cache_storage/cache_storage_blob_client_list.h"
25 #include "third_party/blink/renderer/modules/cache_storage/cache_storage_error.h"
26 #include "third_party/blink/renderer/modules/cache_storage/cache_storage_trace_utils.h"
27 #include "third_party/blink/renderer/modules/cache_storage/cache_utils.h"
28 #include "third_party/blink/renderer/modules/service_worker/service_worker_global_scope.h"
29 #include "third_party/blink/renderer/platform/bindings/script_state.h"
30 #include "third_party/blink/renderer/platform/heap/heap.h"
31 #include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
32 #include "third_party/blink/renderer/platform/instrumentation/tracing/traced_value.h"
33 #include "third_party/blink/renderer/platform/network/http_names.h"
34 
35 namespace mojo {
36 
37 using blink::mojom::blink::CacheQueryOptions;
38 using blink::mojom::blink::CacheQueryOptionsPtr;
39 using blink::mojom::blink::MultiCacheQueryOptions;
40 using blink::mojom::blink::MultiCacheQueryOptionsPtr;
41 
42 template <>
43 struct TypeConverter<MultiCacheQueryOptionsPtr,
44                      const blink::MultiCacheQueryOptions*> {
Convertmojo::TypeConverter45   static MultiCacheQueryOptionsPtr Convert(
46       const blink::MultiCacheQueryOptions* input) {
47     CacheQueryOptionsPtr query_options = CacheQueryOptions::New();
48     query_options->ignore_search = input->ignoreSearch();
49     query_options->ignore_method = input->ignoreMethod();
50     query_options->ignore_vary = input->ignoreVary();
51 
52     MultiCacheQueryOptionsPtr output = MultiCacheQueryOptions::New();
53     output->query_options = std::move(query_options);
54     output->cache_name = input->cacheName();
55     return output;
56   }
57 };
58 
59 }  // namespace mojo
60 
61 namespace blink {
62 
63 namespace {
64 
IsCacheStorageAllowed(ScriptState * script_state)65 bool IsCacheStorageAllowed(ScriptState* script_state) {
66   ExecutionContext* context = ExecutionContext::From(script_state);
67 
68   if (auto* document = Document::DynamicFrom(context)) {
69     LocalFrame* frame = document->GetFrame();
70     if (!frame)
71       return false;
72     if (auto* settings_client = frame->GetContentSettingsClient()) {
73       // This triggers a sync IPC.
74       return settings_client->AllowCacheStorage();
75     }
76     return true;
77   }
78 
79   WebContentSettingsClient* content_settings_client =
80       To<WorkerGlobalScope>(context)->ContentSettingsClient();
81   if (!content_settings_client)
82     return true;
83   // This triggers a sync IPC.
84   return content_settings_client->AllowCacheStorage();
85 }
86 
87 }  // namespace
88 
open(ScriptState * script_state,const String & cache_name)89 ScriptPromise CacheStorage::open(ScriptState* script_state,
90                                  const String& cache_name) {
91   int64_t trace_id = blink::cache_storage::CreateTraceId();
92   TRACE_EVENT_WITH_FLOW1("CacheStorage", "CacheStorage::Open",
93                          TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_OUT,
94                          "name", CacheStorageTracedValue(cache_name));
95 
96   auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
97   ScriptPromise promise = resolver->Promise();
98 
99   if (!IsAllowed(script_state)) {
100     resolver->Reject(
101         MakeGarbageCollected<DOMException>(DOMExceptionCode::kSecurityError));
102     return promise;
103   }
104 
105   // The context may be destroyed and the mojo connection unbound. However the
106   // object may live on, reject any requests after the context is destroyed.
107   if (!cache_storage_remote_) {
108     resolver->Reject(MakeGarbageCollected<DOMException>(
109         DOMExceptionCode::kInvalidStateError));
110     return promise;
111   }
112 
113   ever_used_ = true;
114 
115   // Make sure to bind the CacheStorage object to keep the mojo interface
116   // pointer alive during the operation.  Otherwise GC might prevent the
117   // callback from ever being executed.
118   cache_storage_remote_->Open(
119       cache_name, trace_id,
120       WTF::Bind(
121           [](ScriptPromiseResolver* resolver,
122              GlobalFetch::ScopedFetcher* fetcher, base::TimeTicks start_time,
123              int64_t trace_id, mojom::blink::OpenResultPtr result) {
124             UMA_HISTOGRAM_TIMES("ServiceWorkerCache.CacheStorage.Renderer.Open",
125                                 base::TimeTicks::Now() - start_time);
126             if (!resolver->GetExecutionContext() ||
127                 resolver->GetExecutionContext()->IsContextDestroyed()) {
128               return;
129             }
130             if (result->is_status()) {
131               TRACE_EVENT_WITH_FLOW1(
132                   "CacheStorage", "CacheStorage::Open::Callback",
133                   TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_IN, "status",
134                   CacheStorageTracedValue(result->get_status()));
135               switch (result->get_status()) {
136                 case mojom::blink::CacheStorageError::kErrorNotFound:
137                 case mojom::blink::CacheStorageError::kErrorStorage:
138                   resolver->Resolve();
139                   break;
140                 default:
141                   RejectCacheStorageWithError(resolver, result->get_status());
142                   break;
143               }
144             } else {
145               TRACE_EVENT_WITH_FLOW1(
146                   "CacheStorage", "CacheStorage::Open::Callback",
147                   TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_IN, "status",
148                   "success");
149               // See https://bit.ly/2S0zRAS for task types.
150               resolver->Resolve(MakeGarbageCollected<Cache>(
151                   fetcher, std::move(result->get_cache()),
152                   resolver->GetExecutionContext()->GetTaskRunner(
153                       blink::TaskType::kMiscPlatformAPI)));
154             }
155           },
156           WrapPersistent(resolver), WrapPersistent(scoped_fetcher_.Get()),
157           base::TimeTicks::Now(), trace_id));
158 
159   return promise;
160 }
161 
has(ScriptState * script_state,const String & cache_name)162 ScriptPromise CacheStorage::has(ScriptState* script_state,
163                                 const String& cache_name) {
164   int64_t trace_id = blink::cache_storage::CreateTraceId();
165   TRACE_EVENT_WITH_FLOW1("CacheStorage", "CacheStorage::Has",
166                          TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_OUT,
167                          "name", CacheStorageTracedValue(cache_name));
168 
169   auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
170   ScriptPromise promise = resolver->Promise();
171 
172   if (!IsAllowed(script_state)) {
173     resolver->Reject(
174         MakeGarbageCollected<DOMException>(DOMExceptionCode::kSecurityError));
175     return promise;
176   }
177 
178   // The context may be destroyed and the mojo connection unbound. However the
179   // object may live on, reject any requests after the context is destroyed.
180   if (!cache_storage_remote_) {
181     resolver->Reject(MakeGarbageCollected<DOMException>(
182         DOMExceptionCode::kInvalidStateError));
183     return promise;
184   }
185 
186   ever_used_ = true;
187 
188   // Make sure to bind the CacheStorage object to keep the mojo interface
189   // pointer alive during the operation.  Otherwise GC might prevent the
190   // callback from ever being executed.
191   cache_storage_remote_->Has(
192       cache_name, trace_id,
193       WTF::Bind(
194           [](ScriptPromiseResolver* resolver, base::TimeTicks start_time,
195              int64_t trace_id, mojom::blink::CacheStorageError result) {
196             UMA_HISTOGRAM_TIMES("ServiceWorkerCache.CacheStorage.Renderer.Has",
197                                 base::TimeTicks::Now() - start_time);
198             TRACE_EVENT_WITH_FLOW1(
199                 "CacheStorage", "CacheStorage::Has::Callback",
200                 TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_IN, "status",
201                 CacheStorageTracedValue(result));
202             if (!resolver->GetExecutionContext() ||
203                 resolver->GetExecutionContext()->IsContextDestroyed())
204               return;
205             switch (result) {
206               case mojom::blink::CacheStorageError::kSuccess:
207                 resolver->Resolve(true);
208                 break;
209               case mojom::blink::CacheStorageError::kErrorNotFound:
210                 resolver->Resolve(false);
211                 break;
212               default:
213                 RejectCacheStorageWithError(resolver, result);
214                 break;
215             }
216           },
217           WrapPersistent(resolver), base::TimeTicks::Now(), trace_id));
218 
219   return promise;
220 }
221 
Delete(ScriptState * script_state,const String & cache_name)222 ScriptPromise CacheStorage::Delete(ScriptState* script_state,
223                                    const String& cache_name) {
224   int64_t trace_id = blink::cache_storage::CreateTraceId();
225   TRACE_EVENT_WITH_FLOW1("CacheStorage", "CacheStorage::Delete",
226                          TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_OUT,
227                          "name", CacheStorageTracedValue(cache_name));
228 
229   auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
230   ScriptPromise promise = resolver->Promise();
231 
232   if (!IsAllowed(script_state)) {
233     resolver->Reject(
234         MakeGarbageCollected<DOMException>(DOMExceptionCode::kSecurityError));
235     return promise;
236   }
237 
238   // The context may be destroyed and the mojo connection unbound. However the
239   // object may live on, reject any requests after the context is destroyed.
240   if (!cache_storage_remote_) {
241     resolver->Reject(MakeGarbageCollected<DOMException>(
242         DOMExceptionCode::kInvalidStateError));
243     return promise;
244   }
245 
246   ever_used_ = true;
247 
248   // Make sure to bind the CacheStorage object to keep the mojo interface
249   // pointer alive during the operation.  Otherwise GC might prevent the
250   // callback from ever being executed.
251   cache_storage_remote_->Delete(
252       cache_name, trace_id,
253       WTF::Bind(
254           [](ScriptPromiseResolver* resolver, base::TimeTicks start_time,
255              int64_t trace_id, mojom::blink::CacheStorageError result) {
256             UMA_HISTOGRAM_TIMES(
257                 "ServiceWorkerCache.CacheStorage.Renderer.Delete",
258                 base::TimeTicks::Now() - start_time);
259             TRACE_EVENT_WITH_FLOW1(
260                 "CacheStorage", "CacheStorage::Delete::Callback",
261                 TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_IN, "status",
262                 CacheStorageTracedValue(result));
263             if (!resolver->GetExecutionContext() ||
264                 resolver->GetExecutionContext()->IsContextDestroyed())
265               return;
266             switch (result) {
267               case mojom::blink::CacheStorageError::kSuccess:
268                 resolver->Resolve(true);
269                 break;
270               case mojom::blink::CacheStorageError::kErrorStorage:
271               case mojom::blink::CacheStorageError::kErrorNotFound:
272                 resolver->Resolve(false);
273                 break;
274               default:
275                 RejectCacheStorageWithError(resolver, result);
276                 break;
277             }
278           },
279           WrapPersistent(resolver), base::TimeTicks::Now(), trace_id));
280 
281   return promise;
282 }
283 
keys(ScriptState * script_state)284 ScriptPromise CacheStorage::keys(ScriptState* script_state) {
285   int64_t trace_id = blink::cache_storage::CreateTraceId();
286   TRACE_EVENT_WITH_FLOW0("CacheStorage", "CacheStorage::Keys",
287                          TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_OUT);
288 
289   auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
290   ScriptPromise promise = resolver->Promise();
291 
292   if (!IsAllowed(script_state)) {
293     resolver->Reject(
294         MakeGarbageCollected<DOMException>(DOMExceptionCode::kSecurityError));
295     return promise;
296   }
297 
298   // The context may be destroyed and the mojo connection unbound. However the
299   // object may live on, reject any requests after the context is destroyed.
300   if (!cache_storage_remote_) {
301     resolver->Reject(MakeGarbageCollected<DOMException>(
302         DOMExceptionCode::kInvalidStateError));
303     return promise;
304   }
305 
306   ever_used_ = true;
307 
308   // Make sure to bind the CacheStorage object to keep the mojo interface
309   // pointer alive during the operation.  Otherwise GC might prevent the
310   // callback from ever being executed.
311   cache_storage_remote_->Keys(
312       trace_id,
313       WTF::Bind(
314           [](ScriptPromiseResolver* resolver, base::TimeTicks start_time,
315              int64_t trace_id, const Vector<String>& keys) {
316             UMA_HISTOGRAM_TIMES("ServiceWorkerCache.CacheStorage.Renderer.Keys",
317                                 base::TimeTicks::Now() - start_time);
318             TRACE_EVENT_WITH_FLOW1(
319                 "CacheStorage", "CacheStorage::Keys::Callback",
320                 TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_IN, "key_list",
321                 CacheStorageTracedValue(keys));
322             if (!resolver->GetExecutionContext() ||
323                 resolver->GetExecutionContext()->IsContextDestroyed())
324               return;
325             resolver->Resolve(keys);
326           },
327           WrapPersistent(resolver), base::TimeTicks::Now(), trace_id));
328 
329   return promise;
330 }
331 
match(ScriptState * script_state,const RequestInfo & request,const MultiCacheQueryOptions * options,ExceptionState & exception_state)332 ScriptPromise CacheStorage::match(ScriptState* script_state,
333                                   const RequestInfo& request,
334                                   const MultiCacheQueryOptions* options,
335                                   ExceptionState& exception_state) {
336   DCHECK(!request.IsNull());
337 
338   if (request.IsRequest())
339     return MatchImpl(script_state, request.GetAsRequest(), options);
340   Request* new_request =
341       Request::Create(script_state, request.GetAsUSVString(), exception_state);
342   if (exception_state.HadException())
343     return ScriptPromise();
344   return MatchImpl(script_state, new_request, options);
345 }
346 
MatchImpl(ScriptState * script_state,const Request * request,const MultiCacheQueryOptions * options)347 ScriptPromise CacheStorage::MatchImpl(ScriptState* script_state,
348                                       const Request* request,
349                                       const MultiCacheQueryOptions* options) {
350   int64_t trace_id = blink::cache_storage::CreateTraceId();
351   mojom::blink::FetchAPIRequestPtr mojo_request =
352       request->CreateFetchAPIRequest();
353   mojom::blink::MultiCacheQueryOptionsPtr mojo_options =
354       mojom::blink::MultiCacheQueryOptions::From(options);
355 
356   ExecutionContext* context = ExecutionContext::From(script_state);
357   bool in_related_fetch_event = false;
358   if (auto* global_scope = DynamicTo<ServiceWorkerGlobalScope>(context))
359     in_related_fetch_event = global_scope->HasRelatedFetchEvent(request->url());
360 
361   TRACE_EVENT_WITH_FLOW2("CacheStorage", "CacheStorage::MatchImpl",
362                          TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_OUT,
363                          "request", CacheStorageTracedValue(mojo_request),
364                          "options", CacheStorageTracedValue(mojo_options));
365 
366   auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
367   const ScriptPromise promise = resolver->Promise();
368 
369   if (!IsAllowed(script_state)) {
370     resolver->Reject(
371         MakeGarbageCollected<DOMException>(DOMExceptionCode::kSecurityError));
372     return promise;
373   }
374 
375   // The context may be destroyed and the mojo connection unbound. However the
376   // object may live on, reject any requests after the context is destroyed.
377   if (!cache_storage_remote_) {
378     resolver->Reject(MakeGarbageCollected<DOMException>(
379         DOMExceptionCode::kInvalidStateError));
380     return promise;
381   }
382 
383   if (request->method() != http_names::kGET && !options->ignoreMethod()) {
384     resolver->Resolve();
385     return promise;
386   }
387 
388   ever_used_ = true;
389 
390   // Make sure to bind the CacheStorage object to keep the mojo interface
391   // pointer alive during the operation.  Otherwise GC might prevent the
392   // callback from ever being executed.
393   cache_storage_remote_->Match(
394       std::move(mojo_request), std::move(mojo_options), in_related_fetch_event,
395       trace_id,
396       WTF::Bind(
397           [](ScriptPromiseResolver* resolver, base::TimeTicks start_time,
398              const MultiCacheQueryOptions* options, int64_t trace_id,
399              CacheStorage* self, mojom::blink::MatchResultPtr result) {
400             base::TimeDelta elapsed = base::TimeTicks::Now() - start_time;
401             if (!options->hasCacheName() || options->cacheName().IsEmpty()) {
402               UMA_HISTOGRAM_LONG_TIMES(
403                   "ServiceWorkerCache.CacheStorage.Renderer.MatchAllCaches",
404                   elapsed);
405             } else {
406               UMA_HISTOGRAM_LONG_TIMES(
407                   "ServiceWorkerCache.CacheStorage.Renderer.MatchOneCache",
408                   elapsed);
409             }
410             if (!resolver->GetExecutionContext() ||
411                 resolver->GetExecutionContext()->IsContextDestroyed())
412               return;
413             if (result->is_status()) {
414               TRACE_EVENT_WITH_FLOW1(
415                   "CacheStorage", "CacheStorage::MatchImpl::Callback",
416                   TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_IN, "status",
417                   CacheStorageTracedValue(result->get_status()));
418               switch (result->get_status()) {
419                 case mojom::CacheStorageError::kErrorNotFound:
420                 case mojom::CacheStorageError::kErrorStorage:
421                 case mojom::CacheStorageError::kErrorCacheNameNotFound:
422                   resolver->Resolve();
423                   break;
424                 default:
425                   RejectCacheStorageWithError(resolver, result->get_status());
426                   break;
427               }
428             } else {
429               ScriptState::Scope scope(resolver->GetScriptState());
430               if (result->is_eager_response()) {
431                 TRACE_EVENT_WITH_FLOW1(
432                     "CacheStorage", "CacheStorage::MatchImpl::Callback",
433                     TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_IN,
434                     "eager_response",
435                     CacheStorageTracedValue(
436                         result->get_eager_response()->response));
437                 resolver->Resolve(
438                     CreateEagerResponse(resolver->GetScriptState(),
439                                         std::move(result->get_eager_response()),
440                                         self->blob_client_list_));
441               } else {
442                 TRACE_EVENT_WITH_FLOW1(
443                     "CacheStorage", "CacheStorage::MatchImpl::Callback",
444                     TRACE_ID_GLOBAL(trace_id), TRACE_EVENT_FLAG_FLOW_IN,
445                     "response",
446                     CacheStorageTracedValue(result->get_response()));
447                 resolver->Resolve(Response::Create(resolver->GetScriptState(),
448                                                    *result->get_response()));
449               }
450             }
451           },
452           WrapPersistent(resolver), base::TimeTicks::Now(),
453           WrapPersistent(options), trace_id, WrapPersistent(this)));
454 
455   return promise;
456 }
457 
CacheStorage(ExecutionContext * context,GlobalFetch::ScopedFetcher * fetcher)458 CacheStorage::CacheStorage(ExecutionContext* context,
459                            GlobalFetch::ScopedFetcher* fetcher)
460     : ExecutionContextLifecycleObserver(context),
461       scoped_fetcher_(fetcher),
462       blob_client_list_(MakeGarbageCollected<CacheStorageBlobClientList>()),
463       ever_used_(false) {
464   // See https://bit.ly/2S0zRAS for task types.
465   scoped_refptr<base::SingleThreadTaskRunner> task_runner =
466       context->GetTaskRunner(blink::TaskType::kMiscPlatformAPI);
467 
468   // Service workers may already have a CacheStoragePtr provided as an
469   // optimization.
470   if (auto* service_worker = DynamicTo<ServiceWorkerGlobalScope>(context)) {
471     mojo::PendingRemote<mojom::blink::CacheStorage> info =
472         service_worker->TakeCacheStorage();
473     if (info) {
474       cache_storage_remote_ = mojo::Remote<mojom::blink::CacheStorage>(
475           std::move(info), task_runner);
476       return;
477     }
478   }
479 
480   context->GetBrowserInterfaceBroker().GetInterface(
481       cache_storage_remote_.BindNewPipeAndPassReceiver(task_runner));
482 }
483 
484 CacheStorage::~CacheStorage() = default;
485 
HasPendingActivity() const486 bool CacheStorage::HasPendingActivity() const {
487   // Once the CacheStorage has been used once we keep it alive until the
488   // context goes away.  This allows us to use the existence of this
489   // context as a hint to optimizations such as keeping backend disk_caches
490   // open in the browser process.
491   //
492   // Note, this also keeps the CacheStorage alive during active Cache and
493   // CacheStorage operations.
494   return ever_used_;
495 }
496 
Trace(Visitor * visitor)497 void CacheStorage::Trace(Visitor* visitor) {
498   visitor->Trace(scoped_fetcher_);
499   visitor->Trace(blob_client_list_);
500   ScriptWrappable::Trace(visitor);
501   ExecutionContextLifecycleObserver::Trace(visitor);
502 }
503 
IsAllowed(ScriptState * script_state)504 bool CacheStorage::IsAllowed(ScriptState* script_state) {
505   if (!allowed_.has_value()) {
506     // Cache the IsCacheStorageAllowed() because it triggers a sync IPC.
507     allowed_.emplace(IsCacheStorageAllowed(script_state));
508   }
509   return allowed_.value();
510 }
511 
ContextDestroyed()512 void CacheStorage::ContextDestroyed() {
513   cache_storage_remote_.reset();
514 }
515 
516 }  // namespace blink
517