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 file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "StorageManager.h"
12 #include "ErrorList.h"
13 #include "MainThreadUtils.h"
14 #include "js/CallArgs.h"
15 #include "js/TypeDecls.h"
16 #include "mozilla/Attributes.h"
17 #include "mozilla/ErrorResult.h"
18 #include "mozilla/MacroForEach.h"
19 #include "mozilla/Maybe.h"
20 #include "mozilla/Mutex.h"
21 #include "mozilla/RefPtr.h"
22 #include "mozilla/Telemetry.h"
23 #include "mozilla/TelemetryScalarEnums.h"
24 #include "mozilla/dom/BindingDeclarations.h"
25 #include "mozilla/dom/Document.h"
26 #include "mozilla/dom/Promise.h"
27 #include "mozilla/dom/PromiseWorkerProxy.h"
28 #include "mozilla/dom/StorageManagerBinding.h"
29 #include "mozilla/dom/WorkerCommon.h"
30 #include "mozilla/dom/WorkerPrivate.h"
31 #include "mozilla/dom/WorkerRunnable.h"
32 #include "mozilla/dom/WorkerStatus.h"
33 #include "mozilla/dom/quota/QuotaManagerService.h"
34 #include "nsContentPermissionHelper.h"
35 #include "nsContentUtils.h"
38 #include "nsIGlobalObject.h"
39 #include "nsIPrincipal.h"
40 #include "nsIQuotaCallbacks.h"
41 #include "nsIQuotaManagerService.h"
42 #include "nsIQuotaRequests.h"
43 #include "nsIQuotaResults.h"
44 #include "nsIVariant.h"
45 #include "nsLiteralString.h"
46 #include "nsPIDOMWindow.h"
48 #include "nsStringFlags.h"
49 #include "nsTLiteralString.h"
60 using namespace mozilla::dom::quota
;
62 namespace mozilla::dom
{
66 // This class is used to get quota usage, request persist and check persisted
68 class RequestResolver final
: public nsIQuotaCallback
{
70 enum Type
{ Estimate
, Persist
, Persisted
};
73 class FinishWorkerRunnable
;
75 // If this resolver was created for a window then mPromise must be non-null.
76 // Otherwise mProxy must be non-null.
77 RefPtr
<Promise
> mPromise
;
78 RefPtr
<PromiseWorkerProxy
> mProxy
;
81 StorageEstimate mStorageEstimate
;
86 RequestResolver(Type aType
, Promise
* aPromise
)
91 MOZ_ASSERT(NS_IsMainThread());
95 RequestResolver(Type aType
, PromiseWorkerProxy
* aProxy
)
96 : mProxy(aProxy
), mResultCode(NS_OK
), mType(aType
), mPersisted(false) {
97 MOZ_ASSERT(NS_IsMainThread());
101 void ResolveOrReject();
103 NS_DECL_THREADSAFE_ISUPPORTS
104 NS_DECL_NSIQUOTACALLBACK
107 ~RequestResolver() = default;
109 nsresult
GetStorageEstimate(nsIVariant
* aResult
);
111 nsresult
GetPersisted(nsIVariant
* aResult
);
113 nsresult
OnCompleteInternal(nsIQuotaRequest
* aRequest
);
118 // This class is used to return promise on worker thread.
119 class RequestResolver::FinishWorkerRunnable final
: public WorkerRunnable
{
120 RefPtr
<RequestResolver
> mResolver
;
123 explicit FinishWorkerRunnable(RequestResolver
* aResolver
)
124 : WorkerRunnable(aResolver
->mProxy
->GetWorkerPrivate()),
125 mResolver(aResolver
) {
126 MOZ_ASSERT(NS_IsMainThread());
127 MOZ_ASSERT(aResolver
);
130 bool WorkerRun(JSContext
* aCx
, WorkerPrivate
* aWorkerPrivate
) override
;
133 class EstimateWorkerMainThreadRunnable final
: public WorkerMainThreadRunnable
{
134 RefPtr
<PromiseWorkerProxy
> mProxy
;
137 EstimateWorkerMainThreadRunnable(WorkerPrivate
* aWorkerPrivate
,
138 PromiseWorkerProxy
* aProxy
)
139 : WorkerMainThreadRunnable(aWorkerPrivate
,
140 "StorageManager :: Estimate"_ns
),
142 MOZ_ASSERT(aWorkerPrivate
);
143 aWorkerPrivate
->AssertIsOnWorkerThread();
147 bool MainThreadRun() override
;
150 class PersistedWorkerMainThreadRunnable final
151 : public WorkerMainThreadRunnable
{
152 RefPtr
<PromiseWorkerProxy
> mProxy
;
155 PersistedWorkerMainThreadRunnable(WorkerPrivate
* aWorkerPrivate
,
156 PromiseWorkerProxy
* aProxy
)
157 : WorkerMainThreadRunnable(aWorkerPrivate
,
158 "StorageManager :: Persisted"_ns
),
160 MOZ_ASSERT(aWorkerPrivate
);
161 aWorkerPrivate
->AssertIsOnWorkerThread();
165 bool MainThreadRun() override
;
168 /*******************************************************************************
169 * PersistentStoragePermissionRequest
170 ******************************************************************************/
172 class PersistentStoragePermissionRequest final
173 : public ContentPermissionRequestBase
{
174 RefPtr
<Promise
> mPromise
;
177 PersistentStoragePermissionRequest(nsIPrincipal
* aPrincipal
,
178 nsPIDOMWindowInner
* aWindow
,
180 : ContentPermissionRequestBase(aPrincipal
, aWindow
,
181 "dom.storageManager"_ns
,
182 "persistent-storage"_ns
),
185 MOZ_ASSERT(aPromise
);
190 NS_DECL_ISUPPORTS_INHERITED
191 NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PersistentStoragePermissionRequest
,
192 ContentPermissionRequestBase
)
194 // nsIContentPermissionRequest
195 NS_IMETHOD
Cancel(void) override
;
196 NS_IMETHOD
Allow(JS::HandleValue choices
) override
;
199 ~PersistentStoragePermissionRequest() = default;
202 nsresult
Estimate(nsIPrincipal
* aPrincipal
, nsIQuotaCallback
* aCallback
,
203 nsIQuotaRequest
** aRequest
) {
204 MOZ_ASSERT(aPrincipal
);
205 MOZ_ASSERT(aCallback
);
206 MOZ_ASSERT(aRequest
);
208 // Firefox and Quota Manager have always used the schemeless origin group
209 // (https://storage.spec.whatwg.org/#schemeless-origin-group) for quota limit
210 // purposes. This has been to prevent a site/eTLD+1 from claiming more than
211 // its fair share of storage through the use of sub-domains. Because the limit
212 // is at the group level and the usage needs to make sense in the context of
213 // that limit, we also expose the group usage. Bug 1374970 reflects this
214 // reality and bug 1305665 tracks our plan to eliminate our use of groups for
217 nsCOMPtr
<nsIQuotaManagerService
> qms
= QuotaManagerService::GetOrCreate();
218 if (NS_WARN_IF(!qms
)) {
219 return NS_ERROR_FAILURE
;
222 nsCOMPtr
<nsIQuotaRequest
> request
;
223 nsresult rv
= qms
->Estimate(aPrincipal
, getter_AddRefs(request
));
224 if (NS_WARN_IF(NS_FAILED(rv
))) {
228 MOZ_ALWAYS_SUCCEEDS(request
->SetCallback(aCallback
));
230 request
.forget(aRequest
);
234 nsresult
Persisted(nsIPrincipal
* aPrincipal
, nsIQuotaCallback
* aCallback
,
235 nsIQuotaRequest
** aRequest
) {
236 MOZ_ASSERT(aPrincipal
);
237 MOZ_ASSERT(aCallback
);
238 MOZ_ASSERT(aRequest
);
240 nsCOMPtr
<nsIQuotaManagerService
> qms
= QuotaManagerService::GetOrCreate();
241 if (NS_WARN_IF(!qms
)) {
242 return NS_ERROR_FAILURE
;
245 nsCOMPtr
<nsIQuotaRequest
> request
;
246 nsresult rv
= qms
->Persisted(aPrincipal
, getter_AddRefs(request
));
247 if (NS_WARN_IF(NS_FAILED(rv
))) {
251 // All the methods in nsIQuotaManagerService shouldn't synchronously fire
252 // any callbacks when they are being executed. Even when a result is ready,
253 // a new runnable should be dispatched to current thread to fire the callback
254 // asynchronously. It's safe to set the callback after we call Persisted().
255 MOZ_ALWAYS_SUCCEEDS(request
->SetCallback(aCallback
));
257 request
.forget(aRequest
);
262 already_AddRefed
<Promise
> ExecuteOpOnMainOrWorkerThread(
263 nsIGlobalObject
* aGlobal
, RequestResolver::Type aType
, ErrorResult
& aRv
) {
265 MOZ_ASSERT_IF(aType
== RequestResolver::Type::Persist
, NS_IsMainThread());
267 RefPtr
<Promise
> promise
= Promise::Create(aGlobal
, aRv
);
268 if (NS_WARN_IF(!promise
)) {
272 if (NS_IsMainThread()) {
273 nsCOMPtr
<nsPIDOMWindowInner
> window
= do_QueryInterface(aGlobal
);
274 if (NS_WARN_IF(!window
)) {
275 aRv
.Throw(NS_ERROR_FAILURE
);
279 nsCOMPtr
<Document
> doc
= window
->GetExtantDoc();
280 if (NS_WARN_IF(!doc
)) {
281 aRv
.Throw(NS_ERROR_FAILURE
);
285 nsCOMPtr
<nsIPrincipal
> principal
= doc
->NodePrincipal();
286 MOZ_ASSERT(principal
);
288 // Storage Standard 7. API
289 // If origin is an opaque origin, then reject promise with a TypeError.
290 if (principal
->GetIsNullPrincipal()) {
292 case RequestResolver::Type::Persisted
:
293 promise
->MaybeRejectWithTypeError(
294 "persisted() called for opaque origin");
296 case RequestResolver::Type::Persist
:
297 promise
->MaybeRejectWithTypeError(
298 "persist() called for opaque origin");
300 case RequestResolver::Type::Estimate
:
301 promise
->MaybeRejectWithTypeError(
302 "estimate() called for opaque origin");
306 return promise
.forget();
310 case RequestResolver::Type::Persisted
: {
311 RefPtr
<RequestResolver
> resolver
=
312 new RequestResolver(RequestResolver::Type::Persisted
, promise
);
314 RefPtr
<nsIQuotaRequest
> request
;
315 aRv
= Persisted(principal
, resolver
, getter_AddRefs(request
));
320 case RequestResolver::Type::Persist
: {
321 RefPtr
<PersistentStoragePermissionRequest
> request
=
322 new PersistentStoragePermissionRequest(principal
, window
, promise
);
324 // In private browsing mode, no permission prompt.
325 if (nsContentUtils::IsInPrivateBrowsing(doc
)) {
326 aRv
= request
->Cancel();
327 } else if (!request
->CheckPermissionDelegate()) {
328 aRv
= request
->Cancel();
330 aRv
= request
->Start();
336 case RequestResolver::Type::Estimate
: {
337 RefPtr
<RequestResolver
> resolver
=
338 new RequestResolver(RequestResolver::Type::Estimate
, promise
);
340 RefPtr
<nsIQuotaRequest
> request
;
341 aRv
= Estimate(principal
, resolver
, getter_AddRefs(request
));
347 MOZ_CRASH("Invalid aRequest type!");
350 if (NS_WARN_IF(aRv
.Failed())) {
354 return promise
.forget();
357 WorkerPrivate
* workerPrivate
= GetCurrentThreadWorkerPrivate();
358 MOZ_ASSERT(workerPrivate
);
360 RefPtr
<PromiseWorkerProxy
> promiseProxy
=
361 PromiseWorkerProxy::Create(workerPrivate
, promise
);
362 if (NS_WARN_IF(!promiseProxy
)) {
367 case RequestResolver::Type::Estimate
: {
368 RefPtr
<EstimateWorkerMainThreadRunnable
> runnnable
=
369 new EstimateWorkerMainThreadRunnable(promiseProxy
->GetWorkerPrivate(),
371 runnnable
->Dispatch(Canceling
, aRv
);
376 case RequestResolver::Type::Persisted
: {
377 RefPtr
<PersistedWorkerMainThreadRunnable
> runnnable
=
378 new PersistedWorkerMainThreadRunnable(
379 promiseProxy
->GetWorkerPrivate(), promiseProxy
);
380 runnnable
->Dispatch(Canceling
, aRv
);
386 MOZ_CRASH("Invalid aRequest type");
389 if (NS_WARN_IF(aRv
.Failed())) {
393 return promise
.forget();
398 /*******************************************************************************
399 * Local class implementations
400 ******************************************************************************/
402 void RequestResolver::ResolveOrReject() {
403 class MOZ_STACK_CLASS AutoCleanup final
{
404 RefPtr
<PromiseWorkerProxy
> mProxy
;
407 explicit AutoCleanup(PromiseWorkerProxy
* aProxy
) : mProxy(aProxy
) {
418 RefPtr
<Promise
> promise
;
419 Maybe
<AutoCleanup
> autoCleanup
;
426 promise
= mProxy
->WorkerPromise();
428 // Only clean up for worker case.
429 autoCleanup
.emplace(mProxy
);
434 if (mType
== Type::Estimate
) {
435 if (NS_SUCCEEDED(mResultCode
)) {
436 promise
->MaybeResolve(mStorageEstimate
);
438 promise
->MaybeRejectWithTypeError(
439 "Internal error while estimating storage usage");
445 MOZ_ASSERT(mType
== Type::Persist
|| mType
== Type::Persisted
);
447 if (NS_SUCCEEDED(mResultCode
)) {
448 promise
->MaybeResolve(mPersisted
);
450 promise
->MaybeResolve(false);
454 NS_IMPL_ISUPPORTS(RequestResolver
, nsIQuotaCallback
)
456 nsresult
RequestResolver::GetStorageEstimate(nsIVariant
* aResult
) {
458 MOZ_ASSERT(mType
== Type::Estimate
);
460 MOZ_ASSERT(aResult
->GetDataType() == nsIDataType::VTYPE_INTERFACE_IS
);
463 nsCOMPtr
<nsISupports
> supports
;
464 nsresult rv
= aResult
->GetAsInterface(&iid
, getter_AddRefs(supports
));
465 if (NS_WARN_IF(NS_FAILED(rv
))) {
471 nsCOMPtr
<nsIQuotaEstimateResult
> estimateResult
= do_QueryInterface(supports
);
472 MOZ_ASSERT(estimateResult
);
475 estimateResult
->GetUsage(&mStorageEstimate
.mUsage
.Construct()));
478 estimateResult
->GetLimit(&mStorageEstimate
.mQuota
.Construct()));
483 nsresult
RequestResolver::GetPersisted(nsIVariant
* aResult
) {
485 MOZ_ASSERT(mType
== Type::Persist
|| mType
== Type::Persisted
);
488 uint16_t dataType
= aResult
->GetDataType();
491 if (mType
== Type::Persist
) {
492 MOZ_ASSERT(dataType
== nsIDataType::VTYPE_VOID
);
498 MOZ_ASSERT(dataType
== nsIDataType::VTYPE_BOOL
);
501 nsresult rv
= aResult
->GetAsBool(&persisted
);
502 if (NS_WARN_IF(NS_FAILED(rv
))) {
506 mPersisted
= persisted
;
510 nsresult
RequestResolver::OnCompleteInternal(nsIQuotaRequest
* aRequest
) {
511 MOZ_ASSERT(NS_IsMainThread());
512 MOZ_ASSERT(aRequest
);
515 nsresult rv
= aRequest
->GetResultCode(&resultCode
);
516 if (NS_WARN_IF(NS_FAILED(rv
))) {
520 if (NS_FAILED(resultCode
)) {
524 nsCOMPtr
<nsIVariant
> result
;
525 rv
= aRequest
->GetResult(getter_AddRefs(result
));
526 if (NS_WARN_IF(NS_FAILED(rv
))) {
530 if (mType
== Type::Estimate
) {
531 rv
= GetStorageEstimate(result
);
533 MOZ_ASSERT(mType
== Type::Persist
|| mType
== Type::Persisted
);
535 rv
= GetPersisted(result
);
537 if (NS_WARN_IF(NS_FAILED(rv
))) {
544 nsresult
RequestResolver::Finish() {
545 // In a main thread request.
547 MOZ_ASSERT(mPromise
);
554 // In a worker thread request.
555 MutexAutoLock
lock(mProxy
->Lock());
557 if (NS_WARN_IF(mProxy
->CleanedUp())) {
558 return NS_ERROR_FAILURE
;
561 RefPtr
<FinishWorkerRunnable
> runnable
= new FinishWorkerRunnable(this);
562 if (NS_WARN_IF(!runnable
->Dispatch())) {
563 return NS_ERROR_FAILURE
;
571 RequestResolver::OnComplete(nsIQuotaRequest
* aRequest
) {
572 MOZ_ASSERT(NS_IsMainThread());
573 MOZ_ASSERT(aRequest
);
575 mResultCode
= OnCompleteInternal(aRequest
);
577 nsresult rv
= Finish();
578 if (NS_WARN_IF(NS_FAILED(rv
))) {
585 bool RequestResolver::FinishWorkerRunnable::WorkerRun(
586 JSContext
* aCx
, WorkerPrivate
* aWorkerPrivate
) {
588 MOZ_ASSERT(aWorkerPrivate
);
589 aWorkerPrivate
->AssertIsOnWorkerThread();
591 MOZ_ASSERT(mResolver
);
592 mResolver
->ResolveOrReject();
597 bool EstimateWorkerMainThreadRunnable::MainThreadRun() {
598 MOZ_ASSERT(NS_IsMainThread());
600 nsCOMPtr
<nsIPrincipal
> principal
;
603 MutexAutoLock
lock(mProxy
->Lock());
604 if (mProxy
->CleanedUp()) {
607 principal
= mProxy
->GetWorkerPrivate()->GetPrincipal();
610 MOZ_ASSERT(principal
);
612 RefPtr
<RequestResolver
> resolver
=
613 new RequestResolver(RequestResolver::Type::Estimate
, mProxy
);
615 RefPtr
<nsIQuotaRequest
> request
;
616 nsresult rv
= Estimate(principal
, resolver
, getter_AddRefs(request
));
617 if (NS_WARN_IF(NS_FAILED(rv
))) {
624 bool PersistedWorkerMainThreadRunnable::MainThreadRun() {
625 MOZ_ASSERT(NS_IsMainThread());
627 nsCOMPtr
<nsIPrincipal
> principal
;
630 MutexAutoLock
lock(mProxy
->Lock());
631 if (mProxy
->CleanedUp()) {
634 principal
= mProxy
->GetWorkerPrivate()->GetPrincipal();
637 MOZ_ASSERT(principal
);
639 RefPtr
<RequestResolver
> resolver
=
640 new RequestResolver(RequestResolver::Type::Persisted
, mProxy
);
642 RefPtr
<nsIQuotaRequest
> request
;
643 nsresult rv
= Persisted(principal
, resolver
, getter_AddRefs(request
));
644 if (NS_WARN_IF(NS_FAILED(rv
))) {
651 nsresult
PersistentStoragePermissionRequest::Start() {
652 MOZ_ASSERT(NS_IsMainThread());
655 #ifdef MOZ_WIDGET_ANDROID
656 // on Android calling `ShowPrompt` here calls
657 // `nsContentPermissionUtils::AskPermission` once, and a response of
658 // `PromptResult::Pending` calls it again. This results in multiple requests
659 // for storage access, so we check the prompt prefs only to ensure we only
661 pr
= CheckPromptPrefs();
663 nsresult rv
= ShowPrompt(pr
);
664 if (NS_WARN_IF(NS_FAILED(rv
))) {
668 if (pr
== PromptResult::Granted
) {
669 return Allow(JS::UndefinedHandleValue
);
671 if (pr
== PromptResult::Denied
) {
675 return nsContentPermissionUtils::AskPermission(this, mWindow
);
678 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(
679 PersistentStoragePermissionRequest
, ContentPermissionRequestBase
)
681 NS_IMPL_CYCLE_COLLECTION_INHERITED(PersistentStoragePermissionRequest
,
682 ContentPermissionRequestBase
, mPromise
)
685 PersistentStoragePermissionRequest::Cancel() {
686 MOZ_ASSERT(NS_IsMainThread());
687 MOZ_ASSERT(mPromise
);
689 RefPtr
<RequestResolver
> resolver
=
690 new RequestResolver(RequestResolver::Type::Persisted
, mPromise
);
692 RefPtr
<nsIQuotaRequest
> request
;
694 return Persisted(mPrincipal
, resolver
, getter_AddRefs(request
));
698 PersistentStoragePermissionRequest::Allow(JS::HandleValue aChoices
) {
699 MOZ_ASSERT(NS_IsMainThread());
701 RefPtr
<RequestResolver
> resolver
=
702 new RequestResolver(RequestResolver::Type::Persist
, mPromise
);
704 nsCOMPtr
<nsIQuotaManagerService
> qms
= QuotaManagerService::GetOrCreate();
705 if (NS_WARN_IF(!qms
)) {
706 return NS_ERROR_FAILURE
;
709 RefPtr
<nsIQuotaRequest
> request
;
711 nsresult rv
= qms
->Persist(mPrincipal
, getter_AddRefs(request
));
712 if (NS_WARN_IF(NS_FAILED(rv
))) {
716 MOZ_ALWAYS_SUCCEEDS(request
->SetCallback(resolver
));
721 /*******************************************************************************
723 ******************************************************************************/
725 StorageManager::StorageManager(nsIGlobalObject
* aGlobal
) : mOwner(aGlobal
) {
729 StorageManager::~StorageManager() = default;
731 already_AddRefed
<Promise
> StorageManager::Persisted(ErrorResult
& aRv
) {
734 return ExecuteOpOnMainOrWorkerThread(mOwner
, RequestResolver::Type::Persisted
,
738 already_AddRefed
<Promise
> StorageManager::Persist(ErrorResult
& aRv
) {
741 Telemetry::ScalarAdd(Telemetry::ScalarID::NAVIGATOR_STORAGE_PERSIST_COUNT
, 1);
742 return ExecuteOpOnMainOrWorkerThread(mOwner
, RequestResolver::Type::Persist
,
746 already_AddRefed
<Promise
> StorageManager::Estimate(ErrorResult
& aRv
) {
749 Telemetry::ScalarAdd(Telemetry::ScalarID::NAVIGATOR_STORAGE_ESTIMATE_COUNT
,
751 return ExecuteOpOnMainOrWorkerThread(mOwner
, RequestResolver::Type::Estimate
,
755 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(StorageManager
, mOwner
)
757 NS_IMPL_CYCLE_COLLECTING_ADDREF(StorageManager
)
758 NS_IMPL_CYCLE_COLLECTING_RELEASE(StorageManager
)
760 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(StorageManager
)
761 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
762 NS_INTERFACE_MAP_ENTRY(nsISupports
)
765 JSObject
* StorageManager::WrapObject(JSContext
* aCx
,
766 JS::Handle
<JSObject
*> aGivenProto
) {
767 return StorageManager_Binding::Wrap(aCx
, this, aGivenProto
);
770 } // namespace mozilla::dom