1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "mozilla/dom/cache/Cache.h"
9 #include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject
10 #include "js/PropertyAndElement.h" // JS_GetElement
11 #include "mozilla/dom/Headers.h"
12 #include "mozilla/dom/InternalResponse.h"
13 #include "mozilla/dom/Promise.h"
14 #include "mozilla/dom/PromiseNativeHandler.h"
15 #include "mozilla/dom/Response.h"
16 #include "mozilla/dom/RootedDictionary.h"
17 #include "mozilla/dom/WorkerPrivate.h"
18 #include "mozilla/dom/CacheBinding.h"
19 #include "mozilla/dom/cache/AutoUtils.h"
20 #include "mozilla/dom/cache/CacheChild.h"
21 #include "mozilla/dom/cache/CacheCommon.h"
22 #include "mozilla/dom/cache/CacheWorkerRef.h"
23 #include "mozilla/dom/quota/ResultExtensions.h"
24 #include "mozilla/ErrorResult.h"
25 #include "mozilla/Preferences.h"
26 #include "mozilla/Unused.h"
27 #include "nsIGlobalObject.h"
29 namespace mozilla::dom::cache
{
31 using mozilla::ipc::PBackgroundChild
;
35 enum class PutStatusPolicy
{ Default
, RequireOK
};
37 bool IsValidPutRequestURL(const nsAString
& aUrl
, ErrorResult
& aRv
) {
38 bool validScheme
= false;
40 // make a copy because ProcessURL strips the fragmet
41 NS_ConvertUTF16toUTF8
url(aUrl
);
43 TypeUtils::ProcessURL(url
, &validScheme
, nullptr, nullptr, aRv
);
49 // `url` has been modified, so don't use it here.
50 aRv
.ThrowTypeError
<MSG_INVALID_URL_SCHEME
>("Request",
51 NS_ConvertUTF16toUTF8(aUrl
));
58 static bool IsValidPutRequestMethod(const Request
& aRequest
, ErrorResult
& aRv
) {
60 aRequest
.GetMethod(method
);
61 if (!method
.LowerCaseEqualsLiteral("get")) {
62 aRv
.ThrowTypeError
<MSG_INVALID_REQUEST_METHOD
>(method
);
69 static bool IsValidPutRequestMethod(const RequestOrUSVString
& aRequest
,
71 // If the provided request is a string URL, then it will default to
72 // a valid http method automatically.
73 if (!aRequest
.IsRequest()) {
76 return IsValidPutRequestMethod(aRequest
.GetAsRequest(), aRv
);
79 static bool IsValidPutResponseStatus(Response
& aResponse
,
80 PutStatusPolicy aPolicy
,
82 if ((aPolicy
== PutStatusPolicy::RequireOK
&& !aResponse
.Ok()) ||
83 aResponse
.Status() == 206) {
84 nsCString
type(ResponseTypeValues::GetString(aResponse
.Type()));
87 aResponse
.GetUrl(url
);
89 aRv
.ThrowTypeError
<MSG_CACHE_ADD_FAILED_RESPONSE
>(
90 type
, IntToCString(aResponse
.Status()), NS_ConvertUTF16toUTF8(url
));
99 // Helper class to wait for Add()/AddAll() fetch requests to complete and
100 // then perform a PutAll() with the responses. This class holds a WorkerRef
101 // to keep the Worker thread alive. This is mainly to ensure that Add/AddAll
102 // act the same as other Cache operations that directly create a CacheOpChild
104 class Cache::FetchHandler final
: public PromiseNativeHandler
{
106 FetchHandler(SafeRefPtr
<CacheWorkerRef
> aWorkerRef
, Cache
* aCache
,
107 nsTArray
<SafeRefPtr
<Request
>>&& aRequestList
, Promise
* aPromise
)
108 : mWorkerRef(std::move(aWorkerRef
)),
110 mRequestList(std::move(aRequestList
)),
112 MOZ_ASSERT_IF(!NS_IsMainThread(), mWorkerRef
);
113 MOZ_DIAGNOSTIC_ASSERT(mCache
);
114 MOZ_DIAGNOSTIC_ASSERT(mPromise
);
117 virtual void ResolvedCallback(JSContext
* aCx
, JS::Handle
<JS::Value
> aValue
,
118 ErrorResult
& aRv
) override
{
119 NS_ASSERT_OWNINGTHREAD(FetchHandler
);
121 // Stop holding the worker alive when we leave this method.
122 const SafeRefPtr
<CacheWorkerRef
> workerRef
= std::move(mWorkerRef
);
124 // Promise::All() passed an array of fetch() Promises should give us
125 // an Array of Response objects. The following code unwraps these
126 // JS values back to an nsTArray<RefPtr<Response>>.
128 AutoTArray
<RefPtr
<Response
>, 256> responseList
;
129 responseList
.SetCapacity(mRequestList
.Length());
131 const auto failOnErr
= [this](const auto) { Fail(); };
134 QM_TRY(OkIf(JS::IsArrayObject(aCx
, aValue
, &isArray
)), QM_VOID
, failOnErr
);
135 QM_TRY(OkIf(isArray
), QM_VOID
, failOnErr
);
137 JS::Rooted
<JSObject
*> obj(aCx
, &aValue
.toObject());
140 QM_TRY(OkIf(JS::GetArrayLength(aCx
, obj
, &length
)), QM_VOID
, failOnErr
);
142 for (uint32_t i
= 0; i
< length
; ++i
) {
143 JS::Rooted
<JS::Value
> value(aCx
);
145 QM_TRY(OkIf(JS_GetElement(aCx
, obj
, i
, &value
)), QM_VOID
, failOnErr
);
147 QM_TRY(OkIf(value
.isObject()), QM_VOID
, failOnErr
);
149 JS::Rooted
<JSObject
*> responseObj(aCx
, &value
.toObject());
151 RefPtr
<Response
> response
;
152 QM_TRY(MOZ_TO_RESULT(UNWRAP_OBJECT(Response
, responseObj
, response
)),
155 QM_TRY(OkIf(response
->Type() != ResponseType::Error
), QM_VOID
, failOnErr
);
157 // Do not allow the convenience methods .add()/.addAll() to store failed
158 // or invalid responses. A consequence of this is that these methods
159 // cannot be used to store opaque or opaqueredirect responses since they
160 // always expose a 0 status value.
161 ErrorResult errorResult
;
162 if (!IsValidPutResponseStatus(*response
, PutStatusPolicy::RequireOK
,
164 // TODO: abort the fetch requests we have running (bug 1157434)
165 mPromise
->MaybeReject(std::move(errorResult
));
169 responseList
.AppendElement(std::move(response
));
172 MOZ_DIAGNOSTIC_ASSERT(mRequestList
.Length() == responseList
.Length());
174 // Now store the unwrapped Response list in the Cache.
176 // TODO: Here we use the JSContext as received by the ResolvedCallback, and
177 // its state could be the wrong one. The spec doesn't say anything
178 // about it, yet (bug 1384006)
179 RefPtr
<Promise
> put
=
180 mCache
->PutAll(aCx
, mRequestList
, responseList
, result
);
181 result
.WouldReportJSException();
182 if (NS_WARN_IF(result
.Failed())) {
183 // TODO: abort the fetch requests we have running (bug 1157434)
184 mPromise
->MaybeReject(std::move(result
));
188 // Chain the Cache::Put() promise to the original promise returned to
189 // the content script.
190 mPromise
->MaybeResolve(put
);
193 virtual void RejectedCallback(JSContext
* aCx
, JS::Handle
<JS::Value
> aValue
,
194 ErrorResult
& aRv
) override
{
195 NS_ASSERT_OWNINGTHREAD(FetchHandler
);
200 ~FetchHandler() = default;
202 void Fail() { mPromise
->MaybeRejectWithTypeError
<MSG_FETCH_FAILED
>(); }
204 SafeRefPtr
<CacheWorkerRef
> mWorkerRef
;
205 RefPtr
<Cache
> mCache
;
206 nsTArray
<SafeRefPtr
<Request
>> mRequestList
;
207 RefPtr
<Promise
> mPromise
;
212 NS_IMPL_ISUPPORTS0(Cache::FetchHandler
)
214 NS_IMPL_CYCLE_COLLECTING_ADDREF(mozilla::dom::cache::Cache
);
215 NS_IMPL_CYCLE_COLLECTING_RELEASE(mozilla::dom::cache::Cache
);
216 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(mozilla::dom::cache::Cache
, mGlobal
);
218 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Cache
)
219 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
220 NS_INTERFACE_MAP_ENTRY(nsISupports
)
223 Cache::Cache(nsIGlobalObject
* aGlobal
, CacheChild
* aActor
, Namespace aNamespace
)
224 : mGlobal(aGlobal
), mActor(aActor
), mNamespace(aNamespace
) {
225 MOZ_DIAGNOSTIC_ASSERT(mGlobal
);
226 MOZ_DIAGNOSTIC_ASSERT(mActor
);
227 MOZ_DIAGNOSTIC_ASSERT(mNamespace
!= INVALID_NAMESPACE
);
228 mActor
->SetListener(this);
231 already_AddRefed
<Promise
> Cache::Match(JSContext
* aCx
,
232 const RequestOrUSVString
& aRequest
,
233 const CacheQueryOptions
& aOptions
,
235 if (NS_WARN_IF(!mActor
)) {
236 aRv
.Throw(NS_ERROR_UNEXPECTED
);
240 CacheChild::AutoLock
actorLock(*mActor
);
242 SafeRefPtr
<InternalRequest
> ir
=
243 ToInternalRequest(aCx
, aRequest
, IgnoreBody
, aRv
);
244 if (NS_WARN_IF(aRv
.Failed())) {
248 CacheQueryParams params
;
249 ToCacheQueryParams(params
, aOptions
);
251 AutoChildOpArgs
args(
252 this, CacheMatchArgs(CacheRequest(), params
, GetOpenMode()), 1);
254 args
.Add(*ir
, IgnoreBody
, IgnoreInvalidScheme
, aRv
);
255 if (NS_WARN_IF(aRv
.Failed())) {
259 return ExecuteOp(args
, aRv
);
262 already_AddRefed
<Promise
> Cache::MatchAll(
263 JSContext
* aCx
, const Optional
<RequestOrUSVString
>& aRequest
,
264 const CacheQueryOptions
& aOptions
, ErrorResult
& aRv
) {
265 if (NS_WARN_IF(!mActor
)) {
266 aRv
.Throw(NS_ERROR_UNEXPECTED
);
270 CacheChild::AutoLock
actorLock(*mActor
);
272 CacheQueryParams params
;
273 ToCacheQueryParams(params
, aOptions
);
275 AutoChildOpArgs
args(this,
276 CacheMatchAllArgs(Nothing(), params
, GetOpenMode()), 1);
278 if (aRequest
.WasPassed()) {
279 SafeRefPtr
<InternalRequest
> ir
=
280 ToInternalRequest(aCx
, aRequest
.Value(), IgnoreBody
, aRv
);
285 args
.Add(*ir
, IgnoreBody
, IgnoreInvalidScheme
, aRv
);
291 return ExecuteOp(args
, aRv
);
294 already_AddRefed
<Promise
> Cache::Add(JSContext
* aContext
,
295 const RequestOrUSVString
& aRequest
,
296 CallerType aCallerType
, ErrorResult
& aRv
) {
297 if (NS_WARN_IF(!mActor
)) {
298 aRv
.Throw(NS_ERROR_UNEXPECTED
);
302 CacheChild::AutoLock
actorLock(*mActor
);
304 if (!IsValidPutRequestMethod(aRequest
, aRv
)) {
308 GlobalObject
global(aContext
, mGlobal
->GetGlobalJSObject());
309 MOZ_DIAGNOSTIC_ASSERT(!global
.Failed());
311 nsTArray
<SafeRefPtr
<Request
>> requestList(1);
312 RootedDictionary
<RequestInit
> requestInit(aContext
);
313 SafeRefPtr
<Request
> request
=
314 Request::Constructor(global
, aRequest
, requestInit
, aRv
);
315 if (NS_WARN_IF(aRv
.Failed())) {
320 request
->GetUrl(url
);
321 if (NS_WARN_IF(!IsValidPutRequestURL(url
, aRv
))) {
325 requestList
.AppendElement(std::move(request
));
326 return AddAll(global
, std::move(requestList
), aCallerType
, aRv
);
329 already_AddRefed
<Promise
> Cache::AddAll(
330 JSContext
* aContext
, const Sequence
<OwningRequestOrUSVString
>& aRequestList
,
331 CallerType aCallerType
, ErrorResult
& aRv
) {
332 if (NS_WARN_IF(!mActor
)) {
333 aRv
.Throw(NS_ERROR_UNEXPECTED
);
337 CacheChild::AutoLock
actorLock(*mActor
);
339 GlobalObject
global(aContext
, mGlobal
->GetGlobalJSObject());
340 MOZ_DIAGNOSTIC_ASSERT(!global
.Failed());
342 nsTArray
<SafeRefPtr
<Request
>> requestList(aRequestList
.Length());
343 for (uint32_t i
= 0; i
< aRequestList
.Length(); ++i
) {
344 RequestOrUSVString requestOrString
;
346 if (aRequestList
[i
].IsRequest()) {
347 requestOrString
.SetAsRequest() = aRequestList
[i
].GetAsRequest();
349 !IsValidPutRequestMethod(requestOrString
.GetAsRequest(), aRv
))) {
353 requestOrString
.SetAsUSVString().ShareOrDependUpon(
354 aRequestList
[i
].GetAsUSVString());
357 RootedDictionary
<RequestInit
> requestInit(aContext
);
358 SafeRefPtr
<Request
> request
=
359 Request::Constructor(global
, requestOrString
, requestInit
, aRv
);
360 if (NS_WARN_IF(aRv
.Failed())) {
365 request
->GetUrl(url
);
366 if (NS_WARN_IF(!IsValidPutRequestURL(url
, aRv
))) {
370 requestList
.AppendElement(std::move(request
));
373 return AddAll(global
, std::move(requestList
), aCallerType
, aRv
);
376 already_AddRefed
<Promise
> Cache::Put(JSContext
* aCx
,
377 const RequestOrUSVString
& aRequest
,
378 Response
& aResponse
, ErrorResult
& aRv
) {
379 if (NS_WARN_IF(!mActor
)) {
380 aRv
.Throw(NS_ERROR_UNEXPECTED
);
384 CacheChild::AutoLock
actorLock(*mActor
);
386 if (NS_WARN_IF(!IsValidPutRequestMethod(aRequest
, aRv
))) {
390 if (!IsValidPutResponseStatus(aResponse
, PutStatusPolicy::Default
, aRv
)) {
394 if (NS_WARN_IF(aResponse
.GetPrincipalInfo() &&
395 aResponse
.GetPrincipalInfo()->type() ==
396 mozilla::ipc::PrincipalInfo::TExpandedPrincipalInfo
)) {
397 // WebExtensions Content Scripts can currently run fetch from their global
398 // which will end up to have an expanded principal, but we require that the
399 // contents of Cache storage for the content origin to be same-origin, and
400 // never an expanded principal (See Bug 1753810).
401 aRv
.ThrowSecurityError("Disallowed on WebExtension ContentScript Request");
405 SafeRefPtr
<InternalRequest
> ir
=
406 ToInternalRequest(aCx
, aRequest
, ReadBody
, aRv
);
407 if (NS_WARN_IF(aRv
.Failed())) {
411 AutoChildOpArgs
args(this, CachePutAllArgs(), 1);
413 args
.Add(aCx
, *ir
, ReadBody
, TypeErrorOnInvalidScheme
, aResponse
, aRv
);
414 if (NS_WARN_IF(aRv
.Failed())) {
418 return ExecuteOp(args
, aRv
);
421 already_AddRefed
<Promise
> Cache::Delete(JSContext
* aCx
,
422 const RequestOrUSVString
& aRequest
,
423 const CacheQueryOptions
& aOptions
,
425 if (NS_WARN_IF(!mActor
)) {
426 aRv
.Throw(NS_ERROR_UNEXPECTED
);
430 CacheChild::AutoLock
actorLock(*mActor
);
432 SafeRefPtr
<InternalRequest
> ir
=
433 ToInternalRequest(aCx
, aRequest
, IgnoreBody
, aRv
);
434 if (NS_WARN_IF(aRv
.Failed())) {
438 CacheQueryParams params
;
439 ToCacheQueryParams(params
, aOptions
);
441 AutoChildOpArgs
args(this, CacheDeleteArgs(CacheRequest(), params
), 1);
443 args
.Add(*ir
, IgnoreBody
, IgnoreInvalidScheme
, aRv
);
444 if (NS_WARN_IF(aRv
.Failed())) {
448 return ExecuteOp(args
, aRv
);
451 already_AddRefed
<Promise
> Cache::Keys(
452 JSContext
* aCx
, const Optional
<RequestOrUSVString
>& aRequest
,
453 const CacheQueryOptions
& aOptions
, ErrorResult
& aRv
) {
454 if (NS_WARN_IF(!mActor
)) {
455 aRv
.Throw(NS_ERROR_UNEXPECTED
);
459 CacheChild::AutoLock
actorLock(*mActor
);
461 CacheQueryParams params
;
462 ToCacheQueryParams(params
, aOptions
);
464 AutoChildOpArgs
args(this, CacheKeysArgs(Nothing(), params
, GetOpenMode()),
467 if (aRequest
.WasPassed()) {
468 SafeRefPtr
<InternalRequest
> ir
=
469 ToInternalRequest(aCx
, aRequest
.Value(), IgnoreBody
, aRv
);
470 if (NS_WARN_IF(aRv
.Failed())) {
474 args
.Add(*ir
, IgnoreBody
, IgnoreInvalidScheme
, aRv
);
475 if (NS_WARN_IF(aRv
.Failed())) {
480 return ExecuteOp(args
, aRv
);
483 nsISupports
* Cache::GetParentObject() const { return mGlobal
; }
485 JSObject
* Cache::WrapObject(JSContext
* aContext
,
486 JS::Handle
<JSObject
*> aGivenProto
) {
487 return Cache_Binding::Wrap(aContext
, this, aGivenProto
);
490 void Cache::DestroyInternal(CacheChild
* aActor
) {
491 MOZ_DIAGNOSTIC_ASSERT(mActor
);
492 MOZ_DIAGNOSTIC_ASSERT(mActor
== aActor
);
493 mActor
->ClearListener();
497 nsIGlobalObject
* Cache::GetGlobalObject() const { return mGlobal
; }
500 void Cache::AssertOwningThread() const { NS_ASSERT_OWNINGTHREAD(Cache
); }
503 PBackgroundChild
* Cache::GetIPCManager() {
504 NS_ASSERT_OWNINGTHREAD(Cache
);
505 MOZ_DIAGNOSTIC_ASSERT(mActor
);
506 return mActor
->Manager();
510 NS_ASSERT_OWNINGTHREAD(Cache
);
512 mActor
->StartDestroyFromListener();
513 // DestroyInternal() is called synchronously by StartDestroyFromListener().
514 // So we should have already cleared the mActor.
515 MOZ_DIAGNOSTIC_ASSERT(!mActor
);
519 already_AddRefed
<Promise
> Cache::ExecuteOp(AutoChildOpArgs
& aOpArgs
,
521 MOZ_DIAGNOSTIC_ASSERT(mActor
);
523 RefPtr
<Promise
> promise
= Promise::Create(mGlobal
, aRv
);
524 if (NS_WARN_IF(!promise
)) {
528 mActor
->ExecuteOp(mGlobal
, promise
, this, aOpArgs
.SendAsOpArgs());
529 return promise
.forget();
532 already_AddRefed
<Promise
> Cache::AddAll(
533 const GlobalObject
& aGlobal
, nsTArray
<SafeRefPtr
<Request
>>&& aRequestList
,
534 CallerType aCallerType
, ErrorResult
& aRv
) {
535 MOZ_DIAGNOSTIC_ASSERT(mActor
);
537 // If there is no work to do, then resolve immediately
538 if (aRequestList
.IsEmpty()) {
539 RefPtr
<Promise
> promise
= Promise::Create(mGlobal
, aRv
);
540 if (NS_WARN_IF(!promise
)) {
544 promise
->MaybeResolveWithUndefined();
545 return promise
.forget();
548 AutoTArray
<RefPtr
<Promise
>, 256> fetchList
;
549 fetchList
.SetCapacity(aRequestList
.Length());
551 // Begin fetching each request in parallel. For now, if an error occurs just
552 // abandon our previous fetch calls. In theory we could cancel them in the
553 // future once fetch supports it.
555 for (uint32_t i
= 0; i
< aRequestList
.Length(); ++i
) {
556 RequestOrUSVString requestOrString
;
557 requestOrString
.SetAsRequest() = aRequestList
[i
].unsafeGetRawPtr();
558 RootedDictionary
<RequestInit
> requestInit(aGlobal
.Context());
559 RefPtr
<Promise
> fetch
=
560 FetchRequest(mGlobal
, requestOrString
, requestInit
, aCallerType
, aRv
);
561 if (NS_WARN_IF(aRv
.Failed())) {
565 fetchList
.AppendElement(std::move(fetch
));
568 RefPtr
<Promise
> promise
= Promise::Create(mGlobal
, aRv
);
569 if (NS_WARN_IF(aRv
.Failed())) {
573 RefPtr
<FetchHandler
> handler
=
574 new FetchHandler(mActor
->GetWorkerRefPtr().clonePtr(), this,
575 std::move(aRequestList
), promise
);
577 RefPtr
<Promise
> fetchPromise
=
578 Promise::All(aGlobal
.Context(), fetchList
, aRv
);
579 if (NS_WARN_IF(aRv
.Failed())) {
582 fetchPromise
->AppendNativeHandler(handler
);
584 return promise
.forget();
587 already_AddRefed
<Promise
> Cache::PutAll(
588 JSContext
* aCx
, const nsTArray
<SafeRefPtr
<Request
>>& aRequestList
,
589 const nsTArray
<RefPtr
<Response
>>& aResponseList
, ErrorResult
& aRv
) {
590 MOZ_DIAGNOSTIC_ASSERT(aRequestList
.Length() == aResponseList
.Length());
592 if (NS_WARN_IF(!mActor
)) {
593 aRv
.Throw(NS_ERROR_UNEXPECTED
);
597 CacheChild::AutoLock
actorLock(*mActor
);
599 AutoChildOpArgs
args(this, CachePutAllArgs(), aRequestList
.Length());
601 for (uint32_t i
= 0; i
< aRequestList
.Length(); ++i
) {
602 SafeRefPtr
<InternalRequest
> ir
= aRequestList
[i
]->GetInternalRequest();
603 args
.Add(aCx
, *ir
, ReadBody
, TypeErrorOnInvalidScheme
, *aResponseList
[i
],
605 if (NS_WARN_IF(aRv
.Failed())) {
610 return ExecuteOp(args
, aRv
);
613 OpenMode
Cache::GetOpenMode() const {
614 return mNamespace
== CHROME_ONLY_NAMESPACE
? OpenMode::Eager
: OpenMode::Lazy
;
617 } // namespace mozilla::dom::cache