summaryrefslogtreecommitdiffstats
path: root/dom/storage/LocalStorageCache.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/storage/LocalStorageCache.cpp')
-rw-r--r--dom/storage/LocalStorageCache.cpp615
1 files changed, 615 insertions, 0 deletions
diff --git a/dom/storage/LocalStorageCache.cpp b/dom/storage/LocalStorageCache.cpp
new file mode 100644
index 0000000000..4b55482441
--- /dev/null
+++ b/dom/storage/LocalStorageCache.cpp
@@ -0,0 +1,615 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "LocalStorageCache.h"
+
+#include "Storage.h"
+#include "StorageDBThread.h"
+#include "StorageIPC.h"
+#include "StorageUtils.h"
+#include "LocalStorageManager.h"
+
+#include "nsDOMString.h"
+#include "nsXULAppAPI.h"
+#include "mozilla/Unused.h"
+#include "nsProxyRelease.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla::dom {
+
+#define DOM_STORAGE_CACHE_KEEP_ALIVE_TIME_MS 20000
+
+namespace {
+
+const uint32_t kDefaultSet = 0;
+const uint32_t kSessionSet = 1;
+
+inline uint32_t GetDataSetIndex(bool aPrivateBrowsing,
+ bool aSessionScopedOrLess) {
+ if (!aPrivateBrowsing && aSessionScopedOrLess) {
+ return kSessionSet;
+ }
+
+ return kDefaultSet;
+}
+
+inline uint32_t GetDataSetIndex(const LocalStorage* aStorage) {
+ return GetDataSetIndex(aStorage->IsPrivateBrowsing(),
+ aStorage->IsSessionScopedOrLess());
+}
+
+} // namespace
+
+// LocalStorageCacheBridge
+
+NS_IMPL_ADDREF(LocalStorageCacheBridge)
+
+// Since there is no consumer of return value of Release, we can turn this
+// method to void to make implementation of asynchronous
+// LocalStorageCache::Release much simpler.
+NS_IMETHODIMP_(void) LocalStorageCacheBridge::Release(void) {
+ MOZ_ASSERT(int32_t(mRefCnt) > 0, "dup release");
+ nsrefcnt count = --mRefCnt;
+ NS_LOG_RELEASE(this, count, "LocalStorageCacheBridge");
+ if (0 == count) {
+ mRefCnt = 1; /* stabilize */
+ /* enable this to find non-threadsafe destructors: */
+ /* NS_ASSERT_OWNINGTHREAD(_class); */
+ delete (this);
+ }
+}
+
+// LocalStorageCache
+
+LocalStorageCache::LocalStorageCache(const nsACString* aOriginNoSuffix)
+ : mActor(nullptr),
+ mOriginNoSuffix(*aOriginNoSuffix),
+ mMonitor("LocalStorageCache"),
+ mLoaded(false),
+ mLoadResult(NS_OK),
+ mInitialized(false),
+ mPersistent(false),
+ mPreloadTelemetryRecorded(false) {
+ MOZ_COUNT_CTOR(LocalStorageCache);
+}
+
+LocalStorageCache::~LocalStorageCache() {
+ if (mActor) {
+ mActor->SendDeleteMeInternal();
+ MOZ_ASSERT(!mActor, "SendDeleteMeInternal should have cleared!");
+ }
+
+ if (mManager) {
+ mManager->DropCache(this);
+ }
+
+ MOZ_COUNT_DTOR(LocalStorageCache);
+}
+
+void LocalStorageCache::SetActor(LocalStorageCacheChild* aActor) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aActor);
+ MOZ_ASSERT(!mActor);
+
+ mActor = aActor;
+}
+
+NS_IMETHODIMP_(void)
+LocalStorageCache::Release(void) {
+ // We must actually release on the main thread since the cache removes it
+ // self from the manager's hash table. And we don't want to lock access to
+ // that hash table.
+ if (NS_IsMainThread()) {
+ LocalStorageCacheBridge::Release();
+ return;
+ }
+
+ RefPtr<nsRunnableMethod<LocalStorageCacheBridge, void, false>> event =
+ NewNonOwningRunnableMethod("dom::LocalStorageCacheBridge::Release",
+ static_cast<LocalStorageCacheBridge*>(this),
+ &LocalStorageCacheBridge::Release);
+
+ nsresult rv = NS_DispatchToMainThread(event);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("LocalStorageCache::Release() on a non-main thread");
+ LocalStorageCacheBridge::Release();
+ }
+}
+
+void LocalStorageCache::Init(LocalStorageManager* aManager, bool aPersistent,
+ nsIPrincipal* aPrincipal,
+ const nsACString& aQuotaOriginScope) {
+ MOZ_ASSERT(!aQuotaOriginScope.IsEmpty());
+
+ if (mInitialized) {
+ return;
+ }
+
+ mInitialized = true;
+ aPrincipal->OriginAttributesRef().CreateSuffix(mOriginSuffix);
+ mPrivateBrowsingId = aPrincipal->GetPrivateBrowsingId();
+ mPersistent = aPersistent;
+ mQuotaOriginScope = aQuotaOriginScope;
+
+ if (mPersistent) {
+ mManager = aManager;
+ Preload();
+ }
+
+ // Check the quota string has (or has not) the identical origin suffix as
+ // this storage cache is bound to.
+ MOZ_ASSERT(StringBeginsWith(mQuotaOriginScope, mOriginSuffix));
+ MOZ_ASSERT(mOriginSuffix.IsEmpty() !=
+ StringBeginsWith(mQuotaOriginScope, "^"_ns));
+
+ mUsage = aManager->GetOriginUsage(mQuotaOriginScope, mPrivateBrowsingId);
+}
+
+void LocalStorageCache::NotifyObservers(const LocalStorage* aStorage,
+ const nsAString& aKey,
+ const nsAString& aOldValue,
+ const nsAString& aNewValue) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aStorage);
+
+ if (!mActor) {
+ return;
+ }
+
+ // We want to send a message to the parent in order to broadcast the
+ // StorageEvent correctly to any child process.
+
+ Unused << mActor->SendNotify(aStorage->DocumentURI(), aKey, aOldValue,
+ aNewValue);
+}
+
+inline bool LocalStorageCache::Persist(const LocalStorage* aStorage) const {
+ return mPersistent &&
+ (aStorage->IsPrivateBrowsing() || !aStorage->IsSessionScopedOrLess());
+}
+
+const nsCString LocalStorageCache::Origin() const {
+ return LocalStorageManager::CreateOrigin(mOriginSuffix, mOriginNoSuffix);
+}
+
+LocalStorageCache::Data& LocalStorageCache::DataSet(
+ const LocalStorage* aStorage) {
+ return mData[GetDataSetIndex(aStorage)];
+}
+
+bool LocalStorageCache::ProcessUsageDelta(const LocalStorage* aStorage,
+ int64_t aDelta,
+ const MutationSource aSource) {
+ return ProcessUsageDelta(GetDataSetIndex(aStorage), aDelta, aSource);
+}
+
+bool LocalStorageCache::ProcessUsageDelta(uint32_t aGetDataSetIndex,
+ const int64_t aDelta,
+ const MutationSource aSource) {
+ // Check limit per this origin
+ Data& data = mData[aGetDataSetIndex];
+ uint64_t newOriginUsage = data.mOriginQuotaUsage + aDelta;
+ if (aSource == ContentMutation && aDelta > 0 &&
+ newOriginUsage > LocalStorageManager::GetOriginQuota()) {
+ return false;
+ }
+
+ // Now check eTLD+1 limit
+ if (mUsage &&
+ !mUsage->CheckAndSetETLD1UsageDelta(aGetDataSetIndex, aDelta, aSource)) {
+ return false;
+ }
+
+ // Update size in our data set
+ data.mOriginQuotaUsage = newOriginUsage;
+ return true;
+}
+
+void LocalStorageCache::Preload() {
+ if (mLoaded || !mPersistent) {
+ return;
+ }
+
+ StorageDBChild* storageChild =
+ StorageDBChild::GetOrCreate(mPrivateBrowsingId);
+ if (!storageChild) {
+ mLoaded = true;
+ mLoadResult = NS_ERROR_FAILURE;
+ return;
+ }
+
+ storageChild->AsyncPreload(this);
+}
+
+void LocalStorageCache::WaitForPreload(Telemetry::HistogramID aTelemetryID) {
+ if (!mPersistent) {
+ return;
+ }
+
+ bool loaded = mLoaded;
+
+ // Telemetry of rates of pending preloads
+ if (!mPreloadTelemetryRecorded) {
+ mPreloadTelemetryRecorded = true;
+ Telemetry::Accumulate(
+ Telemetry::LOCALDOMSTORAGE_PRELOAD_PENDING_ON_FIRST_ACCESS, !loaded);
+ }
+
+ if (loaded) {
+ return;
+ }
+
+ // Measure which operation blocks and for how long
+ Telemetry::RuntimeAutoTimer timer(aTelemetryID);
+
+ // If preload already started (i.e. we got some first data, but not all)
+ // SyncPreload will just wait for it to finish rather then synchronously
+ // read from the database. It seems to me more optimal.
+
+ // TODO place for A/B testing (force main thread load vs. let preload finish)
+
+ // No need to check sDatabase for being non-null since preload is either
+ // done before we've shut the DB down or when the DB could not start,
+ // preload has not even be started.
+ StorageDBChild::Get(mPrivateBrowsingId)->SyncPreload(this);
+}
+
+nsresult LocalStorageCache::GetLength(const LocalStorage* aStorage,
+ uint32_t* aRetval) {
+ if (Persist(aStorage)) {
+ WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETLENGTH_BLOCKING_MS);
+ if (NS_FAILED(mLoadResult)) {
+ return mLoadResult;
+ }
+ }
+
+ *aRetval = DataSet(aStorage).mKeys.Count();
+ return NS_OK;
+}
+
+nsresult LocalStorageCache::GetKey(const LocalStorage* aStorage,
+ uint32_t aIndex, nsAString& aRetval) {
+ // XXX: This does a linear search for the key at index, which would
+ // suck if there's a large numer of indexes. Do we care? If so,
+ // maybe we need to have a lazily populated key array here or
+ // something?
+ if (Persist(aStorage)) {
+ WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETKEY_BLOCKING_MS);
+ if (NS_FAILED(mLoadResult)) {
+ return mLoadResult;
+ }
+ }
+
+ aRetval.SetIsVoid(true);
+ for (auto iter = DataSet(aStorage).mKeys.Iter(); !iter.Done(); iter.Next()) {
+ if (aIndex == 0) {
+ aRetval = iter.Key();
+ break;
+ }
+ aIndex--;
+ }
+
+ return NS_OK;
+}
+
+void LocalStorageCache::GetKeys(const LocalStorage* aStorage,
+ nsTArray<nsString>& aKeys) {
+ if (Persist(aStorage)) {
+ WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETALLKEYS_BLOCKING_MS);
+ }
+
+ if (NS_FAILED(mLoadResult)) {
+ return;
+ }
+
+ AppendToArray(aKeys, DataSet(aStorage).mKeys.Keys());
+}
+
+nsresult LocalStorageCache::GetItem(const LocalStorage* aStorage,
+ const nsAString& aKey, nsAString& aRetval) {
+ if (Persist(aStorage)) {
+ WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETVALUE_BLOCKING_MS);
+ if (NS_FAILED(mLoadResult)) {
+ return mLoadResult;
+ }
+ }
+
+ // not using AutoString since we don't want to copy buffer to result
+ nsString value;
+ if (!DataSet(aStorage).mKeys.Get(aKey, &value)) {
+ SetDOMStringToNull(value);
+ }
+
+ aRetval = value;
+
+ return NS_OK;
+}
+
+nsresult LocalStorageCache::SetItem(const LocalStorage* aStorage,
+ const nsAString& aKey,
+ const nsAString& aValue, nsString& aOld,
+ const MutationSource aSource) {
+ // Size of the cache that will change after this action.
+ int64_t delta = 0;
+
+ if (Persist(aStorage)) {
+ WaitForPreload(Telemetry::LOCALDOMSTORAGE_SETVALUE_BLOCKING_MS);
+ if (NS_FAILED(mLoadResult)) {
+ return mLoadResult;
+ }
+ }
+
+ Data& data = DataSet(aStorage);
+ if (!data.mKeys.Get(aKey, &aOld)) {
+ SetDOMStringToNull(aOld);
+
+ // We only consider key size if the key doesn't exist before.
+ delta += static_cast<int64_t>(aKey.Length());
+ }
+
+ delta += static_cast<int64_t>(aValue.Length()) -
+ static_cast<int64_t>(aOld.Length());
+
+ if (!ProcessUsageDelta(aStorage, delta, aSource)) {
+ return NS_ERROR_DOM_QUOTA_EXCEEDED_ERR;
+ }
+
+ if (aValue == aOld && DOMStringIsNull(aValue) == DOMStringIsNull(aOld)) {
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+
+ data.mKeys.InsertOrUpdate(aKey, aValue);
+
+ if (aSource != ContentMutation) {
+ return NS_OK;
+ }
+
+#if !defined(MOZ_WIDGET_ANDROID)
+ NotifyObservers(aStorage, aKey, aOld, aValue);
+#endif
+
+ if (Persist(aStorage)) {
+ StorageDBChild* storageChild = StorageDBChild::Get(mPrivateBrowsingId);
+ if (!storageChild) {
+ NS_ERROR(
+ "Writing to localStorage after the database has been shut down"
+ ", data lose!");
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ if (DOMStringIsNull(aOld)) {
+ return storageChild->AsyncAddItem(this, aKey, aValue);
+ }
+
+ return storageChild->AsyncUpdateItem(this, aKey, aValue);
+ }
+
+ return NS_OK;
+}
+
+nsresult LocalStorageCache::RemoveItem(const LocalStorage* aStorage,
+ const nsAString& aKey, nsString& aOld,
+ const MutationSource aSource) {
+ if (Persist(aStorage)) {
+ WaitForPreload(Telemetry::LOCALDOMSTORAGE_REMOVEKEY_BLOCKING_MS);
+ if (NS_FAILED(mLoadResult)) {
+ return mLoadResult;
+ }
+ }
+
+ Data& data = DataSet(aStorage);
+ if (!data.mKeys.Get(aKey, &aOld)) {
+ SetDOMStringToNull(aOld);
+ return NS_SUCCESS_DOM_NO_OPERATION;
+ }
+
+ // Recalculate the cached data size
+ const int64_t delta = -(static_cast<int64_t>(aOld.Length()) +
+ static_cast<int64_t>(aKey.Length()));
+ Unused << ProcessUsageDelta(aStorage, delta, aSource);
+ data.mKeys.Remove(aKey);
+
+ if (aSource != ContentMutation) {
+ return NS_OK;
+ }
+
+#if !defined(MOZ_WIDGET_ANDROID)
+ NotifyObservers(aStorage, aKey, aOld, VoidString());
+#endif
+
+ if (Persist(aStorage)) {
+ StorageDBChild* storageChild = StorageDBChild::Get(mPrivateBrowsingId);
+ if (!storageChild) {
+ NS_ERROR(
+ "Writing to localStorage after the database has been shut down"
+ ", data lose!");
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ return storageChild->AsyncRemoveItem(this, aKey);
+ }
+
+ return NS_OK;
+}
+
+nsresult LocalStorageCache::Clear(const LocalStorage* aStorage,
+ const MutationSource aSource) {
+ bool refresh = false;
+ if (Persist(aStorage)) {
+ // We need to preload all data (know the size) before we can proceeed
+ // to correctly decrease cached usage number.
+ // XXX as in case of unload, this is not technically needed now, but
+ // after super-scope quota introduction we have to do this. Get telemetry
+ // right now.
+ WaitForPreload(Telemetry::LOCALDOMSTORAGE_CLEAR_BLOCKING_MS);
+ if (NS_FAILED(mLoadResult)) {
+ // When we failed to load data from the database, force delete of the
+ // scope data and make use of the storage possible again.
+ refresh = true;
+ mLoadResult = NS_OK;
+ }
+ }
+
+ Data& data = DataSet(aStorage);
+ bool hadData = !!data.mKeys.Count();
+
+ if (hadData) {
+ Unused << ProcessUsageDelta(aStorage, -data.mOriginQuotaUsage, aSource);
+ data.mKeys.Clear();
+ }
+
+ if (aSource != ContentMutation) {
+ return hadData ? NS_OK : NS_SUCCESS_DOM_NO_OPERATION;
+ }
+
+#if !defined(MOZ_WIDGET_ANDROID)
+ if (hadData) {
+ NotifyObservers(aStorage, VoidString(), VoidString(), VoidString());
+ }
+#endif
+
+ if (Persist(aStorage) && (refresh || hadData)) {
+ StorageDBChild* storageChild = StorageDBChild::Get(mPrivateBrowsingId);
+ if (!storageChild) {
+ NS_ERROR(
+ "Writing to localStorage after the database has been shut down"
+ ", data lose!");
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ return storageChild->AsyncClear(this);
+ }
+
+ return hadData ? NS_OK : NS_SUCCESS_DOM_NO_OPERATION;
+}
+
+int64_t LocalStorageCache::GetOriginQuotaUsage(
+ const LocalStorage* aStorage) const {
+ return mData[GetDataSetIndex(aStorage)].mOriginQuotaUsage;
+}
+
+void LocalStorageCache::UnloadItems(uint32_t aUnloadFlags) {
+ if (aUnloadFlags & kUnloadDefault) {
+ // Must wait for preload to pass correct usage to ProcessUsageDelta
+ // XXX this is not technically needed right now since there is just
+ // per-origin isolated quota handling, but when we introduce super-
+ // -scope quotas, we have to do this. Better to start getting
+ // telemetry right now.
+ WaitForPreload(Telemetry::LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS);
+
+ mData[kDefaultSet].mKeys.Clear();
+ ProcessUsageDelta(kDefaultSet, -mData[kDefaultSet].mOriginQuotaUsage);
+ }
+
+ if (aUnloadFlags & kUnloadSession) {
+ mData[kSessionSet].mKeys.Clear();
+ ProcessUsageDelta(kSessionSet, -mData[kSessionSet].mOriginQuotaUsage);
+ }
+
+#ifdef DOM_STORAGE_TESTS
+ if (aUnloadFlags & kTestReload) {
+ WaitForPreload(Telemetry::LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS);
+
+ mData[kDefaultSet].mKeys.Clear();
+ mLoaded = false; // This is only used in testing code
+ Preload();
+ }
+#endif
+}
+
+// LocalStorageCacheBridge
+
+uint32_t LocalStorageCache::LoadedCount() {
+ MonitorAutoLock monitor(mMonitor);
+ Data& data = mData[kDefaultSet];
+ return data.mKeys.Count();
+}
+
+bool LocalStorageCache::LoadItem(const nsAString& aKey,
+ const nsAString& aValue) {
+ MonitorAutoLock monitor(mMonitor);
+ if (mLoaded) {
+ return false;
+ }
+
+ Data& data = mData[kDefaultSet];
+ data.mKeys.LookupOrInsertWith(aKey, [&] {
+ data.mOriginQuotaUsage += aKey.Length() + aValue.Length();
+ return nsString(aValue);
+ });
+ return true;
+}
+
+void LocalStorageCache::LoadDone(nsresult aRv) {
+ MonitorAutoLock monitor(mMonitor);
+ mLoadResult = aRv;
+ mLoaded = true;
+ monitor.Notify();
+}
+
+void LocalStorageCache::LoadWait() {
+ MonitorAutoLock monitor(mMonitor);
+ while (!mLoaded) {
+ monitor.Wait();
+ }
+}
+
+// StorageUsage
+
+StorageUsage::StorageUsage(const nsACString& aOriginScope)
+ : mOriginScope(aOriginScope) {
+ mUsage[kDefaultSet] = mUsage[kSessionSet] = 0LL;
+}
+
+namespace {
+
+class LoadUsageRunnable : public Runnable {
+ public:
+ LoadUsageRunnable(int64_t* aUsage, const int64_t aDelta)
+ : Runnable("dom::LoadUsageRunnable"), mTarget(aUsage), mDelta(aDelta) {}
+
+ private:
+ int64_t* mTarget;
+ int64_t mDelta;
+
+ NS_IMETHOD Run() override {
+ *mTarget = mDelta;
+ return NS_OK;
+ }
+};
+
+} // namespace
+
+void StorageUsage::LoadUsage(const int64_t aUsage) {
+ // Using kDefaultSet index since it is the index for the persitent data
+ // stored in the database we have just loaded usage for.
+ if (!NS_IsMainThread()) {
+ // In single process scenario we get this call from the DB thread
+ RefPtr<LoadUsageRunnable> r =
+ new LoadUsageRunnable(mUsage + kDefaultSet, aUsage);
+ NS_DispatchToMainThread(r);
+ } else {
+ // On a child process we get this on the main thread already
+ mUsage[kDefaultSet] += aUsage;
+ }
+}
+
+bool StorageUsage::CheckAndSetETLD1UsageDelta(
+ uint32_t aDataSetIndex, const int64_t aDelta,
+ const LocalStorageCache::MutationSource aSource) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ int64_t newUsage = mUsage[aDataSetIndex] + aDelta;
+ if (aSource == LocalStorageCache::ContentMutation && aDelta > 0 &&
+ newUsage > LocalStorageManager::GetSiteQuota()) {
+ return false;
+ }
+
+ mUsage[aDataSetIndex] = newUsage;
+ return true;
+}
+
+} // namespace mozilla::dom