Bug 1874684 - Part 6: Limit day length calculations to safe integers. r=mgaudet
[gecko.git] / dom / storage / LocalStorageCache.cpp
blob16ebe8ca3b72c66793a74a25a5b8a41440570c7a
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 "LocalStorageCache.h"
9 #include "Storage.h"
10 #include "StorageDBThread.h"
11 #include "StorageIPC.h"
12 #include "StorageUtils.h"
13 #include "LocalStorageManager.h"
15 #include "nsDOMString.h"
16 #include "nsXULAppAPI.h"
17 #include "mozilla/Unused.h"
18 #include "nsProxyRelease.h"
19 #include "nsThreadUtils.h"
21 namespace mozilla::dom {
23 #define DOM_STORAGE_CACHE_KEEP_ALIVE_TIME_MS 20000
25 namespace {
27 const uint32_t kDefaultSet = 0;
28 const uint32_t kSessionSet = 1;
30 inline uint32_t GetDataSetIndex(bool aPrivateBrowsing,
31 bool aSessionScopedOrLess) {
32 if (!aPrivateBrowsing && aSessionScopedOrLess) {
33 return kSessionSet;
36 return kDefaultSet;
39 inline uint32_t GetDataSetIndex(const LocalStorage* aStorage) {
40 // A session only mode doesn't exist anymore, so having a separate data set
41 // for it here is basically useless. This code is only kept until we remove
42 // the old / legacy LocalStorage implementation.
43 return GetDataSetIndex(aStorage->IsPrivateBrowsing(),
44 aStorage->IsPrivateBrowsingOrLess());
47 } // namespace
49 // LocalStorageCacheBridge
51 NS_IMPL_ADDREF(LocalStorageCacheBridge)
53 // Since there is no consumer of return value of Release, we can turn this
54 // method to void to make implementation of asynchronous
55 // LocalStorageCache::Release much simpler.
56 NS_IMETHODIMP_(void) LocalStorageCacheBridge::Release(void) {
57 MOZ_ASSERT(int32_t(mRefCnt) > 0, "dup release");
58 nsrefcnt count = --mRefCnt;
59 NS_LOG_RELEASE(this, count, "LocalStorageCacheBridge");
60 if (0 == count) {
61 mRefCnt = 1; /* stabilize */
62 /* enable this to find non-threadsafe destructors: */
63 /* NS_ASSERT_OWNINGTHREAD(_class); */
64 delete (this);
68 // LocalStorageCache
70 LocalStorageCache::LocalStorageCache(const nsACString* aOriginNoSuffix)
71 : mActor(nullptr),
72 mOriginNoSuffix(*aOriginNoSuffix),
73 mMonitor("LocalStorageCache"),
74 mLoaded(false),
75 mLoadResult(NS_OK),
76 mInitialized(false),
77 mPersistent(false),
78 mPreloadTelemetryRecorded(false) {
79 MOZ_COUNT_CTOR(LocalStorageCache);
82 LocalStorageCache::~LocalStorageCache() {
83 if (mActor) {
84 mActor->SendDeleteMeInternal();
85 MOZ_ASSERT(!mActor, "SendDeleteMeInternal should have cleared!");
88 if (mManager) {
89 mManager->DropCache(this);
92 MOZ_COUNT_DTOR(LocalStorageCache);
95 void LocalStorageCache::SetActor(LocalStorageCacheChild* aActor) {
96 AssertIsOnOwningThread();
97 MOZ_ASSERT(aActor);
98 MOZ_ASSERT(!mActor);
100 mActor = aActor;
103 NS_IMETHODIMP_(void)
104 LocalStorageCache::Release(void) {
105 // We must actually release on the main thread since the cache removes it
106 // self from the manager's hash table. And we don't want to lock access to
107 // that hash table.
108 if (NS_IsMainThread()) {
109 LocalStorageCacheBridge::Release();
110 return;
113 RefPtr<nsRunnableMethod<LocalStorageCacheBridge, void, false>> event =
114 NewNonOwningRunnableMethod("dom::LocalStorageCacheBridge::Release",
115 static_cast<LocalStorageCacheBridge*>(this),
116 &LocalStorageCacheBridge::Release);
118 nsresult rv = NS_DispatchToMainThread(event);
119 if (NS_FAILED(rv)) {
120 NS_WARNING("LocalStorageCache::Release() on a non-main thread");
121 LocalStorageCacheBridge::Release();
125 void LocalStorageCache::Init(LocalStorageManager* aManager, bool aPersistent,
126 nsIPrincipal* aPrincipal,
127 const nsACString& aQuotaOriginScope) {
128 MOZ_ASSERT(!aQuotaOriginScope.IsEmpty());
130 if (mInitialized) {
131 return;
134 mInitialized = true;
135 aPrincipal->OriginAttributesRef().CreateSuffix(mOriginSuffix);
136 mPrivateBrowsingId = aPrincipal->GetPrivateBrowsingId();
137 mPersistent = aPersistent;
138 mQuotaOriginScope = aQuotaOriginScope;
140 if (mPersistent) {
141 mManager = aManager;
142 Preload();
145 // Check the quota string has (or has not) the identical origin suffix as
146 // this storage cache is bound to.
147 MOZ_ASSERT(StringBeginsWith(mQuotaOriginScope, mOriginSuffix));
148 MOZ_ASSERT(mOriginSuffix.IsEmpty() !=
149 StringBeginsWith(mQuotaOriginScope, "^"_ns));
151 mUsage = aManager->GetOriginUsage(mQuotaOriginScope, mPrivateBrowsingId);
154 void LocalStorageCache::NotifyObservers(const LocalStorage* aStorage,
155 const nsAString& aKey,
156 const nsAString& aOldValue,
157 const nsAString& aNewValue) {
158 AssertIsOnOwningThread();
159 MOZ_ASSERT(aStorage);
161 if (!mActor) {
162 return;
165 // We want to send a message to the parent in order to broadcast the
166 // StorageEvent correctly to any child process.
168 Unused << mActor->SendNotify(aStorage->DocumentURI(), aKey, aOldValue,
169 aNewValue);
172 inline bool LocalStorageCache::Persist(const LocalStorage* aStorage) const {
173 return mPersistent && (aStorage->IsPrivateBrowsing() ||
174 !aStorage->IsPrivateBrowsingOrLess());
177 const nsCString LocalStorageCache::Origin() const {
178 return LocalStorageManager::CreateOrigin(mOriginSuffix, mOriginNoSuffix);
181 LocalStorageCache::Data& LocalStorageCache::DataSet(
182 const LocalStorage* aStorage) {
183 return mData[GetDataSetIndex(aStorage)];
186 bool LocalStorageCache::ProcessUsageDelta(const LocalStorage* aStorage,
187 int64_t aDelta,
188 const MutationSource aSource) {
189 return ProcessUsageDelta(GetDataSetIndex(aStorage), aDelta, aSource);
192 bool LocalStorageCache::ProcessUsageDelta(uint32_t aGetDataSetIndex,
193 const int64_t aDelta,
194 const MutationSource aSource) {
195 // Check limit per this origin
196 Data& data = mData[aGetDataSetIndex];
197 uint64_t newOriginUsage = data.mOriginQuotaUsage + aDelta;
198 if (aSource == ContentMutation && aDelta > 0 &&
199 newOriginUsage > LocalStorageManager::GetOriginQuota()) {
200 return false;
203 // Now check eTLD+1 limit
204 if (mUsage &&
205 !mUsage->CheckAndSetETLD1UsageDelta(aGetDataSetIndex, aDelta, aSource)) {
206 return false;
209 // Update size in our data set
210 data.mOriginQuotaUsage = newOriginUsage;
211 return true;
214 void LocalStorageCache::Preload() {
215 if (mLoaded || !mPersistent) {
216 return;
219 StorageDBChild* storageChild =
220 StorageDBChild::GetOrCreate(mPrivateBrowsingId);
221 if (!storageChild) {
222 mLoaded = true;
223 mLoadResult = NS_ERROR_FAILURE;
224 return;
227 storageChild->AsyncPreload(this);
230 void LocalStorageCache::WaitForPreload(Telemetry::HistogramID aTelemetryID) {
231 if (!mPersistent) {
232 return;
235 bool loaded = mLoaded;
237 // Telemetry of rates of pending preloads
238 if (!mPreloadTelemetryRecorded) {
239 mPreloadTelemetryRecorded = true;
240 Telemetry::Accumulate(
241 Telemetry::LOCALDOMSTORAGE_PRELOAD_PENDING_ON_FIRST_ACCESS, !loaded);
244 if (loaded) {
245 return;
248 // Measure which operation blocks and for how long
249 Telemetry::RuntimeAutoTimer timer(aTelemetryID);
251 // If preload already started (i.e. we got some first data, but not all)
252 // SyncPreload will just wait for it to finish rather then synchronously
253 // read from the database. It seems to me more optimal.
255 // TODO place for A/B testing (force main thread load vs. let preload finish)
257 // No need to check sDatabase for being non-null since preload is either
258 // done before we've shut the DB down or when the DB could not start,
259 // preload has not even be started.
260 StorageDBChild::Get(mPrivateBrowsingId)->SyncPreload(this);
263 nsresult LocalStorageCache::GetLength(const LocalStorage* aStorage,
264 uint32_t* aRetval) {
265 if (Persist(aStorage)) {
266 WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETLENGTH_BLOCKING_MS);
267 if (NS_FAILED(mLoadResult)) {
268 return mLoadResult;
272 *aRetval = DataSet(aStorage).mKeys.Count();
273 return NS_OK;
276 nsresult LocalStorageCache::GetKey(const LocalStorage* aStorage,
277 uint32_t aIndex, nsAString& aRetval) {
278 // XXX: This does a linear search for the key at index, which would
279 // suck if there's a large numer of indexes. Do we care? If so,
280 // maybe we need to have a lazily populated key array here or
281 // something?
282 if (Persist(aStorage)) {
283 WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETKEY_BLOCKING_MS);
284 if (NS_FAILED(mLoadResult)) {
285 return mLoadResult;
289 aRetval.SetIsVoid(true);
290 for (auto iter = DataSet(aStorage).mKeys.Iter(); !iter.Done(); iter.Next()) {
291 if (aIndex == 0) {
292 aRetval = iter.Key();
293 break;
295 aIndex--;
298 return NS_OK;
301 void LocalStorageCache::GetKeys(const LocalStorage* aStorage,
302 nsTArray<nsString>& aKeys) {
303 if (Persist(aStorage)) {
304 WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETALLKEYS_BLOCKING_MS);
307 if (NS_FAILED(mLoadResult)) {
308 return;
311 AppendToArray(aKeys, DataSet(aStorage).mKeys.Keys());
314 nsresult LocalStorageCache::GetItem(const LocalStorage* aStorage,
315 const nsAString& aKey, nsAString& aRetval) {
316 if (Persist(aStorage)) {
317 WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETVALUE_BLOCKING_MS);
318 if (NS_FAILED(mLoadResult)) {
319 return mLoadResult;
323 // not using AutoString since we don't want to copy buffer to result
324 nsString value;
325 if (!DataSet(aStorage).mKeys.Get(aKey, &value)) {
326 SetDOMStringToNull(value);
329 aRetval = value;
331 return NS_OK;
334 nsresult LocalStorageCache::SetItem(const LocalStorage* aStorage,
335 const nsAString& aKey,
336 const nsAString& aValue, nsString& aOld,
337 const MutationSource aSource) {
338 // Size of the cache that will change after this action.
339 int64_t delta = 0;
341 if (Persist(aStorage)) {
342 WaitForPreload(Telemetry::LOCALDOMSTORAGE_SETVALUE_BLOCKING_MS);
343 if (NS_FAILED(mLoadResult)) {
344 return mLoadResult;
348 Data& data = DataSet(aStorage);
349 if (!data.mKeys.Get(aKey, &aOld)) {
350 SetDOMStringToNull(aOld);
352 // We only consider key size if the key doesn't exist before.
353 delta += static_cast<int64_t>(aKey.Length());
356 delta += static_cast<int64_t>(aValue.Length()) -
357 static_cast<int64_t>(aOld.Length());
359 if (!ProcessUsageDelta(aStorage, delta, aSource)) {
360 return NS_ERROR_DOM_QUOTA_EXCEEDED_ERR;
363 if (aValue == aOld && DOMStringIsNull(aValue) == DOMStringIsNull(aOld)) {
364 return NS_SUCCESS_DOM_NO_OPERATION;
367 data.mKeys.InsertOrUpdate(aKey, aValue);
369 if (aSource != ContentMutation) {
370 return NS_OK;
373 #if !defined(MOZ_WIDGET_ANDROID)
374 NotifyObservers(aStorage, aKey, aOld, aValue);
375 #endif
377 if (Persist(aStorage)) {
378 StorageDBChild* storageChild = StorageDBChild::Get(mPrivateBrowsingId);
379 if (!storageChild) {
380 NS_ERROR(
381 "Writing to localStorage after the database has been shut down"
382 ", data lose!");
383 return NS_ERROR_NOT_INITIALIZED;
386 if (DOMStringIsNull(aOld)) {
387 return storageChild->AsyncAddItem(this, aKey, aValue);
390 return storageChild->AsyncUpdateItem(this, aKey, aValue);
393 return NS_OK;
396 nsresult LocalStorageCache::RemoveItem(const LocalStorage* aStorage,
397 const nsAString& aKey, nsString& aOld,
398 const MutationSource aSource) {
399 if (Persist(aStorage)) {
400 WaitForPreload(Telemetry::LOCALDOMSTORAGE_REMOVEKEY_BLOCKING_MS);
401 if (NS_FAILED(mLoadResult)) {
402 return mLoadResult;
406 Data& data = DataSet(aStorage);
407 if (!data.mKeys.Get(aKey, &aOld)) {
408 SetDOMStringToNull(aOld);
409 return NS_SUCCESS_DOM_NO_OPERATION;
412 // Recalculate the cached data size
413 const int64_t delta = -(static_cast<int64_t>(aOld.Length()) +
414 static_cast<int64_t>(aKey.Length()));
415 Unused << ProcessUsageDelta(aStorage, delta, aSource);
416 data.mKeys.Remove(aKey);
418 if (aSource != ContentMutation) {
419 return NS_OK;
422 #if !defined(MOZ_WIDGET_ANDROID)
423 NotifyObservers(aStorage, aKey, aOld, VoidString());
424 #endif
426 if (Persist(aStorage)) {
427 StorageDBChild* storageChild = StorageDBChild::Get(mPrivateBrowsingId);
428 if (!storageChild) {
429 NS_ERROR(
430 "Writing to localStorage after the database has been shut down"
431 ", data lose!");
432 return NS_ERROR_NOT_INITIALIZED;
435 return storageChild->AsyncRemoveItem(this, aKey);
438 return NS_OK;
441 nsresult LocalStorageCache::Clear(const LocalStorage* aStorage,
442 const MutationSource aSource) {
443 bool refresh = false;
444 if (Persist(aStorage)) {
445 // We need to preload all data (know the size) before we can proceeed
446 // to correctly decrease cached usage number.
447 // XXX as in case of unload, this is not technically needed now, but
448 // after super-scope quota introduction we have to do this. Get telemetry
449 // right now.
450 WaitForPreload(Telemetry::LOCALDOMSTORAGE_CLEAR_BLOCKING_MS);
451 if (NS_FAILED(mLoadResult)) {
452 // When we failed to load data from the database, force delete of the
453 // scope data and make use of the storage possible again.
454 refresh = true;
455 mLoadResult = NS_OK;
459 Data& data = DataSet(aStorage);
460 bool hadData = !!data.mKeys.Count();
462 if (hadData) {
463 Unused << ProcessUsageDelta(aStorage, -data.mOriginQuotaUsage, aSource);
464 data.mKeys.Clear();
467 if (aSource != ContentMutation) {
468 return hadData ? NS_OK : NS_SUCCESS_DOM_NO_OPERATION;
471 #if !defined(MOZ_WIDGET_ANDROID)
472 if (hadData) {
473 NotifyObservers(aStorage, VoidString(), VoidString(), VoidString());
475 #endif
477 if (Persist(aStorage) && (refresh || hadData)) {
478 StorageDBChild* storageChild = StorageDBChild::Get(mPrivateBrowsingId);
479 if (!storageChild) {
480 NS_ERROR(
481 "Writing to localStorage after the database has been shut down"
482 ", data lose!");
483 return NS_ERROR_NOT_INITIALIZED;
486 return storageChild->AsyncClear(this);
489 return hadData ? NS_OK : NS_SUCCESS_DOM_NO_OPERATION;
492 int64_t LocalStorageCache::GetOriginQuotaUsage(
493 const LocalStorage* aStorage) const {
494 return mData[GetDataSetIndex(aStorage)].mOriginQuotaUsage;
497 void LocalStorageCache::UnloadItems(uint32_t aUnloadFlags) {
498 if (aUnloadFlags & kUnloadDefault) {
499 // Must wait for preload to pass correct usage to ProcessUsageDelta
500 // XXX this is not technically needed right now since there is just
501 // per-origin isolated quota handling, but when we introduce super-
502 // -scope quotas, we have to do this. Better to start getting
503 // telemetry right now.
504 WaitForPreload(Telemetry::LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS);
506 mData[kDefaultSet].mKeys.Clear();
507 ProcessUsageDelta(kDefaultSet, -mData[kDefaultSet].mOriginQuotaUsage);
510 if (aUnloadFlags & kUnloadSession) {
511 mData[kSessionSet].mKeys.Clear();
512 ProcessUsageDelta(kSessionSet, -mData[kSessionSet].mOriginQuotaUsage);
515 #ifdef DOM_STORAGE_TESTS
516 if (aUnloadFlags & kTestReload) {
517 WaitForPreload(Telemetry::LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS);
519 mData[kDefaultSet].mKeys.Clear();
520 mLoaded = false; // This is only used in testing code
521 Preload();
523 #endif
526 // LocalStorageCacheBridge
528 uint32_t LocalStorageCache::LoadedCount() {
529 MonitorAutoLock monitor(mMonitor);
530 Data& data = mData[kDefaultSet];
531 return data.mKeys.Count();
534 bool LocalStorageCache::LoadItem(const nsAString& aKey,
535 const nsAString& aValue) {
536 MonitorAutoLock monitor(mMonitor);
537 if (mLoaded) {
538 return false;
541 Data& data = mData[kDefaultSet];
542 data.mKeys.LookupOrInsertWith(aKey, [&] {
543 data.mOriginQuotaUsage += aKey.Length() + aValue.Length();
544 return nsString(aValue);
546 return true;
549 void LocalStorageCache::LoadDone(nsresult aRv) {
550 MonitorAutoLock monitor(mMonitor);
551 mLoadResult = aRv;
552 mLoaded = true;
553 monitor.Notify();
556 void LocalStorageCache::LoadWait() {
557 MonitorAutoLock monitor(mMonitor);
558 while (!mLoaded) {
559 monitor.Wait();
563 // StorageUsage
565 StorageUsage::StorageUsage(const nsACString& aOriginScope)
566 : mOriginScope(aOriginScope) {
567 mUsage[kDefaultSet] = mUsage[kSessionSet] = 0LL;
570 namespace {
572 class LoadUsageRunnable : public Runnable {
573 public:
574 LoadUsageRunnable(int64_t* aUsage, const int64_t aDelta)
575 : Runnable("dom::LoadUsageRunnable"), mTarget(aUsage), mDelta(aDelta) {}
577 private:
578 int64_t* mTarget;
579 int64_t mDelta;
581 NS_IMETHOD Run() override {
582 *mTarget = mDelta;
583 return NS_OK;
587 } // namespace
589 void StorageUsage::LoadUsage(const int64_t aUsage) {
590 // Using kDefaultSet index since it is the index for the persitent data
591 // stored in the database we have just loaded usage for.
592 if (!NS_IsMainThread()) {
593 // In single process scenario we get this call from the DB thread
594 RefPtr<LoadUsageRunnable> r =
595 new LoadUsageRunnable(mUsage + kDefaultSet, aUsage);
596 NS_DispatchToMainThread(r);
597 } else {
598 // On a child process we get this on the main thread already
599 mUsage[kDefaultSet] += aUsage;
603 bool StorageUsage::CheckAndSetETLD1UsageDelta(
604 uint32_t aDataSetIndex, const int64_t aDelta,
605 const LocalStorageCache::MutationSource aSource) {
606 MOZ_ASSERT(NS_IsMainThread());
608 int64_t newUsage = mUsage[aDataSetIndex] + aDelta;
609 if (aSource == LocalStorageCache::ContentMutation && aDelta > 0 &&
610 newUsage > LocalStorageManager::GetSiteQuota()) {
611 return false;
614 mUsage[aDataSetIndex] = newUsage;
615 return true;
618 } // namespace mozilla::dom