diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /dom/storage | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
38 files changed, 10547 insertions, 0 deletions
diff --git a/dom/storage/LocalStorage.cpp b/dom/storage/LocalStorage.cpp new file mode 100644 index 0000000000..ec75566242 --- /dev/null +++ b/dom/storage/LocalStorage.cpp @@ -0,0 +1,216 @@ +/* -*- 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 "LocalStorage.h" +#include "LocalStorageCache.h" +#include "LocalStorageManager.h" +#include "StorageUtils.h" + +#include "nsIPrincipal.h" + +#include "mozilla/dom/PermissionMessageUtils.h" +#include "mozilla/dom/StorageBinding.h" +#include "mozilla/dom/StorageEvent.h" +#include "mozilla/dom/StorageEventBinding.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/Preferences.h" +#include "mozilla/EnumSet.h" +#include "nsThreadUtils.h" +#include "nsContentUtils.h" +#include "nsServiceManagerUtils.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(LocalStorage) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(LocalStorage, Storage) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mManager) + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(LocalStorage, Storage) + CycleCollectionNoteChild( + cb, NS_ISUPPORTS_CAST(nsIDOMStorageManager*, tmp->mManager.get()), + "mManager"); +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LocalStorage) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END_INHERITING(Storage) + +NS_IMPL_ADDREF_INHERITED(LocalStorage, Storage) +NS_IMPL_RELEASE_INHERITED(LocalStorage, Storage) + +LocalStorage::LocalStorage(nsPIDOMWindowInner* aWindow, + LocalStorageManager* aManager, + LocalStorageCache* aCache, + const nsAString& aDocumentURI, + nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal, bool aIsPrivate) + : Storage(aWindow, aPrincipal, aStoragePrincipal), + mManager(aManager), + mCache(aCache), + mDocumentURI(aDocumentURI), + mIsPrivate(aIsPrivate) { + mCache->Preload(); +} + +LocalStorage::~LocalStorage() = default; + +int64_t LocalStorage::GetOriginQuotaUsage() const { + return mCache->GetOriginQuotaUsage(this); +} + +uint32_t LocalStorage::GetLength(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + if (!CanUseStorage(aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return 0; + } + + uint32_t length; + aRv = mCache->GetLength(this, &length); + return length; +} + +void LocalStorage::Key(uint32_t aIndex, nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { + if (!CanUseStorage(aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + aRv = mCache->GetKey(this, aIndex, aResult); +} + +void LocalStorage::GetItem(const nsAString& aKey, nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { + if (!CanUseStorage(aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + aRv = mCache->GetItem(this, aKey, aResult); +} + +void LocalStorage::SetItem(const nsAString& aKey, const nsAString& aData, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { + if (!CanUseStorage(aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsString data; + bool ok = data.Assign(aData, fallible); + if (!ok) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + nsString old; + aRv = mCache->SetItem(this, aKey, data, old); + if (aRv.Failed()) { + return; + } + + if (!aRv.ErrorCodeIs(NS_SUCCESS_DOM_NO_OPERATION)) { + OnChange(aKey, old, aData); + } +} + +void LocalStorage::RemoveItem(const nsAString& aKey, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + if (!CanUseStorage(aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsAutoString old; + aRv = mCache->RemoveItem(this, aKey, old); + if (aRv.Failed()) { + return; + } + + if (!aRv.ErrorCodeIs(NS_SUCCESS_DOM_NO_OPERATION)) { + OnChange(aKey, old, VoidString()); + } +} + +void LocalStorage::Clear(nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { + if (!CanUseStorage(aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + aRv = mCache->Clear(this); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + if (!aRv.ErrorCodeIs(NS_SUCCESS_DOM_NO_OPERATION)) { + OnChange(VoidString(), VoidString(), VoidString()); + } +} + +void LocalStorage::OnChange(const nsAString& aKey, const nsAString& aOldValue, + const nsAString& aNewValue) { + NotifyChange(/* aStorage */ this, StoragePrincipal(), aKey, aOldValue, + aNewValue, /* aStorageType */ u"localStorage", mDocumentURI, + mIsPrivate, /* aImmediateDispatch */ false); +} + +void LocalStorage::ApplyEvent(StorageEvent* aStorageEvent) { + MOZ_ASSERT(aStorageEvent); + + nsAutoString key; + nsAutoString old; + nsAutoString value; + + aStorageEvent->GetKey(key); + aStorageEvent->GetNewValue(value); + + // No key means clearing the full storage. + if (key.IsVoid()) { + MOZ_ASSERT(value.IsVoid()); + mCache->Clear(this, LocalStorageCache::E10sPropagated); + return; + } + + // No new value means removing the key. + if (value.IsVoid()) { + mCache->RemoveItem(this, key, old, LocalStorageCache::E10sPropagated); + return; + } + + // Otherwise, we set the new value. + mCache->SetItem(this, key, value, old, LocalStorageCache::E10sPropagated); +} + +void LocalStorage::GetSupportedNames(nsTArray<nsString>& aKeys) { + if (!CanUseStorage(*nsContentUtils::SubjectPrincipal())) { + // return just an empty array + aKeys.Clear(); + return; + } + + mCache->GetKeys(this, aKeys); +} + +bool LocalStorage::IsForkOf(const Storage* aOther) const { + MOZ_ASSERT(aOther); + if (aOther->Type() != eLocalStorage) { + return false; + } + + return mCache == static_cast<const LocalStorage*>(aOther)->mCache; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/LocalStorage.h b/dom/storage/LocalStorage.h new file mode 100644 index 0000000000..e4baa439ed --- /dev/null +++ b/dom/storage/LocalStorage.h @@ -0,0 +1,85 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_LocalStorage_h +#define mozilla_dom_LocalStorage_h + +#include "Storage.h" +#include "nsWeakReference.h" + +namespace mozilla { +namespace dom { + +class LocalStorageCache; +class LocalStorageManager; +class StorageEvent; + +class LocalStorage final : public Storage, public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(LocalStorage, Storage) + + StorageType Type() const override { return eLocalStorage; } + + LocalStorageManager* GetManager() const { return mManager; } + + LocalStorageCache const* GetCache() const { return mCache; } + + const nsString& DocumentURI() const { return mDocumentURI; } + + LocalStorage(nsPIDOMWindowInner* aWindow, LocalStorageManager* aManager, + LocalStorageCache* aCache, const nsAString& aDocumentURI, + nsIPrincipal* aPrincipal, nsIPrincipal* aStoragePrincipal, + bool aIsPrivate); + + bool IsForkOf(const Storage* aOther) const override; + + // WebIDL + + int64_t GetOriginQuotaUsage() const override; + + uint32_t GetLength(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) override; + + void Key(uint32_t aIndex, nsAString& aResult, nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) override; + + void GetItem(const nsAString& aKey, nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) override; + + void GetSupportedNames(nsTArray<nsString>& aKeys) override; + + void SetItem(const nsAString& aKey, const nsAString& aValue, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) override; + + void RemoveItem(const nsAString& aKey, nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) override; + + void Clear(nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) override; + + void ApplyEvent(StorageEvent* aStorageEvent); + + private: + ~LocalStorage(); + + friend class LocalStorageManager; + friend class LocalStorageCache; + + RefPtr<LocalStorageManager> mManager; + RefPtr<LocalStorageCache> mCache; + nsString mDocumentURI; + + // Whether this storage is running in private-browsing window. + bool mIsPrivate : 1; + + void OnChange(const nsAString& aKey, const nsAString& aOldValue, + const nsAString& aNewValue); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_LocalStorage_h diff --git a/dom/storage/LocalStorageCache.cpp b/dom/storage/LocalStorageCache.cpp new file mode 100644 index 0000000000..a06564a92c --- /dev/null +++ b/dom/storage/LocalStorageCache.cpp @@ -0,0 +1,621 @@ +/* -*- 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 { +namespace 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 nsString& aKey, + const nsString& aOldValue, + const nsString& 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; + } + + for (auto iter = DataSet(aStorage).mKeys.Iter(); !iter.Done(); iter.Next()) { + aKeys.AppendElement(iter.Key()); + } +} + +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 nsString& 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.Put(aKey, aValue); + + if (aSource != ContentMutation) { + return NS_OK; + } + +#if !defined(MOZ_WIDGET_ANDROID) + NotifyObservers(aStorage, nsString(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, nsString(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 nsString& aValue) { + MonitorAutoLock monitor(mMonitor); + if (mLoaded) { + return false; + } + + Data& data = mData[kDefaultSet]; + if (data.mKeys.Get(aKey, nullptr)) { + return true; // don't stop, just don't override + } + + data.mKeys.Put(aKey, aValue); + data.mOriginQuotaUsage += aKey.Length() + aValue.Length(); + 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 dom +} // namespace mozilla diff --git a/dom/storage/LocalStorageCache.h b/dom/storage/LocalStorageCache.h new file mode 100644 index 0000000000..8b85b8ed2f --- /dev/null +++ b/dom/storage/LocalStorageCache.h @@ -0,0 +1,308 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_LocalStorageCache_h +#define mozilla_dom_LocalStorageCache_h + +#include "nsIPrincipal.h" + +#include "nsString.h" +#include "nsDataHashtable.h" +#include "nsHashKeys.h" +#include "mozilla/Monitor.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Atomics.h" + +namespace mozilla { +namespace dom { + +class LocalStorage; +class LocalStorageCacheChild; +class LocalStorageManager; +class StorageUsage; +class StorageDBBridge; + +// Interface class on which only the database or IPC may call. +// Used to populate the cache with DB data. +class LocalStorageCacheBridge { + public: + NS_IMETHOD_(MozExternalRefCountType) AddRef(void); + NS_IMETHOD_(void) Release(void); + + // The origin of the cache, result is concatenation of OriginNoSuffix() and + // OriginSuffix(), see below. + virtual const nsCString Origin() const = 0; + + // The origin attributes suffix alone, this is usually passed as an + // |aOriginSuffix| argument to various methods + virtual const nsCString& OriginSuffix() const = 0; + + // The origin in the database usage format (reversed) and without the suffix + virtual const nsCString& OriginNoSuffix() const = 0; + + // Whether the cache is already fully loaded + virtual bool Loaded() = 0; + + // How many items has so far been loaded into the cache, used + // for optimization purposes + virtual uint32_t LoadedCount() = 0; + + // Called by the database to load a key and its value to the cache + virtual bool LoadItem(const nsAString& aKey, const nsString& aValue) = 0; + + // Called by the database after all keys and values has been loaded + // to this cache + virtual void LoadDone(nsresult aRv) = 0; + + // Use to synchronously wait until the cache gets fully loaded with data, + // this method exits after LoadDone has been called + virtual void LoadWait() = 0; + + protected: + virtual ~LocalStorageCacheBridge() = default; + + ThreadSafeAutoRefCnt mRefCnt; + NS_DECL_OWNINGTHREAD +}; + +// Implementation of scope cache that is responsible for preloading data +// for persistent storage (localStorage) and hold data for non-private, +// private and session-only cookie modes. It is also responsible for +// persisting data changes using the database, works as a write-back cache. +class LocalStorageCache : public LocalStorageCacheBridge { + public: + void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(LocalStorage); } + + void SetActor(LocalStorageCacheChild* aActor); + + void ClearActor() { + AssertIsOnOwningThread(); + + mActor = nullptr; + } + + NS_IMETHOD_(void) Release(void) override; + + enum MutationSource { + // The mutation is a result of an explicit JS mutation in this process. + // The mutation should be sent to the sDatabase. Quota will be checked and + // QuotaExceededError may be returned without the mutation being applied. + ContentMutation, + // The mutation initially was triggered in a different process and is being + // propagated to this cache via LocalStorage::ApplyEvent. The mutation + // should + // not be sent to the sDatabase because the originating process is already + // doing that. (In addition to the redundant writes being wasteful, there + // is the potential for other processes to see inconsistent state from the + // database while preloading.) Quota will be updated but not checked + // because it's assumed it was checked in another process and data-coherency + // is more important than slightly exceeding quota. + E10sPropagated + }; + + // Note: We pass aOriginNoSuffix through the ctor here, because + // LocalStorageCacheHashKey's ctor is creating this class and + // accepts reversed-origin-no-suffix as an argument - the hashing key. + explicit LocalStorageCache(const nsACString* aOriginNoSuffix); + + protected: + virtual ~LocalStorageCache(); + + public: + void Init(LocalStorageManager* aManager, bool aPersistent, + nsIPrincipal* aPrincipal, const nsACString& aQuotaOriginScope); + + // Get size of per-origin data. + int64_t GetOriginQuotaUsage(const LocalStorage* aStorage) const; + + // Starts async preload of this cache if it persistent and not loaded. + void Preload(); + + // The set of methods that are invoked by DOM storage web API. + // We are passing the LocalStorage object just to let the cache + // read properties like mPrivate and mSessionOnly. + // Get* methods return error when load from the database has failed. + nsresult GetLength(const LocalStorage* aStorage, uint32_t* aRetval); + nsresult GetKey(const LocalStorage* aStorage, uint32_t index, + nsAString& aRetval); + nsresult GetItem(const LocalStorage* aStorage, const nsAString& aKey, + nsAString& aRetval); + nsresult SetItem(const LocalStorage* aStorage, const nsAString& aKey, + const nsString& aValue, nsString& aOld, + const MutationSource aSource = ContentMutation); + nsresult RemoveItem(const LocalStorage* aStorage, const nsAString& aKey, + nsString& aOld, + const MutationSource aSource = ContentMutation); + nsresult Clear(const LocalStorage* aStorage, + const MutationSource aSource = ContentMutation); + + void GetKeys(const LocalStorage* aStorage, nsTArray<nsString>& aKeys); + + // LocalStorageCacheBridge + + const nsCString Origin() const override; + const nsCString& OriginNoSuffix() const override { return mOriginNoSuffix; } + const nsCString& OriginSuffix() const override { return mOriginSuffix; } + bool Loaded() override { return mLoaded; } + uint32_t LoadedCount() override; + bool LoadItem(const nsAString& aKey, const nsString& aValue) override; + void LoadDone(nsresult aRv) override; + void LoadWait() override; + + // Cache keeps 3 sets of data: regular, private and session-only. + // This class keeps keys and values for a set and also caches + // size of the data for quick per-origin quota checking. + class Data { + public: + Data() : mOriginQuotaUsage(0) {} + int64_t mOriginQuotaUsage; + nsDataHashtable<nsStringHashKey, nsString> mKeys; + }; + + public: + // Number of data sets we keep: default, session + static const uint32_t kDataSetCount = 2; + + private: + // API to clear the cache data, this is invoked by chrome operations + // like cookie deletion. + friend class LocalStorageManager; + + static const uint32_t kUnloadDefault = 1 << 0; + static const uint32_t kUnloadSession = 1 << 1; + static const uint32_t kUnloadComplete = kUnloadDefault | kUnloadSession; + +#ifdef DOM_STORAGE_TESTS + static const uint32_t kTestReload = 1 << 15; +#endif + + void UnloadItems(uint32_t aUnloadFlags); + + private: + // Synchronously blocks until the cache is fully loaded from the database + void WaitForPreload(mozilla::Telemetry::HistogramID aTelemetryID); + + // Helper to get one of the 3 data sets (regular, private, session) + Data& DataSet(const LocalStorage* aStorage); + + // Used for firing storage events and synchronization of caches in other + // content processes. + void NotifyObservers(const LocalStorage* aStorage, const nsString& aKey, + const nsString& aOldValue, const nsString& aNewValue); + + // Whether the storage change is about to persist + bool Persist(const LocalStorage* aStorage) const; + + // Changes the quota usage on the given data set if it fits the quota. + // If not, then false is returned and no change to the set must be done. + // A special case is if aSource==E10sPropagated, then we will return true even + // if the change would put us over quota. This is done to ensure coherency of + // caches between processes in the face of races. It does allow an attacker + // to potentially use N multiples of the quota storage limit if they can + // arrange for their origin to execute code in N processes. However, this is + // not considered a particularly concerning threat model because it's already + // very possible for a rogue page to attempt to intentionally fill up the + // user's storage through the use of multiple domains. + bool ProcessUsageDelta(uint32_t aGetDataSetIndex, const int64_t aDelta, + const MutationSource aSource = ContentMutation); + bool ProcessUsageDelta(const LocalStorage* aStorage, const int64_t aDelta, + const MutationSource aSource = ContentMutation); + + private: + // When a cache is reponsible for its life time (in case of localStorage data + // cache) we need to refer our manager since removal of the cache from the + // hash table is handled in the destructor by call to the manager. Cache + // could potentially overlive the manager, hence the hard ref. + RefPtr<LocalStorageManager> mManager; + + // Reference to the usage counter object we check on for eTLD+1 quota limit. + // Obtained from the manager during initialization (Init method). + RefPtr<StorageUsage> mUsage; + + // The LocalStorageCacheChild is created at the same time of this class. + // In normal operation, the actor will be synchronously cleared in our + // destructor when we tell it to delete itself. In a shutdown-related edge + // case in the parent process for JSM's, it is possible for the actor to be + // destroyed while this class remains alive, in which case it will be nulled + // out. + LocalStorageCacheChild* mActor; + + // The origin this cache belongs to in the "DB format", i.e. reversed + nsCString mOriginNoSuffix; + + // The origin attributes suffix + nsCString mOriginSuffix; + + // The eTLD+1 scope used to count quota usage. It is in the reversed format + // and contains the origin attributes suffix. + nsCString mQuotaOriginScope; + + // Non-private Browsing, Private Browsing and Session Only sets. + Data mData[kDataSetCount]; + + // This monitor is used to wait for full load of data. + mozilla::Monitor mMonitor; + + // Flag that is initially false. When the cache is about to work with + // the database (i.e. it is persistent) this flags is set to true after + // all keys and coresponding values are loaded from the database. + // This flag never goes from true back to false. Since this flag is + // critical for mData hashtable synchronization, it's made atomic. + Atomic<bool, ReleaseAcquire> mLoaded; + + // Result of load from the database. Valid after mLoaded flag has been set. + nsresult mLoadResult; + + // Expected to be only 0 or 1. + uint32_t mPrivateBrowsingId; + + // Init() method has been called + bool mInitialized : 1; + + // This cache is about to be bound with the database (i.e. it has + // to load from the DB first and has to persist when modifying the + // default data set.) + bool mPersistent : 1; + + // Whether we have already captured state of the cache preload on our first + // access. + bool mPreloadTelemetryRecorded : 1; +}; + +// StorageUsage +// Infrastructure to manage and check eTLD+1 quota +class StorageUsageBridge { + public: + NS_INLINE_DECL_THREADSAFE_VIRTUAL_REFCOUNTING(StorageUsageBridge) + + virtual const nsCString& OriginScope() = 0; + virtual void LoadUsage(const int64_t aUsage) = 0; + + protected: + // Protected destructor, to discourage deletion outside of Release(): + virtual ~StorageUsageBridge() = default; +}; + +class StorageUsage : public StorageUsageBridge { + public: + explicit StorageUsage(const nsACString& aOriginScope); + + bool CheckAndSetETLD1UsageDelta( + uint32_t aDataSetIndex, int64_t aUsageDelta, + const LocalStorageCache::MutationSource aSource); + + private: + const nsCString& OriginScope() override { return mOriginScope; } + void LoadUsage(const int64_t aUsage) override; + + nsCString mOriginScope; + int64_t mUsage[LocalStorageCache::kDataSetCount]; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_LocalStorageCache_h diff --git a/dom/storage/LocalStorageManager.cpp b/dom/storage/LocalStorageManager.cpp new file mode 100644 index 0000000000..1a9f566c42 --- /dev/null +++ b/dom/storage/LocalStorageManager.cpp @@ -0,0 +1,450 @@ +/* -*- 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 "LocalStorageManager.h" +#include "LocalStorage.h" +#include "StorageDBThread.h" +#include "StorageIPC.h" +#include "StorageUtils.h" + +#include "nsIEffectiveTLDService.h" + +#include "nsPIDOMWindow.h" +#include "nsNetUtil.h" +#include "nsNetCID.h" +#include "nsPrintfCString.h" +#include "nsXULAppAPI.h" +#include "nsThreadUtils.h" +#include "nsIObserverService.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/LocalStorageCommon.h" + +namespace mozilla { +namespace dom { + +using namespace StorageUtils; + +LocalStorageManager* LocalStorageManager::sSelf = nullptr; + +// static +uint32_t LocalStorageManager::GetOriginQuota() { + return StaticPrefs::dom_storage_default_quota() * 1024; // pref is in kBs +} + +// static +uint32_t LocalStorageManager::GetSiteQuota() { + return std::max(StaticPrefs::dom_storage_default_quota(), + StaticPrefs::dom_storage_default_site_quota()) * + 1024; // pref is in kBs +} + +NS_IMPL_ISUPPORTS(LocalStorageManager, nsIDOMStorageManager, + nsILocalStorageManager) + +LocalStorageManager::LocalStorageManager() : mCaches(8) { + MOZ_ASSERT(!NextGenLocalStorageEnabled()); + + StorageObserver* observer = StorageObserver::Self(); + NS_ASSERTION( + observer, + "No StorageObserver, cannot observe private data delete notifications!"); + + if (observer) { + observer->AddSink(this); + } + + NS_ASSERTION(!sSelf, + "Somebody is trying to " + "do_CreateInstance(\"@mozilla/dom/localStorage-manager;1\""); + sSelf = this; + + if (!XRE_IsParentProcess()) { + // Do this only on the child process. The thread IPC bridge + // is also used to communicate chrome observer notifications. + // Note: must be called after we set sSelf + for (const uint32_t id : {0, 1}) { + StorageDBChild::GetOrCreate(id); + } + } +} + +LocalStorageManager::~LocalStorageManager() { + StorageObserver* observer = StorageObserver::Self(); + if (observer) { + observer->RemoveSink(this); + } + + sSelf = nullptr; +} + +// static +nsAutoCString LocalStorageManager::CreateOrigin( + const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix) { + // Note: some hard-coded sqlite statements are dependent on the format this + // method returns. Changing this without updating those sqlite statements + // will cause malfunction. + + nsAutoCString scope; + scope.Append(aOriginSuffix); + scope.Append(':'); + scope.Append(aOriginNoSuffix); + return scope; +} + +LocalStorageCache* LocalStorageManager::GetCache( + const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix) { + CacheOriginHashtable* table = mCaches.LookupOrAdd(aOriginSuffix); + LocalStorageCacheHashKey* entry = table->GetEntry(aOriginNoSuffix); + if (!entry) { + return nullptr; + } + + return entry->cache(); +} + +already_AddRefed<StorageUsage> LocalStorageManager::GetOriginUsage( + const nsACString& aOriginNoSuffix, const uint32_t aPrivateBrowsingId) { + RefPtr<StorageUsage> usage; + if (mUsages.Get(aOriginNoSuffix, &usage)) { + return usage.forget(); + } + + usage = new StorageUsage(aOriginNoSuffix); + + StorageDBChild* storageChild = + StorageDBChild::GetOrCreate(aPrivateBrowsingId); + if (storageChild) { + storageChild->AsyncGetUsage(usage); + } + + mUsages.Put(aOriginNoSuffix, usage); + + return usage.forget(); +} + +already_AddRefed<LocalStorageCache> LocalStorageManager::PutCache( + const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix, + const nsACString& aQuotaKey, nsIPrincipal* aPrincipal) { + CacheOriginHashtable* table = mCaches.LookupOrAdd(aOriginSuffix); + LocalStorageCacheHashKey* entry = table->PutEntry(aOriginNoSuffix); + RefPtr<LocalStorageCache> cache = entry->cache(); + + // Lifetime handled by the cache, do persist + cache->Init(this, true, aPrincipal, aQuotaKey); + return cache.forget(); +} + +void LocalStorageManager::DropCache(LocalStorageCache* aCache) { + if (!NS_IsMainThread()) { + NS_WARNING( + "StorageManager::DropCache called on a non-main thread, shutting " + "down?"); + } + + CacheOriginHashtable* table = mCaches.LookupOrAdd(aCache->OriginSuffix()); + table->RemoveEntry(aCache->OriginNoSuffix()); +} + +nsresult LocalStorageManager::GetStorageInternal( + CreateMode aCreateMode, mozIDOMWindow* aWindow, nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal, const nsAString& aDocumentURI, + bool aPrivate, Storage** aRetval) { + nsAutoCString originAttrSuffix; + nsAutoCString originKey; + nsAutoCString quotaKey; + + aStoragePrincipal->OriginAttributesRef().CreateSuffix(originAttrSuffix); + + nsresult rv = aStoragePrincipal->GetStorageOriginKey(originKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_NOT_AVAILABLE; + } + + rv = aStoragePrincipal->GetLocalStorageQuotaKey(quotaKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_NOT_AVAILABLE; + } + + RefPtr<LocalStorageCache> cache = GetCache(originAttrSuffix, originKey); + + // Get or create a cache for the given scope + if (!cache) { + if (aCreateMode == CreateMode::UseIfExistsNeverCreate) { + *aRetval = nullptr; + return NS_OK; + } + + if (aCreateMode == CreateMode::CreateIfShouldPreload) { + const uint32_t privateBrowsingId = + aStoragePrincipal->GetPrivateBrowsingId(); + + // This is a demand to just preload the cache, if the scope has + // no data stored, bypass creation and preload of the cache. + StorageDBChild* db = StorageDBChild::Get(privateBrowsingId); + if (db) { + if (!db->ShouldPreloadOrigin(LocalStorageManager::CreateOrigin( + originAttrSuffix, originKey))) { + return NS_OK; + } + } else { + if (originKey.EqualsLiteral("knalb.:about")) { + return NS_OK; + } + } + } + +#if !defined(MOZ_WIDGET_ANDROID) + ::mozilla::ipc::PBackgroundChild* backgroundActor = + ::mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + return NS_ERROR_FAILURE; + } + + ::mozilla::ipc::PrincipalInfo principalInfo; + rv = mozilla::ipc::PrincipalToPrincipalInfo(aStoragePrincipal, + &principalInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + uint32_t privateBrowsingId; + rv = aStoragePrincipal->GetPrivateBrowsingId(&privateBrowsingId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } +#endif + + // There is always a single instance of a cache per scope + // in a single instance of a DOM storage manager. + cache = PutCache(originAttrSuffix, originKey, quotaKey, aStoragePrincipal); + +#if !defined(MOZ_WIDGET_ANDROID) + LocalStorageCacheChild* actor = new LocalStorageCacheChild(cache); + + MOZ_ALWAYS_TRUE( + backgroundActor->SendPBackgroundLocalStorageCacheConstructor( + actor, principalInfo, originKey, privateBrowsingId)); + + cache->SetActor(actor); +#endif + } + + if (aRetval) { + nsCOMPtr<nsPIDOMWindowInner> inner = nsPIDOMWindowInner::From(aWindow); + + RefPtr<Storage> storage = + new LocalStorage(inner, this, cache, aDocumentURI, aPrincipal, + aStoragePrincipal, aPrivate); + storage.forget(aRetval); + } + + return NS_OK; +} + +NS_IMETHODIMP +LocalStorageManager::PrecacheStorage(nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal, + Storage** aRetval) { + return GetStorageInternal(CreateMode::CreateIfShouldPreload, nullptr, + aPrincipal, aStoragePrincipal, u""_ns, false, + aRetval); +} + +NS_IMETHODIMP +LocalStorageManager::CreateStorage(mozIDOMWindow* aWindow, + nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal, + const nsAString& aDocumentURI, bool aPrivate, + Storage** aRetval) { + return GetStorageInternal(CreateMode::CreateAlways, aWindow, aPrincipal, + aStoragePrincipal, aDocumentURI, aPrivate, aRetval); +} + +NS_IMETHODIMP +LocalStorageManager::GetStorage(mozIDOMWindow* aWindow, + nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal, bool aPrivate, + Storage** aRetval) { + return GetStorageInternal(CreateMode::UseIfExistsNeverCreate, aWindow, + aPrincipal, aStoragePrincipal, u""_ns, aPrivate, + aRetval); +} + +NS_IMETHODIMP +LocalStorageManager::CloneStorage(Storage* aStorage) { + // Cloning is supported only for sessionStorage + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +LocalStorageManager::CheckStorage(nsIPrincipal* aPrincipal, Storage* aStorage, + bool* aRetval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aStorage); + MOZ_ASSERT(aRetval); + + // Only used by sessionStorage. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +LocalStorageManager::GetNextGenLocalStorageEnabled(bool* aResult) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aResult); + + *aResult = NextGenLocalStorageEnabled(); + return NS_OK; +} + +NS_IMETHODIMP +LocalStorageManager::Preload(nsIPrincipal* aPrincipal, JSContext* aContext, + Promise** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(_retval); + + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +LocalStorageManager::IsPreloaded(nsIPrincipal* aPrincipal, JSContext* aContext, + Promise** _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(_retval); + + return NS_ERROR_NOT_IMPLEMENTED; +} + +void LocalStorageManager::ClearCaches(uint32_t aUnloadFlags, + const OriginAttributesPattern& aPattern, + const nsACString& aOriginScope) { + for (auto iter1 = mCaches.Iter(); !iter1.Done(); iter1.Next()) { + OriginAttributes oa; + DebugOnly<bool> rv = oa.PopulateFromSuffix(iter1.Key()); + MOZ_ASSERT(rv); + if (!aPattern.Matches(oa)) { + // This table doesn't match the given origin attributes pattern + continue; + } + + CacheOriginHashtable* table = iter1.UserData(); + + for (auto iter2 = table->Iter(); !iter2.Done(); iter2.Next()) { + LocalStorageCache* cache = iter2.Get()->cache(); + + if (aOriginScope.IsEmpty() || + StringBeginsWith(cache->OriginNoSuffix(), aOriginScope)) { + cache->UnloadItems(aUnloadFlags); + } + } + } +} + +nsresult LocalStorageManager::Observe(const char* aTopic, + const nsAString& aOriginAttributesPattern, + const nsACString& aOriginScope) { + OriginAttributesPattern pattern; + if (!pattern.Init(aOriginAttributesPattern)) { + NS_ERROR("Cannot parse origin attributes pattern"); + return NS_ERROR_FAILURE; + } + + // Clear everything, caches + database + if (!strcmp(aTopic, "cookie-cleared")) { + ClearCaches(LocalStorageCache::kUnloadComplete, pattern, ""_ns); + return NS_OK; + } + + // Clear everything, caches + database + if (!strcmp(aTopic, "extension:purge-localStorage-caches")) { + ClearCaches(LocalStorageCache::kUnloadComplete, pattern, aOriginScope); + return NS_OK; + } + + if (!strcmp(aTopic, "browser:purge-sessionStorage")) { + // This is only meant for SessionStorageManager. + return NS_OK; + } + + // Clear from caches everything that has been stored + // while in session-only mode + if (!strcmp(aTopic, "session-only-cleared")) { + ClearCaches(LocalStorageCache::kUnloadSession, pattern, aOriginScope); + return NS_OK; + } + + // Clear all private-browsing caches + if (!strcmp(aTopic, "private-browsing-data-cleared")) { + ClearCaches(LocalStorageCache::kUnloadComplete, pattern, ""_ns); + return NS_OK; + } + + // Clear localStorage data beloging to an origin pattern + if (!strcmp(aTopic, "origin-attr-pattern-cleared")) { + ClearCaches(LocalStorageCache::kUnloadComplete, pattern, ""_ns); + return NS_OK; + } + + if (!strcmp(aTopic, "profile-change")) { + // For case caches are still referenced - clear them completely + ClearCaches(LocalStorageCache::kUnloadComplete, pattern, ""_ns); + mCaches.Clear(); + return NS_OK; + } + +#ifdef DOM_STORAGE_TESTS + if (!strcmp(aTopic, "test-reload")) { + // This immediately completely reloads all caches from the database. + ClearCaches(LocalStorageCache::kTestReload, pattern, ""_ns); + return NS_OK; + } + + if (!strcmp(aTopic, "test-flushed")) { + if (!XRE_IsParentProcess()) { + nsCOMPtr<nsIObserverService> obs = + mozilla::services::GetObserverService(); + if (obs) { + obs->NotifyObservers(nullptr, "domstorage-test-flushed", nullptr); + } + } + + return NS_OK; + } +#endif + + NS_ERROR("Unexpected topic"); + return NS_ERROR_UNEXPECTED; +} + +// static +LocalStorageManager* LocalStorageManager::Self() { + MOZ_ASSERT(!NextGenLocalStorageEnabled()); + + return sSelf; +} + +LocalStorageManager* LocalStorageManager::Ensure() { + MOZ_ASSERT(!NextGenLocalStorageEnabled()); + + if (sSelf) { + return sSelf; + } + + // Cause sSelf to be populated. + nsCOMPtr<nsIDOMStorageManager> initializer = + do_GetService("@mozilla.org/dom/localStorage-manager;1"); + MOZ_ASSERT(sSelf, "Didn't initialize?"); + + return sSelf; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/LocalStorageManager.h b/dom/storage/LocalStorageManager.h new file mode 100644 index 0000000000..2ebd3681ac --- /dev/null +++ b/dom/storage/LocalStorageManager.h @@ -0,0 +1,140 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_StorageManager_h +#define mozilla_dom_StorageManager_h + +#include "nsIDOMStorageManager.h" +#include "nsILocalStorageManager.h" +#include "StorageObserver.h" + +#include "LocalStorage.h" +#include "LocalStorageCache.h" +#include "mozilla/dom/Storage.h" + +#include "nsTHashtable.h" +#include "nsDataHashtable.h" +#include "nsClassHashtable.h" +#include "nsHashKeys.h" + +namespace mozilla { + +class OriginAttributesPattern; + +namespace dom { + +class LocalStorageManager final : public nsIDOMStorageManager, + public nsILocalStorageManager, + public StorageObserverSink { + NS_DECL_ISUPPORTS + NS_DECL_NSIDOMSTORAGEMANAGER + NS_DECL_NSILOCALSTORAGEMANAGER + + public: + LocalStorageManager(); + + // Reads the preference for DOM storage quota + static uint32_t GetOriginQuota(); + + // Reads the preference for DOM storage site quota + static uint32_t GetSiteQuota(); + + // Gets (but not ensures) cache for the given scope + LocalStorageCache* GetCache(const nsACString& aOriginSuffix, + const nsACString& aOriginNoSuffix); + + // Returns object keeping usage cache for the scope. + already_AddRefed<StorageUsage> GetOriginUsage( + const nsACString& aOriginNoSuffix, uint32_t aPrivateBrowsingId); + + static nsAutoCString CreateOrigin(const nsACString& aOriginSuffix, + const nsACString& aOriginNoSuffix); + + private: + ~LocalStorageManager(); + + // StorageObserverSink, handler to various chrome clearing notification + nsresult Observe(const char* aTopic, + const nsAString& aOriginAttributesPattern, + const nsACString& aOriginScope) override; + + // Since nsTHashtable doesn't like multiple inheritance, we have to aggregate + // LocalStorageCache into the entry. + class LocalStorageCacheHashKey : public nsCStringHashKey { + public: + explicit LocalStorageCacheHashKey(const nsACString* aKey) + : nsCStringHashKey(aKey), mCache(new LocalStorageCache(aKey)) {} + + LocalStorageCacheHashKey(LocalStorageCacheHashKey&& aOther) + : nsCStringHashKey(std::move(aOther)), + mCache(std::move(aOther.mCache)), + mCacheRef(std::move(aOther.mCacheRef)) { + NS_ERROR("Shouldn't be called"); + } + + LocalStorageCache* cache() { return mCache; } + // Keep the cache referenced forever, used for sessionStorage. + void HardRef() { mCacheRef = mCache; } + + private: + // weak ref only since cache references its manager. + LocalStorageCache* mCache; + // hard ref when this is sessionStorage to keep it alive forever. + RefPtr<LocalStorageCache> mCacheRef; + }; + + // Ensures cache for a scope, when it doesn't exist it is created and + // initalized, this also starts preload of persistent data. + already_AddRefed<LocalStorageCache> PutCache( + const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix, + const nsACString& aQuotaKey, nsIPrincipal* aPrincipal); + + enum class CreateMode { + // GetStorage: do not create if it's not already in memory. + UseIfExistsNeverCreate, + // CreateStorage: Create it if it's not already in memory. + CreateAlways, + // PrecacheStorage: Create only if the database says we ShouldPreloadOrigin. + CreateIfShouldPreload + }; + + // Helper for creation of DOM storage objects + nsresult GetStorageInternal(CreateMode aCreate, mozIDOMWindow* aWindow, + nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal, + const nsAString& aDocumentURI, bool aPrivate, + Storage** aRetval); + + // Suffix->origin->cache map + typedef nsTHashtable<LocalStorageCacheHashKey> CacheOriginHashtable; + nsClassHashtable<nsCStringHashKey, CacheOriginHashtable> mCaches; + + void ClearCaches(uint32_t aUnloadFlags, + const OriginAttributesPattern& aPattern, + const nsACString& aKeyPrefix); + + // Global getter of localStorage manager service + static LocalStorageManager* Self(); + + // Like Self, but creates an instance if we're not yet initialized. + static LocalStorageManager* Ensure(); + + private: + // Keeps usage cache objects for eTLD+1 scopes we have touched. + nsDataHashtable<nsCStringHashKey, RefPtr<StorageUsage> > mUsages; + + friend class LocalStorageCache; + friend class StorageDBChild; + // Releases cache since it is no longer referrered by any Storage object. + virtual void DropCache(LocalStorageCache* aCache); + + static LocalStorageManager* sSelf; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_StorageManager_h diff --git a/dom/storage/PBackgroundLocalStorageCache.ipdl b/dom/storage/PBackgroundLocalStorageCache.ipdl new file mode 100644 index 0000000000..c949a25dc3 --- /dev/null +++ b/dom/storage/PBackgroundLocalStorageCache.ipdl @@ -0,0 +1,42 @@ +/* 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 protocol PBackground; + +include PBackgroundSharedTypes; + +namespace mozilla { +namespace dom { + +async protocol PBackgroundLocalStorageCache +{ + manager PBackground; + +parent: + async DeleteMe(); + + async Notify(nsString documentURI, + nsString key, + nsString oldValue, + nsString newValue); + +child: + // The principalInfo and privateBrowsingId could instead be retained by the + // LocalStorageCacheChild/LocalStorageCache instead of being re-transmitted. + // However, these changes are a temporary optimization intended for uplift, + // and this constant factor overhead is very small compared to the upside of + // filtering. + async Observe(PrincipalInfo principalInfo, + PrincipalInfo cachePrincipalInfo, + uint32_t privateBrowsingId, + nsString documentURI, + nsString key, + nsString oldValue, + nsString newValue); + + async __delete__(); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/PBackgroundSessionStorageCache.ipdl b/dom/storage/PBackgroundSessionStorageCache.ipdl new file mode 100644 index 0000000000..eceecc5d00 --- /dev/null +++ b/dom/storage/PBackgroundSessionStorageCache.ipdl @@ -0,0 +1,68 @@ +/* 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 protocol PBackground; +include protocol PBackgroundSessionStorageManager; + +namespace mozilla { +namespace dom { + +struct SSSetItemInfo +{ + nsString key; + nsString value; +}; + +struct SSRemoveItemInfo +{ + nsString key; +}; + +struct SSClearInfo +{ +}; + +/** + * Union of SessionStorage mutation types. + */ +union SSWriteInfo +{ + SSSetItemInfo; + SSRemoveItemInfo; + SSClearInfo; +}; + +sync refcounted protocol PBackgroundSessionStorageCache +{ + manager PBackgroundSessionStorageManager; + + parent: + async DeleteMe(); + + /** + * Copy SessionStorageCache from the parent process to the content process. + * See SessionStorageManager documentation for more details. + * + * This needs to be synchronous because SessionStorage's semantics are + * synchronous. Note that the BackgroundSessionStorageManager in the + * PBackground parent already has the answers to this request immediately + * available without needing to consult any other threads or perform any I/O. + */ + sync Load() + returns (SSSetItemInfo[] aDefaultData, SSSetItemInfo[] aSessionData); + + /** + * Send changes for SessionStorageCache from a content process to the parent + * process so that the data in the parent can be updated to be in sync with + * the content. See SessionStorageManager documentation for more details. + */ + async Checkpoint(SSWriteInfo[] aDefaultWriteInfos, + SSWriteInfo[] aSessionWriteInfos); + + child: + async __delete__(); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/PBackgroundSessionStorageManager.ipdl b/dom/storage/PBackgroundSessionStorageManager.ipdl new file mode 100644 index 0000000000..685593eb6f --- /dev/null +++ b/dom/storage/PBackgroundSessionStorageManager.ipdl @@ -0,0 +1,26 @@ +/* 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 protocol PBackground; +include protocol PBackgroundSessionStorageCache; + +namespace mozilla { +namespace dom { + +sync refcounted protocol PBackgroundSessionStorageManager +{ + manager PBackground; + manages PBackgroundSessionStorageCache; + + parent: + async PBackgroundSessionStorageCache(nsCString aOriginAttrs, nsCString aOriginKey); + + async DeleteMe(); + + child: + async __delete__(); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/PBackgroundStorage.ipdl b/dom/storage/PBackgroundStorage.ipdl new file mode 100644 index 0000000000..9b84f0e795 --- /dev/null +++ b/dom/storage/PBackgroundStorage.ipdl @@ -0,0 +1,69 @@ +/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */ +/* vim: set sw=4 ts=8 et tw=80 ft=cpp : */ +/* 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 protocol PBackground; + +include "mozilla/dom/quota/SerializationHelpers.h"; + +using mozilla::OriginAttributesPattern + from "mozilla/OriginAttributes.h"; + +namespace mozilla { +namespace dom { + +/* This protocol bridges async access to the database thread running on the + * parent process and caches running on the child process. + */ +sync protocol PBackgroundStorage +{ + manager PBackground; + +parent: + async DeleteMe(); + + sync Preload(nsCString originSuffix, + nsCString originNoSuffix, + uint32_t alreadyLoadedCount) + returns (nsString[] keys, nsString[] values, nsresult rv); + + async AsyncPreload(nsCString originSuffix, nsCString originNoSuffix, + bool priority); + async AsyncGetUsage(nsCString scope); + async AsyncAddItem(nsCString originSuffix, nsCString originNoSuffix, + nsString key, nsString value); + async AsyncUpdateItem(nsCString originSuffix, nsCString originNoSuffix, + nsString key, nsString value); + async AsyncRemoveItem(nsCString originSuffix, nsCString originNoSuffix, + nsString key); + async AsyncClear(nsCString originSuffix, nsCString originNoSuffix); + async AsyncFlush(); + + // These are privileged operations for use only by the observer API for + // delayed initialization and clearing origins and will never be used from + // content process children. Ideally these would be guarded by checks or + // exist on a separate, privileged interface, but PBackgroundStorage is + // already insecure. + async Startup(); + async ClearAll(); + async ClearMatchingOrigin(nsCString originNoSuffix); + async ClearMatchingOriginAttributes(OriginAttributesPattern pattern); + +child: + async __delete__(); + + async Observe(nsCString topic, + nsString originAttributesPattern, + nsCString originScope); + async OriginsHavingData(nsCString[] origins); + async LoadItem(nsCString originSuffix, nsCString originNoSuffix, nsString key, + nsString value); + async LoadDone(nsCString originSuffix, nsCString originNoSuffix, nsresult rv); + async LoadUsage(nsCString scope, int64_t usage); + async Error(nsresult rv); +}; + +} +} diff --git a/dom/storage/PSessionStorageObserver.ipdl b/dom/storage/PSessionStorageObserver.ipdl new file mode 100644 index 0000000000..9c05fd1c1a --- /dev/null +++ b/dom/storage/PSessionStorageObserver.ipdl @@ -0,0 +1,41 @@ +/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */ +/* vim: set sw=4 ts=8 et tw=80 ft=cpp : */ +/* 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 protocol PContent; + +namespace mozilla { +namespace dom { + +/** + * Protocol used to relay chrome observer notifications related to clearing data + * to SessionStorageManager instances in content processes. A single instance is + * created by each content process when LocalStorage NextGen is enabled. When + * LSNG is disabled, the notifications are instead propagated via + * PBackgroundStorageChild. This does mean there are potentially slight ordering + * differences in when the notification will be received and processed. It's + * okay for this protocol to be managed by PContent rather than PBackground + * because these notifications are both rare and to-child-only. (Legacy + * LocalStorage was moved to PBackground from PContent because of parent-process + * main-thread contention for the processing of "parent:" messages in a very + * performance-sensitive context!) + */ +async protocol PSessionStorageObserver +{ + manager PContent; + +parent: + async DeleteMe(); + +child: + async __delete__(); + + async Observe(nsCString topic, + nsString originAttributesPattern, + nsCString originScope); +}; + +} +} diff --git a/dom/storage/PartitionedLocalStorage.cpp b/dom/storage/PartitionedLocalStorage.cpp new file mode 100644 index 0000000000..c0375e69a3 --- /dev/null +++ b/dom/storage/PartitionedLocalStorage.cpp @@ -0,0 +1,137 @@ +/* -*- 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 "PartitionedLocalStorage.h" +#include "SessionStorageCache.h" +#include "nsContentUtils.h" + +#include "mozilla/dom/StorageBinding.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(PartitionedLocalStorage, Storage); + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PartitionedLocalStorage) +NS_INTERFACE_MAP_END_INHERITING(Storage) + +NS_IMPL_ADDREF_INHERITED(PartitionedLocalStorage, Storage) +NS_IMPL_RELEASE_INHERITED(PartitionedLocalStorage, Storage) + +PartitionedLocalStorage::PartitionedLocalStorage( + nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal, SessionStorageCache* aCache) + : Storage(aWindow, aPrincipal, aStoragePrincipal), mCache(aCache) {} + +PartitionedLocalStorage::~PartitionedLocalStorage() = default; + +int64_t PartitionedLocalStorage::GetOriginQuotaUsage() const { + return mCache->GetOriginQuotaUsage(SessionStorageCache::eSessionSetType); +} + +uint32_t PartitionedLocalStorage::GetLength(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + if (!CanUseStorage(aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return 0; + } + + return mCache->Length(SessionStorageCache::eSessionSetType); +} + +void PartitionedLocalStorage::Key(uint32_t aIndex, nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + if (!CanUseStorage(aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + mCache->Key(SessionStorageCache::eSessionSetType, aIndex, aResult); +} + +void PartitionedLocalStorage::GetItem(const nsAString& aKey, nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + if (!CanUseStorage(aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + mCache->GetItem(SessionStorageCache::eSessionSetType, aKey, aResult); +} + +void PartitionedLocalStorage::GetSupportedNames(nsTArray<nsString>& aKeys) { + if (!CanUseStorage(*nsContentUtils::SubjectPrincipal())) { + // return just an empty array + aKeys.Clear(); + return; + } + + mCache->GetKeys(SessionStorageCache::eSessionSetType, aKeys); +} + +void PartitionedLocalStorage::SetItem(const nsAString& aKey, + const nsAString& aValue, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + if (!CanUseStorage(aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsString oldValue; + nsresult rv = mCache->SetItem(SessionStorageCache::eSessionSetType, aKey, + aValue, oldValue); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return; + } + + if (rv == NS_SUCCESS_DOM_NO_OPERATION) { + return; + } +} + +void PartitionedLocalStorage::RemoveItem(const nsAString& aKey, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + if (!CanUseStorage(aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsString oldValue; + nsresult rv = + mCache->RemoveItem(SessionStorageCache::eSessionSetType, aKey, oldValue); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + if (rv == NS_SUCCESS_DOM_NO_OPERATION) { + return; + } +} + +void PartitionedLocalStorage::Clear(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + uint32_t length = GetLength(aSubjectPrincipal, aRv); + if (!length) { + return; + } + + mCache->Clear(SessionStorageCache::eSessionSetType); +} + +bool PartitionedLocalStorage::IsForkOf(const Storage* aOther) const { + MOZ_ASSERT(aOther); + if (aOther->Type() != eLocalStorage) { + return false; + } + + return mCache == static_cast<const PartitionedLocalStorage*>(aOther)->mCache; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/PartitionedLocalStorage.h b/dom/storage/PartitionedLocalStorage.h new file mode 100644 index 0000000000..4e9937f9fa --- /dev/null +++ b/dom/storage/PartitionedLocalStorage.h @@ -0,0 +1,66 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_PartitionedLocalStorage_h +#define mozilla_dom_PartitionedLocalStorage_h + +#include "Storage.h" + +class nsIPrincipal; + +namespace mozilla { +namespace dom { + +class SessionStorageCache; + +// PartitionedLocalStorage is a in-memory-only storage exposed to trackers. It +// doesn't share data with other contexts. + +class PartitionedLocalStorage final : public Storage { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PartitionedLocalStorage, Storage) + + PartitionedLocalStorage(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal, + SessionStorageCache* aCache); + + StorageType Type() const override { return ePartitionedLocalStorage; } + + int64_t GetOriginQuotaUsage() const override; + + bool IsForkOf(const Storage* aStorage) const override; + + // WebIDL + uint32_t GetLength(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) override; + + void Key(uint32_t aIndex, nsAString& aResult, nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) override; + + void GetItem(const nsAString& aKey, nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) override; + + void GetSupportedNames(nsTArray<nsString>& aKeys) override; + + void SetItem(const nsAString& aKey, const nsAString& aValue, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) override; + + void RemoveItem(const nsAString& aKey, nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) override; + + void Clear(nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) override; + + private: + ~PartitionedLocalStorage(); + + RefPtr<SessionStorageCache> mCache; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_PartitionedLocalStorage_h diff --git a/dom/storage/SessionStorage.cpp b/dom/storage/SessionStorage.cpp new file mode 100644 index 0000000000..b9328f620b --- /dev/null +++ b/dom/storage/SessionStorage.cpp @@ -0,0 +1,265 @@ +/* -*- 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 "SessionStorage.h" + +#include "SessionStorageCache.h" +#include "SessionStorageManager.h" + +#include "mozilla/dom/StorageBinding.h" +#include "mozilla/Preferences.h" +#include "nsContentUtils.h" +#include "nsIPrincipal.h" +#include "nsPIDOMWindow.h" +#include "nsThreadUtils.h" + +#define DATASET \ + (!IsPrivateBrowsing() && IsSessionScopedOrLess() \ + ? SessionStorageCache::eSessionSetType \ + : SessionStorageCache::eDefaultSetType) + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(SessionStorage, Storage, mManager); + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SessionStorage) +NS_INTERFACE_MAP_END_INHERITING(Storage) + +NS_IMPL_ADDREF_INHERITED(SessionStorage, Storage) +NS_IMPL_RELEASE_INHERITED(SessionStorage, Storage) + +SessionStorage::SessionStorage(nsPIDOMWindowInner* aWindow, + nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal, + SessionStorageCache* aCache, + SessionStorageManager* aManager, + const nsAString& aDocumentURI, bool aIsPrivate) + : Storage(aWindow, aPrincipal, aStoragePrincipal), + mCache(aCache), + mManager(aManager), + mDocumentURI(aDocumentURI), + mIsPrivate(aIsPrivate), + mHasPendingStableStateCallback(false) { + MOZ_ASSERT(aCache); +} + +SessionStorage::~SessionStorage() = default; + +int64_t SessionStorage::GetOriginQuotaUsage() const { + nsresult rv = EnsureCacheLoadedOrCloned(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return 0; + } + + return mCache->GetOriginQuotaUsage(DATASET); +} + +uint32_t SessionStorage::GetLength(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + if (!CanUseStorage(aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return 0; + } + + nsresult rv = EnsureCacheLoadedOrCloned(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return 0; + } + + return mCache->Length(DATASET); +} + +void SessionStorage::Key(uint32_t aIndex, nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { + if (!CanUseStorage(aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsresult rv = EnsureCacheLoadedOrCloned(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return; + } + + mCache->Key(DATASET, aIndex, aResult); +} + +void SessionStorage::GetItem(const nsAString& aKey, nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + if (!CanUseStorage(aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsresult rv = EnsureCacheLoadedOrCloned(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return; + } + + mCache->GetItem(DATASET, aKey, aResult); +} + +void SessionStorage::GetSupportedNames(nsTArray<nsString>& aKeys) { + if (!CanUseStorage(*nsContentUtils::SubjectPrincipal())) { + // return just an empty array + aKeys.Clear(); + return; + } + + nsresult rv = EnsureCacheLoadedOrCloned(); + if (NS_WARN_IF(NS_FAILED(rv))) { + // return just an empty array + aKeys.Clear(); + return; + } + + mCache->GetKeys(DATASET, aKeys); +} + +void SessionStorage::SetItem(const nsAString& aKey, const nsAString& aValue, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + if (!CanUseStorage(aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsresult rv = EnsureCacheLoadedOrCloned(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return; + } + + nsString oldValue; + rv = mCache->SetItem(DATASET, aKey, aValue, oldValue); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return; + } + + if (rv == NS_SUCCESS_DOM_NO_OPERATION) { + return; + } + + BroadcastChangeNotification(aKey, oldValue, aValue); +} + +void SessionStorage::RemoveItem(const nsAString& aKey, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + if (!CanUseStorage(aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsresult rv = EnsureCacheLoadedOrCloned(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return; + } + + nsString oldValue; + rv = mCache->RemoveItem(DATASET, aKey, oldValue); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + if (rv == NS_SUCCESS_DOM_NO_OPERATION) { + return; + } + + BroadcastChangeNotification(aKey, oldValue, VoidString()); +} + +void SessionStorage::Clear(nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { + uint32_t length = GetLength(aSubjectPrincipal, aRv); + if (!length) { + return; + } + + nsresult rv = EnsureCacheLoadedOrCloned(); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return; + } + + mCache->Clear(DATASET); + BroadcastChangeNotification(VoidString(), VoidString(), VoidString()); +} + +void SessionStorage::BroadcastChangeNotification(const nsAString& aKey, + const nsAString& aOldValue, + const nsAString& aNewValue) { + NotifyChange(this, StoragePrincipal(), aKey, aOldValue, aNewValue, + u"sessionStorage", mDocumentURI, mIsPrivate, false); + + // Sync changes on SessionStorageCache at the next statble state. + if (mManager->CanLoadData()) { + MaybeScheduleStableStateCallback(); + } +} + +bool SessionStorage::IsForkOf(const Storage* aOther) const { + MOZ_ASSERT(aOther); + if (aOther->Type() != eSessionStorage) { + return false; + } + + return mCache == static_cast<const SessionStorage*>(aOther)->mCache; +} + +void SessionStorage::MaybeScheduleStableStateCallback() { + AssertIsOnOwningThread(); + + if (!mHasPendingStableStateCallback) { + nsContentUtils::RunInStableState( + NewRunnableMethod("SessionStorage::StableStateCallback", this, + &SessionStorage::StableStateCallback)); + + mHasPendingStableStateCallback = true; + } +} + +void SessionStorage::StableStateCallback() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mHasPendingStableStateCallback); + MOZ_ASSERT(mManager); + MOZ_ASSERT(mCache); + + mHasPendingStableStateCallback = false; + + if (mManager->CanLoadData()) { + mManager->CheckpointData(*Principal(), *mCache); + } +} + +nsresult SessionStorage::EnsureCacheLoadedOrCloned() const { + AssertIsOnOwningThread(); + MOZ_ASSERT(mManager); + + if (!mManager->CanLoadData()) { + return NS_OK; + } + + // Ensure manager actor. + nsresult rv = mManager->EnsureManager(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Ensure cache is loaded or cloned. + if (mCache->WasLoadedOrCloned()) { + return NS_OK; + } + + return mManager->LoadData(*Principal(), *mCache); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/SessionStorage.h b/dom/storage/SessionStorage.h new file mode 100644 index 0000000000..c6e6aabd40 --- /dev/null +++ b/dom/storage/SessionStorage.h @@ -0,0 +1,88 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_SessionStorage_h +#define mozilla_dom_SessionStorage_h + +#include "Storage.h" + +class nsIPrincipal; + +namespace mozilla { +namespace dom { + +class SessionStorageCache; +class SessionStorageManager; + +class SessionStorage final : public Storage { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(SessionStorage, Storage) + + SessionStorage(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal, SessionStorageCache* aCache, + SessionStorageManager* aManager, const nsAString& aDocumentURI, + bool aIsPrivate); + + void AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(SessionStorage); + } + + StorageType Type() const override { return eSessionStorage; } + + SessionStorageManager* Manager() const { return mManager; } + + SessionStorageCache* Cache() const { return mCache; } + + int64_t GetOriginQuotaUsage() const override; + + bool IsForkOf(const Storage* aStorage) const override; + + // WebIDL + uint32_t GetLength(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) override; + + void Key(uint32_t aIndex, nsAString& aResult, nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) override; + + void GetItem(const nsAString& aKey, nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) override; + + void GetSupportedNames(nsTArray<nsString>& aKeys) override; + + void SetItem(const nsAString& aKey, const nsAString& aValue, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) override; + + void RemoveItem(const nsAString& aKey, nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) override; + + void Clear(nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) override; + + private: + ~SessionStorage(); + + void BroadcastChangeNotification(const nsAString& aKey, + const nsAString& aOldValue, + const nsAString& aNewValue); + + void MaybeScheduleStableStateCallback(); + + void StableStateCallback(); + + nsresult EnsureCacheLoadedOrCloned() const; + + RefPtr<SessionStorageCache> mCache; + RefPtr<SessionStorageManager> mManager; + + nsString mDocumentURI; + bool mIsPrivate; + bool mHasPendingStableStateCallback; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_SessionStorage_h diff --git a/dom/storage/SessionStorageCache.cpp b/dom/storage/SessionStorageCache.cpp new file mode 100644 index 0000000000..0e6d5aac16 --- /dev/null +++ b/dom/storage/SessionStorageCache.cpp @@ -0,0 +1,333 @@ +/* -*- 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 "SessionStorageCache.h" + +#include "LocalStorageManager.h" +#include "StorageIPC.h" +#include "mozilla/dom/LSWriteOptimizer.h" +#include "mozilla/dom/PBackgroundSessionStorageCache.h" +#include "nsDOMString.h" + +namespace mozilla { +namespace dom { + +void SSWriteOptimizer::Enumerate(nsTArray<SSWriteInfo>& aWriteInfos) { + AssertIsOnOwningThread(); + + // The mWriteInfos hash table contains all write infos, but it keeps them in + // an arbitrary order, which means write infos need to be sorted before being + // processed. + + nsTArray<NotNull<WriteInfo*>> writeInfos; + GetSortedWriteInfos(writeInfos); + + for (const auto& writeInfo : writeInfos) { + switch (writeInfo->GetType()) { + case WriteInfo::InsertItem: { + const auto& insertItemInfo = + static_cast<const InsertItemInfo&>(*writeInfo); + + aWriteInfos.AppendElement( + SSSetItemInfo{nsString{insertItemInfo.GetKey()}, + nsString{insertItemInfo.GetValue()}}); + + break; + } + + case WriteInfo::UpdateItem: { + const auto& updateItemInfo = + static_cast<const UpdateItemInfo&>(*writeInfo); + + if (updateItemInfo.UpdateWithMove()) { + // See the comment in LSWriteOptimizer::InsertItem for more details + // about the UpdateWithMove flag. + + aWriteInfos.AppendElement( + SSRemoveItemInfo{nsString{updateItemInfo.GetKey()}}); + } + + aWriteInfos.AppendElement( + SSSetItemInfo{nsString{updateItemInfo.GetKey()}, + nsString{updateItemInfo.GetValue()}}); + + break; + } + + case WriteInfo::DeleteItem: { + const auto& deleteItemInfo = + static_cast<const DeleteItemInfo&>(*writeInfo); + + aWriteInfos.AppendElement( + SSRemoveItemInfo{nsString{deleteItemInfo.GetKey()}}); + + break; + } + + case WriteInfo::Truncate: { + aWriteInfos.AppendElement(SSClearInfo{}); + + break; + } + + default: + MOZ_CRASH("Bad type!"); + } + } +} + +SessionStorageCache::SessionStorageCache() + : mActor(nullptr), mLoadedOrCloned(false) {} + +SessionStorageCache::~SessionStorageCache() { + if (mActor) { + mActor->SendDeleteMeInternal(); + MOZ_ASSERT(!mActor, "SendDeleteMeInternal should have cleared!"); + } +} + +SessionStorageCache::DataSet* SessionStorageCache::Set( + DataSetType aDataSetType) { + if (aDataSetType == eDefaultSetType) { + return &mDefaultSet; + } + + MOZ_ASSERT(aDataSetType == eSessionSetType); + + return &mSessionSet; +} + +int64_t SessionStorageCache::GetOriginQuotaUsage(DataSetType aDataSetType) { + return Set(aDataSetType)->mOriginQuotaUsage; +} + +uint32_t SessionStorageCache::Length(DataSetType aDataSetType) { + return Set(aDataSetType)->mKeys.Count(); +} + +void SessionStorageCache::Key(DataSetType aDataSetType, uint32_t aIndex, + nsAString& aResult) { + aResult.SetIsVoid(true); + for (auto iter = Set(aDataSetType)->mKeys.Iter(); !iter.Done(); iter.Next()) { + if (aIndex == 0) { + aResult = iter.Key(); + return; + } + aIndex--; + } +} + +void SessionStorageCache::GetItem(DataSetType aDataSetType, + const nsAString& aKey, nsAString& aResult) { + // not using AutoString since we don't want to copy buffer to result + nsString value; + if (!Set(aDataSetType)->mKeys.Get(aKey, &value)) { + SetDOMStringToNull(value); + } + aResult = value; +} + +void SessionStorageCache::GetKeys(DataSetType aDataSetType, + nsTArray<nsString>& aKeys) { + for (auto iter = Set(aDataSetType)->mKeys.Iter(); !iter.Done(); iter.Next()) { + aKeys.AppendElement(iter.Key()); + } +} + +nsresult SessionStorageCache::SetItem(DataSetType aDataSetType, + const nsAString& aKey, + const nsAString& aValue, + nsString& aOldValue, + bool aRecordWriteInfo) { + int64_t delta = 0; + DataSet* dataSet = Set(aDataSetType); + MOZ_ASSERT(dataSet); + + if (!dataSet->mKeys.Get(aKey, &aOldValue)) { + SetDOMStringToNull(aOldValue); + + // 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>(aOldValue.Length()); + + if (aValue == aOldValue && + DOMStringIsNull(aValue) == DOMStringIsNull(aOldValue)) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + + if (!dataSet->ProcessUsageDelta(delta)) { + return NS_ERROR_DOM_QUOTA_EXCEEDED_ERR; + } + + if (aRecordWriteInfo && XRE_IsContentProcess()) { + if (DOMStringIsNull(aOldValue)) { + dataSet->mWriteOptimizer.InsertItem(aKey, aValue); + } else { + dataSet->mWriteOptimizer.UpdateItem(aKey, aValue); + } + } + + dataSet->mKeys.Put(aKey, nsString(aValue)); + return NS_OK; +} + +nsresult SessionStorageCache::RemoveItem(DataSetType aDataSetType, + const nsAString& aKey, + nsString& aOldValue, + bool aRecordWriteInfo) { + DataSet* dataSet = Set(aDataSetType); + MOZ_ASSERT(dataSet); + + if (!dataSet->mKeys.Get(aKey, &aOldValue)) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + + // Recalculate the cached data size + dataSet->ProcessUsageDelta(-(static_cast<int64_t>(aOldValue.Length()) + + static_cast<int64_t>(aKey.Length()))); + + if (aRecordWriteInfo && XRE_IsContentProcess()) { + dataSet->mWriteOptimizer.DeleteItem(aKey); + } + + dataSet->mKeys.Remove(aKey); + return NS_OK; +} + +void SessionStorageCache::Clear(DataSetType aDataSetType, + bool aByUserInteraction, + bool aRecordWriteInfo) { + DataSet* dataSet = Set(aDataSetType); + MOZ_ASSERT(dataSet); + + dataSet->ProcessUsageDelta(-dataSet->mOriginQuotaUsage); + + if (aRecordWriteInfo && XRE_IsContentProcess()) { + dataSet->mWriteOptimizer.Truncate(); + } + + dataSet->mKeys.Clear(); +} + +void SessionStorageCache::ResetWriteInfos(DataSetType aDataSetType) { + Set(aDataSetType)->mWriteOptimizer.Reset(); +} + +already_AddRefed<SessionStorageCache> SessionStorageCache::Clone() const { + RefPtr<SessionStorageCache> cache = new SessionStorageCache(); + + cache->mDefaultSet.mOriginQuotaUsage = mDefaultSet.mOriginQuotaUsage; + for (auto iter = mDefaultSet.mKeys.ConstIter(); !iter.Done(); iter.Next()) { + cache->mDefaultSet.mKeys.Put(iter.Key(), iter.Data()); + cache->mDefaultSet.mWriteOptimizer.InsertItem(iter.Key(), iter.Data()); + } + + cache->mSessionSet.mOriginQuotaUsage = mSessionSet.mOriginQuotaUsage; + for (auto iter = mSessionSet.mKeys.ConstIter(); !iter.Done(); iter.Next()) { + cache->mSessionSet.mKeys.Put(iter.Key(), iter.Data()); + cache->mSessionSet.mWriteOptimizer.InsertItem(iter.Key(), iter.Data()); + } + + return cache.forget(); +} + +nsTArray<SSSetItemInfo> SessionStorageCache::SerializeData( + DataSetType aDataSetType) { + nsTArray<SSSetItemInfo> data; + for (auto iter = Set(aDataSetType)->mKeys.Iter(); !iter.Done(); iter.Next()) { + SSSetItemInfo keyValuePair; + keyValuePair.key() = iter.Key(); + keyValuePair.value() = iter.Data(); + data.EmplaceBack(std::move(keyValuePair)); + } + return data; +} + +nsTArray<SSWriteInfo> SessionStorageCache::SerializeWriteInfos( + DataSetType aDataSetType) { + nsTArray<SSWriteInfo> writeInfos; + Set(aDataSetType)->mWriteOptimizer.Enumerate(writeInfos); + return writeInfos; +} + +void SessionStorageCache::DeserializeData( + DataSetType aDataSetType, const nsTArray<SSSetItemInfo>& aData) { + Clear(aDataSetType, false, /* aRecordWriteInfo */ false); + for (const auto& keyValuePair : aData) { + nsString oldValue; + SetItem(aDataSetType, keyValuePair.key(), keyValuePair.value(), oldValue, + false); + } +} + +void SessionStorageCache::DeserializeWriteInfos( + DataSetType aDataSetType, const nsTArray<SSWriteInfo>& aInfos) { + for (const auto& writeInfo : aInfos) { + switch (writeInfo.type()) { + case SSWriteInfo::TSSSetItemInfo: { + const SSSetItemInfo& info = writeInfo.get_SSSetItemInfo(); + + nsString oldValue; + SetItem(aDataSetType, info.key(), info.value(), oldValue, + /* aRecordWriteInfo */ false); + + break; + } + + case SSWriteInfo::TSSRemoveItemInfo: { + const SSRemoveItemInfo& info = writeInfo.get_SSRemoveItemInfo(); + + nsString oldValue; + RemoveItem(aDataSetType, info.key(), oldValue, + /* aRecordWriteInfo */ false); + + break; + } + + case SSWriteInfo::TSSClearInfo: { + Clear(aDataSetType, false, /* aRecordWriteInfo */ false); + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + } +} + +void SessionStorageCache::SetActor(SessionStorageCacheChild* aActor) { + AssertIsOnMainThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(!mActor); + + mActor = aActor; +} + +void SessionStorageCache::ClearActor() { + AssertIsOnMainThread(); + MOZ_ASSERT(mActor); + + mActor = nullptr; +} + +bool SessionStorageCache::DataSet::ProcessUsageDelta(int64_t aDelta) { + // Check limit per this origin + uint64_t newOriginUsage = mOriginQuotaUsage + aDelta; + if (aDelta > 0 && newOriginUsage > LocalStorageManager::GetOriginQuota()) { + return false; + } + + // Update size in our data set + mOriginQuotaUsage = newOriginUsage; + return true; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/SessionStorageCache.h b/dom/storage/SessionStorageCache.h new file mode 100644 index 0000000000..54a8fc22c1 --- /dev/null +++ b/dom/storage/SessionStorageCache.h @@ -0,0 +1,114 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_SessionStorageCache_h +#define mozilla_dom_SessionStorageCache_h + +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/LSWriteOptimizerImpl.h" +#include "nsDataHashtable.h" + +namespace mozilla { +namespace dom { + +class SSSetItemInfo; +class SSWriteInfo; +class SessionStorageCacheChild; + +/** + * Coalescing manipulation queue used by `SessionStorageCache`. Used by + * `SessionStorageCache` to buffer and coalesce manipulations before they + * are sent to the parent process. + */ +class SSWriteOptimizer final : public LSWriteOptimizer<nsAString, nsString> { + public: + void Enumerate(nsTArray<SSWriteInfo>& aWriteInfos); +}; + +class SessionStorageCache final { + public: + NS_INLINE_DECL_REFCOUNTING(SessionStorageCache) + + SessionStorageCache(); + + enum DataSetType { + eDefaultSetType, + eSessionSetType, + }; + + int64_t GetOriginQuotaUsage(DataSetType aDataSetType); + + uint32_t Length(DataSetType aDataSetType); + + void Key(DataSetType aDataSetType, uint32_t aIndex, nsAString& aResult); + + void GetItem(DataSetType aDataSetType, const nsAString& aKey, + nsAString& aResult); + + void GetKeys(DataSetType aDataSetType, nsTArray<nsString>& aKeys); + + nsresult SetItem(DataSetType aDataSetType, const nsAString& aKey, + const nsAString& aValue, nsString& aOldValue, + bool aRecordWriteInfo = true); + + nsresult RemoveItem(DataSetType aDataSetType, const nsAString& aKey, + nsString& aOldValue, bool aRecordWriteInfo = true); + + void Clear(DataSetType aDataSetType, bool aByUserInteraction = true, + bool aRecordWriteInfo = true); + + void ResetWriteInfos(DataSetType aDataSetType); + + already_AddRefed<SessionStorageCache> Clone() const; + + nsTArray<SSSetItemInfo> SerializeData(DataSetType aDataSetType); + + nsTArray<SSWriteInfo> SerializeWriteInfos(DataSetType aDataSetType); + + void DeserializeData(DataSetType aDataSetType, + const nsTArray<SSSetItemInfo>& aData); + + void DeserializeWriteInfos(DataSetType aDataSetType, + const nsTArray<SSWriteInfo>& aInfos); + + void SetActor(SessionStorageCacheChild* aActor); + + SessionStorageCacheChild* Actor() const { return mActor; } + + void ClearActor(); + + void SetLoadedOrCloned() { mLoadedOrCloned = true; } + + bool WasLoadedOrCloned() const { return mLoadedOrCloned; } + + private: + ~SessionStorageCache(); + + struct DataSet { + DataSet() : mOriginQuotaUsage(0) {} + + bool ProcessUsageDelta(int64_t aDelta); + + nsDataHashtable<nsStringHashKey, nsString> mKeys; + + SSWriteOptimizer mWriteOptimizer; + + int64_t mOriginQuotaUsage; + }; + + DataSet* Set(DataSetType aDataSetType); + + DataSet mDefaultSet; + DataSet mSessionSet; + + SessionStorageCacheChild* mActor; + bool mLoadedOrCloned; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_SessionStorageCache_h diff --git a/dom/storage/SessionStorageManager.cpp b/dom/storage/SessionStorageManager.cpp new file mode 100644 index 0000000000..f19fd71811 --- /dev/null +++ b/dom/storage/SessionStorageManager.cpp @@ -0,0 +1,684 @@ +/* -*- 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 "SessionStorageManager.h" + +#include "StorageIPC.h" +#include "SessionStorage.h" +#include "SessionStorageCache.h" +#include "SessionStorageObserver.h" +#include "StorageUtils.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/LocalStorageCommon.h" +#include "mozilla/dom/PBackgroundSessionStorageCache.h" +#include "mozilla/dom/PBackgroundSessionStorageManager.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "nsDataHashtable.h" +#include "nsThreadUtils.h" + +namespace mozilla { +namespace dom { + +using namespace StorageUtils; + +// Parent process, background thread hashmap that stores top context id and +// manager pair. +static StaticAutoPtr< + nsRefPtrHashtable<nsUint64HashKey, BackgroundSessionStorageManager>> + sManagers; + +bool RecvShutdownBackgroundSessionStorageManagers() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + sManagers = nullptr; + return true; +} + +void RecvPropagateBackgroundSessionStorageManager( + uint64_t aCurrentTopContextId, uint64_t aTargetTopContextId) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + if (sManagers) { + if (RefPtr<BackgroundSessionStorageManager> mgr = + sManagers->Get(aCurrentTopContextId)) { + // Assuming the target top browsing context should haven't been + // registered yet. + MOZ_DIAGNOSTIC_ASSERT(!sManagers->GetWeak(aTargetTopContextId)); + sManagers->Put(aTargetTopContextId, std::move(mgr)); + } + } +} + +bool RecvRemoveBackgroundSessionStorageManager(uint64_t aTopContextId) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + if (sManagers) { + sManagers->Remove(aTopContextId); + } + + return true; +} + +SessionStorageManagerBase::OriginRecord* +SessionStorageManagerBase::GetOriginRecord( + const nsACString& aOriginAttrs, const nsACString& aOriginKey, + const bool aMakeIfNeeded, SessionStorageCache* const aCloneFrom) { + OriginKeyHashTable* table; + if (!mOATable.Get(aOriginAttrs, &table)) { + if (aMakeIfNeeded) { + table = new OriginKeyHashTable(); + mOATable.Put(aOriginAttrs, table); + } else { + return nullptr; + } + } + + OriginRecord* originRecord; + if (!table->Get(aOriginKey, &originRecord)) { + if (aMakeIfNeeded) { + originRecord = new OriginRecord(); + if (aCloneFrom) { + originRecord->mCache = aCloneFrom->Clone(); + } else { + originRecord->mCache = new SessionStorageCache(); + } + table->Put(aOriginKey, originRecord); + } else { + return nullptr; + } + } + + return originRecord; +} + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SessionStorageManager) + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY(nsIDOMStorageManager) + NS_INTERFACE_MAP_ENTRY(nsIDOMSessionStorageManager) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION(SessionStorageManager, mBrowsingContext) +NS_IMPL_CYCLE_COLLECTING_ADDREF(SessionStorageManager) +NS_IMPL_CYCLE_COLLECTING_RELEASE(SessionStorageManager) + +SessionStorageManager::SessionStorageManager( + RefPtr<BrowsingContext> aBrowsingContext) + : mBrowsingContext(std::move(aBrowsingContext)), mActor(nullptr) { + AssertIsOnMainThread(); + + StorageObserver* observer = StorageObserver::Self(); + NS_ASSERTION( + observer, + "No StorageObserver, cannot observe private data delete notifications!"); + + if (observer) { + observer->AddSink(this); + } + + if (!XRE_IsParentProcess() && NextGenLocalStorageEnabled()) { + // When LSNG is enabled the thread IPC bridge doesn't exist, so we have to + // create own protocol to distribute chrome observer notifications to + // content processes. + mObserver = SessionStorageObserver::Get(); + + if (!mObserver) { + ContentChild* contentActor = ContentChild::GetSingleton(); + MOZ_ASSERT(contentActor); + + RefPtr<SessionStorageObserver> observer = new SessionStorageObserver(); + + SessionStorageObserverChild* actor = + new SessionStorageObserverChild(observer); + + MOZ_ALWAYS_TRUE( + contentActor->SendPSessionStorageObserverConstructor(actor)); + + observer->SetActor(actor); + + mObserver = std::move(observer); + } + } +} + +SessionStorageManager::~SessionStorageManager() { + StorageObserver* observer = StorageObserver::Self(); + if (observer) { + observer->RemoveSink(this); + } + + if (mActor) { + mActor->SendDeleteMeInternal(); + MOZ_ASSERT(!mActor, "SendDeleteMeInternal should have cleared!"); + } +} + +bool SessionStorageManager::CanLoadData() { + AssertIsOnMainThread(); + + return mBrowsingContext && !mBrowsingContext->IsDiscarded(); +} + +void SessionStorageManager::SetActor(SessionStorageManagerChild* aActor) { + AssertIsOnMainThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(!mActor); + + mActor = aActor; +} + +bool SessionStorageManager::ActorExists() const { + AssertIsOnMainThread(); + + return mActor; +} + +void SessionStorageManager::ClearActor() { + AssertIsOnMainThread(); + MOZ_ASSERT(mActor); + + mActor = nullptr; +} + +nsresult SessionStorageManager::EnsureManager() { + AssertIsOnMainThread(); + MOZ_ASSERT(CanLoadData()); + + if (ActorExists()) { + return NS_OK; + } + + ::mozilla::ipc::PBackgroundChild* backgroundActor = + ::mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + return NS_ERROR_FAILURE; + } + + RefPtr<SessionStorageManagerChild> actor = + new SessionStorageManagerChild(this); + MOZ_ASSERT(actor); + + MOZ_ALWAYS_TRUE( + backgroundActor->SendPBackgroundSessionStorageManagerConstructor( + actor, mBrowsingContext->Top()->Id())); + + SetActor(actor); + + return NS_OK; +} + +SessionStorageCacheChild* SessionStorageManager::EnsureCache( + const nsCString& aOriginAttrs, const nsCString& aOriginKey, + SessionStorageCache& aCache) { + AssertIsOnMainThread(); + MOZ_ASSERT(CanLoadData()); + MOZ_ASSERT(ActorExists()); + + if (aCache.Actor()) { + return aCache.Actor(); + } + + RefPtr<SessionStorageCacheChild> actor = + new SessionStorageCacheChild(&aCache); + MOZ_ALWAYS_TRUE(mActor->SendPBackgroundSessionStorageCacheConstructor( + actor, aOriginAttrs, aOriginKey)); + + aCache.SetActor(actor); + + return actor; +} + +nsresult SessionStorageManager::LoadData(nsIPrincipal& aPrincipal, + SessionStorageCache& aCache) { + AssertIsOnMainThread(); + MOZ_ASSERT(mActor); + + nsAutoCString originKey; + nsresult rv = aPrincipal.GetStorageOriginKey(originKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoCString originAttributes; + aPrincipal.OriginAttributesRef().CreateSuffix(originAttributes); + + auto* const originRecord = + GetOriginRecord(originAttributes, originKey, true, nullptr); + MOZ_ASSERT(originRecord); + + if (originRecord->mLoaded) { + return NS_OK; + } + + RefPtr<SessionStorageCacheChild> cacheActor = + EnsureCache(originAttributes, originKey, aCache); + + nsTArray<SSSetItemInfo> defaultData; + nsTArray<SSSetItemInfo> sessionData; + if (!cacheActor->SendLoad(&defaultData, &sessionData)) { + return NS_ERROR_FAILURE; + } + + originRecord->mCache->DeserializeData(SessionStorageCache::eDefaultSetType, + defaultData); + originRecord->mCache->DeserializeData(SessionStorageCache::eSessionSetType, + sessionData); + + originRecord->mLoaded.Flip(); + aCache.SetLoadedOrCloned(); + + return NS_OK; +} + +void SessionStorageManager::CheckpointData(nsIPrincipal& aPrincipal, + SessionStorageCache& aCache) { + AssertIsOnMainThread(); + MOZ_ASSERT(mActor); + + nsAutoCString originKey; + nsresult rv = aPrincipal.GetStorageOriginKey(originKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsAutoCString originAttributes; + aPrincipal.OriginAttributesRef().CreateSuffix(originAttributes); + + return CheckpointDataInternal(originAttributes, originKey, aCache); +} + +void SessionStorageManager::CheckpointDataInternal( + const nsCString& aOriginAttrs, const nsCString& aOriginKey, + SessionStorageCache& aCache) { + AssertIsOnMainThread(); + MOZ_ASSERT(mActor); + + nsTArray<SSWriteInfo> defaultWriteInfos = + aCache.SerializeWriteInfos(SessionStorageCache::eDefaultSetType); + nsTArray<SSWriteInfo> sessionWriteInfos = + aCache.SerializeWriteInfos(SessionStorageCache::eSessionSetType); + + if (defaultWriteInfos.IsEmpty() && sessionWriteInfos.IsEmpty()) { + return; + } + + RefPtr<SessionStorageCacheChild> cacheActor = + EnsureCache(aOriginAttrs, aOriginKey, aCache); + + Unused << cacheActor->SendCheckpoint(defaultWriteInfos, sessionWriteInfos); + + aCache.ResetWriteInfos(SessionStorageCache::eDefaultSetType); + aCache.ResetWriteInfos(SessionStorageCache::eSessionSetType); +} + +NS_IMETHODIMP +SessionStorageManager::PrecacheStorage(nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal, + Storage** aRetval) { + // Nothing to preload. + return NS_OK; +} + +NS_IMETHODIMP +SessionStorageManager::GetSessionStorageCache( + nsIPrincipal* aPrincipal, nsIPrincipal* aStoragePrincipal, + RefPtr<SessionStorageCache>* aRetVal) { + return GetSessionStorageCacheHelper(aStoragePrincipal, true, nullptr, + aRetVal); +} + +nsresult SessionStorageManager::GetSessionStorageCacheHelper( + nsIPrincipal* aPrincipal, bool aMakeIfNeeded, + SessionStorageCache* aCloneFrom, RefPtr<SessionStorageCache>* aRetVal) { + nsAutoCString originKey; + nsAutoCString originAttributes; + nsresult rv = aPrincipal->GetStorageOriginKey(originKey); + aPrincipal->OriginAttributesRef().CreateSuffix(originAttributes); + if (NS_FAILED(rv)) { + return NS_ERROR_NOT_AVAILABLE; + } + + return GetSessionStorageCacheHelper(originAttributes, originKey, + aMakeIfNeeded, aCloneFrom, aRetVal); +} + +nsresult SessionStorageManager::GetSessionStorageCacheHelper( + const nsACString& aOriginAttrs, const nsACString& aOriginKey, + bool aMakeIfNeeded, SessionStorageCache* aCloneFrom, + RefPtr<SessionStorageCache>* aRetVal) { + if (OriginRecord* const originRecord = GetOriginRecord( + aOriginAttrs, aOriginKey, aMakeIfNeeded, aCloneFrom)) { + *aRetVal = originRecord->mCache; + } else { + *aRetVal = nullptr; + } + return NS_OK; +} + +NS_IMETHODIMP +SessionStorageManager::CreateStorage(mozIDOMWindow* aWindow, + nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal, + const nsAString& aDocumentURI, + bool aPrivate, Storage** aRetval) { + RefPtr<SessionStorageCache> cache; + nsresult rv = GetSessionStorageCache(aPrincipal, aStoragePrincipal, &cache); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsPIDOMWindowInner> inner = nsPIDOMWindowInner::From(aWindow); + + RefPtr<SessionStorage> storage = + new SessionStorage(inner, aPrincipal, aStoragePrincipal, cache, this, + aDocumentURI, aPrivate); + + storage.forget(aRetval); + return NS_OK; +} + +NS_IMETHODIMP +SessionStorageManager::GetStorage(mozIDOMWindow* aWindow, + nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal, + bool aPrivate, Storage** aRetval) { + *aRetval = nullptr; + + RefPtr<SessionStorageCache> cache; + nsresult rv = + GetSessionStorageCacheHelper(aStoragePrincipal, false, nullptr, &cache); + if (NS_FAILED(rv) || !cache) { + return rv; + } + + nsCOMPtr<nsPIDOMWindowInner> inner = nsPIDOMWindowInner::From(aWindow); + + RefPtr<SessionStorage> storage = new SessionStorage( + inner, aPrincipal, aStoragePrincipal, cache, this, u""_ns, aPrivate); + + storage.forget(aRetval); + return NS_OK; +} + +NS_IMETHODIMP +SessionStorageManager::CloneStorage(Storage* aStorage) { + if (NS_WARN_IF(!aStorage)) { + return NS_ERROR_UNEXPECTED; + } + + if (aStorage->Type() != Storage::eSessionStorage) { + return NS_ERROR_UNEXPECTED; + } + + // ToDo: At the momnet, we clone the cache on the child process and then + // send the checkpoint. It would be nicer if we either serailizing all the + // data and sync to the parent process directly or clonig storage on the + // parnet process and sync it to the child process on demand. + + RefPtr<SessionStorageCache> cache; + nsresult rv = GetSessionStorageCacheHelper( + aStorage->StoragePrincipal(), true, + static_cast<SessionStorage*>(aStorage)->Cache(), &cache); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // If cache was cloned from other storage, then we shouldn't load the cache + // at the first access. + cache->SetLoadedOrCloned(); + + if (CanLoadData()) { + rv = EnsureManager(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + CheckpointData(*aStorage->StoragePrincipal(), *cache); + } + + return rv; +} + +NS_IMETHODIMP +SessionStorageManager::CheckStorage(nsIPrincipal* aPrincipal, Storage* aStorage, + bool* aRetval) { + if (NS_WARN_IF(!aStorage)) { + return NS_ERROR_UNEXPECTED; + } + + if (!aPrincipal) { + return NS_ERROR_NOT_AVAILABLE; + } + + *aRetval = false; + + RefPtr<SessionStorageCache> cache; + nsresult rv = + GetSessionStorageCacheHelper(aPrincipal, false, nullptr, &cache); + if (NS_FAILED(rv) || !cache) { + return rv; + } + + if (aStorage->Type() != Storage::eSessionStorage) { + return NS_OK; + } + + RefPtr<SessionStorage> sessionStorage = + static_cast<SessionStorage*>(aStorage); + if (sessionStorage->Cache() != cache) { + return NS_OK; + } + + if (!StorageUtils::PrincipalsEqual(aStorage->StoragePrincipal(), + aPrincipal)) { + return NS_OK; + } + + *aRetval = true; + return NS_OK; +} + +void SessionStorageManager::ClearStorages( + ClearStorageType aType, const OriginAttributesPattern& aPattern, + const nsACString& aOriginScope) { + if (CanLoadData()) { + nsresult rv = EnsureManager(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } + + for (auto iter1 = mOATable.Iter(); !iter1.Done(); iter1.Next()) { + OriginAttributes oa; + DebugOnly<bool> ok = oa.PopulateFromSuffix(iter1.Key()); + MOZ_ASSERT(ok); + if (!aPattern.Matches(oa)) { + // This table doesn't match the given origin attributes pattern + continue; + } + + OriginKeyHashTable* table = iter1.UserData(); + for (auto iter2 = table->Iter(); !iter2.Done(); iter2.Next()) { + if (aOriginScope.IsEmpty() || + StringBeginsWith(iter2.Key(), aOriginScope)) { + const auto cache = iter2.Data()->mCache; + if (aType == eAll) { + cache->Clear(SessionStorageCache::eDefaultSetType, false); + cache->Clear(SessionStorageCache::eSessionSetType, false); + } else { + MOZ_ASSERT(aType == eSessionOnly); + cache->Clear(SessionStorageCache::eSessionSetType, false); + } + + if (CanLoadData()) { + MOZ_ASSERT(ActorExists()); + CheckpointDataInternal(nsCString{iter1.Key()}, nsCString{iter2.Key()}, + *cache); + } + } + } + } +} + +nsresult SessionStorageManager::Observe( + const char* aTopic, const nsAString& aOriginAttributesPattern, + const nsACString& aOriginScope) { + OriginAttributesPattern pattern; + if (!pattern.Init(aOriginAttributesPattern)) { + NS_ERROR("Cannot parse origin attributes pattern"); + return NS_ERROR_FAILURE; + } + + // Clear everything, caches + database + if (!strcmp(aTopic, "cookie-cleared")) { + ClearStorages(eAll, pattern, ""_ns); + return NS_OK; + } + + // Clear from caches everything that has been stored + // while in session-only mode + if (!strcmp(aTopic, "session-only-cleared")) { + ClearStorages(eSessionOnly, pattern, aOriginScope); + return NS_OK; + } + + // Clear everything (including so and pb data) from caches and database + // for the given domain and subdomains. + if (!strcmp(aTopic, "browser:purge-sessionStorage")) { + ClearStorages(eAll, pattern, aOriginScope); + return NS_OK; + } + + if (!strcmp(aTopic, "profile-change")) { + // For case caches are still referenced - clear them completely + ClearStorages(eAll, pattern, ""_ns); + mOATable.Clear(); + return NS_OK; + } + + return NS_OK; +} + +SessionStorageManager::OriginRecord::~OriginRecord() = default; + +// static +void BackgroundSessionStorageManager::RemoveManager(uint64_t aTopContextId) { + MOZ_ASSERT(XRE_IsParentProcess()); + AssertIsOnMainThread(); + + ::mozilla::ipc::PBackgroundChild* backgroundActor = + ::mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + return; + } + + if (NS_WARN_IF(!backgroundActor->SendRemoveBackgroundSessionStorageManager( + aTopContextId))) { + return; + } +} + +// static +void BackgroundSessionStorageManager::PropagateManager( + uint64_t aCurrentTopContextId, uint64_t aTargetTopContextId) { + MOZ_ASSERT(XRE_IsParentProcess()); + AssertIsOnMainThread(); + MOZ_ASSERT(aCurrentTopContextId != aTargetTopContextId); + + ::mozilla::ipc::PBackgroundChild* backgroundActor = + ::mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + return; + } + + if (NS_WARN_IF(!backgroundActor->SendPropagateBackgroundSessionStorageManager( + aCurrentTopContextId, aTargetTopContextId))) { + return; + } +} + +// static +BackgroundSessionStorageManager* BackgroundSessionStorageManager::GetOrCreate( + uint64_t aTopContextId) { + MOZ_ASSERT(XRE_IsParentProcess()); + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + if (!sManagers) { + sManagers = new nsRefPtrHashtable<nsUint64HashKey, + BackgroundSessionStorageManager>(); + NS_DispatchToMainThread(NS_NewRunnableFunction( + "dom::BackgroundSessionStorageManager::GetOrCreate", [] { + RunOnShutdown( + [] { + ::mozilla::ipc::PBackgroundChild* backgroundActor = ::mozilla:: + ipc::BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + return; + } + + if (NS_WARN_IF( + !backgroundActor + ->SendShutdownBackgroundSessionStorageManagers())) { + return; + } + }, + ShutdownPhase::Shutdown); + })); + } + + return sManagers->LookupForAdd(aTopContextId).OrInsert([] { + return new BackgroundSessionStorageManager(); + }); +} + +BackgroundSessionStorageManager::BackgroundSessionStorageManager() { + MOZ_ASSERT(XRE_IsParentProcess()); + ::mozilla::ipc::AssertIsOnBackgroundThread(); +} + +BackgroundSessionStorageManager::~BackgroundSessionStorageManager() = default; + +void BackgroundSessionStorageManager::CopyDataToContentProcess( + const nsACString& aOriginAttrs, const nsACString& aOriginKey, + nsTArray<SSSetItemInfo>& aDefaultData, + nsTArray<SSSetItemInfo>& aSessionData) { + MOZ_ASSERT(XRE_IsParentProcess()); + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + auto* const originRecord = + GetOriginRecord(aOriginAttrs, aOriginKey, false, nullptr); + if (!originRecord) { + return; + } + + aDefaultData = + originRecord->mCache->SerializeData(SessionStorageCache::eDefaultSetType); + aSessionData = + originRecord->mCache->SerializeData(SessionStorageCache::eSessionSetType); +} + +void BackgroundSessionStorageManager::UpdateData( + const nsACString& aOriginAttrs, const nsACString& aOriginKey, + const nsTArray<SSWriteInfo>& aDefaultWriteInfos, + const nsTArray<SSWriteInfo>& aSessionWriteInfos) { + MOZ_ASSERT(XRE_IsParentProcess()); + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + auto* const originRecord = + GetOriginRecord(aOriginAttrs, aOriginKey, true, nullptr); + MOZ_ASSERT(originRecord); + + originRecord->mCache->DeserializeWriteInfos( + SessionStorageCache::eDefaultSetType, aDefaultWriteInfos); + originRecord->mCache->DeserializeWriteInfos( + SessionStorageCache::eSessionSetType, aSessionWriteInfos); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/SessionStorageManager.h b/dom/storage/SessionStorageManager.h new file mode 100644 index 0000000000..fed41bb4db --- /dev/null +++ b/dom/storage/SessionStorageManager.h @@ -0,0 +1,191 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_SessionStorageManager_h +#define mozilla_dom_SessionStorageManager_h + +#include "StorageObserver.h" + +#include "mozilla/dom/FlippedOnce.h" +#include "nsIDOMStorageManager.h" +#include "nsClassHashtable.h" +#include "nsCycleCollectionParticipant.h" +#include "nsHashKeys.h" + +namespace mozilla { +class OriginAttributesPattern; + +namespace dom { + +bool RecvShutdownBackgroundSessionStorageManagers(); +void RecvPropagateBackgroundSessionStorageManager(uint64_t aCurrentTopContextId, + uint64_t aTargetTopContextId); +bool RecvRemoveBackgroundSessionStorageManager(uint64_t aTopContextId); + +class BrowsingContext; +class ContentParent; +class SSSetItemInfo; +class SSWriteInfo; +class SessionStorageCache; +class SessionStorageCacheChild; +class SessionStorageManagerChild; +class SessionStorageManagerParent; +class SessionStorageObserver; +struct OriginRecord; + +// sessionStorage is a data store that's unique to each tab (i.e. top-level +// browsing context) and origin. Before the advent of Fission all the data +// for a given tab could be stored in a single content process; now each +// site-specific process stores only its portion of the data. As content +// processes terminate, their sessionStorage data needs to be saved in the +// parent process, in case the same origin appears again in the tab (e.g. +// by navigating an iframe element). Therefore SessionStorageManager +// objects exist in both the parent and content processes. +// +// Whenever a write operation for SessionStorage executes, the content process +// sends the changes to the parent process at the next stable state. Whenever a +// content process navigates to an origin for the first time in a given tab, the +// parent process sends it the saved data. SessionStorageCache has a flag +// (mLoadedOrCloned) to ensure that it's only be loaded or cloned once. +// +// Note: the current implementation is expected to be replaced by a new +// implementation using LSNG. +class SessionStorageManagerBase { + public: + SessionStorageManagerBase() = default; + + protected: + ~SessionStorageManagerBase() = default; + + struct OriginRecord { + OriginRecord() = default; + OriginRecord(OriginRecord&&) = default; + OriginRecord& operator=(OriginRecord&&) = default; + ~OriginRecord(); + + RefPtr<SessionStorageCache> mCache; + + // A flag to ensure that cache is only loaded once. + FlippedOnce<false> mLoaded; + }; + + OriginRecord* GetOriginRecord(const nsACString& aOriginAttrs, + const nsACString& aOriginKey, + bool aMakeIfNeeded, + SessionStorageCache* aCloneFrom); + + using OriginKeyHashTable = nsClassHashtable<nsCStringHashKey, OriginRecord>; + nsClassHashtable<nsCStringHashKey, OriginKeyHashTable> mOATable; +}; + +class SessionStorageManager final : public SessionStorageManagerBase, + public nsIDOMSessionStorageManager, + public StorageObserverSink { + public: + explicit SessionStorageManager(RefPtr<BrowsingContext> aBrowsingContext); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_NSIDOMSTORAGEMANAGER + NS_DECL_NSIDOMSESSIONSTORAGEMANAGER + + NS_DECL_CYCLE_COLLECTION_CLASS(SessionStorageManager) + + bool CanLoadData(); + + void SetActor(SessionStorageManagerChild* aActor); + + bool ActorExists() const; + + void ClearActor(); + + nsresult EnsureManager(); + + nsresult LoadData(nsIPrincipal& aPrincipal, SessionStorageCache& aCache); + + void CheckpointData(nsIPrincipal& aPrincipal, SessionStorageCache& aCache); + + private: + ~SessionStorageManager(); + + // StorageObserverSink, handler to various chrome clearing notification + nsresult Observe(const char* aTopic, + const nsAString& aOriginAttributesPattern, + const nsACString& aOriginScope) override; + + nsresult GetSessionStorageCacheHelper(nsIPrincipal* aPrincipal, + bool aMakeIfNeeded, + SessionStorageCache* aCloneFrom, + RefPtr<SessionStorageCache>* aRetVal); + + nsresult GetSessionStorageCacheHelper(const nsACString& aOriginAttrs, + const nsACString& aOriginKey, + bool aMakeIfNeeded, + SessionStorageCache* aCloneFrom, + RefPtr<SessionStorageCache>* aRetVal); + + enum ClearStorageType { + eAll, + eSessionOnly, + }; + void ClearStorages(ClearStorageType aType, + const OriginAttributesPattern& aPattern, + const nsACString& aOriginScope); + + SessionStorageCacheChild* EnsureCache(const nsCString& aOriginAttrs, + const nsCString& aOriginKey, + SessionStorageCache& aCache); + + void CheckpointDataInternal(const nsCString& aOriginAttrs, + const nsCString& aOriginKey, + SessionStorageCache& aCache); + + RefPtr<SessionStorageObserver> mObserver; + + RefPtr<BrowsingContext> mBrowsingContext; + + SessionStorageManagerChild* mActor; +}; + +/** + * A specialized SessionStorageManager class that lives on the parent process + * background thread. It is a shadow copy of SessionStorageManager and it's used + * to preserve SessionStorageCaches for the other SessionStorageManagers. + */ +class BackgroundSessionStorageManager final : public SessionStorageManagerBase { + public: + // Parent process getter function. + static BackgroundSessionStorageManager* GetOrCreate(uint64_t aTopContextId); + + NS_INLINE_DECL_REFCOUNTING(BackgroundSessionStorageManager); + + // Only called by CanonicalBrowsingContext::ReplaceBy on the parent process. + static void PropagateManager(uint64_t aCurrentTopContextId, + uint64_t aTargetTopContextId); + + // Only called by CanonicalBrowsingContext::CanonicalDiscard on parent + // process. + static void RemoveManager(uint64_t aTopContextId); + + void CopyDataToContentProcess(const nsACString& aOriginAttrs, + const nsACString& aOriginKey, + nsTArray<SSSetItemInfo>& aDefaultData, + nsTArray<SSSetItemInfo>& aSessionData); + + void UpdateData(const nsACString& aOriginAttrs, const nsACString& aOriginKey, + const nsTArray<SSWriteInfo>& aDefaultWriteInfos, + const nsTArray<SSWriteInfo>& aSessionWriteInfos); + + private: + // Only be called by GetOrCreate() on the parent process. + explicit BackgroundSessionStorageManager(); + + ~BackgroundSessionStorageManager(); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_SessionStorageManager_h diff --git a/dom/storage/SessionStorageObserver.cpp b/dom/storage/SessionStorageObserver.cpp new file mode 100644 index 0000000000..edfe745528 --- /dev/null +++ b/dom/storage/SessionStorageObserver.cpp @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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 "SessionStorageObserver.h" +#include "StorageIPC.h" +#include "mozilla/dom/LocalStorageCommon.h" + +namespace mozilla { +namespace dom { + +namespace { + +SessionStorageObserver* gSessionStorageObserver = nullptr; + +} // namespace + +SessionStorageObserver::SessionStorageObserver() : mActor(nullptr) { + AssertIsOnOwningThread(); + MOZ_ASSERT(NextGenLocalStorageEnabled()); + + MOZ_ASSERT(!gSessionStorageObserver); + gSessionStorageObserver = this; +} + +SessionStorageObserver::~SessionStorageObserver() { + AssertIsOnOwningThread(); + + if (mActor) { + mActor->SendDeleteMeInternal(); + MOZ_ASSERT(!mActor, "SendDeleteMeInternal should have cleared!"); + } + + MOZ_ASSERT(gSessionStorageObserver); + gSessionStorageObserver = nullptr; +} + +// static +SessionStorageObserver* SessionStorageObserver::Get() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(NextGenLocalStorageEnabled()); + + return gSessionStorageObserver; +} + +void SessionStorageObserver::SetActor(SessionStorageObserverChild* aActor) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(!mActor); + + mActor = aActor; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/SessionStorageObserver.h b/dom/storage/SessionStorageObserver.h new file mode 100644 index 0000000000..db414e21de --- /dev/null +++ b/dom/storage/SessionStorageObserver.h @@ -0,0 +1,65 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_SessionStorageObserver_h +#define mozilla_dom_SessionStorageObserver_h + +#include "nsISupportsImpl.h" + +namespace mozilla { +namespace dom { + +class SessionStorageObserverChild; + +/** + * Effectively just a refcounted life-cycle management wrapper around + * SessionStorageObserverChild which exists to receive chrome observer + * notifications from the main process. + * + * ## Lifecycle ## + * - Created by SessionStorageManager::SessionStorageManager. Placed in the + * gSessionStorageObserver variable for subsequent SessionStorageManager's via + * SessionStorageObserver::Get lookup. + * - The SessionStorageObserverChild directly handles "Observe" messages, + * shunting them directly to StorageObserver::Notify which distributes them to + * individual observer sinks. + * - Destroyed when refcount goes to zero due to all owning + * SessionStorageManager being destroyed. + */ +class SessionStorageObserver final { + friend class SessionStorageManager; + + SessionStorageObserverChild* mActor; + + public: + static SessionStorageObserver* Get(); + + NS_INLINE_DECL_REFCOUNTING(SessionStorageObserver) + + void AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(SessionStorageObserver); + } + + void SetActor(SessionStorageObserverChild* aActor); + + void ClearActor() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mActor); + + mActor = nullptr; + } + + private: + // Only created by SessionStorageManager. + SessionStorageObserver(); + + ~SessionStorageObserver(); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_SessionStorageObserver_h diff --git a/dom/storage/Storage.cpp b/dom/storage/Storage.cpp new file mode 100644 index 0000000000..a7bd1fe7a5 --- /dev/null +++ b/dom/storage/Storage.cpp @@ -0,0 +1,153 @@ +/* -*- 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 "Storage.h" +#include "StorageNotifierService.h" + +#include "mozilla/dom/StorageBinding.h" +#include "mozilla/dom/StorageEvent.h" +#include "mozilla/dom/StorageEventBinding.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/Preferences.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/Services.h" +#include "mozilla/StorageAccess.h" +#include "nsIObserverService.h" +#include "nsPIDOMWindow.h" + +namespace mozilla { +namespace dom { + +static const char kStorageEnabled[] = "dom.storage.enabled"; + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Storage, mWindow, mPrincipal, + mStoragePrincipal) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(Storage) +NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_LAST_RELEASE(Storage, LastRelease()) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Storage) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +Storage::Storage(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal) + : mWindow(aWindow), + mPrincipal(aPrincipal), + mStoragePrincipal(aStoragePrincipal), + mPrivateBrowsing(false), + mSessionScopedOrLess(false) { + MOZ_ASSERT(aPrincipal); + + if (mPrincipal->IsSystemPrincipal()) { + mPrivateBrowsing = false; + mSessionScopedOrLess = false; + } else if (mWindow) { + uint32_t rejectedReason = 0; + StorageAccess access = StorageAllowedForWindow(mWindow, &rejectedReason); + + mPrivateBrowsing = access == StorageAccess::ePrivateBrowsing; + mSessionScopedOrLess = access <= StorageAccess::eSessionScoped; + } +} + +Storage::~Storage() = default; + +/* static */ +bool Storage::StoragePrefIsEnabled() { + return mozilla::Preferences::GetBool(kStorageEnabled); +} + +bool Storage::CanUseStorage(nsIPrincipal& aSubjectPrincipal) { + if (!StoragePrefIsEnabled()) { + return false; + } + + return aSubjectPrincipal.Subsumes(mPrincipal); +} + +/* virtual */ +JSObject* Storage::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return Storage_Binding::Wrap(aCx, this, aGivenProto); +} + +namespace { + +class StorageNotifierRunnable : public Runnable { + public: + StorageNotifierRunnable(nsISupports* aSubject, const char16_t* aStorageType, + bool aPrivateBrowsing) + : Runnable("StorageNotifierRunnable"), + mSubject(aSubject), + mStorageType(aStorageType), + mPrivateBrowsing(aPrivateBrowsing) {} + + NS_IMETHOD + Run() override { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->NotifyObservers(mSubject, + mPrivateBrowsing + ? "dom-private-storage2-changed" + : "dom-storage2-changed", + mStorageType); + } + return NS_OK; + } + + private: + nsCOMPtr<nsISupports> mSubject; + const char16_t* mStorageType; + const bool mPrivateBrowsing; +}; + +} // namespace + +/* static */ +void Storage::NotifyChange(Storage* aStorage, nsIPrincipal* aPrincipal, + const nsAString& aKey, const nsAString& aOldValue, + const nsAString& aNewValue, + const char16_t* aStorageType, + const nsAString& aDocumentURI, bool aIsPrivate, + bool aImmediateDispatch) { + StorageEventInit dict; + dict.mBubbles = false; + dict.mCancelable = false; + dict.mKey = aKey; + dict.mNewValue = aNewValue; + dict.mOldValue = aOldValue; + dict.mStorageArea = aStorage; + dict.mUrl = aDocumentURI; + + // Note, this DOM event should never reach JS. It is cloned later in + // nsGlobalWindow. + RefPtr<StorageEvent> event = + StorageEvent::Constructor(nullptr, u"storage"_ns, dict); + + event->SetPrincipal(aPrincipal); + + // This will send the event to any registered window. + StorageNotifierService::Broadcast(event, aStorageType, aIsPrivate, + aImmediateDispatch); + + // This runnable is mainly used by devtools. Windows receive notification by + // StorageNotifierService. + + RefPtr<StorageNotifierRunnable> r = + new StorageNotifierRunnable(event, aStorageType, aIsPrivate); + + if (aImmediateDispatch) { + Unused << r->Run(); + } else { + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()); + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/Storage.h b/dom/storage/Storage.h new file mode 100644 index 0000000000..f197e1cb9d --- /dev/null +++ b/dom/storage/Storage.h @@ -0,0 +1,174 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_Storage_h +#define mozilla_dom_Storage_h + +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/Maybe.h" +#include "nsCycleCollectionParticipant.h" +#include "nsCOMPtr.h" +#include "nsWrapperCache.h" +#include "nsISupports.h" +#include "nsTArrayForwardDeclare.h" +#include "nsString.h" + +class nsIPrincipal; +class nsPIDOMWindowInner; + +namespace mozilla { +namespace dom { + +class Storage : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(Storage) + + Storage(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal, + nsIPrincipal* aStoragePrincipal); + + static bool StoragePrefIsEnabled(); + + enum StorageType { + eSessionStorage, + eLocalStorage, + ePartitionedLocalStorage, + }; + + virtual StorageType Type() const = 0; + + virtual bool IsForkOf(const Storage* aStorage) const = 0; + + virtual int64_t GetOriginQuotaUsage() const = 0; + + nsIPrincipal* Principal() const { return mPrincipal; } + + nsIPrincipal* StoragePrincipal() const { return mStoragePrincipal; } + + bool IsPrivateBrowsing() const { return mPrivateBrowsing; } + + bool IsSessionScopedOrLess() const { return mSessionScopedOrLess; } + + // WebIDL + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + nsPIDOMWindowInner* GetParentObject() const { return mWindow; } + + virtual uint32_t GetLength(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) = 0; + + virtual void Key(uint32_t aIndex, nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) = 0; + + virtual void GetItem(const nsAString& aKey, nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) = 0; + + virtual void GetSupportedNames(nsTArray<nsString>& aKeys) = 0; + + void NamedGetter(const nsAString& aKey, bool& aFound, nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { + GetItem(aKey, aResult, aSubjectPrincipal, aRv); + aFound = !aResult.IsVoid(); + } + + virtual void SetItem(const nsAString& aKey, const nsAString& aValue, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) = 0; + + void NamedSetter(const nsAString& aKey, const nsAString& aValue, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { + SetItem(aKey, aValue, aSubjectPrincipal, aRv); + } + + virtual void RemoveItem(const nsAString& aKey, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) = 0; + + void NamedDeleter(const nsAString& aKey, bool& aFound, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { + RemoveItem(aKey, aSubjectPrincipal, aRv); + + aFound = !aRv.ErrorCodeIs(NS_SUCCESS_DOM_NO_OPERATION); + } + + virtual void Clear(nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) = 0; + + // The attribute in the WebIDL interface has rather confusing name. So we + // shouldn't use this method internally. IsSessionScopedOrLess should be used + // directly. + bool IsSessionOnly() const { return IsSessionScopedOrLess(); } + + ////////////////////////////////////////////////////////////////////////////// + // Testing Methods: + // + // These methods are exposed on the `Storage` WebIDL interface behind a + // preference for the benefit of automated-tests. They are not exposed to + // content. See `Storage.webidl` for more details. + + virtual void Open(nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) {} + + virtual void Close(nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) {} + + virtual void BeginExplicitSnapshot(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) {} + + virtual void EndExplicitSnapshot(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) {} + + virtual bool GetHasActiveSnapshot(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + return false; + } + + ////////////////////////////////////////////////////////////////////////////// + + // Dispatch storage notification events on all impacted pages in the current + // process as well as for consumption by devtools. Pages receive the + // notification via StorageNotifierService (not observers like in the past), + // while devtools does receive the notification via the observer service. + // + // aStorage can be null if this method is called by LocalStorageCacheChild. + // + // aImmediateDispatch is for use by child IPC code (LocalStorageCacheChild) + // so that PBackground ordering can be maintained. Without this, the event + // would be enqueued and run in a future turn of the event loop, potentially + // allowing other PBackground Recv* methods to trigger script that wants to + // assume our localstorage changes have already been applied. This is the + // case for message manager messages which are used by ContentTask testing + // logic and webextensions. + static void NotifyChange(Storage* aStorage, nsIPrincipal* aPrincipal, + const nsAString& aKey, const nsAString& aOldValue, + const nsAString& aNewValue, + const char16_t* aStorageType, + const nsAString& aDocumentURI, bool aIsPrivate, + bool aImmediateDispatch); + + protected: + virtual ~Storage(); + + // The method checks whether the caller can use a storage. + bool CanUseStorage(nsIPrincipal& aSubjectPrincipal); + + virtual void LastRelease() {} + + private: + nsCOMPtr<nsPIDOMWindowInner> mWindow; + nsCOMPtr<nsIPrincipal> mPrincipal; + nsCOMPtr<nsIPrincipal> mStoragePrincipal; + + bool mPrivateBrowsing : 1; + + // Whether storage is set to persist data only per session, may change + // dynamically and is set by CanUseStorage function that is called + // before any operation on the storage. + bool mSessionScopedOrLess : 1; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_Storage_h diff --git a/dom/storage/StorageActivityService.cpp b/dom/storage/StorageActivityService.cpp new file mode 100644 index 0000000000..bee3ea72ee --- /dev/null +++ b/dom/storage/StorageActivityService.cpp @@ -0,0 +1,314 @@ +/* -*- 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 "StorageActivityService.h" + +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPtr.h" +#include "nsCOMPtr.h" +#include "nsComponentManagerUtils.h" +#include "nsIMutableArray.h" +#include "nsIObserverService.h" +#include "nsIPrincipal.h" +#include "nsSupportsPrimitives.h" +#include "nsXPCOM.h" + +// This const is used to know when origin activities should be purged because +// too old. This value should be in sync with what the UI needs. +#define TIME_MAX_SECS 86400 /* 24 hours */ + +namespace mozilla { +namespace dom { + +static StaticRefPtr<StorageActivityService> gStorageActivityService; +static bool gStorageActivityShutdown = false; + +/* static */ +void StorageActivityService::SendActivity(nsIPrincipal* aPrincipal) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!aPrincipal || BasePrincipal::Cast(aPrincipal)->Kind() != + BasePrincipal::eContentPrincipal) { + // Only content principals. + return; + } + + RefPtr<StorageActivityService> service = GetOrCreate(); + if (NS_WARN_IF(!service)) { + return; + } + + service->SendActivityInternal(aPrincipal); +} + +/* static */ +void StorageActivityService::SendActivity( + const mozilla::ipc::PrincipalInfo& aPrincipalInfo) { + if (aPrincipalInfo.type() != + mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) { + // only content principal. + return; + } + + RefPtr<Runnable> r = NS_NewRunnableFunction( + "StorageActivityService::SendActivity", [aPrincipalInfo]() { + MOZ_ASSERT(NS_IsMainThread()); + + auto principalOrErr = + mozilla::ipc::PrincipalInfoToPrincipal(aPrincipalInfo); + + if (principalOrErr.isOk()) { + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + StorageActivityService::SendActivity(principal); + } else { + NS_WARNING( + "Could not obtain principal from " + "mozilla::ipc::PrincipalInfoToPrincipal"); + } + }); + + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()); +} + +/* static */ +void StorageActivityService::SendActivity(const nsACString& aOrigin) { + MOZ_ASSERT(XRE_IsParentProcess()); + + nsCString origin; + origin.Assign(aOrigin); + + RefPtr<Runnable> r = NS_NewRunnableFunction( + "StorageActivityService::SendActivity", [origin]() { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<StorageActivityService> service = GetOrCreate(); + if (NS_WARN_IF(!service)) { + return; + } + + service->SendActivityInternal(origin); + }); + + if (NS_IsMainThread()) { + Unused << r->Run(); + } else { + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()); + } +} + +/* static */ +already_AddRefed<StorageActivityService> StorageActivityService::GetOrCreate() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!gStorageActivityService && !gStorageActivityShutdown) { + RefPtr<StorageActivityService> service = new StorageActivityService(); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return nullptr; + } + + nsresult rv = + obs->AddObserver(service, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + gStorageActivityService = service; + } + + RefPtr<StorageActivityService> service = gStorageActivityService; + return service.forget(); +} + +StorageActivityService::StorageActivityService() { + MOZ_ASSERT(NS_IsMainThread()); +} + +StorageActivityService::~StorageActivityService() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mTimer); +} + +void StorageActivityService::SendActivityInternal(nsIPrincipal* aPrincipal) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(BasePrincipal::Cast(aPrincipal)->Kind() == + BasePrincipal::eContentPrincipal); + + if (!XRE_IsParentProcess()) { + SendActivityToParent(aPrincipal); + return; + } + + nsAutoCString origin; + nsresult rv = aPrincipal->GetOrigin(origin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + SendActivityInternal(origin); +} + +void StorageActivityService::SendActivityInternal(const nsACString& aOrigin) { + MOZ_ASSERT(XRE_IsParentProcess()); + + mActivities.Put(aOrigin, PR_Now()); + MaybeStartTimer(); +} + +void StorageActivityService::SendActivityToParent(nsIPrincipal* aPrincipal) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!XRE_IsParentProcess()); + + ::mozilla::ipc::PBackgroundChild* actor = + ::mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!actor)) { + return; + } + + mozilla::ipc::PrincipalInfo principalInfo; + nsresult rv = + mozilla::ipc::PrincipalToPrincipalInfo(aPrincipal, &principalInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + actor->SendStorageActivity(principalInfo); +} + +NS_IMETHODIMP +StorageActivityService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)); + + MaybeStopTimer(); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + } + + gStorageActivityShutdown = true; + gStorageActivityService = nullptr; + return NS_OK; +} + +void StorageActivityService::MaybeStartTimer() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mTimer) { + mTimer = do_CreateInstance(NS_TIMER_CONTRACTID); + mTimer->InitWithCallback(this, 1000 * 5 * 60 /* any 5 minutes */, + nsITimer::TYPE_REPEATING_SLACK); + } +} + +void StorageActivityService::MaybeStopTimer() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } +} + +NS_IMETHODIMP +StorageActivityService::Notify(nsITimer* aTimer) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mTimer == aTimer); + + uint64_t now = PR_Now(); + + for (auto iter = mActivities.Iter(); !iter.Done(); iter.Next()) { + if ((now - iter.UserData()) / PR_USEC_PER_SEC > TIME_MAX_SECS) { + iter.Remove(); + } + } + + // If no activities, let's stop the timer. + if (mActivities.Count() == 0) { + MaybeStopTimer(); + } + + return NS_OK; +} + +NS_IMETHODIMP +StorageActivityService::GetActiveOrigins(PRTime aFrom, PRTime aTo, + nsIArray** aRetval) { + uint64_t now = PR_Now(); + if (((now - aFrom) / PR_USEC_PER_SEC) > TIME_MAX_SECS || aFrom >= aTo) { + return NS_ERROR_INVALID_ARG; + } + + nsresult rv = NS_OK; + nsCOMPtr<nsIMutableArray> devices = + do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + for (auto iter = mActivities.Iter(); !iter.Done(); iter.Next()) { + if (iter.UserData() >= aFrom && iter.UserData() <= aTo) { + RefPtr<BasePrincipal> principal = + BasePrincipal::CreateContentPrincipal(iter.Key()); + MOZ_ASSERT(principal); + + rv = devices->AppendElement(principal); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + devices.forget(aRetval); + return NS_OK; +} + +NS_IMETHODIMP +StorageActivityService::MoveOriginInTime(nsIPrincipal* aPrincipal, + PRTime aWhen) { + if (!XRE_IsParentProcess()) { + return NS_ERROR_FAILURE; + } + + nsAutoCString origin; + nsresult rv = aPrincipal->GetOrigin(origin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mActivities.Put(origin, aWhen / PR_USEC_PER_SEC); + return NS_OK; +} + +NS_IMETHODIMP +StorageActivityService::TestOnlyReset() { + mActivities.Clear(); + return NS_OK; +} + +NS_INTERFACE_MAP_BEGIN(StorageActivityService) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIStorageActivityService) + NS_INTERFACE_MAP_ENTRY(nsIStorageActivityService) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsITimerCallback) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END + +NS_IMPL_ADDREF(StorageActivityService) +NS_IMPL_RELEASE(StorageActivityService) + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/StorageActivityService.h b/dom/storage/StorageActivityService.h new file mode 100644 index 0000000000..dce5c430e1 --- /dev/null +++ b/dom/storage/StorageActivityService.h @@ -0,0 +1,69 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_StorageActivityService_h +#define mozilla_dom_StorageActivityService_h + +#include "nsDataHashtable.h" +#include "nsIObserver.h" +#include "nsIStorageActivityService.h" +#include "nsITimer.h" +#include "nsWeakReference.h" + +namespace mozilla { + +namespace ipc { +class PrincipalInfo; +} // namespace ipc + +namespace dom { + +class StorageActivityService final : public nsIStorageActivityService, + public nsIObserver, + public nsITimerCallback, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTORAGEACTIVITYSERVICE + NS_DECL_NSIOBSERVER + NS_DECL_NSITIMERCALLBACK + + // Main-thread only. + static void SendActivity(nsIPrincipal* aPrincipal); + + // Thread-safe. + static void SendActivity(const mozilla::ipc::PrincipalInfo& aPrincipalInfo); + + // Thread-safe but for parent process only! + static void SendActivity(const nsACString& aOrigin); + + // Used by XPCOM. Don't use it, use SendActivity() instead. + static already_AddRefed<StorageActivityService> GetOrCreate(); + + private: + StorageActivityService(); + ~StorageActivityService(); + + void SendActivityInternal(nsIPrincipal* aPrincipal); + + void SendActivityInternal(const nsACString& aOrigin); + + void SendActivityToParent(nsIPrincipal* aPrincipal); + + void MaybeStartTimer(); + + void MaybeStopTimer(); + + // Activities grouped by origin (+OriginAttributes). + nsDataHashtable<nsCStringHashKey, PRTime> mActivities; + + nsCOMPtr<nsITimer> mTimer; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_StorageActivityService_h diff --git a/dom/storage/StorageDBThread.cpp b/dom/storage/StorageDBThread.cpp new file mode 100644 index 0000000000..0820d47faa --- /dev/null +++ b/dom/storage/StorageDBThread.cpp @@ -0,0 +1,1636 @@ +/* -*- 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 "StorageDBThread.h" +#include "StorageDBUpdater.h" +#include "StorageUtils.h" +#include "LocalStorageCache.h" +#include "LocalStorageManager.h" + +#include "nsComponentManagerUtils.h" +#include "nsDirectoryServiceUtils.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsThreadUtils.h" +#include "nsProxyRelease.h" +#include "mozStorageCID.h" +#include "mozStorageHelper.h" +#include "mozIStorageService.h" +#include "mozIStorageBindingParams.h" +#include "mozIStorageValueArray.h" +#include "mozIStorageFunction.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "nsIObserverService.h" +#include "nsThread.h" +#include "nsThreadManager.h" +#include "nsVariant.h" +#include "mozilla/EventQueue.h" +#include "mozilla/IOInterposer.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/ThreadEventQueue.h" +#include "mozilla/Services.h" +#include "mozilla/Tokenizer.h" +#include "GeckoProfiler.h" + +// How long we collect write oprerations +// before they are flushed to the database +// In milliseconds. +#define FLUSHING_INTERVAL_MS 5000 + +// Write Ahead Log's maximum size is 512KB +#define MAX_WAL_SIZE_BYTES 512 * 1024 + +// Current version of the database schema +#define CURRENT_SCHEMA_VERSION 2 + +namespace mozilla { +namespace dom { + +using namespace StorageUtils; + +namespace { // anon + +StorageDBThread* sStorageThread[2] = {nullptr, nullptr}; + +// False until we shut the storage thread down. +bool sStorageThreadDown[2] = {false, false}; + +} // namespace + +// XXX Fix me! +#if 0 +StorageDBBridge::StorageDBBridge() +{ +} +#endif + +class StorageDBThread::InitHelper final : public Runnable { + nsCOMPtr<nsIEventTarget> mOwningThread; + mozilla::Mutex mMutex; + mozilla::CondVar mCondVar; + nsString mProfilePath; + nsresult mMainThreadResultCode; + bool mWaiting; + + public: + InitHelper() + : Runnable("dom::StorageDBThread::InitHelper"), + mOwningThread(GetCurrentEventTarget()), + mMutex("InitHelper::mMutex"), + mCondVar(mMutex, "InitHelper::mCondVar"), + mMainThreadResultCode(NS_OK), + mWaiting(true) {} + + // Because of the `sync Preload` IPC, we need to be able to synchronously + // initialize, which includes consulting and initializing + // some main-thread-only APIs. Bug 1386441 discusses improving this situation. + nsresult SyncDispatchAndReturnProfilePath(nsAString& aProfilePath); + + private: + ~InitHelper() override = default; + + nsresult RunOnMainThread(); + + NS_DECL_NSIRUNNABLE +}; + +class StorageDBThread::NoteBackgroundThreadRunnable final : public Runnable { + // Expected to be only 0 or 1. + const uint32_t mPrivateBrowsingId; + nsCOMPtr<nsIEventTarget> mOwningThread; + + public: + explicit NoteBackgroundThreadRunnable(const uint32_t aPrivateBrowsingId) + : Runnable("dom::StorageDBThread::NoteBackgroundThreadRunnable"), + mPrivateBrowsingId(aPrivateBrowsingId), + mOwningThread(GetCurrentEventTarget()) {} + + private: + ~NoteBackgroundThreadRunnable() override = default; + + NS_DECL_NSIRUNNABLE +}; + +StorageDBThread::StorageDBThread(const uint32_t aPrivateBrowsingId) + : mThread(nullptr), + mThreadObserver(new ThreadObserver()), + mStopIOThread(false), + mWALModeEnabled(false), + mDBReady(false), + mStatus(NS_OK), + mWorkerStatements(mWorkerConnection), + mReaderStatements(mReaderConnection), + mFlushImmediately(false), + mPrivateBrowsingId(aPrivateBrowsingId), + mPriorityCounter(0) { + MOZ_ASSERT(aPrivateBrowsingId <= 1); +} + +// static +StorageDBThread* StorageDBThread::Get(const uint32_t aPrivateBrowsingId) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(aPrivateBrowsingId <= 1); + + return sStorageThread[aPrivateBrowsingId]; +} + +// static +StorageDBThread* StorageDBThread::GetOrCreate( + const nsString& aProfilePath, const uint32_t aPrivateBrowsingId) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(aPrivateBrowsingId <= 1); + + StorageDBThread*& storageThread = sStorageThread[aPrivateBrowsingId]; + if (storageThread || sStorageThreadDown[aPrivateBrowsingId]) { + // When sStorageThreadDown is at true, sStorageThread is null. + // Checking sStorageThreadDown flag here prevents reinitialization of + // the storage thread after shutdown. + return storageThread; + } + + auto newStorageThread = MakeUnique<StorageDBThread>(aPrivateBrowsingId); + + nsresult rv = newStorageThread->Init(aProfilePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + storageThread = newStorageThread.release(); + + return storageThread; +} + +// static +nsresult StorageDBThread::GetProfilePath(nsString& aProfilePath) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + // Need to determine location on the main thread, since + // NS_GetSpecialDirectory accesses the atom table that can + // only be accessed on the main thread. + nsCOMPtr<nsIFile> profileDir; + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(profileDir)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = profileDir->GetPath(aProfilePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // This service has to be started on the main thread currently. + nsCOMPtr<mozIStorageService> ss = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult StorageDBThread::Init(const nsString& aProfilePath) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + if (mPrivateBrowsingId == 0) { + nsresult rv; + + nsString profilePath; + if (aProfilePath.IsEmpty()) { + RefPtr<InitHelper> helper = new InitHelper(); + + rv = helper->SyncDispatchAndReturnProfilePath(profilePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + profilePath = aProfilePath; + } + + mDatabaseFile = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mDatabaseFile->InitWithPath(profilePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mDatabaseFile->Append(u"webappsstore.sqlite"_ns); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Need to keep the lock to avoid setting mThread later then + // the thread body executes. + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); + + mThread = PR_CreateThread(PR_USER_THREAD, &StorageDBThread::ThreadFunc, this, + PR_PRIORITY_LOW, PR_GLOBAL_THREAD, + PR_JOINABLE_THREAD, 262144); + if (!mThread) { + return NS_ERROR_OUT_OF_MEMORY; + } + + RefPtr<NoteBackgroundThreadRunnable> runnable = + new NoteBackgroundThreadRunnable(mPrivateBrowsingId); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable)); + + return NS_OK; +} + +nsresult StorageDBThread::Shutdown() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + if (!mThread) { + return NS_ERROR_NOT_INITIALIZED; + } + + Telemetry::AutoTimer<Telemetry::LOCALDOMSTORAGE_SHUTDOWN_DATABASE_MS> timer; + + { + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); + + // After we stop, no other operations can be accepted + mFlushImmediately = true; + mStopIOThread = true; + monitor.Notify(); + } + + PR_JoinThread(mThread); + mThread = nullptr; + + return mStatus; +} + +void StorageDBThread::SyncPreload(LocalStorageCacheBridge* aCache, + bool aForceSync) { + AUTO_PROFILER_LABEL("StorageDBThread::SyncPreload", OTHER); + if (!aForceSync && aCache->LoadedCount()) { + // Preload already started for this cache, just wait for it to finish. + // LoadWait will exit after LoadDone on the cache has been called. + SetHigherPriority(); + aCache->LoadWait(); + SetDefaultPriority(); + return; + } + + // Bypass sync load when an update is pending in the queue to write, we would + // get incosistent data in the cache. Also don't allow sync main-thread + // preload when DB open and init is still pending on the background thread. + if (mDBReady && mWALModeEnabled) { + bool pendingTasks; + { + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); + pendingTasks = mPendingTasks.IsOriginUpdatePending( + aCache->OriginSuffix(), aCache->OriginNoSuffix()) || + mPendingTasks.IsOriginClearPending( + aCache->OriginSuffix(), aCache->OriginNoSuffix()); + } + + if (!pendingTasks) { + // WAL is enabled, thus do the load synchronously on the main thread. + DBOperation preload(DBOperation::opPreload, aCache); + preload.PerformAndFinalize(this); + return; + } + } + + // Need to go asynchronously since WAL is not allowed or scheduled updates + // need to be flushed first. + // Schedule preload for this cache as the first operation. + nsresult rv = + InsertDBOp(new DBOperation(DBOperation::opPreloadUrgent, aCache)); + + // LoadWait exits after LoadDone of the cache has been called. + if (NS_SUCCEEDED(rv)) { + aCache->LoadWait(); + } +} + +void StorageDBThread::AsyncFlush() { + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); + mFlushImmediately = true; + monitor.Notify(); +} + +bool StorageDBThread::ShouldPreloadOrigin(const nsACString& aOrigin) { + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); + return mOriginsHavingData.Contains(aOrigin); +} + +void StorageDBThread::GetOriginsHavingData(nsTArray<nsCString>* aOrigins) { + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); + for (auto iter = mOriginsHavingData.Iter(); !iter.Done(); iter.Next()) { + aOrigins->AppendElement(iter.Get()->GetKey()); + } +} + +nsresult StorageDBThread::InsertDBOp(StorageDBThread::DBOperation* aOperation) { + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); + + // Sentinel to don't forget to delete the operation when we exit early. + UniquePtr<StorageDBThread::DBOperation> opScope(aOperation); + + if (NS_FAILED(mStatus)) { + MonitorAutoUnlock unlock(mThreadObserver->GetMonitor()); + aOperation->Finalize(mStatus); + return mStatus; + } + + if (mStopIOThread) { + // Thread use after shutdown demanded. + MOZ_ASSERT(false); + return NS_ERROR_NOT_INITIALIZED; + } + + switch (aOperation->Type()) { + case DBOperation::opPreload: + case DBOperation::opPreloadUrgent: + if (mPendingTasks.IsOriginUpdatePending(aOperation->OriginSuffix(), + aOperation->OriginNoSuffix())) { + // If there is a pending update operation for the scope first do the + // flush before we preload the cache. This may happen in an extremely + // rare case when a child process throws away its cache before flush on + // the parent has finished. If we would preloaded the cache as a + // priority operation before the pending flush, we would have got an + // inconsistent cache content. + mFlushImmediately = true; + } else if (mPendingTasks.IsOriginClearPending( + aOperation->OriginSuffix(), + aOperation->OriginNoSuffix())) { + // The scope is scheduled to be cleared, so just quickly load as empty. + // We need to do this to prevent load of the DB data before the scope + // has actually been cleared from the database. Preloads are processed + // immediately before update and clear operations on the database that + // are flushed periodically in batches. + MonitorAutoUnlock unlock(mThreadObserver->GetMonitor()); + aOperation->Finalize(NS_OK); + return NS_OK; + } + [[fallthrough]]; + + case DBOperation::opGetUsage: + if (aOperation->Type() == DBOperation::opPreloadUrgent) { + SetHigherPriority(); // Dropped back after urgent preload execution + mPreloads.InsertElementAt(0, aOperation); + } else { + mPreloads.AppendElement(aOperation); + } + + // DB operation adopted, don't delete it. + Unused << opScope.release(); + + // Immediately start executing this. + monitor.Notify(); + break; + + default: + // Update operations are first collected, coalesced and then flushed + // after a short time. + mPendingTasks.Add(aOperation); + + // DB operation adopted, don't delete it. + Unused << opScope.release(); + + ScheduleFlush(); + break; + } + + return NS_OK; +} + +void StorageDBThread::SetHigherPriority() { + ++mPriorityCounter; + PR_SetThreadPriority(mThread, PR_PRIORITY_URGENT); +} + +void StorageDBThread::SetDefaultPriority() { + if (--mPriorityCounter <= 0) { + PR_SetThreadPriority(mThread, PR_PRIORITY_LOW); + } +} + +void StorageDBThread::ThreadFunc(void* aArg) { + { + auto queue = MakeRefPtr<ThreadEventQueue>(MakeUnique<EventQueue>()); + Unused << nsThreadManager::get().CreateCurrentThread( + queue, nsThread::NOT_MAIN_THREAD); + } + + AUTO_PROFILER_REGISTER_THREAD("localStorage DB"); + NS_SetCurrentThreadName("localStorage DB"); + mozilla::IOInterposer::RegisterCurrentThread(); + + StorageDBThread* thread = static_cast<StorageDBThread*>(aArg); + thread->ThreadFunc(); + mozilla::IOInterposer::UnregisterCurrentThread(); +} + +void StorageDBThread::ThreadFunc() { + nsresult rv = InitDatabase(); + + MonitorAutoLock lockMonitor(mThreadObserver->GetMonitor()); + + if (NS_FAILED(rv)) { + mStatus = rv; + mStopIOThread = true; + return; + } + + // Create an nsIThread for the current PRThread, so we can observe runnables + // dispatched to it. + nsCOMPtr<nsIThread> thread = NS_GetCurrentThread(); + nsCOMPtr<nsIThreadInternal> threadInternal = do_QueryInterface(thread); + MOZ_ASSERT(threadInternal); // Should always succeed. + threadInternal->SetObserver(mThreadObserver); + + while (MOZ_LIKELY(!mStopIOThread || mPreloads.Length() || + mPendingTasks.HasTasks() || + mThreadObserver->HasPendingEvents())) { + // Process xpcom events first. + while (MOZ_UNLIKELY(mThreadObserver->HasPendingEvents())) { + mThreadObserver->ClearPendingEvents(); + MonitorAutoUnlock unlock(mThreadObserver->GetMonitor()); + bool processedEvent; + do { + rv = thread->ProcessNextEvent(false, &processedEvent); + } while (NS_SUCCEEDED(rv) && processedEvent); + } + + TimeDuration timeUntilFlush = TimeUntilFlush(); + if (MOZ_UNLIKELY(timeUntilFlush.IsZero())) { + // Flush time is up or flush has been forced, do it now. + UnscheduleFlush(); + if (mPendingTasks.Prepare()) { + { + MonitorAutoUnlock unlockMonitor(mThreadObserver->GetMonitor()); + rv = mPendingTasks.Execute(this); + } + + if (!mPendingTasks.Finalize(rv)) { + mStatus = rv; + NS_WARNING("localStorage DB access broken"); + } + } + NotifyFlushCompletion(); + } else if (MOZ_LIKELY(mPreloads.Length())) { + UniquePtr<DBOperation> op(mPreloads[0]); + mPreloads.RemoveElementAt(0); + { + MonitorAutoUnlock unlockMonitor(mThreadObserver->GetMonitor()); + op->PerformAndFinalize(this); + } + + if (op->Type() == DBOperation::opPreloadUrgent) { + SetDefaultPriority(); // urgent preload unscheduled + } + } else if (MOZ_UNLIKELY(!mStopIOThread)) { + AUTO_PROFILER_LABEL("StorageDBThread::ThreadFunc::Wait", IDLE); + lockMonitor.Wait(timeUntilFlush); + } + } // thread loop + + mStatus = ShutdownDatabase(); + + if (threadInternal) { + threadInternal->SetObserver(nullptr); + } +} + +NS_IMPL_ISUPPORTS(StorageDBThread::ThreadObserver, nsIThreadObserver) + +NS_IMETHODIMP +StorageDBThread::ThreadObserver::OnDispatchedEvent() { + MonitorAutoLock lock(mMonitor); + mHasPendingEvents = true; + lock.Notify(); + return NS_OK; +} + +NS_IMETHODIMP +StorageDBThread::ThreadObserver::OnProcessNextEvent(nsIThreadInternal* aThread, + bool mayWait) { + return NS_OK; +} + +NS_IMETHODIMP +StorageDBThread::ThreadObserver::AfterProcessNextEvent( + nsIThreadInternal* aThread, bool eventWasProcessed) { + return NS_OK; +} + +nsresult StorageDBThread::OpenDatabaseConnection() { + nsresult rv; + + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<mozIStorageService> service = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (mPrivateBrowsingId == 0) { + MOZ_ASSERT(mDatabaseFile); + + rv = service->OpenUnsharedDatabase(mDatabaseFile, + getter_AddRefs(mWorkerConnection)); + if (rv == NS_ERROR_FILE_CORRUPTED) { + // delete the db and try opening again + rv = mDatabaseFile->Remove(false); + NS_ENSURE_SUCCESS(rv, rv); + rv = service->OpenUnsharedDatabase(mDatabaseFile, + getter_AddRefs(mWorkerConnection)); + } + } else { + MOZ_ASSERT(mPrivateBrowsingId == 1); + + rv = service->OpenSpecialDatabase(kMozStorageMemoryStorageKey, + "lsprivatedb"_ns, + getter_AddRefs(mWorkerConnection)); + } + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult StorageDBThread::OpenAndUpdateDatabase() { + nsresult rv; + + // Here we are on the worker thread. This opens the worker connection. + MOZ_ASSERT(!NS_IsMainThread()); + + rv = OpenDatabaseConnection(); + NS_ENSURE_SUCCESS(rv, rv); + + // SQLite doesn't support WAL journals for in-memory databases. + if (mPrivateBrowsingId == 0) { + rv = TryJournalMode(); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult StorageDBThread::InitDatabase() { + nsresult rv; + + // Here we are on the worker thread. This opens the worker connection. + MOZ_ASSERT(!NS_IsMainThread()); + + rv = OpenAndUpdateDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + + rv = StorageDBUpdater::Update(mWorkerConnection); + if (NS_FAILED(rv)) { + if (mPrivateBrowsingId == 0) { + // Update has failed, rather throw the database away and try + // opening and setting it up again. + rv = mWorkerConnection->Close(); + mWorkerConnection = nullptr; + NS_ENSURE_SUCCESS(rv, rv); + + rv = mDatabaseFile->Remove(false); + NS_ENSURE_SUCCESS(rv, rv); + + rv = OpenAndUpdateDatabase(); + } + NS_ENSURE_SUCCESS(rv, rv); + } + + // Create a read-only clone + (void)mWorkerConnection->Clone(true, getter_AddRefs(mReaderConnection)); + NS_ENSURE_TRUE(mReaderConnection, NS_ERROR_FAILURE); + + // Database open and all initiation operation are done. Switching this flag + // to true allow main thread to read directly from the database. If we would + // allow this sooner, we would have opened a window where main thread read + // might operate on a totally broken and incosistent database. + mDBReady = true; + + // List scopes having any stored data + nsCOMPtr<mozIStorageStatement> stmt; + // Note: result of this select must match StorageManager::CreateOrigin() + rv = mWorkerConnection->CreateStatement( + nsLiteralCString("SELECT DISTINCT originAttributes || ':' || originKey " + "FROM webappsstore2"), + getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + mozStorageStatementScoper scope(stmt); + + bool exists; + while (NS_SUCCEEDED(rv = stmt->ExecuteStep(&exists)) && exists) { + nsAutoCString foundOrigin; + rv = stmt->GetUTF8String(0, foundOrigin); + NS_ENSURE_SUCCESS(rv, rv); + + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); + mOriginsHavingData.PutEntry(foundOrigin); + } + + return NS_OK; +} + +nsresult StorageDBThread::SetJournalMode(bool aIsWal) { + nsresult rv; + + nsAutoCString stmtString(MOZ_STORAGE_UNIQUIFY_QUERY_STR + "PRAGMA journal_mode = "); + if (aIsWal) { + stmtString.AppendLiteral("wal"); + } else { + stmtString.AppendLiteral("truncate"); + } + + nsCOMPtr<mozIStorageStatement> stmt; + rv = mWorkerConnection->CreateStatement(stmtString, getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + mozStorageStatementScoper scope(stmt); + + bool hasResult = false; + rv = stmt->ExecuteStep(&hasResult); + NS_ENSURE_SUCCESS(rv, rv); + if (!hasResult) { + return NS_ERROR_FAILURE; + } + + nsAutoCString journalMode; + rv = stmt->GetUTF8String(0, journalMode); + NS_ENSURE_SUCCESS(rv, rv); + if ((aIsWal && !journalMode.EqualsLiteral("wal")) || + (!aIsWal && !journalMode.EqualsLiteral("truncate"))) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult StorageDBThread::TryJournalMode() { + nsresult rv; + + rv = SetJournalMode(true); + if (NS_FAILED(rv)) { + mWALModeEnabled = false; + + rv = SetJournalMode(false); + NS_ENSURE_SUCCESS(rv, rv); + } else { + mWALModeEnabled = true; + + rv = ConfigureWALBehavior(); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult StorageDBThread::ConfigureWALBehavior() { + // Get the DB's page size + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = mWorkerConnection->CreateStatement( + nsLiteralCString(MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA page_size"), + getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasResult = false; + rv = stmt->ExecuteStep(&hasResult); + NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hasResult, NS_ERROR_FAILURE); + + int32_t pageSize = 0; + rv = stmt->GetInt32(0, &pageSize); + NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && pageSize > 0, NS_ERROR_UNEXPECTED); + + // Set the threshold for auto-checkpointing the WAL. + // We don't want giant logs slowing down reads & shutdown. + int32_t thresholdInPages = + static_cast<int32_t>(MAX_WAL_SIZE_BYTES / pageSize); + nsAutoCString thresholdPragma("PRAGMA wal_autocheckpoint = "); + thresholdPragma.AppendInt(thresholdInPages); + rv = mWorkerConnection->ExecuteSimpleSQL(thresholdPragma); + NS_ENSURE_SUCCESS(rv, rv); + + // Set the maximum WAL log size to reduce footprint on mobile (large empty + // WAL files will be truncated) + nsAutoCString journalSizePragma("PRAGMA journal_size_limit = "); + // bug 600307: mak recommends setting this to 3 times the auto-checkpoint + // threshold + journalSizePragma.AppendInt(MAX_WAL_SIZE_BYTES * 3); + rv = mWorkerConnection->ExecuteSimpleSQL(journalSizePragma); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult StorageDBThread::ShutdownDatabase() { + // Has to be called on the worker thread. + MOZ_ASSERT(!NS_IsMainThread()); + + nsresult rv = mStatus; + + mDBReady = false; + + // Finalize the cached statements. + mReaderStatements.FinalizeStatements(); + mWorkerStatements.FinalizeStatements(); + + if (mReaderConnection) { + // No need to sync access to mReaderConnection since the main thread + // is right now joining this thread, unable to execute any events. + mReaderConnection->Close(); + mReaderConnection = nullptr; + } + + if (mWorkerConnection) { + rv = mWorkerConnection->Close(); + mWorkerConnection = nullptr; + } + + return rv; +} + +void StorageDBThread::ScheduleFlush() { + if (mDirtyEpoch) { + return; // Already scheduled + } + + // Must be non-zero to indicate we are scheduled + mDirtyEpoch = TimeStamp::Now(); + + // Wake the monitor from indefinite sleep... + (mThreadObserver->GetMonitor()).Notify(); +} + +void StorageDBThread::UnscheduleFlush() { + // We are just about to do the flush, drop flags + mFlushImmediately = false; + mDirtyEpoch = TimeStamp(); +} + +TimeDuration StorageDBThread::TimeUntilFlush() { + if (mFlushImmediately) { + return 0; // Do it now regardless the timeout. + } + + if (!mDirtyEpoch) { + return TimeDuration::Forever(); // No pending task... + } + + TimeStamp now = TimeStamp::Now(); + TimeDuration age = now - mDirtyEpoch; + static const TimeDuration kMaxAge = + TimeDuration::FromMilliseconds(FLUSHING_INTERVAL_MS); + if (age > kMaxAge) { + return 0; // It is time. + } + + return kMaxAge - age; // Time left. This is used to sleep the monitor. +} + +void StorageDBThread::NotifyFlushCompletion() { +#ifdef DOM_STORAGE_TESTS + if (!NS_IsMainThread()) { + RefPtr<nsRunnableMethod<StorageDBThread, void, false>> event = + NewNonOwningRunnableMethod( + "dom::StorageDBThread::NotifyFlushCompletion", this, + &StorageDBThread::NotifyFlushCompletion); + NS_DispatchToMainThread(event); + return; + } + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->NotifyObservers(nullptr, "domstorage-test-flushed", nullptr); + } +#endif +} + +// Helper SQL function classes + +namespace { + +class OriginAttrsPatternMatchSQLFunction final : public mozIStorageFunction { + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEFUNCTION + + explicit OriginAttrsPatternMatchSQLFunction( + OriginAttributesPattern const& aPattern) + : mPattern(aPattern) {} + + private: + OriginAttrsPatternMatchSQLFunction() = delete; + ~OriginAttrsPatternMatchSQLFunction() = default; + + OriginAttributesPattern mPattern; +}; + +NS_IMPL_ISUPPORTS(OriginAttrsPatternMatchSQLFunction, mozIStorageFunction) + +NS_IMETHODIMP +OriginAttrsPatternMatchSQLFunction::OnFunctionCall( + mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) { + nsresult rv; + + nsAutoCString suffix; + rv = aFunctionArguments->GetUTF8String(0, suffix); + NS_ENSURE_SUCCESS(rv, rv); + + OriginAttributes oa; + bool success = oa.PopulateFromSuffix(suffix); + NS_ENSURE_TRUE(success, NS_ERROR_FAILURE); + bool result = mPattern.Matches(oa); + + RefPtr<nsVariant> outVar(new nsVariant()); + rv = outVar->SetAsBool(result); + NS_ENSURE_SUCCESS(rv, rv); + + outVar.forget(aResult); + return NS_OK; +} + +} // namespace + +// StorageDBThread::DBOperation + +StorageDBThread::DBOperation::DBOperation(const OperationType aType, + LocalStorageCacheBridge* aCache, + const nsAString& aKey, + const nsAString& aValue) + : mType(aType), mCache(aCache), mKey(aKey), mValue(aValue) { + MOZ_ASSERT(mType == opPreload || mType == opPreloadUrgent || + mType == opAddItem || mType == opUpdateItem || + mType == opRemoveItem || mType == opClear || mType == opClearAll); + MOZ_COUNT_CTOR(StorageDBThread::DBOperation); +} + +StorageDBThread::DBOperation::DBOperation(const OperationType aType, + StorageUsageBridge* aUsage) + : mType(aType), mUsage(aUsage) { + MOZ_ASSERT(mType == opGetUsage); + MOZ_COUNT_CTOR(StorageDBThread::DBOperation); +} + +StorageDBThread::DBOperation::DBOperation(const OperationType aType, + const nsACString& aOriginNoSuffix) + : mType(aType), mCache(nullptr), mOrigin(aOriginNoSuffix) { + MOZ_ASSERT(mType == opClearMatchingOrigin); + MOZ_COUNT_CTOR(StorageDBThread::DBOperation); +} + +StorageDBThread::DBOperation::DBOperation( + const OperationType aType, const OriginAttributesPattern& aOriginNoSuffix) + : mType(aType), mCache(nullptr), mOriginPattern(aOriginNoSuffix) { + MOZ_ASSERT(mType == opClearMatchingOriginAttributes); + MOZ_COUNT_CTOR(StorageDBThread::DBOperation); +} + +StorageDBThread::DBOperation::~DBOperation() { + MOZ_COUNT_DTOR(StorageDBThread::DBOperation); +} + +const nsCString StorageDBThread::DBOperation::OriginNoSuffix() const { + if (mCache) { + return mCache->OriginNoSuffix(); + } + + return ""_ns; +} + +const nsCString StorageDBThread::DBOperation::OriginSuffix() const { + if (mCache) { + return mCache->OriginSuffix(); + } + + return ""_ns; +} + +const nsCString StorageDBThread::DBOperation::Origin() const { + if (mCache) { + return mCache->Origin(); + } + + return mOrigin; +} + +const nsCString StorageDBThread::DBOperation::Target() const { + switch (mType) { + case opAddItem: + case opUpdateItem: + case opRemoveItem: + return Origin() + "|"_ns + NS_ConvertUTF16toUTF8(mKey); + + default: + return Origin(); + } +} + +void StorageDBThread::DBOperation::PerformAndFinalize( + StorageDBThread* aThread) { + Finalize(Perform(aThread)); +} + +nsresult StorageDBThread::DBOperation::Perform(StorageDBThread* aThread) { + nsresult rv; + + switch (mType) { + case opPreload: + case opPreloadUrgent: { + // Already loaded? + if (mCache->Loaded()) { + break; + } + + StatementCache* statements; + if (MOZ_UNLIKELY(::mozilla::ipc::IsOnBackgroundThread())) { + statements = &aThread->mReaderStatements; + } else { + statements = &aThread->mWorkerStatements; + } + + // OFFSET is an optimization when we have to do a sync load + // and cache has already loaded some parts asynchronously. + // It skips keys we have already loaded. + nsCOMPtr<mozIStorageStatement> stmt = statements->GetCachedStatement( + "SELECT key, value FROM webappsstore2 " + "WHERE originAttributes = :originAttributes AND originKey = " + ":originKey " + "ORDER BY key LIMIT -1 OFFSET :offset"); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scope(stmt); + + rv = stmt->BindUTF8StringByName("originAttributes"_ns, + mCache->OriginSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindUTF8StringByName("originKey"_ns, mCache->OriginNoSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt32ByName("offset"_ns, + static_cast<int32_t>(mCache->LoadedCount())); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists; + while (NS_SUCCEEDED(rv = stmt->ExecuteStep(&exists)) && exists) { + nsAutoString key; + rv = stmt->GetString(0, key); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString value; + rv = stmt->GetString(1, value); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mCache->LoadItem(key, value)) { + break; + } + } + // The loop condition's call to ExecuteStep() may have terminated because + // !NS_SUCCEEDED(), we need an early return to cover that case. This also + // covers success cases as well, but that's inductively safe. + NS_ENSURE_SUCCESS(rv, rv); + break; + } + + case opGetUsage: { + // Bug 1676410 fixed a regression caused by bug 1165214. However, it + // turns out that 100% correct checking of the eTLD+1 usage is not + // possible to recover easily, see bug 1683299. +#if 0 + // This is how it should be done, but due to other problems like lack + // of usage synchronization between content processes, we temporarily + // disabled the matching using "%". + + nsCOMPtr<mozIStorageStatement> stmt = + aThread->mWorkerStatements.GetCachedStatement( + "SELECT SUM(LENGTH(key) + LENGTH(value)) FROM webappsstore2 " + "WHERE (originAttributes || ':' || originKey) LIKE :usageOrigin " + "ESCAPE '\\'"); + NS_ENSURE_STATE(stmt); + + mozStorageStatementScoper scope(stmt); + + // The database schema is built around cleverly reversing domain names + // (the "originKey") so that we can efficiently group usage by eTLD+1. + // "foo.example.org" has an eTLD+1 of ".example.org". They reverse to + // "gro.elpmaxe.oof" and "gro.elpmaxe." respectively, noting that the + // reversed eTLD+1 is a prefix of its reversed sub-domain. To this end, + // we can calculate all of the usage for an eTLD+1 by summing up all the + // rows which have the reversed eTLD+1 as a prefix. In SQL we can + // accomplish this using LIKE which provides for case-insensitive + // matching with "_" as a single-character wildcard match and "%" any + // sequence of zero or more characters. So by suffixing the reversed + // eTLD+1 and using "%" we get our case-insensitive (domain names are + // case-insensitive) matching. Note that although legal domain names + // don't include "_" or "%", file origins can include them, so we need + // to escape our OriginScope for correctness. + nsAutoCString originScopeEscaped; + rv = stmt->EscapeUTF8StringForLIKE(mUsage->OriginScope(), '\\', + originScopeEscaped); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindUTF8StringByName("usageOrigin"_ns, + originScopeEscaped + "%"_ns); + NS_ENSURE_SUCCESS(rv, rv); +#else + // This is the code before bug 1676410 and bug 1676973. The returned + // usage will be zero in most of the cases, but due to lack of usage + // synchronization between content processes we have to live with this + // semi-broken behaviour because it causes less harm than the matching + // using "%". + + nsCOMPtr<mozIStorageStatement> stmt = + aThread->mWorkerStatements.GetCachedStatement( + "SELECT SUM(LENGTH(key) + LENGTH(value)) FROM webappsstore2 " + "WHERE (originAttributes || ':' || originKey) LIKE :usageOrigin"); + NS_ENSURE_STATE(stmt); + + mozStorageStatementScoper scope(stmt); + + rv = stmt->BindUTF8StringByName("usageOrigin"_ns, mUsage->OriginScope()); + NS_ENSURE_SUCCESS(rv, rv); +#endif + + bool exists; + rv = stmt->ExecuteStep(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + int64_t usage = 0; + if (exists) { + rv = stmt->GetInt64(0, &usage); + NS_ENSURE_SUCCESS(rv, rv); + } + + mUsage->LoadUsage(usage); + break; + } + + case opAddItem: + case opUpdateItem: { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<mozIStorageStatement> stmt = + aThread->mWorkerStatements.GetCachedStatement( + "INSERT OR REPLACE INTO webappsstore2 (originAttributes, " + "originKey, scope, key, value) " + "VALUES (:originAttributes, :originKey, :scope, :key, :value) "); + NS_ENSURE_STATE(stmt); + + mozStorageStatementScoper scope(stmt); + + rv = stmt->BindUTF8StringByName("originAttributes"_ns, + mCache->OriginSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindUTF8StringByName("originKey"_ns, mCache->OriginNoSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + // Filling the 'scope' column just for downgrade compatibility reasons + rv = stmt->BindUTF8StringByName( + "scope"_ns, + Scheme0Scope(mCache->OriginSuffix(), mCache->OriginNoSuffix())); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindStringByName("key"_ns, mKey); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindStringByName("value"_ns, mValue); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + MonitorAutoLock monitor(aThread->mThreadObserver->GetMonitor()); + aThread->mOriginsHavingData.PutEntry(Origin()); + break; + } + + case opRemoveItem: { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<mozIStorageStatement> stmt = + aThread->mWorkerStatements.GetCachedStatement( + "DELETE FROM webappsstore2 " + "WHERE originAttributes = :originAttributes AND originKey = " + ":originKey " + "AND key = :key "); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scope(stmt); + + rv = stmt->BindUTF8StringByName("originAttributes"_ns, + mCache->OriginSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindUTF8StringByName("originKey"_ns, mCache->OriginNoSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindStringByName("key"_ns, mKey); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + break; + } + + case opClear: { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<mozIStorageStatement> stmt = + aThread->mWorkerStatements.GetCachedStatement( + "DELETE FROM webappsstore2 " + "WHERE originAttributes = :originAttributes AND originKey = " + ":originKey"); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scope(stmt); + + rv = stmt->BindUTF8StringByName("originAttributes"_ns, + mCache->OriginSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindUTF8StringByName("originKey"_ns, mCache->OriginNoSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + MonitorAutoLock monitor(aThread->mThreadObserver->GetMonitor()); + aThread->mOriginsHavingData.RemoveEntry(Origin()); + break; + } + + case opClearAll: { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<mozIStorageStatement> stmt = + aThread->mWorkerStatements.GetCachedStatement( + "DELETE FROM webappsstore2"); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scope(stmt); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + MonitorAutoLock monitor(aThread->mThreadObserver->GetMonitor()); + aThread->mOriginsHavingData.Clear(); + break; + } + + case opClearMatchingOrigin: { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<mozIStorageStatement> stmt = + aThread->mWorkerStatements.GetCachedStatement( + "DELETE FROM webappsstore2" + " WHERE originKey GLOB :scope"); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scope(stmt); + + rv = stmt->BindUTF8StringByName("scope"_ns, mOrigin + "*"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + // No need to selectively clear mOriginsHavingData here. That hashtable + // only prevents preload for scopes with no data. Leaving a false record + // in it has a negligible effect on performance. + break; + } + + case opClearMatchingOriginAttributes: { + MOZ_ASSERT(!NS_IsMainThread()); + + // Register the ORIGIN_ATTRS_PATTERN_MATCH function, initialized with the + // pattern + nsCOMPtr<mozIStorageFunction> patternMatchFunction( + new OriginAttrsPatternMatchSQLFunction(mOriginPattern)); + + rv = aThread->mWorkerConnection->CreateFunction( + "ORIGIN_ATTRS_PATTERN_MATCH"_ns, 1, patternMatchFunction); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<mozIStorageStatement> stmt = + aThread->mWorkerStatements.GetCachedStatement( + "DELETE FROM webappsstore2" + " WHERE ORIGIN_ATTRS_PATTERN_MATCH(originAttributes)"); + + if (stmt) { + mozStorageStatementScoper scope(stmt); + rv = stmt->Execute(); + } else { + rv = NS_ERROR_UNEXPECTED; + } + + // Always remove the function + aThread->mWorkerConnection->RemoveFunction( + "ORIGIN_ATTRS_PATTERN_MATCH"_ns); + + NS_ENSURE_SUCCESS(rv, rv); + + // No need to selectively clear mOriginsHavingData here. That hashtable + // only prevents preload for scopes with no data. Leaving a false record + // in it has a negligible effect on performance. + break; + } + + default: + NS_ERROR("Unknown task type"); + break; + } + + return NS_OK; +} + +void StorageDBThread::DBOperation::Finalize(nsresult aRv) { + switch (mType) { + case opPreloadUrgent: + case opPreload: + if (NS_FAILED(aRv)) { + // When we are here, something failed when loading from the database. + // Notify that the storage is loaded to prevent deadlock of the main + // thread, even though it is actually empty or incomplete. + NS_WARNING("Failed to preload localStorage"); + } + + mCache->LoadDone(aRv); + break; + + case opGetUsage: + if (NS_FAILED(aRv)) { + mUsage->LoadUsage(0); + } + + break; + + default: + if (NS_FAILED(aRv)) { + NS_WARNING( + "localStorage update/clear operation failed," + " data may not persist or clean up"); + } + + break; + } +} + +// StorageDBThread::PendingOperations + +StorageDBThread::PendingOperations::PendingOperations() + : mFlushFailureCount(0) {} + +bool StorageDBThread::PendingOperations::HasTasks() const { + return !!mUpdates.Count() || !!mClears.Count(); +} + +namespace { + +bool OriginPatternMatches(const nsACString& aOriginSuffix, + const OriginAttributesPattern& aPattern) { + OriginAttributes oa; + DebugOnly<bool> rv = oa.PopulateFromSuffix(aOriginSuffix); + MOZ_ASSERT(rv); + return aPattern.Matches(oa); +} + +} // namespace + +bool StorageDBThread::PendingOperations::CheckForCoalesceOpportunity( + DBOperation* aNewOp, DBOperation::OperationType aPendingType, + DBOperation::OperationType aNewType) { + if (aNewOp->Type() != aNewType) { + return false; + } + + StorageDBThread::DBOperation* pendingTask; + if (!mUpdates.Get(aNewOp->Target(), &pendingTask)) { + return false; + } + + if (pendingTask->Type() != aPendingType) { + return false; + } + + return true; +} + +void StorageDBThread::PendingOperations::Add( + StorageDBThread::DBOperation* aOperation) { + // Optimize: when a key to remove has never been written to disk + // just bypass this operation. A key is new when an operation scheduled + // to write it to the database is of type opAddItem. + if (CheckForCoalesceOpportunity(aOperation, DBOperation::opAddItem, + DBOperation::opRemoveItem)) { + mUpdates.Remove(aOperation->Target()); + delete aOperation; + return; + } + + // Optimize: when changing a key that is new and has never been + // written to disk, keep type of the operation to store it at opAddItem. + // This allows optimization to just forget adding a new key when + // it is removed from the storage before flush. + if (CheckForCoalesceOpportunity(aOperation, DBOperation::opAddItem, + DBOperation::opUpdateItem)) { + aOperation->mType = DBOperation::opAddItem; + } + + // Optimize: to prevent lose of remove operation on a key when doing + // remove/set/remove on a previously existing key we have to change + // opAddItem to opUpdateItem on the new operation when there is opRemoveItem + // pending for the key. + if (CheckForCoalesceOpportunity(aOperation, DBOperation::opRemoveItem, + DBOperation::opAddItem)) { + aOperation->mType = DBOperation::opUpdateItem; + } + + switch (aOperation->Type()) { + // Operations on single keys + + case DBOperation::opAddItem: + case DBOperation::opUpdateItem: + case DBOperation::opRemoveItem: + // Override any existing operation for the target (=scope+key). + mUpdates.Put(aOperation->Target(), aOperation); + break; + + // Clear operations + + case DBOperation::opClear: + case DBOperation::opClearMatchingOrigin: + case DBOperation::opClearMatchingOriginAttributes: + // Drop all update (insert/remove) operations for equivavelent or matching + // scope. We do this as an optimization as well as a must based on the + // logic, if we would not delete the update tasks, changes would have been + // stored to the database after clear operations have been executed. + for (auto iter = mUpdates.Iter(); !iter.Done(); iter.Next()) { + const auto& pendingTask = iter.Data(); + + if (aOperation->Type() == DBOperation::opClear && + (pendingTask->OriginNoSuffix() != aOperation->OriginNoSuffix() || + pendingTask->OriginSuffix() != aOperation->OriginSuffix())) { + continue; + } + + if (aOperation->Type() == DBOperation::opClearMatchingOrigin && + !StringBeginsWith(pendingTask->OriginNoSuffix(), + aOperation->Origin())) { + continue; + } + + if (aOperation->Type() == + DBOperation::opClearMatchingOriginAttributes && + !OriginPatternMatches(pendingTask->OriginSuffix(), + aOperation->OriginPattern())) { + continue; + } + + iter.Remove(); + } + + mClears.Put(aOperation->Target(), aOperation); + break; + + case DBOperation::opClearAll: + // Drop simply everything, this is a super-operation. + mUpdates.Clear(); + mClears.Clear(); + mClears.Put(aOperation->Target(), aOperation); + break; + + default: + MOZ_ASSERT(false); + break; + } +} + +bool StorageDBThread::PendingOperations::Prepare() { + // Called under the lock + + // First collect clear operations and then updates, we can + // do this since whenever a clear operation for a scope is + // scheduled, we drop all updates matching that scope. So, + // all scope-related update operations we have here now were + // scheduled after the clear operations. + for (auto iter = mClears.Iter(); !iter.Done(); iter.Next()) { + mExecList.AppendElement(std::move(iter.Data())); + } + mClears.Clear(); + + for (auto iter = mUpdates.Iter(); !iter.Done(); iter.Next()) { + mExecList.AppendElement(std::move(iter.Data())); + } + mUpdates.Clear(); + + return !!mExecList.Length(); +} + +nsresult StorageDBThread::PendingOperations::Execute(StorageDBThread* aThread) { + // Called outside the lock + + mozStorageTransaction transaction(aThread->mWorkerConnection, false); + + nsresult rv; + + for (uint32_t i = 0; i < mExecList.Length(); ++i) { + const auto& task = mExecList[i]; + rv = task->Perform(aThread); + if (NS_FAILED(rv)) { + return rv; + } + } + + rv = transaction.Commit(); + if (NS_FAILED(rv)) { + return rv; + } + + return NS_OK; +} + +bool StorageDBThread::PendingOperations::Finalize(nsresult aRv) { + // Called under the lock + + // The list is kept on a failure to retry it + if (NS_FAILED(aRv)) { + // XXX Followup: we may try to reopen the database and flush these + // pending tasks, however testing showed that even though I/O is actually + // broken some amount of operations is left in sqlite+system buffers and + // seems like successfully flushed to disk. + // Tested by removing a flash card and disconnecting from network while + // using a network drive on Windows system. + NS_WARNING("Flush operation on localStorage database failed"); + + ++mFlushFailureCount; + + return mFlushFailureCount >= 5; + } + + mFlushFailureCount = 0; + mExecList.Clear(); + return true; +} + +namespace { + +bool FindPendingClearForOrigin( + const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix, + StorageDBThread::DBOperation* aPendingOperation) { + if (aPendingOperation->Type() == StorageDBThread::DBOperation::opClearAll) { + return true; + } + + if (aPendingOperation->Type() == StorageDBThread::DBOperation::opClear && + aOriginNoSuffix == aPendingOperation->OriginNoSuffix() && + aOriginSuffix == aPendingOperation->OriginSuffix()) { + return true; + } + + if (aPendingOperation->Type() == + StorageDBThread::DBOperation::opClearMatchingOrigin && + StringBeginsWith(aOriginNoSuffix, aPendingOperation->Origin())) { + return true; + } + + if (aPendingOperation->Type() == + StorageDBThread::DBOperation::opClearMatchingOriginAttributes && + OriginPatternMatches(aOriginSuffix, aPendingOperation->OriginPattern())) { + return true; + } + + return false; +} + +} // namespace + +bool StorageDBThread::PendingOperations::IsOriginClearPending( + const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix) const { + // Called under the lock + + for (auto iter = mClears.ConstIter(); !iter.Done(); iter.Next()) { + if (FindPendingClearForOrigin(aOriginSuffix, aOriginNoSuffix, + iter.UserData())) { + return true; + } + } + + for (uint32_t i = 0; i < mExecList.Length(); ++i) { + if (FindPendingClearForOrigin(aOriginSuffix, aOriginNoSuffix, + mExecList[i].get())) { + return true; + } + } + + return false; +} + +namespace { + +bool FindPendingUpdateForOrigin( + const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix, + StorageDBThread::DBOperation* aPendingOperation) { + if ((aPendingOperation->Type() == StorageDBThread::DBOperation::opAddItem || + aPendingOperation->Type() == + StorageDBThread::DBOperation::opUpdateItem || + aPendingOperation->Type() == + StorageDBThread::DBOperation::opRemoveItem) && + aOriginNoSuffix == aPendingOperation->OriginNoSuffix() && + aOriginSuffix == aPendingOperation->OriginSuffix()) { + return true; + } + + return false; +} + +} // namespace + +bool StorageDBThread::PendingOperations::IsOriginUpdatePending( + const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix) const { + // Called under the lock + + for (auto iter = mUpdates.ConstIter(); !iter.Done(); iter.Next()) { + if (FindPendingUpdateForOrigin(aOriginSuffix, aOriginNoSuffix, + iter.UserData())) { + return true; + } + } + + for (uint32_t i = 0; i < mExecList.Length(); ++i) { + if (FindPendingUpdateForOrigin(aOriginSuffix, aOriginNoSuffix, + mExecList[i].get())) { + return true; + } + } + + return false; +} + +nsresult StorageDBThread::InitHelper::SyncDispatchAndReturnProfilePath( + nsAString& aProfilePath) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this)); + + mozilla::MutexAutoLock autolock(mMutex); + while (mWaiting) { + mCondVar.Wait(); + } + + if (NS_WARN_IF(NS_FAILED(mMainThreadResultCode))) { + return mMainThreadResultCode; + } + + aProfilePath = mProfilePath; + return NS_OK; +} + +NS_IMETHODIMP +StorageDBThread::InitHelper::Run() { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv = GetProfilePath(mProfilePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + mMainThreadResultCode = rv; + } + + mozilla::MutexAutoLock lock(mMutex); + MOZ_ASSERT(mWaiting); + + mWaiting = false; + mCondVar.Notify(); + + return NS_OK; +} + +NS_IMETHODIMP +StorageDBThread::NoteBackgroundThreadRunnable::Run() { + MOZ_ASSERT(NS_IsMainThread()); + + StorageObserver* observer = StorageObserver::Self(); + MOZ_ASSERT(observer); + + observer->NoteBackgroundThread(mPrivateBrowsingId, mOwningThread); + + return NS_OK; +} + +NS_IMETHODIMP +StorageDBThread::ShutdownRunnable::Run() { + if (NS_IsMainThread()) { + mDone = true; + + return NS_OK; + } + + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + StorageDBThread*& storageThread = sStorageThread[mPrivateBrowsingId]; + if (storageThread) { + sStorageThreadDown[mPrivateBrowsingId] = true; + + storageThread->Shutdown(); + + delete storageThread; + storageThread = nullptr; + } + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this)); + + return NS_OK; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/StorageDBThread.h b/dom/storage/StorageDBThread.h new file mode 100644 index 0000000000..ee2f57d066 --- /dev/null +++ b/dom/storage/StorageDBThread.h @@ -0,0 +1,493 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_StorageDBThread_h +#define mozilla_dom_StorageDBThread_h + +#include "prthread.h" +#include "prinrval.h" +#include "nsTArray.h" +#include "mozilla/Atomics.h" +#include "mozilla/Monitor.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/storage/StatementCache.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtr.h" +#include "nsString.h" +#include "nsCOMPtr.h" +#include "nsClassHashtable.h" +#include "nsIFile.h" +#include "nsIThreadInternal.h" +#include "nsThreadUtils.h" + +class mozIStorageConnection; + +namespace mozilla { +namespace dom { + +class LocalStorageCacheBridge; +class StorageUsageBridge; +class StorageUsage; + +typedef mozilla::storage::StatementCache<mozIStorageStatement> StatementCache; + +// XXX Fix me! +// 1. Move comments to StorageDBThread/StorageDBChild. +// 2. Devirtualize relevant methods in StorageDBThread/StorageDBChild. +// 3. Remove relevant methods in StorageDBThread/StorageDBChild that are +// unused. +// 4. Remove this class completely. +// +// See bug 1387636 for more details. +#if 0 +// Interface used by the cache to post operations to the asynchronous +// database thread or process. +class StorageDBBridge +{ +public: + StorageDBBridge(); + virtual ~StorageDBBridge() {} + + // Ensures the database engine is started + virtual nsresult Init() = 0; + + // Releases the database and disallows its usage + virtual nsresult Shutdown() = 0; + + // Asynchronously fills the cache with data from the database for first use. + // When |aPriority| is true, the preload operation is scheduled as the first + // one. This method is responsible to keep hard reference to the cache for + // the time of the preload or, when preload cannot be performed, call + // LoadDone() immediately. + virtual void AsyncPreload(LocalStorageCacheBridge* aCache, + bool aPriority = false) = 0; + + // Asynchronously fill the |usage| object with actual usage of data by its + // scope. The scope is eTLD+1 tops, never deeper subdomains. + virtual void AsyncGetUsage(StorageUsageBridge* aUsage) = 0; + + // Synchronously fills the cache, when |aForceSync| is false and cache already + // got some data before, the method waits for the running preload to finish + virtual void SyncPreload(LocalStorageCacheBridge* aCache, + bool aForceSync = false) = 0; + + // Called when an existing key is modified in the storage, schedules update to + // the database + virtual nsresult AsyncAddItem(LocalStorageCacheBridge* aCache, + const nsAString& aKey, + const nsAString& aValue) = 0; + + // Called when an existing key is modified in the storage, schedules update to + // the database + virtual nsresult AsyncUpdateItem(LocalStorageCacheBridge* aCache, + const nsAString& aKey, + const nsAString& aValue) = 0; + + // Called when an item is removed from the storage, schedules delete of the + // key + virtual nsresult AsyncRemoveItem(LocalStorageCacheBridge* aCache, + const nsAString& aKey) = 0; + + // Called when the whole storage is cleared by the DOM API, schedules delete + // of the scope + virtual nsresult AsyncClear(LocalStorageCacheBridge* aCache) = 0; + + // Called when chrome deletes e.g. cookies, schedules delete of the whole + // database + virtual void AsyncClearAll() = 0; + + // Called when only a domain and its subdomains is about to clear + virtual void AsyncClearMatchingOrigin(const nsACString& aOriginNoSuffix) = 0; + + // Called when data matching an origin pattern have to be cleared + virtual void AsyncClearMatchingOriginAttributes(const OriginAttributesPattern& aPattern) = 0; + + // Forces scheduled DB operations to be early flushed to the disk + virtual void AsyncFlush() = 0; + + // Check whether the scope has any data stored on disk and is thus allowed to + // preload + virtual bool ShouldPreloadOrigin(const nsACString& aOriginNoSuffix) = 0; +}; +#endif + +// The implementation of the the database engine, this directly works +// with the sqlite or any other db API we are based on +// This class is resposible for collecting and processing asynchronous +// DB operations over caches (LocalStorageCache) communicating though +// LocalStorageCacheBridge interface class +class StorageDBThread final { + public: + class PendingOperations; + + // Representation of a singe database task, like adding and removing keys, + // (pre)loading the whole origin data, cleaning. + class DBOperation { + public: + typedef enum { + // Only operation that reads data from the database + opPreload, + // The same as opPreload, just executed with highest priority + opPreloadUrgent, + + // Load usage of a scope + opGetUsage, + + // Operations invoked by the DOM content API + opAddItem, + opUpdateItem, + opRemoveItem, + // Clears a specific single origin data + opClear, + + // Operations invoked by chrome + + // Clear all the data stored in the database, for all scopes, no + // exceptions + opClearAll, + // Clear data under a domain and all its subdomains regardless + // OriginAttributes value + opClearMatchingOrigin, + // Clear all data matching an OriginAttributesPattern regardless a domain + opClearMatchingOriginAttributes, + } OperationType; + + explicit DBOperation(const OperationType aType, + LocalStorageCacheBridge* aCache = nullptr, + const nsAString& aKey = u""_ns, + const nsAString& aValue = u""_ns); + DBOperation(const OperationType aType, StorageUsageBridge* aUsage); + DBOperation(const OperationType aType, const nsACString& aOriginNoSuffix); + DBOperation(const OperationType aType, + const OriginAttributesPattern& aOriginNoSuffix); + ~DBOperation(); + + // Executes the operation, doesn't necessarity have to be called on the I/O + // thread + void PerformAndFinalize(StorageDBThread* aThread); + + // Finalize the operation, i.e. do any internal cleanup and finish calls + void Finalize(nsresult aRv); + + // The operation type + OperationType Type() const { return mType; } + + // The origin in the database usage format (reversed) + const nsCString OriginNoSuffix() const; + + // The origin attributes suffix + const nsCString OriginSuffix() const; + + // |origin suffix + origin key| the operation is working with or a scope + // pattern to delete with simple SQL's "LIKE %" from the database. + const nsCString Origin() const; + + // |origin suffix + origin key + key| the operation is working with + const nsCString Target() const; + + // Pattern to delete matching data with this op + const OriginAttributesPattern& OriginPattern() const { + return mOriginPattern; + } + + private: + // The operation implementation body + nsresult Perform(StorageDBThread* aThread); + + friend class PendingOperations; + OperationType mType; + RefPtr<LocalStorageCacheBridge> mCache; + RefPtr<StorageUsageBridge> mUsage; + nsString const mKey; + nsString const mValue; + nsCString const mOrigin; + OriginAttributesPattern const mOriginPattern; + }; + + // Encapsulation of collective and coalescing logic for all pending operations + // except preloads that are handled separately as priority operations + class PendingOperations { + public: + PendingOperations(); + + // Method responsible for coalescing redundant update operations with the + // same |Target()| or clear operations with the same or matching |Origin()| + void Add(DBOperation* aOperation); + + // True when there are some scheduled operations to flush on disk + bool HasTasks() const; + + // Moves collected operations to a local flat list to allow execution of the + // operation list out of the thread lock + bool Prepare(); + + // Executes the previously |Prepared()'ed| list of operations, returns + // result, but doesn't handle it in any way in case of a failure + nsresult Execute(StorageDBThread* aThread); + + // Finalizes the pending operation list, returns false when too many + // operations failed to flush what indicates a long standing issue with the + // database access. + bool Finalize(nsresult aRv); + + // true when a clear that deletes the given origin attr pattern and/or + // origin key is among the pending operations; when a preload for that scope + // is being scheduled, it must be finished right away + bool IsOriginClearPending(const nsACString& aOriginSuffix, + const nsACString& aOriginNoSuffix) const; + + // Checks whether there is a pending update operation for this scope. + bool IsOriginUpdatePending(const nsACString& aOriginSuffix, + const nsACString& aOriginNoSuffix) const; + + private: + // Returns true iff new operation is of type newType and there is a pending + // operation of type pendingType for the same key (target). + bool CheckForCoalesceOpportunity(DBOperation* aNewOp, + DBOperation::OperationType aPendingType, + DBOperation::OperationType aNewType); + + // List of all clearing operations, executed first + nsClassHashtable<nsCStringHashKey, DBOperation> mClears; + + // List of all update/insert operations, executed as second + nsClassHashtable<nsCStringHashKey, DBOperation> mUpdates; + + // Collection of all tasks, valid only between Prepare() and Execute() + nsTArray<UniquePtr<DBOperation> > mExecList; + + // Number of failing flush attempts + uint32_t mFlushFailureCount; + }; + + class ThreadObserver final : public nsIThreadObserver { + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITHREADOBSERVER + + ThreadObserver() + : mHasPendingEvents(false), mMonitor("StorageThreadMonitor") {} + + bool HasPendingEvents() { + mMonitor.AssertCurrentThreadOwns(); + return mHasPendingEvents; + } + void ClearPendingEvents() { + mMonitor.AssertCurrentThreadOwns(); + mHasPendingEvents = false; + } + Monitor& GetMonitor() { return mMonitor; } + + private: + virtual ~ThreadObserver() = default; + bool mHasPendingEvents; + // The monitor we drive the thread with + Monitor mMonitor; + }; + + class InitHelper; + + class NoteBackgroundThreadRunnable; + + class ShutdownRunnable : public Runnable { + // Expected to be only 0 or 1. + const uint32_t mPrivateBrowsingId; + // Only touched on the main thread. + bool& mDone; + + public: + explicit ShutdownRunnable(const uint32_t aPrivateBrowsingId, bool& aDone) + : Runnable("dom::StorageDBThread::ShutdownRunnable"), + mPrivateBrowsingId(aPrivateBrowsingId), + mDone(aDone) { + MOZ_ASSERT(NS_IsMainThread()); + } + + private: + ~ShutdownRunnable() = default; + + NS_DECL_NSIRUNNABLE + }; + + public: + explicit StorageDBThread(uint32_t aPrivateBrowsingId); + virtual ~StorageDBThread() = default; + + static StorageDBThread* Get(uint32_t aPrivateBrowsingId); + + static StorageDBThread* GetOrCreate(const nsString& aProfilePath, + uint32_t aPrivateBrowsingId); + + static nsresult GetProfilePath(nsString& aProfilePath); + + virtual nsresult Init(const nsString& aProfilePath); + + // Flushes all uncommited data and stops the I/O thread. + virtual nsresult Shutdown(); + + virtual void AsyncPreload(LocalStorageCacheBridge* aCache, + bool aPriority = false) { + InsertDBOp(new DBOperation( + aPriority ? DBOperation::opPreloadUrgent : DBOperation::opPreload, + aCache)); + } + + virtual void SyncPreload(LocalStorageCacheBridge* aCache, + bool aForce = false); + + virtual void AsyncGetUsage(StorageUsageBridge* aUsage) { + InsertDBOp(new DBOperation(DBOperation::opGetUsage, aUsage)); + } + + virtual nsresult AsyncAddItem(LocalStorageCacheBridge* aCache, + const nsAString& aKey, + const nsAString& aValue) { + return InsertDBOp( + new DBOperation(DBOperation::opAddItem, aCache, aKey, aValue)); + } + + virtual nsresult AsyncUpdateItem(LocalStorageCacheBridge* aCache, + const nsAString& aKey, + const nsAString& aValue) { + return InsertDBOp( + new DBOperation(DBOperation::opUpdateItem, aCache, aKey, aValue)); + } + + virtual nsresult AsyncRemoveItem(LocalStorageCacheBridge* aCache, + const nsAString& aKey) { + return InsertDBOp(new DBOperation(DBOperation::opRemoveItem, aCache, aKey)); + } + + virtual nsresult AsyncClear(LocalStorageCacheBridge* aCache) { + return InsertDBOp(new DBOperation(DBOperation::opClear, aCache)); + } + + virtual void AsyncClearAll() { + InsertDBOp(new DBOperation(DBOperation::opClearAll)); + } + + virtual void AsyncClearMatchingOrigin(const nsACString& aOriginNoSuffix) { + InsertDBOp( + new DBOperation(DBOperation::opClearMatchingOrigin, aOriginNoSuffix)); + } + + virtual void AsyncClearMatchingOriginAttributes( + const OriginAttributesPattern& aPattern) { + InsertDBOp(new DBOperation(DBOperation::opClearMatchingOriginAttributes, + aPattern)); + } + + virtual void AsyncFlush(); + + virtual bool ShouldPreloadOrigin(const nsACString& aOrigin); + + // Get the complete list of scopes having data. + void GetOriginsHavingData(nsTArray<nsCString>* aOrigins); + + private: + nsCOMPtr<nsIFile> mDatabaseFile; + PRThread* mThread; + + // Used to observe runnables dispatched to our thread and to monitor it. + RefPtr<ThreadObserver> mThreadObserver; + + // Flag to stop, protected by the monitor returned by + // mThreadObserver->GetMonitor(). + bool mStopIOThread; + + // Whether WAL is enabled + bool mWALModeEnabled; + + // Whether DB has already been open, avoid races between main thread reads + // and pending DB init in the background I/O thread + Atomic<bool, ReleaseAcquire> mDBReady; + + // State of the database initiation + nsresult mStatus; + + // List of origins (including origin attributes suffix) having data, for + // optimization purposes only + nsTHashtable<nsCStringHashKey> mOriginsHavingData; + + // Connection used by the worker thread for all read and write ops + nsCOMPtr<mozIStorageConnection> mWorkerConnection; + + // Connection used only on the main thread for sync read operations + nsCOMPtr<mozIStorageConnection> mReaderConnection; + + StatementCache mWorkerStatements; + StatementCache mReaderStatements; + + // Time the first pending operation has been added to the pending operations + // list + TimeStamp mDirtyEpoch; + + // Flag to force immediate flush of all pending operations + bool mFlushImmediately; + + // List of preloading operations, in chronological or priority order. + // Executed prioritly over pending update operations. + nsTArray<DBOperation*> mPreloads; + + // Collector of pending update operations + PendingOperations mPendingTasks; + + // Expected to be only 0 or 1. + const uint32_t mPrivateBrowsingId; + + // Counter of calls for thread priority rising. + int32_t mPriorityCounter; + + // Helper to direct an operation to one of the arrays above; + // also checks IsOriginClearPending for preloads + nsresult InsertDBOp(DBOperation* aOperation); + + // Opens the database, first thing we do after start of the thread. + nsresult OpenDatabaseConnection(); + nsresult OpenAndUpdateDatabase(); + nsresult InitDatabase(); + nsresult ShutdownDatabase(); + + // Tries to establish WAL mode + nsresult SetJournalMode(bool aIsWal); + nsresult TryJournalMode(); + + // Sets the threshold for auto-checkpointing the WAL. + nsresult ConfigureWALBehavior(); + + void SetHigherPriority(); + void SetDefaultPriority(); + + // Ensures we flush pending tasks in some reasonble time + void ScheduleFlush(); + + // Called when flush of pending tasks is being executed + void UnscheduleFlush(); + + // This method is used for two purposes: + // 1. as a value passed to monitor.Wait() method + // 2. as in indicator that flush has to be performed + // + // Return: + // - TimeDuration::Forever() when no pending tasks are scheduled + // - Non-zero TimeDuration when tasks have been scheduled, but it + // is still not time to perform the flush ; it is actual time to + // wait until the flush has to happen. + // - 0 TimeDuration when it is time to do the flush + TimeDuration TimeUntilFlush(); + + // Notifies to the main thread that flush has completed + void NotifyFlushCompletion(); + + // Thread loop + static void ThreadFunc(void* aArg); + void ThreadFunc(); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_StorageDBThread_h diff --git a/dom/storage/StorageDBUpdater.cpp b/dom/storage/StorageDBUpdater.cpp new file mode 100644 index 0000000000..df74ac1da0 --- /dev/null +++ b/dom/storage/StorageDBUpdater.cpp @@ -0,0 +1,471 @@ +/* -*- 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 "LocalStorageManager.h" +#include "StorageUtils.h" + +#include "mozIStorageBindingParams.h" +#include "mozIStorageValueArray.h" +#include "mozIStorageFunction.h" +#include "mozilla/BasePrincipal.h" +#include "nsVariant.h" +#include "mozilla/Tokenizer.h" +#include "mozIStorageConnection.h" +#include "mozStorageHelper.h" + +// Current version of the database schema +#define CURRENT_SCHEMA_VERSION 2 + +namespace mozilla { +namespace dom { + +using namespace StorageUtils; + +namespace { + +class nsReverseStringSQLFunction final : public mozIStorageFunction { + ~nsReverseStringSQLFunction() = default; + + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEFUNCTION +}; + +NS_IMPL_ISUPPORTS(nsReverseStringSQLFunction, mozIStorageFunction) + +NS_IMETHODIMP +nsReverseStringSQLFunction::OnFunctionCall( + mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) { + nsresult rv; + + nsAutoCString stringToReverse; + rv = aFunctionArguments->GetUTF8String(0, stringToReverse); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString result; + ReverseString(stringToReverse, result); + + RefPtr<nsVariant> outVar(new nsVariant()); + rv = outVar->SetAsAUTF8String(result); + NS_ENSURE_SUCCESS(rv, rv); + + outVar.forget(aResult); + return NS_OK; +} + +// "scope" to "origin attributes suffix" and "origin key" convertor + +class ExtractOriginData : protected mozilla::Tokenizer { + public: + ExtractOriginData(const nsACString& scope, nsACString& suffix, + nsACString& origin) + : mozilla::Tokenizer(scope) { + using mozilla::OriginAttributes; + + // Parse optional appId:isInIsolatedMozBrowserElement: string, in case + // we don't find it, the scope is our new origin key and suffix + // is empty. + suffix.Truncate(); + origin.Assign(scope); + + // Bail out if it isn't appId. + // AppId doesn't exist any more but we could have old storage data... + uint32_t appId; + if (!ReadInteger(&appId)) { + return; + } + + // Should be followed by a colon. + if (!CheckChar(':')) { + return; + } + + // Bail out if it isn't 'isolatedBrowserFlag'. + nsDependentCSubstring isolatedBrowserFlag; + if (!ReadWord(isolatedBrowserFlag)) { + return; + } + + bool inIsolatedMozBrowser = isolatedBrowserFlag == "t"; + bool notInIsolatedBrowser = isolatedBrowserFlag == "f"; + if (!inIsolatedMozBrowser && !notInIsolatedBrowser) { + return; + } + + // Should be followed by a colon. + if (!CheckChar(':')) { + return; + } + + // OK, we have found appId and inIsolatedMozBrowser flag, create the suffix + // from it and take the rest as the origin key. + + // If the profile went through schema 1 -> schema 0 -> schema 1 switching + // we may have stored the full attributes origin suffix when there were + // more than just appId and inIsolatedMozBrowser set on storage principal's + // OriginAttributes. + // + // To preserve full uniqueness we store this suffix to the scope key. + // Schema 0 code will just ignore it while keeping the scoping unique. + // + // The whole scope string is in one of the following forms (when we are + // here): + // + // "1001:f:^appId=1001&inBrowser=false&addonId=101:gro.allizom.rxd.:https:443" + // "1001:f:gro.allizom.rxd.:https:443" + // | + // +- the parser cursor position. + // + // If there is '^', the full origin attributes suffix follows. We search + // for ':' since it is the delimiter used in the scope string and is never + // contained in the origin attributes suffix. Remaining string after + // the comma is the reversed-domain+schema+port tuple. + Record(); + if (CheckChar('^')) { + Token t; + while (Next(t)) { + if (t.Equals(Token::Char(':'))) { + Claim(suffix); + break; + } + } + } else { + OriginAttributes attrs(inIsolatedMozBrowser); + attrs.CreateSuffix(suffix); + } + + // Consume the rest of the input as "origin". + origin.Assign(Substring(mCursor, mEnd)); + } +}; + +class GetOriginParticular final : public mozIStorageFunction { + public: + enum EParticular { ORIGIN_ATTRIBUTES_SUFFIX, ORIGIN_KEY }; + + explicit GetOriginParticular(EParticular aParticular) + : mParticular(aParticular) {} + + private: + GetOriginParticular() = delete; + ~GetOriginParticular() = default; + + EParticular mParticular; + + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEFUNCTION +}; + +NS_IMPL_ISUPPORTS(GetOriginParticular, mozIStorageFunction) + +NS_IMETHODIMP +GetOriginParticular::OnFunctionCall(mozIStorageValueArray* aFunctionArguments, + nsIVariant** aResult) { + nsresult rv; + + nsAutoCString scope; + rv = aFunctionArguments->GetUTF8String(0, scope); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString suffix, origin; + ExtractOriginData extractor(scope, suffix, origin); + + nsCOMPtr<nsIWritableVariant> outVar(new nsVariant()); + + switch (mParticular) { + case EParticular::ORIGIN_ATTRIBUTES_SUFFIX: + rv = outVar->SetAsAUTF8String(suffix); + break; + case EParticular::ORIGIN_KEY: + rv = outVar->SetAsAUTF8String(origin); + break; + } + + NS_ENSURE_SUCCESS(rv, rv); + + outVar.forget(aResult); + return NS_OK; +} + +class StripOriginAddonId final : public mozIStorageFunction { + public: + explicit StripOriginAddonId() = default; + + private: + ~StripOriginAddonId() = default; + + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEFUNCTION +}; + +NS_IMPL_ISUPPORTS(StripOriginAddonId, mozIStorageFunction) + +NS_IMETHODIMP +StripOriginAddonId::OnFunctionCall(mozIStorageValueArray* aFunctionArguments, + nsIVariant** aResult) { + nsresult rv; + + nsAutoCString suffix; + rv = aFunctionArguments->GetUTF8String(0, suffix); + NS_ENSURE_SUCCESS(rv, rv); + + // Deserialize and re-serialize to automatically drop any obsolete origin + // attributes. + OriginAttributes oa; + bool ok = oa.PopulateFromSuffix(suffix); + NS_ENSURE_TRUE(ok, NS_ERROR_FAILURE); + + nsAutoCString newSuffix; + oa.CreateSuffix(newSuffix); + + nsCOMPtr<nsIWritableVariant> outVar = new nsVariant(); + rv = outVar->SetAsAUTF8String(newSuffix); + NS_ENSURE_SUCCESS(rv, rv); + + outVar.forget(aResult); + return NS_OK; +} + +} // namespace + +namespace StorageDBUpdater { + +nsresult CreateSchema1Tables(mozIStorageConnection* aWorkerConnection) { + nsresult rv; + + rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString( + "CREATE TABLE IF NOT EXISTS webappsstore2 (" + "originAttributes TEXT, " + "originKey TEXT, " + "scope TEXT, " // Only for schema0 downgrade compatibility + "key TEXT, " + "value TEXT)")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aWorkerConnection->ExecuteSimpleSQL( + nsLiteralCString("CREATE UNIQUE INDEX IF NOT EXISTS origin_key_index" + " ON webappsstore2(originAttributes, originKey, key)")); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult Update(mozIStorageConnection* aWorkerConnection) { + nsresult rv; + + mozStorageTransaction transaction(aWorkerConnection, false); + + bool doVacuum = false; + + int32_t schemaVer; + rv = aWorkerConnection->GetSchemaVersion(&schemaVer); + NS_ENSURE_SUCCESS(rv, rv); + + // downgrade (v0) -> upgrade (v1+) specific code + if (schemaVer >= 1) { + bool schema0IndexExists; + rv = aWorkerConnection->IndexExists("scope_key_index"_ns, + &schema0IndexExists); + NS_ENSURE_SUCCESS(rv, rv); + + if (schema0IndexExists) { + // If this index exists, the database (already updated to schema >1) + // has been run again on schema 0 code. That recreated that index + // and might store some new rows while updating only the 'scope' column. + // For such added rows we must fill the new 'origin*' columns correctly + // otherwise there would be a data loss. The safest way to do it is to + // simply run the whole update to schema 1 again. + schemaVer = 0; + } + } + + switch (schemaVer) { + case 0: { + bool webappsstore2Exists, webappsstoreExists, moz_webappsstoreExists; + + rv = aWorkerConnection->TableExists("webappsstore2"_ns, + &webappsstore2Exists); + NS_ENSURE_SUCCESS(rv, rv); + rv = aWorkerConnection->TableExists("webappsstore"_ns, + &webappsstoreExists); + NS_ENSURE_SUCCESS(rv, rv); + rv = aWorkerConnection->TableExists("moz_webappsstore"_ns, + &moz_webappsstoreExists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!webappsstore2Exists && !webappsstoreExists && + !moz_webappsstoreExists) { + // The database is empty, this is the first start. Just create the + // schema table and break to the next version to update to, i.e. bypass + // update from the old version. + + rv = CreateSchema1Tables(aWorkerConnection); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aWorkerConnection->SetSchemaVersion(CURRENT_SCHEMA_VERSION); + NS_ENSURE_SUCCESS(rv, rv); + + break; + } + + doVacuum = true; + + // Ensure Gecko 1.9.1 storage table + rv = aWorkerConnection->ExecuteSimpleSQL( + nsLiteralCString("CREATE TABLE IF NOT EXISTS webappsstore2 (" + "scope TEXT, " + "key TEXT, " + "value TEXT, " + "secure INTEGER, " + "owner TEXT)")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aWorkerConnection->ExecuteSimpleSQL( + nsLiteralCString("CREATE UNIQUE INDEX IF NOT EXISTS scope_key_index" + " ON webappsstore2(scope, key)")); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<mozIStorageFunction> function1(new nsReverseStringSQLFunction()); + NS_ENSURE_TRUE(function1, NS_ERROR_OUT_OF_MEMORY); + + rv = aWorkerConnection->CreateFunction("REVERSESTRING"_ns, 1, function1); + NS_ENSURE_SUCCESS(rv, rv); + + // Check if there is storage of Gecko 1.9.0 and if so, upgrade that + // storage to actual webappsstore2 table and drop the obsolete table. + // First process this newer table upgrade to priority potential duplicates + // from older storage table. + if (webappsstoreExists) { + rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString( + "INSERT OR IGNORE INTO " + "webappsstore2(scope, key, value, secure, owner) " + "SELECT REVERSESTRING(domain) || '.:', key, value, secure, owner " + "FROM webappsstore")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aWorkerConnection->ExecuteSimpleSQL("DROP TABLE webappsstore"_ns); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Check if there is storage of Gecko 1.8 and if so, upgrade that storage + // to actual webappsstore2 table and drop the obsolete table. Potential + // duplicates will be ignored. + if (moz_webappsstoreExists) { + rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString( + "INSERT OR IGNORE INTO " + "webappsstore2(scope, key, value, secure, owner) " + "SELECT REVERSESTRING(domain) || '.:', key, value, secure, domain " + "FROM moz_webappsstore")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aWorkerConnection->ExecuteSimpleSQL( + "DROP TABLE moz_webappsstore"_ns); + NS_ENSURE_SUCCESS(rv, rv); + } + + aWorkerConnection->RemoveFunction("REVERSESTRING"_ns); + + // Update the scoping to match the new implememntation: split to oa suffix + // and origin key First rename the old table, we want to remove some + // columns no longer needed, but even before that drop all indexes from it + // (CREATE IF NOT EXISTS for index on the new table would falsely find the + // index!) + rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString( + "DROP INDEX IF EXISTS webappsstore2.origin_key_index")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString( + "DROP INDEX IF EXISTS webappsstore2.scope_key_index")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString( + "ALTER TABLE webappsstore2 RENAME TO webappsstore2_old")); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<mozIStorageFunction> oaSuffixFunc(new GetOriginParticular( + GetOriginParticular::ORIGIN_ATTRIBUTES_SUFFIX)); + rv = aWorkerConnection->CreateFunction("GET_ORIGIN_SUFFIX"_ns, 1, + oaSuffixFunc); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<mozIStorageFunction> originKeyFunc( + new GetOriginParticular(GetOriginParticular::ORIGIN_KEY)); + rv = aWorkerConnection->CreateFunction("GET_ORIGIN_KEY"_ns, 1, + originKeyFunc); + NS_ENSURE_SUCCESS(rv, rv); + + // Here we ensure this schema tables when we are updating. + rv = CreateSchema1Tables(aWorkerConnection); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString( + "INSERT OR IGNORE INTO " + "webappsstore2 (originAttributes, originKey, scope, key, value) " + "SELECT GET_ORIGIN_SUFFIX(scope), GET_ORIGIN_KEY(scope), scope, key, " + "value " + "FROM webappsstore2_old")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aWorkerConnection->ExecuteSimpleSQL( + "DROP TABLE webappsstore2_old"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + aWorkerConnection->RemoveFunction("GET_ORIGIN_SUFFIX"_ns); + aWorkerConnection->RemoveFunction("GET_ORIGIN_KEY"_ns); + + rv = aWorkerConnection->SetSchemaVersion(1); + NS_ENSURE_SUCCESS(rv, rv); + + [[fallthrough]]; + } + case 1: { + nsCOMPtr<mozIStorageFunction> oaStripAddonId(new StripOriginAddonId()); + rv = aWorkerConnection->CreateFunction("STRIP_ADDON_ID"_ns, 1, + oaStripAddonId); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString( + "UPDATE webappsstore2 " + "SET originAttributes = STRIP_ADDON_ID(originAttributes) " + "WHERE originAttributes LIKE '^%'")); + NS_ENSURE_SUCCESS(rv, rv); + + aWorkerConnection->RemoveFunction("STRIP_ADDON_ID"_ns); + + rv = aWorkerConnection->SetSchemaVersion(2); + NS_ENSURE_SUCCESS(rv, rv); + + [[fallthrough]]; + } + case CURRENT_SCHEMA_VERSION: + // Ensure the tables and indexes are up. This is mostly a no-op + // in common scenarios. + rv = CreateSchema1Tables(aWorkerConnection); + NS_ENSURE_SUCCESS(rv, rv); + + // Nothing more to do here, this is the current schema version + break; + + default: + MOZ_ASSERT(false); + break; + } // switch + + rv = transaction.Commit(); + NS_ENSURE_SUCCESS(rv, rv); + + if (doVacuum) { + // In some cases this can make the disk file of the database significantly + // smaller. VACUUM cannot be executed inside a transaction. + rv = aWorkerConnection->ExecuteSimpleSQL("VACUUM"_ns); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +} // namespace StorageDBUpdater +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/StorageDBUpdater.h b/dom/storage/StorageDBUpdater.h new file mode 100644 index 0000000000..4d802135a5 --- /dev/null +++ b/dom/storage/StorageDBUpdater.h @@ -0,0 +1,22 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_StorageDBUpdater_h +#define mozilla_dom_StorageDBUpdater_h + +namespace mozilla { +namespace dom { + +namespace StorageDBUpdater { + +nsresult Update(mozIStorageConnection* aWorkerConnection); + +} // namespace StorageDBUpdater + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_StorageDBUpdater_h diff --git a/dom/storage/StorageIPC.cpp b/dom/storage/StorageIPC.cpp new file mode 100644 index 0000000000..20aeac427c --- /dev/null +++ b/dom/storage/StorageIPC.cpp @@ -0,0 +1,1555 @@ +/* -*- 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 "StorageIPC.h" + +#include "StorageUtils.h" +#include "LocalStorageManager.h" +#include "SessionStorageObserver.h" +#include "SessionStorageManager.h" +#include "SessionStorageCache.h" + +#include "mozilla/dom/LocalStorageCommon.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/PBackgroundParent.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/Unused.h" +#include "nsCOMPtr.h" +#include "nsIPrincipal.h" +#include "nsThreadUtils.h" + +namespace mozilla { +namespace dom { + +namespace { + +typedef nsClassHashtable<nsCStringHashKey, nsTArray<LocalStorageCacheParent*>> + LocalStorageCacheParentHashtable; + +StaticAutoPtr<LocalStorageCacheParentHashtable> gLocalStorageCacheParents; + +StorageDBChild* sStorageChild[2] = {nullptr, nullptr}; + +// False until we shut the storage child down. +bool sStorageChildDown[2] = {false, false}; + +} // namespace + +LocalStorageCacheChild::LocalStorageCacheChild(LocalStorageCache* aCache) + : mCache(aCache) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aCache); + aCache->AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(LocalStorageCacheChild); +} + +LocalStorageCacheChild::~LocalStorageCacheChild() { + AssertIsOnOwningThread(); + + MOZ_COUNT_DTOR(LocalStorageCacheChild); +} + +void LocalStorageCacheChild::SendDeleteMeInternal() { + AssertIsOnOwningThread(); + + if (mCache) { + mCache->ClearActor(); + mCache = nullptr; + + MOZ_ALWAYS_TRUE(PBackgroundLocalStorageCacheChild::SendDeleteMe()); + } +} + +void LocalStorageCacheChild::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + if (mCache) { + mCache->ClearActor(); + mCache = nullptr; + } +} + +mozilla::ipc::IPCResult LocalStorageCacheChild::RecvObserve( + const PrincipalInfo& aPrincipalInfo, + const PrincipalInfo& aCachePrincipalInfo, + const uint32_t& aPrivateBrowsingId, const nsString& aDocumentURI, + const nsString& aKey, const nsString& aOldValue, + const nsString& aNewValue) { + AssertIsOnOwningThread(); + + auto principalOrErr = PrincipalInfoToPrincipal(aPrincipalInfo); + if (NS_WARN_IF(principalOrErr.isErr())) { + return IPC_FAIL_NO_REASON(this); + } + + auto cachePrincipalOrErr = PrincipalInfoToPrincipal(aCachePrincipalInfo); + if (NS_WARN_IF(cachePrincipalOrErr.isErr())) { + return IPC_FAIL_NO_REASON(this); + } + + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + nsCOMPtr<nsIPrincipal> cachePrincipal = cachePrincipalOrErr.unwrap(); + + if (StorageUtils::PrincipalsEqual(principal, cachePrincipal)) { + Storage::NotifyChange(/* aStorage */ nullptr, principal, aKey, aOldValue, + aNewValue, + /* aStorageType */ u"localStorage", aDocumentURI, + /* aIsPrivate */ !!aPrivateBrowsingId, + /* aImmediateDispatch */ true); + } + + return IPC_OK(); +} + +// ---------------------------------------------------------------------------- +// Child +// ---------------------------------------------------------------------------- + +class StorageDBChild::ShutdownObserver final : public nsIObserver { + // Expected to be only 0 or 1. + const uint32_t mPrivateBrowsingId; + + public: + explicit ShutdownObserver(const uint32_t aPrivateBrowsingId) + : mPrivateBrowsingId(aPrivateBrowsingId) { + MOZ_ASSERT(NS_IsMainThread()); + } + + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + private: + ~ShutdownObserver() { MOZ_ASSERT(NS_IsMainThread()); } +}; + +void StorageDBChild::AddIPDLReference() { + MOZ_ASSERT(!mIPCOpen, "Attempting to retain multiple IPDL references"); + mIPCOpen = true; + AddRef(); +} + +void StorageDBChild::ReleaseIPDLReference() { + MOZ_ASSERT(mIPCOpen, "Attempting to release non-existent IPDL reference"); + mIPCOpen = false; + Release(); +} + +StorageDBChild::StorageDBChild(LocalStorageManager* aManager, + const uint32_t aPrivateBrowsingId) + : mManager(aManager), + mPrivateBrowsingId(aPrivateBrowsingId), + mStatus(NS_OK), + mIPCOpen(false) { + MOZ_ASSERT(aPrivateBrowsingId <= 1); + MOZ_ASSERT(!NextGenLocalStorageEnabled()); +} + +StorageDBChild::~StorageDBChild() = default; + +// static +StorageDBChild* StorageDBChild::Get(const uint32_t aPrivateBrowsingId) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrivateBrowsingId <= 1); + MOZ_ASSERT(!NextGenLocalStorageEnabled()); + + return sStorageChild[aPrivateBrowsingId]; +} + +// static +StorageDBChild* StorageDBChild::GetOrCreate(const uint32_t aPrivateBrowsingId) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrivateBrowsingId <= 1); + MOZ_ASSERT(!NextGenLocalStorageEnabled()); + + StorageDBChild*& storageChild = sStorageChild[aPrivateBrowsingId]; + if (storageChild || sStorageChildDown[aPrivateBrowsingId]) { + // When sStorageChildDown is at true, sStorageChild is null. + // Checking sStorageChildDown flag here prevents reinitialization of + // the storage child after shutdown. + return storageChild; + } + + // Use LocalStorageManager::Ensure in case we're called from + // DOMSessionStorageManager's initializer and we haven't yet initialized the + // local storage manager. + RefPtr<StorageDBChild> newStorageChild = + new StorageDBChild(LocalStorageManager::Ensure(), aPrivateBrowsingId); + + nsresult rv = newStorageChild->Init(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + newStorageChild.forget(&storageChild); + + return storageChild; +} + +nsTHashtable<nsCStringHashKey>& StorageDBChild::OriginsHavingData() { + if (!mOriginsHavingData) { + mOriginsHavingData = MakeUnique<nsTHashtable<nsCStringHashKey>>(); + } + + return *mOriginsHavingData; +} + +nsresult StorageDBChild::Init() { + MOZ_ASSERT(NS_IsMainThread()); + + ::mozilla::ipc::PBackgroundChild* actor = + ::mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!actor)) { + return NS_ERROR_FAILURE; + } + + nsString profilePath; + if (XRE_IsParentProcess() && mPrivateBrowsingId == 0) { + nsresult rv = StorageDBThread::GetProfilePath(profilePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + AddIPDLReference(); + + actor->SendPBackgroundStorageConstructor(this, profilePath, + mPrivateBrowsingId); + + nsCOMPtr<nsIObserverService> observerService = services::GetObserverService(); + MOZ_ASSERT(observerService); + + nsCOMPtr<nsIObserver> observer = new ShutdownObserver(mPrivateBrowsingId); + + MOZ_ALWAYS_SUCCEEDS( + observerService->AddObserver(observer, "xpcom-shutdown", false)); + + return NS_OK; +} + +nsresult StorageDBChild::Shutdown() { + // There is nothing to do here, IPC will release automatically and + // the actual thread running on the parent process will also stop + // automatically in profile-before-change topic observer. + return NS_OK; +} + +void StorageDBChild::AsyncPreload(LocalStorageCacheBridge* aCache, + bool aPriority) { + if (mIPCOpen) { + // Adding ref to cache for the time of preload. This ensures a reference to + // to the cache and that all keys will load into this cache object. + mLoadingCaches.PutEntry(aCache); + SendAsyncPreload(aCache->OriginSuffix(), aCache->OriginNoSuffix(), + aPriority); + } else { + // No IPC, no love. But the LoadDone call is expected. + aCache->LoadDone(NS_ERROR_UNEXPECTED); + } +} + +void StorageDBChild::AsyncGetUsage(StorageUsageBridge* aUsage) { + if (mIPCOpen) { + SendAsyncGetUsage(aUsage->OriginScope()); + } +} + +void StorageDBChild::SyncPreload(LocalStorageCacheBridge* aCache, + bool aForceSync) { + if (NS_FAILED(mStatus)) { + aCache->LoadDone(mStatus); + return; + } + + if (!mIPCOpen) { + aCache->LoadDone(NS_ERROR_UNEXPECTED); + return; + } + + // There is no way to put the child process to a wait state to receive all + // incoming async responses from the parent, hence we have to do a sync + // preload instead. We are smart though, we only demand keys that are left to + // load in case the async preload has already loaded some keys. + nsTArray<nsString> keys, values; + nsresult rv; + SendPreload(aCache->OriginSuffix(), aCache->OriginNoSuffix(), + aCache->LoadedCount(), &keys, &values, &rv); + + for (uint32_t i = 0; i < keys.Length(); ++i) { + aCache->LoadItem(keys[i], values[i]); + } + + aCache->LoadDone(rv); +} + +nsresult StorageDBChild::AsyncAddItem(LocalStorageCacheBridge* aCache, + const nsAString& aKey, + const nsAString& aValue) { + if (NS_FAILED(mStatus) || !mIPCOpen) { + return mStatus; + } + + SendAsyncAddItem(aCache->OriginSuffix(), aCache->OriginNoSuffix(), + nsString(aKey), nsString(aValue)); + OriginsHavingData().PutEntry(aCache->Origin()); + return NS_OK; +} + +nsresult StorageDBChild::AsyncUpdateItem(LocalStorageCacheBridge* aCache, + const nsAString& aKey, + const nsAString& aValue) { + if (NS_FAILED(mStatus) || !mIPCOpen) { + return mStatus; + } + + SendAsyncUpdateItem(aCache->OriginSuffix(), aCache->OriginNoSuffix(), + nsString(aKey), nsString(aValue)); + OriginsHavingData().PutEntry(aCache->Origin()); + return NS_OK; +} + +nsresult StorageDBChild::AsyncRemoveItem(LocalStorageCacheBridge* aCache, + const nsAString& aKey) { + if (NS_FAILED(mStatus) || !mIPCOpen) { + return mStatus; + } + + SendAsyncRemoveItem(aCache->OriginSuffix(), aCache->OriginNoSuffix(), + nsString(aKey)); + return NS_OK; +} + +nsresult StorageDBChild::AsyncClear(LocalStorageCacheBridge* aCache) { + if (NS_FAILED(mStatus) || !mIPCOpen) { + return mStatus; + } + + SendAsyncClear(aCache->OriginSuffix(), aCache->OriginNoSuffix()); + OriginsHavingData().RemoveEntry(aCache->Origin()); + return NS_OK; +} + +bool StorageDBChild::ShouldPreloadOrigin(const nsACString& aOrigin) { + // Return true if we didn't receive the origins list yet. + // I tend to rather preserve a bit of early-after-start performance + // than a bit of memory here. + return !mOriginsHavingData || mOriginsHavingData->Contains(aOrigin); +} + +mozilla::ipc::IPCResult StorageDBChild::RecvObserve( + const nsCString& aTopic, const nsString& aOriginAttributesPattern, + const nsCString& aOriginScope) { + MOZ_ASSERT(!XRE_IsParentProcess()); + + StorageObserver::Self()->Notify(aTopic.get(), aOriginAttributesPattern, + aOriginScope); + return IPC_OK(); +} + +mozilla::ipc::IPCResult StorageDBChild::RecvOriginsHavingData( + nsTArray<nsCString>&& aOrigins) { + // Force population of mOriginsHavingData even if there are no origins so that + // ShouldPreloadOrigin does not generate false positives for all origins. + if (!aOrigins.Length()) { + Unused << OriginsHavingData(); + } + + for (uint32_t i = 0; i < aOrigins.Length(); ++i) { + OriginsHavingData().PutEntry(aOrigins[i]); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult StorageDBChild::RecvLoadItem( + const nsCString& aOriginSuffix, const nsCString& aOriginNoSuffix, + const nsString& aKey, const nsString& aValue) { + LocalStorageCache* aCache = + mManager->GetCache(aOriginSuffix, aOriginNoSuffix); + if (aCache) { + aCache->LoadItem(aKey, aValue); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult StorageDBChild::RecvLoadDone( + const nsCString& aOriginSuffix, const nsCString& aOriginNoSuffix, + const nsresult& aRv) { + LocalStorageCache* aCache = + mManager->GetCache(aOriginSuffix, aOriginNoSuffix); + if (aCache) { + aCache->LoadDone(aRv); + + // Just drop reference to this cache now since the load is done. + mLoadingCaches.RemoveEntry(static_cast<LocalStorageCacheBridge*>(aCache)); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult StorageDBChild::RecvLoadUsage( + const nsCString& aOriginNoSuffix, const int64_t& aUsage) { + RefPtr<StorageUsageBridge> scopeUsage = + mManager->GetOriginUsage(aOriginNoSuffix, mPrivateBrowsingId); + scopeUsage->LoadUsage(aUsage); + return IPC_OK(); +} + +mozilla::ipc::IPCResult StorageDBChild::RecvError(const nsresult& aRv) { + mStatus = aRv; + return IPC_OK(); +} + +NS_IMPL_ISUPPORTS(StorageDBChild::ShutdownObserver, nsIObserver) + +NS_IMETHODIMP +StorageDBChild::ShutdownObserver::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!strcmp(aTopic, "xpcom-shutdown")); + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (NS_WARN_IF(!observerService)) { + return NS_ERROR_FAILURE; + } + + Unused << observerService->RemoveObserver(this, "xpcom-shutdown"); + + StorageDBChild*& storageChild = sStorageChild[mPrivateBrowsingId]; + if (storageChild) { + sStorageChildDown[mPrivateBrowsingId] = true; + + MOZ_ALWAYS_TRUE(storageChild->PBackgroundStorageChild::SendDeleteMe()); + + NS_RELEASE(storageChild); + storageChild = nullptr; + } + + return NS_OK; +} + +SessionStorageObserverChild::SessionStorageObserverChild( + SessionStorageObserver* aObserver) + : mObserver(aObserver) { + AssertIsOnOwningThread(); + MOZ_ASSERT(NextGenLocalStorageEnabled()); + MOZ_ASSERT(aObserver); + aObserver->AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(SessionStorageObserverChild); +} + +SessionStorageObserverChild::~SessionStorageObserverChild() { + AssertIsOnOwningThread(); + + MOZ_COUNT_DTOR(SessionStorageObserverChild); +} + +void SessionStorageObserverChild::SendDeleteMeInternal() { + AssertIsOnOwningThread(); + + if (mObserver) { + mObserver->ClearActor(); + mObserver = nullptr; + + // Don't check result here since IPC may no longer be available due to + // SessionStorageManager (which holds a strong reference to + // SessionStorageObserver) being destroyed very late in the game. + PSessionStorageObserverChild::SendDeleteMe(); + } +} + +void SessionStorageObserverChild::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + if (mObserver) { + mObserver->ClearActor(); + mObserver = nullptr; + } +} + +mozilla::ipc::IPCResult SessionStorageObserverChild::RecvObserve( + const nsCString& aTopic, const nsString& aOriginAttributesPattern, + const nsCString& aOriginScope) { + AssertIsOnOwningThread(); + + StorageObserver::Self()->Notify(aTopic.get(), aOriginAttributesPattern, + aOriginScope); + return IPC_OK(); +} + +SessionStorageCacheChild::SessionStorageCacheChild(SessionStorageCache* aCache) + : mCache(aCache) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mCache); + + MOZ_COUNT_CTOR(SessionStorageCacheChild); +} + +SessionStorageCacheChild::~SessionStorageCacheChild() { + AssertIsOnOwningThread(); + + MOZ_COUNT_DTOR(SessionStorageCacheChild); +} + +void SessionStorageCacheChild::SendDeleteMeInternal() { + AssertIsOnOwningThread(); + + if (mCache) { + mCache->ClearActor(); + mCache = nullptr; + + MOZ_ALWAYS_TRUE(PBackgroundSessionStorageCacheChild::SendDeleteMe()); + } +} + +void SessionStorageCacheChild::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + if (mCache) { + mCache->ClearActor(); + mCache = nullptr; + } +} + +SessionStorageManagerChild::SessionStorageManagerChild( + SessionStorageManager* aSSManager) + : mSSManager(aSSManager) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mSSManager); + + MOZ_COUNT_CTOR(SessionStorageManagerChild); +} + +SessionStorageManagerChild::~SessionStorageManagerChild() { + AssertIsOnOwningThread(); + + MOZ_COUNT_DTOR(SessionStorageManagerChild); +} + +void SessionStorageManagerChild::SendDeleteMeInternal() { + AssertIsOnOwningThread(); + + if (mSSManager) { + mSSManager->ClearActor(); + mSSManager = nullptr; + + MOZ_ALWAYS_TRUE(PBackgroundSessionStorageManagerChild::SendDeleteMe()); + } +} + +void SessionStorageManagerChild::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + if (mSSManager) { + mSSManager->ClearActor(); + mSSManager = nullptr; + } +} + +LocalStorageCacheParent::LocalStorageCacheParent( + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, + const nsACString& aOriginKey, uint32_t aPrivateBrowsingId) + : mPrincipalInfo(aPrincipalInfo), + mOriginKey(aOriginKey), + mPrivateBrowsingId(aPrivateBrowsingId), + mActorDestroyed(false) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); +} + +LocalStorageCacheParent::~LocalStorageCacheParent() { + MOZ_ASSERT(mActorDestroyed); +} + +void LocalStorageCacheParent::ActorDestroy(ActorDestroyReason aWhy) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + mActorDestroyed = true; + + MOZ_ASSERT(gLocalStorageCacheParents); + + nsTArray<LocalStorageCacheParent*>* array; + gLocalStorageCacheParents->Get(mOriginKey, &array); + MOZ_ASSERT(array); + + array->RemoveElement(this); + + if (array->IsEmpty()) { + gLocalStorageCacheParents->Remove(mOriginKey); + } + + if (!gLocalStorageCacheParents->Count()) { + gLocalStorageCacheParents = nullptr; + } +} + +mozilla::ipc::IPCResult LocalStorageCacheParent::RecvDeleteMe() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + IProtocol* mgr = Manager(); + if (!PBackgroundLocalStorageCacheParent::Send__delete__(this)) { + return IPC_FAIL_NO_REASON(mgr); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult LocalStorageCacheParent::RecvNotify( + const nsString& aDocumentURI, const nsString& aKey, + const nsString& aOldValue, const nsString& aNewValue) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(gLocalStorageCacheParents); + + nsTArray<LocalStorageCacheParent*>* array; + gLocalStorageCacheParents->Get(mOriginKey, &array); + MOZ_ASSERT(array); + + for (LocalStorageCacheParent* localStorageCacheParent : *array) { + if (localStorageCacheParent != this) { + // When bug 1443925 is fixed, we can compare mPrincipalInfo against + // localStorageCacheParent->PrincipalInfo() here on the background thread + // instead of posting it to the main thread. The advantage of doing so is + // that it would save an IPC message in the case where the principals do + // not match. + Unused << localStorageCacheParent->SendObserve( + mPrincipalInfo, localStorageCacheParent->PrincipalInfo(), + mPrivateBrowsingId, aDocumentURI, aKey, aOldValue, aNewValue); + } + } + + return IPC_OK(); +} + +// ---------------------------------------------------------------------------- +// Parent +// ---------------------------------------------------------------------------- + +class StorageDBParent::ObserverSink : public StorageObserverSink { + nsCOMPtr<nsIEventTarget> mOwningEventTarget; + + // Only touched on the PBackground thread. + StorageDBParent* MOZ_NON_OWNING_REF mActor; + + public: + explicit ObserverSink(StorageDBParent* aActor) + : mOwningEventTarget(GetCurrentEventTarget()), mActor(aActor) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + } + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(StorageDBParent::ObserverSink); + + void Start(); + + void Stop(); + + private: + ~ObserverSink() = default; + + void AddSink(); + + void RemoveSink(); + + void Notify(const nsCString& aTopic, const nsString& aOriginAttributesPattern, + const nsCString& aOriginScope); + + // StorageObserverSink + nsresult Observe(const char* aTopic, const nsAString& aOriginAttrPattern, + const nsACString& aOriginScope) override; +}; + +NS_IMPL_ADDREF(StorageDBParent) +NS_IMPL_RELEASE(StorageDBParent) + +void StorageDBParent::AddIPDLReference() { + MOZ_ASSERT(!mIPCOpen, "Attempting to retain multiple IPDL references"); + mIPCOpen = true; + AddRef(); +} + +void StorageDBParent::ReleaseIPDLReference() { + MOZ_ASSERT(mIPCOpen, "Attempting to release non-existent IPDL reference"); + mIPCOpen = false; + Release(); +} + +namespace {} // namespace + +StorageDBParent::StorageDBParent(const nsString& aProfilePath, + const uint32_t aPrivateBrowsingId) + : mProfilePath(aProfilePath), + mPrivateBrowsingId(aPrivateBrowsingId), + mIPCOpen(false) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + // We are always open by IPC only + AddIPDLReference(); +} + +StorageDBParent::~StorageDBParent() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + if (mObserverSink) { + mObserverSink->Stop(); + mObserverSink = nullptr; + } +} + +void StorageDBParent::Init() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + PBackgroundParent* actor = Manager(); + MOZ_ASSERT(actor); + + if (::mozilla::ipc::BackgroundParent::IsOtherProcessActor(actor)) { + mObserverSink = new ObserverSink(this); + mObserverSink->Start(); + } + + StorageDBThread* storageThread = StorageDBThread::Get(mPrivateBrowsingId); + if (storageThread) { + nsTArray<nsCString> scopes; + storageThread->GetOriginsHavingData(&scopes); + mozilla::Unused << SendOriginsHavingData(scopes); + } +} + +StorageDBParent::CacheParentBridge* StorageDBParent::NewCache( + const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix) { + return new CacheParentBridge(this, aOriginSuffix, aOriginNoSuffix); +} + +void StorageDBParent::ActorDestroy(ActorDestroyReason aWhy) { + // Implement me! Bug 1005169 +} + +mozilla::ipc::IPCResult StorageDBParent::RecvDeleteMe() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + IProtocol* mgr = Manager(); + if (!PBackgroundStorageParent::Send__delete__(this)) { + return IPC_FAIL_NO_REASON(mgr); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult StorageDBParent::RecvAsyncPreload( + const nsCString& aOriginSuffix, const nsCString& aOriginNoSuffix, + const bool& aPriority) { + StorageDBThread* storageThread = + StorageDBThread::GetOrCreate(mProfilePath, mPrivateBrowsingId); + if (!storageThread) { + return IPC_FAIL_NO_REASON(this); + } + + storageThread->AsyncPreload(NewCache(aOriginSuffix, aOriginNoSuffix), + aPriority); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult StorageDBParent::RecvAsyncGetUsage( + const nsCString& aOriginNoSuffix) { + StorageDBThread* storageThread = + StorageDBThread::GetOrCreate(mProfilePath, mPrivateBrowsingId); + if (!storageThread) { + return IPC_FAIL_NO_REASON(this); + } + + // The object releases it self in LoadUsage method + RefPtr<UsageParentBridge> usage = + new UsageParentBridge(this, aOriginNoSuffix); + + storageThread->AsyncGetUsage(usage); + + return IPC_OK(); +} + +namespace { + +// We need another implementation of LocalStorageCacheBridge to do +// synchronous IPC preload. This class just receives Load* notifications +// and fills the returning arguments of RecvPreload with the database +// values for us. +class SyncLoadCacheHelper : public LocalStorageCacheBridge { + public: + SyncLoadCacheHelper(const nsCString& aOriginSuffix, + const nsCString& aOriginNoSuffix, + uint32_t aAlreadyLoadedCount, nsTArray<nsString>* aKeys, + nsTArray<nsString>* aValues, nsresult* rv) + : mMonitor("DOM Storage SyncLoad IPC"), + mSuffix(aOriginSuffix), + mOrigin(aOriginNoSuffix), + mKeys(aKeys), + mValues(aValues), + mRv(rv), + mLoaded(false), + mLoadedCount(aAlreadyLoadedCount) { + // Precaution + *mRv = NS_ERROR_UNEXPECTED; + } + + virtual const nsCString Origin() const override { + return LocalStorageManager::CreateOrigin(mSuffix, mOrigin); + } + virtual const nsCString& OriginNoSuffix() const override { return mOrigin; } + virtual const nsCString& OriginSuffix() const override { return mSuffix; } + virtual bool Loaded() override { return mLoaded; } + virtual uint32_t LoadedCount() override { return mLoadedCount; } + virtual bool LoadItem(const nsAString& aKey, + const nsString& aValue) override { + // Called on the aCache background thread + MOZ_ASSERT(!mLoaded); + if (mLoaded) { + return false; + } + + ++mLoadedCount; + mKeys->AppendElement(aKey); + mValues->AppendElement(aValue); + return true; + } + + virtual void LoadDone(nsresult aRv) override { + // Called on the aCache background thread + MonitorAutoLock monitor(mMonitor); + MOZ_ASSERT(!mLoaded && mRv); + mLoaded = true; + if (mRv) { + *mRv = aRv; + mRv = nullptr; + } + monitor.Notify(); + } + + virtual void LoadWait() override { + // Called on the main thread, exits after LoadDone() call + MonitorAutoLock monitor(mMonitor); + while (!mLoaded) { + monitor.Wait(); + } + } + + private: + Monitor mMonitor; + nsCString mSuffix, mOrigin; + nsTArray<nsString>* mKeys; + nsTArray<nsString>* mValues; + nsresult* mRv; + bool mLoaded; + uint32_t mLoadedCount; +}; + +} // namespace + +mozilla::ipc::IPCResult StorageDBParent::RecvPreload( + const nsCString& aOriginSuffix, const nsCString& aOriginNoSuffix, + const uint32_t& aAlreadyLoadedCount, nsTArray<nsString>* aKeys, + nsTArray<nsString>* aValues, nsresult* aRv) { + StorageDBThread* storageThread = + StorageDBThread::GetOrCreate(mProfilePath, mPrivateBrowsingId); + if (!storageThread) { + return IPC_FAIL_NO_REASON(this); + } + + RefPtr<SyncLoadCacheHelper> cache( + new SyncLoadCacheHelper(aOriginSuffix, aOriginNoSuffix, + aAlreadyLoadedCount, aKeys, aValues, aRv)); + + storageThread->SyncPreload(cache, true); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult StorageDBParent::RecvAsyncAddItem( + const nsCString& aOriginSuffix, const nsCString& aOriginNoSuffix, + const nsString& aKey, const nsString& aValue) { + StorageDBThread* storageThread = + StorageDBThread::GetOrCreate(mProfilePath, mPrivateBrowsingId); + if (!storageThread) { + return IPC_FAIL_NO_REASON(this); + } + + nsresult rv = storageThread->AsyncAddItem( + NewCache(aOriginSuffix, aOriginNoSuffix), aKey, aValue); + if (NS_FAILED(rv) && mIPCOpen) { + mozilla::Unused << SendError(rv); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult StorageDBParent::RecvAsyncUpdateItem( + const nsCString& aOriginSuffix, const nsCString& aOriginNoSuffix, + const nsString& aKey, const nsString& aValue) { + StorageDBThread* storageThread = + StorageDBThread::GetOrCreate(mProfilePath, mPrivateBrowsingId); + if (!storageThread) { + return IPC_FAIL_NO_REASON(this); + } + + nsresult rv = storageThread->AsyncUpdateItem( + NewCache(aOriginSuffix, aOriginNoSuffix), aKey, aValue); + if (NS_FAILED(rv) && mIPCOpen) { + mozilla::Unused << SendError(rv); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult StorageDBParent::RecvAsyncRemoveItem( + const nsCString& aOriginSuffix, const nsCString& aOriginNoSuffix, + const nsString& aKey) { + StorageDBThread* storageThread = + StorageDBThread::GetOrCreate(mProfilePath, mPrivateBrowsingId); + if (!storageThread) { + return IPC_FAIL_NO_REASON(this); + } + + nsresult rv = storageThread->AsyncRemoveItem( + NewCache(aOriginSuffix, aOriginNoSuffix), aKey); + if (NS_FAILED(rv) && mIPCOpen) { + mozilla::Unused << SendError(rv); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult StorageDBParent::RecvAsyncClear( + const nsCString& aOriginSuffix, const nsCString& aOriginNoSuffix) { + StorageDBThread* storageThread = + StorageDBThread::GetOrCreate(mProfilePath, mPrivateBrowsingId); + if (!storageThread) { + return IPC_FAIL_NO_REASON(this); + } + + nsresult rv = + storageThread->AsyncClear(NewCache(aOriginSuffix, aOriginNoSuffix)); + if (NS_FAILED(rv) && mIPCOpen) { + mozilla::Unused << SendError(rv); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult StorageDBParent::RecvAsyncFlush() { + StorageDBThread* storageThread = StorageDBThread::Get(mPrivateBrowsingId); + if (!storageThread) { + return IPC_FAIL_NO_REASON(this); + } + + storageThread->AsyncFlush(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult StorageDBParent::RecvStartup() { + StorageDBThread* storageThread = + StorageDBThread::GetOrCreate(mProfilePath, mPrivateBrowsingId); + if (!storageThread) { + return IPC_FAIL_NO_REASON(this); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult StorageDBParent::RecvClearAll() { + StorageDBThread* storageThread = + StorageDBThread::GetOrCreate(mProfilePath, mPrivateBrowsingId); + if (!storageThread) { + return IPC_FAIL_NO_REASON(this); + } + + storageThread->AsyncClearAll(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult StorageDBParent::RecvClearMatchingOrigin( + const nsCString& aOriginNoSuffix) { + StorageDBThread* storageThread = + StorageDBThread::GetOrCreate(mProfilePath, mPrivateBrowsingId); + if (!storageThread) { + return IPC_FAIL_NO_REASON(this); + } + + storageThread->AsyncClearMatchingOrigin(aOriginNoSuffix); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult StorageDBParent::RecvClearMatchingOriginAttributes( + const OriginAttributesPattern& aPattern) { + StorageDBThread* storageThread = + StorageDBThread::GetOrCreate(mProfilePath, mPrivateBrowsingId); + if (!storageThread) { + return IPC_FAIL_NO_REASON(this); + } + + storageThread->AsyncClearMatchingOriginAttributes(aPattern); + + return IPC_OK(); +} + +void StorageDBParent::Observe(const nsCString& aTopic, + const nsString& aOriginAttributesPattern, + const nsCString& aOriginScope) { + if (mIPCOpen) { + mozilla::Unused << SendObserve(aTopic, aOriginAttributesPattern, + aOriginScope); + } +} + +namespace { + +// Results must be sent back on the main thread +class LoadRunnable : public Runnable { + public: + enum TaskType { loadItem, loadDone }; + + LoadRunnable(StorageDBParent* aParent, TaskType aType, + const nsACString& aOriginSuffix, + const nsACString& aOriginNoSuffix, + const nsAString& aKey = u""_ns, const nsAString& aValue = u""_ns) + : Runnable("dom::LoadRunnable"), + mParent(aParent), + mType(aType), + mSuffix(aOriginSuffix), + mOrigin(aOriginNoSuffix), + mKey(aKey), + mValue(aValue), + mRv(NS_ERROR_NOT_INITIALIZED) {} + + LoadRunnable(StorageDBParent* aParent, TaskType aType, + const nsACString& aOriginSuffix, + const nsACString& aOriginNoSuffix, nsresult aRv) + : Runnable("dom::LoadRunnable"), + mParent(aParent), + mType(aType), + mSuffix(aOriginSuffix), + mOrigin(aOriginNoSuffix), + mRv(aRv) {} + + private: + RefPtr<StorageDBParent> mParent; + TaskType mType; + nsCString mSuffix, mOrigin; + nsString mKey; + nsString mValue; + nsresult mRv; + + NS_IMETHOD Run() override { + if (!mParent->IPCOpen()) { + return NS_OK; + } + + switch (mType) { + case loadItem: + mozilla::Unused << mParent->SendLoadItem(mSuffix, mOrigin, mKey, + mValue); + break; + case loadDone: + mozilla::Unused << mParent->SendLoadDone(mSuffix, mOrigin, mRv); + break; + } + + mParent = nullptr; + + return NS_OK; + } +}; + +} // namespace + +// StorageDBParent::CacheParentBridge + +const nsCString StorageDBParent::CacheParentBridge::Origin() const { + return LocalStorageManager::CreateOrigin(mOriginSuffix, mOriginNoSuffix); +} + +bool StorageDBParent::CacheParentBridge::LoadItem(const nsAString& aKey, + const nsString& aValue) { + if (mLoaded) { + return false; + } + + ++mLoadedCount; + + RefPtr<LoadRunnable> r = + new LoadRunnable(mParent, LoadRunnable::loadItem, mOriginSuffix, + mOriginNoSuffix, aKey, aValue); + + MOZ_ALWAYS_SUCCEEDS(mOwningEventTarget->Dispatch(r, NS_DISPATCH_NORMAL)); + + return true; +} + +void StorageDBParent::CacheParentBridge::LoadDone(nsresult aRv) { + // Prevent send of duplicate LoadDone. + if (mLoaded) { + return; + } + + mLoaded = true; + + RefPtr<LoadRunnable> r = new LoadRunnable( + mParent, LoadRunnable::loadDone, mOriginSuffix, mOriginNoSuffix, aRv); + + MOZ_ALWAYS_SUCCEEDS(mOwningEventTarget->Dispatch(r, NS_DISPATCH_NORMAL)); +} + +void StorageDBParent::CacheParentBridge::LoadWait() { + // Should never be called on this implementation + MOZ_ASSERT(false); +} + +// XXX Fix me! +// This should be just: +// NS_IMPL_RELEASE_WITH_DESTROY(StorageDBParent::CacheParentBridge, Destroy) +// But due to different strings used for refcount logging and different return +// types, this is done manually for now. +NS_IMETHODIMP_(void) +StorageDBParent::CacheParentBridge::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); */ + Destroy(); + } +} + +void StorageDBParent::CacheParentBridge::Destroy() { + if (mOwningEventTarget->IsOnCurrentThread()) { + delete this; + return; + } + + RefPtr<Runnable> destroyRunnable = NewNonOwningRunnableMethod( + "CacheParentBridge::Destroy", this, &CacheParentBridge::Destroy); + + MOZ_ALWAYS_SUCCEEDS( + mOwningEventTarget->Dispatch(destroyRunnable, NS_DISPATCH_NORMAL)); +} + +// StorageDBParent::UsageParentBridge + +namespace { + +class UsageRunnable : public Runnable { + public: + UsageRunnable(StorageDBParent* aParent, const nsACString& aOriginScope, + const int64_t& aUsage) + : Runnable("dom::UsageRunnable"), + mParent(aParent), + mOriginScope(aOriginScope), + mUsage(aUsage) {} + + private: + NS_IMETHOD Run() override { + if (!mParent->IPCOpen()) { + return NS_OK; + } + + mozilla::Unused << mParent->SendLoadUsage(mOriginScope, mUsage); + + mParent = nullptr; + + return NS_OK; + } + + RefPtr<StorageDBParent> mParent; + nsCString mOriginScope; + int64_t mUsage; +}; + +} // namespace + +void StorageDBParent::UsageParentBridge::LoadUsage(const int64_t aUsage) { + RefPtr<UsageRunnable> r = new UsageRunnable(mParent, mOriginScope, aUsage); + + MOZ_ALWAYS_SUCCEEDS(mOwningEventTarget->Dispatch(r, NS_DISPATCH_NORMAL)); +} + +// XXX Fix me! +// This should be just: +// NS_IMPL_RELEASE_WITH_DESTROY(StorageDBParent::UsageParentBridge, Destroy) +// But due to different strings used for refcount logging, this is done manually +// for now. +NS_IMETHODIMP_(MozExternalRefCountType) +StorageDBParent::UsageParentBridge::Release(void) { + MOZ_ASSERT(int32_t(mRefCnt) > 0, "dup release"); + nsrefcnt count = --mRefCnt; + NS_LOG_RELEASE(this, count, "StorageUsageBridge"); + if (count == 0) { + Destroy(); + return 0; + } + return count; +} + +void StorageDBParent::UsageParentBridge::Destroy() { + if (mOwningEventTarget->IsOnCurrentThread()) { + delete this; + return; + } + + RefPtr<Runnable> destroyRunnable = NewNonOwningRunnableMethod( + "UsageParentBridge::Destroy", this, &UsageParentBridge::Destroy); + + MOZ_ALWAYS_SUCCEEDS( + mOwningEventTarget->Dispatch(destroyRunnable, NS_DISPATCH_NORMAL)); +} + +void StorageDBParent::ObserverSink::Start() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + RefPtr<Runnable> runnable = + NewRunnableMethod("StorageDBParent::ObserverSink::AddSink", this, + &StorageDBParent::ObserverSink::AddSink); + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable)); +} + +void StorageDBParent::ObserverSink::Stop() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + mActor = nullptr; + + RefPtr<Runnable> runnable = + NewRunnableMethod("StorageDBParent::ObserverSink::RemoveSink", this, + &StorageDBParent::ObserverSink::RemoveSink); + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable)); +} + +void StorageDBParent::ObserverSink::AddSink() { + MOZ_ASSERT(NS_IsMainThread()); + + StorageObserver* observer = StorageObserver::Self(); + if (observer) { + observer->AddSink(this); + } +} + +void StorageDBParent::ObserverSink::RemoveSink() { + MOZ_ASSERT(NS_IsMainThread()); + + StorageObserver* observer = StorageObserver::Self(); + if (observer) { + observer->RemoveSink(this); + } +} + +void StorageDBParent::ObserverSink::Notify( + const nsCString& aTopic, const nsString& aOriginAttributesPattern, + const nsCString& aOriginScope) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + if (mActor) { + mActor->Observe(aTopic, aOriginAttributesPattern, aOriginScope); + } +} + +nsresult StorageDBParent::ObserverSink::Observe( + const char* aTopic, const nsAString& aOriginAttributesPattern, + const nsACString& aOriginScope) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<Runnable> runnable = NewRunnableMethod<nsCString, nsString, nsCString>( + "StorageDBParent::ObserverSink::Observe2", this, + &StorageDBParent::ObserverSink::Notify, aTopic, aOriginAttributesPattern, + aOriginScope); + + MOZ_ALWAYS_SUCCEEDS( + mOwningEventTarget->Dispatch(runnable, NS_DISPATCH_NORMAL)); + + return NS_OK; +} + +SessionStorageObserverParent::SessionStorageObserverParent() + : mActorDestroyed(false) { + MOZ_ASSERT(NS_IsMainThread()); + + StorageObserver* observer = StorageObserver::Self(); + if (observer) { + observer->AddSink(this); + } +} + +SessionStorageObserverParent::~SessionStorageObserverParent() { + MOZ_ASSERT(mActorDestroyed); + + StorageObserver* observer = StorageObserver::Self(); + if (observer) { + observer->RemoveSink(this); + } +} + +void SessionStorageObserverParent::ActorDestroy(ActorDestroyReason aWhy) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mActorDestroyed); + + mActorDestroyed = true; +} + +mozilla::ipc::IPCResult SessionStorageObserverParent::RecvDeleteMe() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mActorDestroyed); + + IProtocol* mgr = Manager(); + if (!PSessionStorageObserverParent::Send__delete__(this)) { + return IPC_FAIL_NO_REASON(mgr); + } + return IPC_OK(); +} + +nsresult SessionStorageObserverParent::Observe( + const char* aTopic, const nsAString& aOriginAttributesPattern, + const nsACString& aOriginScope) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mActorDestroyed) { + mozilla::Unused << SendObserve(nsCString(aTopic), + nsString(aOriginAttributesPattern), + nsCString(aOriginScope)); + } + return NS_OK; +} + +SessionStorageCacheParent::SessionStorageCacheParent( + const nsCString& aOriginAttrs, const nsCString& aOriginKey, + SessionStorageManagerParent* aActor) + : mOriginAttrs(aOriginAttrs), + mOriginKey(aOriginKey), + mManagerActor(aActor) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(mManagerActor); +} + +SessionStorageCacheParent::~SessionStorageCacheParent() = default; + +void SessionStorageCacheParent::ActorDestroy(ActorDestroyReason aWhy) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + mManagerActor = nullptr; +} + +mozilla::ipc::IPCResult SessionStorageCacheParent::RecvLoad( + nsTArray<SSSetItemInfo>* aDefaultData, + nsTArray<SSSetItemInfo>* aSessionData) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(mManagerActor); + + mLoadReceived.Flip(); + + RefPtr<BackgroundSessionStorageManager> manager = mManagerActor->GetManager(); + MOZ_ASSERT(manager); + + manager->CopyDataToContentProcess(mOriginAttrs, mOriginKey, *aDefaultData, + *aSessionData); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult SessionStorageCacheParent::RecvCheckpoint( + nsTArray<SSWriteInfo>&& aDefaultWriteInfos, + nsTArray<SSWriteInfo>&& aSessionWriteInfos) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(mManagerActor); + + RefPtr<BackgroundSessionStorageManager> manager = mManagerActor->GetManager(); + MOZ_ASSERT(manager); + + manager->UpdateData(mOriginAttrs, mOriginKey, aDefaultWriteInfos, + aSessionWriteInfos); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult SessionStorageCacheParent::RecvDeleteMe() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(mManagerActor); + + mManagerActor = nullptr; + + IProtocol* mgr = Manager(); + if (!PBackgroundSessionStorageCacheParent::Send__delete__(this)) { + return IPC_FAIL( + mgr, "Failed to delete PBackgroundSessionStorageCacheParent actor"); + } + return IPC_OK(); +} + +SessionStorageManagerParent::SessionStorageManagerParent(uint64_t aTopContextId) + : mBackgroundManager( + BackgroundSessionStorageManager::GetOrCreate(aTopContextId)) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(mBackgroundManager); +} + +SessionStorageManagerParent::~SessionStorageManagerParent() = default; + +void SessionStorageManagerParent::ActorDestroy(ActorDestroyReason aWhy) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + mBackgroundManager = nullptr; +} + +already_AddRefed<PBackgroundSessionStorageCacheParent> +SessionStorageManagerParent::AllocPBackgroundSessionStorageCacheParent( + const nsCString& aOriginAttrs, const nsCString& aOriginKey) { + return MakeAndAddRef<SessionStorageCacheParent>(aOriginAttrs, aOriginKey, + this); +} + +BackgroundSessionStorageManager* SessionStorageManagerParent::GetManager() + const { + return mBackgroundManager; +} + +mozilla::ipc::IPCResult SessionStorageManagerParent::RecvDeleteMe() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(mBackgroundManager); + + mBackgroundManager = nullptr; + + IProtocol* mgr = Manager(); + if (!PBackgroundSessionStorageManagerParent::Send__delete__(this)) { + return IPC_FAIL( + mgr, "Failed to delete PBackgroundSessionStorageManagerParent actor"); + } + return IPC_OK(); +} + +/******************************************************************************* + * Exported functions + ******************************************************************************/ + +PBackgroundLocalStorageCacheParent* AllocPBackgroundLocalStorageCacheParent( + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, + const nsCString& aOriginKey, const uint32_t& aPrivateBrowsingId) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + RefPtr<LocalStorageCacheParent> actor = new LocalStorageCacheParent( + aPrincipalInfo, aOriginKey, aPrivateBrowsingId); + + // Transfer ownership to IPDL. + return actor.forget().take(); +} + +mozilla::ipc::IPCResult RecvPBackgroundLocalStorageCacheConstructor( + mozilla::ipc::PBackgroundParent* aBackgroundActor, + PBackgroundLocalStorageCacheParent* aActor, + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, + const nsCString& aOriginKey, const uint32_t& aPrivateBrowsingId) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + auto* actor = static_cast<LocalStorageCacheParent*>(aActor); + + if (!gLocalStorageCacheParents) { + gLocalStorageCacheParents = new LocalStorageCacheParentHashtable(); + } + + nsTArray<LocalStorageCacheParent*>* array; + if (!gLocalStorageCacheParents->Get(aOriginKey, &array)) { + array = new nsTArray<LocalStorageCacheParent*>(); + gLocalStorageCacheParents->Put(aOriginKey, array); + } + array->AppendElement(actor); + + // We are currently trusting the content process not to lie to us. It is + // future work to consult the ClientManager to determine whether this is a + // legitimate origin for the content process. + + return IPC_OK(); +} + +bool DeallocPBackgroundLocalStorageCacheParent( + PBackgroundLocalStorageCacheParent* aActor) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + RefPtr<LocalStorageCacheParent> actor = + dont_AddRef(static_cast<LocalStorageCacheParent*>(aActor)); + + return true; +} + +PBackgroundStorageParent* AllocPBackgroundStorageParent( + const nsString& aProfilePath, const uint32_t& aPrivateBrowsingId) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + return new StorageDBParent(aProfilePath, aPrivateBrowsingId); +} + +mozilla::ipc::IPCResult RecvPBackgroundStorageConstructor( + PBackgroundStorageParent* aActor, const nsString& aProfilePath, + const uint32_t& aPrivateBrowsingId) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + auto* actor = static_cast<StorageDBParent*>(aActor); + actor->Init(); + return IPC_OK(); +} + +bool DeallocPBackgroundStorageParent(PBackgroundStorageParent* aActor) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + StorageDBParent* actor = static_cast<StorageDBParent*>(aActor); + actor->ReleaseIPDLReference(); + return true; +} + +PSessionStorageObserverParent* AllocPSessionStorageObserverParent() { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<SessionStorageObserverParent> actor = + new SessionStorageObserverParent(); + + // Transfer ownership to IPDL. + return actor.forget().take(); +} + +bool RecvPSessionStorageObserverConstructor( + PSessionStorageObserverParent* aActor) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aActor); + + return true; +} + +bool DeallocPSessionStorageObserverParent( + PSessionStorageObserverParent* aActor) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + RefPtr<SessionStorageObserverParent> actor = + dont_AddRef(static_cast<SessionStorageObserverParent*>(aActor)); + + return true; +} + +already_AddRefed<PBackgroundSessionStorageManagerParent> +AllocPBackgroundSessionStorageManagerParent(const uint64_t& aTopContextId) { + return MakeAndAddRef<SessionStorageManagerParent>(aTopContextId); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/StorageIPC.h b/dom/storage/StorageIPC.h new file mode 100644 index 0000000000..d421fbe4d1 --- /dev/null +++ b/dom/storage/StorageIPC.h @@ -0,0 +1,608 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_StorageIPC_h +#define mozilla_dom_StorageIPC_h + +#include "LocalStorageCache.h" +#include "StorageDBThread.h" +#include "StorageObserver.h" + +#include "mozilla/Mutex.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/FlippedOnce.h" +#include "mozilla/dom/PBackgroundLocalStorageCacheChild.h" +#include "mozilla/dom/PBackgroundLocalStorageCacheParent.h" +#include "mozilla/dom/PBackgroundSessionStorageCacheChild.h" +#include "mozilla/dom/PBackgroundSessionStorageCacheParent.h" +#include "mozilla/dom/PBackgroundSessionStorageManagerChild.h" +#include "mozilla/dom/PBackgroundSessionStorageManagerParent.h" +#include "mozilla/dom/PBackgroundStorageChild.h" +#include "mozilla/dom/PBackgroundStorageParent.h" +#include "mozilla/dom/PSessionStorageObserverChild.h" +#include "mozilla/dom/PSessionStorageObserverParent.h" +#include "nsClassHashtable.h" + +namespace mozilla { + +class OriginAttributesPattern; + +namespace ipc { + +class BackgroundChildImpl; +class PrincipalInfo; + +} // namespace ipc + +namespace dom { + +class LocalStorageManager; +class PBackgroundStorageParent; +class PSessionStorageObserverParent; +class SessionStorageCache; +class SessionStorageCacheParent; +class SessionStorageManager; +class SessionStorageManagerParent; +class BackgroundSessionStorageManager; +class SessionStorageObserver; + +class LocalStorageCacheChild final : public PBackgroundLocalStorageCacheChild { + friend class mozilla::ipc::BackgroundChildImpl; + friend class LocalStorageCache; + friend class LocalStorageManager; + + // LocalStorageCache effectively owns this instance, although IPC handles its + // allocation/deallocation. When the LocalStorageCache destructor runs, it + // will invoke SendDeleteMeInternal() which will trigger both instances to + // drop their mutual references and cause IPC to destroy the actor after the + // DeleteMe round-trip. + LocalStorageCache* MOZ_NON_OWNING_REF mCache; + + NS_DECL_OWNINGTHREAD + + public: + void AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(LocalStorageCacheChild); + } + + private: + // Only created by LocalStorageManager. + explicit LocalStorageCacheChild(LocalStorageCache* aCache); + + // Only destroyed by mozilla::ipc::BackgroundChildImpl. + ~LocalStorageCacheChild(); + + // Only called by LocalStorageCache. + void SendDeleteMeInternal(); + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvObserve(const PrincipalInfo& aPrincipalInfo, + const PrincipalInfo& aCachePrincipalInfo, + const uint32_t& aPrivateBrowsingId, + const nsString& aDocumentURI, + const nsString& aKey, + const nsString& aOldValue, + const nsString& aNewValue) override; +}; + +// Child side of the IPC protocol, exposes as DB interface but +// is responsible to send all requests to the parent process +// and expects asynchronous answers. Those are then transparently +// forwarded back to consumers on the child process. +class StorageDBChild final : public PBackgroundStorageChild { + class ShutdownObserver; + + virtual ~StorageDBChild(); + + public: + StorageDBChild(LocalStorageManager* aManager, uint32_t aPrivateBrowsingId); + + static StorageDBChild* Get(uint32_t aPrivateBrowsingId); + + static StorageDBChild* GetOrCreate(uint32_t aPrivateBrowsingId); + + NS_INLINE_DECL_REFCOUNTING(StorageDBChild); + + void AddIPDLReference(); + void ReleaseIPDLReference(); + + virtual nsresult Init(); + virtual nsresult Shutdown(); + + virtual void AsyncPreload(LocalStorageCacheBridge* aCache, + bool aPriority = false); + virtual void AsyncGetUsage(StorageUsageBridge* aUsage); + + virtual void SyncPreload(LocalStorageCacheBridge* aCache, + bool aForceSync = false); + + virtual nsresult AsyncAddItem(LocalStorageCacheBridge* aCache, + const nsAString& aKey, const nsAString& aValue); + virtual nsresult AsyncUpdateItem(LocalStorageCacheBridge* aCache, + const nsAString& aKey, + const nsAString& aValue); + virtual nsresult AsyncRemoveItem(LocalStorageCacheBridge* aCache, + const nsAString& aKey); + virtual nsresult AsyncClear(LocalStorageCacheBridge* aCache); + + virtual void AsyncClearAll() { + if (mOriginsHavingData) { + mOriginsHavingData->Clear(); /* NO-OP on the child process otherwise */ + } + } + + virtual void AsyncClearMatchingOrigin(const nsACString& aOriginNoSuffix) { + MOZ_CRASH("Shouldn't be called!"); + } + + virtual void AsyncClearMatchingOriginAttributes( + const OriginAttributesPattern& aPattern) { + MOZ_CRASH("Shouldn't be called!"); + } + + virtual void AsyncFlush() { MOZ_CRASH("Shouldn't be called!"); } + + virtual bool ShouldPreloadOrigin(const nsACString& aOriginNoSuffix); + + private: + mozilla::ipc::IPCResult RecvObserve(const nsCString& aTopic, + const nsString& aOriginAttributesPattern, + const nsCString& aOriginScope) override; + mozilla::ipc::IPCResult RecvLoadItem(const nsCString& aOriginSuffix, + const nsCString& aOriginNoSuffix, + const nsString& aKey, + const nsString& aValue) override; + mozilla::ipc::IPCResult RecvLoadDone(const nsCString& aOriginSuffix, + const nsCString& aOriginNoSuffix, + const nsresult& aRv) override; + mozilla::ipc::IPCResult RecvOriginsHavingData( + nsTArray<nsCString>&& aOrigins) override; + mozilla::ipc::IPCResult RecvLoadUsage(const nsCString& aOriginNoSuffix, + const int64_t& aUsage) override; + mozilla::ipc::IPCResult RecvError(const nsresult& aRv) override; + + nsTHashtable<nsCStringHashKey>& OriginsHavingData(); + + // Held to get caches to forward answers to. + RefPtr<LocalStorageManager> mManager; + + // Origins having data hash, for optimization purposes only + UniquePtr<nsTHashtable<nsCStringHashKey>> mOriginsHavingData; + + // List of caches waiting for preload. This ensures the contract that + // AsyncPreload call references the cache for time of the preload. + nsTHashtable<nsRefPtrHashKey<LocalStorageCacheBridge>> mLoadingCaches; + + // Expected to be only 0 or 1. + const uint32_t mPrivateBrowsingId; + + // Status of the remote database + nsresult mStatus; + + bool mIPCOpen; +}; + +class SessionStorageObserverChild final : public PSessionStorageObserverChild { + friend class SessionStorageManager; + friend class SessionStorageObserver; + + // SessionStorageObserver effectively owns this instance, although IPC handles + // its allocation/deallocation. When the SessionStorageObserver destructor + // runs, it will invoke SendDeleteMeInternal() which will trigger both + // instances to drop their mutual references and cause IPC to destroy the + // actor after the DeleteMe round-trip. + SessionStorageObserver* MOZ_NON_OWNING_REF mObserver; + + NS_DECL_OWNINGTHREAD + + public: + void AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(LocalStorageCacheChild); + } + + private: + // Only created by SessionStorageManager. + explicit SessionStorageObserverChild(SessionStorageObserver* aObserver); + + ~SessionStorageObserverChild(); + + // Only called by SessionStorageObserver. + void SendDeleteMeInternal(); + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvObserve(const nsCString& aTopic, + const nsString& aOriginAttributesPattern, + const nsCString& aOriginScope) override; +}; + +class SessionStorageCacheChild final + : public PBackgroundSessionStorageCacheChild { + friend class PBackgroundSessionStorageCacheChild; + friend class SessionStorageCache; + friend class SessionStorageManager; + friend class mozilla::ipc::BackgroundChildImpl; + + // SessionStorageManagerChild effectively owns this instance, although IPC + // handles its allocation/deallocation. When the SessionStorageManager + // destructor runs, it will invoke SendDeleteMeInternal() which will trigger + // both instances to drop their mutual references and cause IPC to destroy the + // actor after the DeleteMe round-trip. + SessionStorageCache* MOZ_NON_OWNING_REF mCache; + + NS_INLINE_DECL_REFCOUNTING(mozilla::dom::SessionStorageCacheChild, override) + + public: + void AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(SesionStoragManagerChild); + } + + private: + // Only created by SessionStorageManager. + explicit SessionStorageCacheChild(SessionStorageCache* aCache); + + // Only destroyed by mozilla::ipc::BackgroundChildImpl. + ~SessionStorageCacheChild(); + + // Only called by SessionStorageCache. + void SendDeleteMeInternal(); + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; +}; + +class SessionStorageManagerChild final + : public PBackgroundSessionStorageManagerChild { + friend class PBackgroundSessionStorageManagerChild; + friend class SessionStorage; + friend class SessionStorageManager; + friend class mozilla::ipc::BackgroundChildImpl; + + // SessionStorageManager effectively owns this instance, although IPC handles + // its allocation/deallocation. When the SessionStorageManager destructor + // runs, it will invoke SendDeleteMeInternal() which will trigger both + // instances to drop their mutual references and cause IPC to destroy the + // actor after the DeleteMe round-trip. + SessionStorageManager* MOZ_NON_OWNING_REF mSSManager; + + NS_INLINE_DECL_REFCOUNTING(mozilla::dom::SessionStorageManagerChild, override) + + public: + void AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(SesionStoragManagerChild); + } + + private: + // Only created by SessionStorage. + explicit SessionStorageManagerChild(SessionStorageManager* aSSManager); + + // Only destroyed by mozilla::ipc::BackgroundChildImpl. + ~SessionStorageManagerChild(); + + // Only called by SessionStorageManager. + void SendDeleteMeInternal(); + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; +}; + +class LocalStorageCacheParent final + : public PBackgroundLocalStorageCacheParent { + const PrincipalInfo mPrincipalInfo; + const nsCString mOriginKey; + uint32_t mPrivateBrowsingId; + bool mActorDestroyed; + + public: + // Created in AllocPBackgroundLocalStorageCacheParent. + LocalStorageCacheParent(const mozilla::ipc::PrincipalInfo& aPrincipalInfo, + const nsACString& aOriginKey, + uint32_t aPrivateBrowsingId); + + NS_INLINE_DECL_REFCOUNTING(mozilla::dom::LocalStorageCacheParent) + + const PrincipalInfo& PrincipalInfo() const { return mPrincipalInfo; } + + private: + // Reference counted. + ~LocalStorageCacheParent(); + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvDeleteMe() override; + + mozilla::ipc::IPCResult RecvNotify(const nsString& aDocumentURI, + const nsString& aKey, + const nsString& aOldValue, + const nsString& aNewValue) override; +}; + +// Receives async requests from child processes and is responsible +// to send back responses from the DB thread. Exposes as a fake +// LocalStorageCache consumer. +// Also responsible for forwardning all chrome operation notifications +// such as cookie cleaning etc to the child process. +class StorageDBParent final : public PBackgroundStorageParent { + class ObserverSink; + + virtual ~StorageDBParent(); + + public: + StorageDBParent(const nsString& aProfilePath, uint32_t aPrivateBrowsingId); + + void Init(); + + NS_IMETHOD_(MozExternalRefCountType) AddRef(void); + NS_IMETHOD_(MozExternalRefCountType) Release(void); + + void AddIPDLReference(); + void ReleaseIPDLReference(); + + bool IPCOpen() { return mIPCOpen; } + + public: + // Fake cache class receiving async callbacks from DB thread, sending + // them back to appropriate cache object on the child process. + class CacheParentBridge : public LocalStorageCacheBridge { + public: + CacheParentBridge(StorageDBParent* aParentDB, + const nsACString& aOriginSuffix, + const nsACString& aOriginNoSuffix) + : mOwningEventTarget(GetCurrentSerialEventTarget()), + mParent(aParentDB), + mOriginSuffix(aOriginSuffix), + mOriginNoSuffix(aOriginNoSuffix), + mLoaded(false), + mLoadedCount(0) {} + virtual ~CacheParentBridge() = default; + + // LocalStorageCacheBridge + virtual const nsCString Origin() const override; + virtual const nsCString& OriginNoSuffix() const override { + return mOriginNoSuffix; + } + virtual const nsCString& OriginSuffix() const override { + return mOriginSuffix; + } + virtual bool Loaded() override { return mLoaded; } + virtual uint32_t LoadedCount() override { return mLoadedCount; } + + virtual bool LoadItem(const nsAString& aKey, + const nsString& aValue) override; + virtual void LoadDone(nsresult aRv) override; + virtual void LoadWait() override; + + NS_IMETHOD_(void) + Release(void) override; + + private: + void Destroy(); + + nsCOMPtr<nsISerialEventTarget> mOwningEventTarget; + RefPtr<StorageDBParent> mParent; + nsCString mOriginSuffix, mOriginNoSuffix; + bool mLoaded; + uint32_t mLoadedCount; + }; + + // Fake usage class receiving async callbacks from DB thread + class UsageParentBridge : public StorageUsageBridge { + public: + UsageParentBridge(StorageDBParent* aParentDB, + const nsACString& aOriginScope) + : mOwningEventTarget(GetCurrentSerialEventTarget()), + mParent(aParentDB), + mOriginScope(aOriginScope) {} + virtual ~UsageParentBridge() = default; + + // StorageUsageBridge + virtual const nsCString& OriginScope() override { return mOriginScope; } + virtual void LoadUsage(const int64_t usage) override; + + NS_IMETHOD_(MozExternalRefCountType) + Release(void) override; + + private: + void Destroy(); + + nsCOMPtr<nsISerialEventTarget> mOwningEventTarget; + RefPtr<StorageDBParent> mParent; + nsCString mOriginScope; + }; + + private: + // IPC + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + mozilla::ipc::IPCResult RecvDeleteMe() override; + + mozilla::ipc::IPCResult RecvAsyncPreload(const nsCString& aOriginSuffix, + const nsCString& aOriginNoSuffix, + const bool& aPriority) override; + mozilla::ipc::IPCResult RecvPreload(const nsCString& aOriginSuffix, + const nsCString& aOriginNoSuffix, + const uint32_t& aAlreadyLoadedCount, + nsTArray<nsString>* aKeys, + nsTArray<nsString>* aValues, + nsresult* aRv) override; + mozilla::ipc::IPCResult RecvAsyncGetUsage( + const nsCString& aOriginNoSuffix) override; + mozilla::ipc::IPCResult RecvAsyncAddItem(const nsCString& aOriginSuffix, + const nsCString& aOriginNoSuffix, + const nsString& aKey, + const nsString& aValue) override; + mozilla::ipc::IPCResult RecvAsyncUpdateItem(const nsCString& aOriginSuffix, + const nsCString& aOriginNoSuffix, + const nsString& aKey, + const nsString& aValue) override; + mozilla::ipc::IPCResult RecvAsyncRemoveItem(const nsCString& aOriginSuffix, + const nsCString& aOriginNoSuffix, + const nsString& aKey) override; + mozilla::ipc::IPCResult RecvAsyncClear( + const nsCString& aOriginSuffix, + const nsCString& aOriginNoSuffix) override; + mozilla::ipc::IPCResult RecvAsyncFlush() override; + + mozilla::ipc::IPCResult RecvStartup() override; + mozilla::ipc::IPCResult RecvClearAll() override; + mozilla::ipc::IPCResult RecvClearMatchingOrigin( + const nsCString& aOriginNoSuffix) override; + mozilla::ipc::IPCResult RecvClearMatchingOriginAttributes( + const OriginAttributesPattern& aPattern) override; + + void Observe(const nsCString& aTopic, const nsString& aOriginAttrPattern, + const nsCString& aOriginScope); + + private: + CacheParentBridge* NewCache(const nsACString& aOriginSuffix, + const nsACString& aOriginNoSuffix); + + RefPtr<ObserverSink> mObserverSink; + + // A hack to deal with deadlock between the parent process main thread and + // background thread when invoking StorageDBThread::GetOrCreate because it + // cannot safely perform a synchronous dispatch back to the main thread + // (because we are already synchronously doing things on the stack). + // Populated for the same process actors, empty for other process actors. + nsString mProfilePath; + + // Expected to be only 0 or 1. + const uint32_t mPrivateBrowsingId; + + ThreadSafeAutoRefCnt mRefCnt; + NS_DECL_OWNINGTHREAD + + // True when IPC channel is open and Send*() methods are OK to use. + bool mIPCOpen; +}; + +class SessionStorageObserverParent final : public PSessionStorageObserverParent, + public StorageObserverSink { + bool mActorDestroyed; + + public: + // Created in AllocPSessionStorageObserverParent. + SessionStorageObserverParent(); + + NS_INLINE_DECL_REFCOUNTING(mozilla::dom::SessionStorageObserverParent) + + private: + // Reference counted. + ~SessionStorageObserverParent(); + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvDeleteMe() override; + + // StorageObserverSink + nsresult Observe(const char* aTopic, const nsAString& aOriginAttrPattern, + const nsACString& aOriginScope) override; +}; + +class SessionStorageCacheParent final + : public PBackgroundSessionStorageCacheParent { + friend class PBackgroundSessionStorageCacheParent; + const nsCString mOriginAttrs; + const nsCString mOriginKey; + + RefPtr<SessionStorageManagerParent> mManagerActor; + FlippedOnce<false> mLoadReceived; + + public: + SessionStorageCacheParent(const nsCString& aOriginAttrs, + const nsCString& aOriginKey, + SessionStorageManagerParent* aActor); + + NS_INLINE_DECL_REFCOUNTING(mozilla::dom::SessionStorageCacheParent, override) + + private: + ~SessionStorageCacheParent(); + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvLoad( + nsTArray<SSSetItemInfo>* aDefaultData, + nsTArray<SSSetItemInfo>* aSessionData) override; + + mozilla::ipc::IPCResult RecvCheckpoint( + nsTArray<SSWriteInfo>&& aDefaultWriteInfos, + nsTArray<SSWriteInfo>&& aSessionWriteInfos) override; + + mozilla::ipc::IPCResult RecvDeleteMe() override; +}; + +class SessionStorageManagerParent final + : public PBackgroundSessionStorageManagerParent { + friend class PBackgroundSessionStorageManagerParent; + + RefPtr<BackgroundSessionStorageManager> mBackgroundManager; + + public: + explicit SessionStorageManagerParent(uint64_t aTopContextId); + + NS_INLINE_DECL_REFCOUNTING(mozilla::dom::SessionStorageManagerParent, + override) + + already_AddRefed<PBackgroundSessionStorageCacheParent> + AllocPBackgroundSessionStorageCacheParent( + const nsCString& aOriginAttrs, const nsCString& aOriginKey) override; + + BackgroundSessionStorageManager* GetManager() const; + + private: + ~SessionStorageManagerParent(); + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvDeleteMe() override; +}; + +PBackgroundLocalStorageCacheParent* AllocPBackgroundLocalStorageCacheParent( + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, + const nsCString& aOriginKey, const uint32_t& aPrivateBrowsingId); + +mozilla::ipc::IPCResult RecvPBackgroundLocalStorageCacheConstructor( + mozilla::ipc::PBackgroundParent* aBackgroundActor, + PBackgroundLocalStorageCacheParent* aActor, + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, + const nsCString& aOriginKey, const uint32_t& aPrivateBrowsingId); + +bool DeallocPBackgroundLocalStorageCacheParent( + PBackgroundLocalStorageCacheParent* aActor); + +PBackgroundStorageParent* AllocPBackgroundStorageParent( + const nsString& aProfilePath, const uint32_t& aPrivateBrowsingId); + +mozilla::ipc::IPCResult RecvPBackgroundStorageConstructor( + PBackgroundStorageParent* aActor, const nsString& aProfilePath, + const uint32_t& aPrivateBrowsingId); + +bool DeallocPBackgroundStorageParent(PBackgroundStorageParent* aActor); + +PSessionStorageObserverParent* AllocPSessionStorageObserverParent(); + +bool RecvPSessionStorageObserverConstructor( + PSessionStorageObserverParent* aActor); + +bool DeallocPSessionStorageObserverParent( + PSessionStorageObserverParent* aActor); + +already_AddRefed<PBackgroundSessionStorageCacheParent> +AllocPBackgroundSessionStorageCacheParent(const nsCString& aOriginAttrs, + const nsCString& aOriginKey); + +already_AddRefed<PBackgroundSessionStorageManagerParent> +AllocPBackgroundSessionStorageManagerParent(const uint64_t& aTopContextId); +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_StorageIPC_h diff --git a/dom/storage/StorageNotifierService.cpp b/dom/storage/StorageNotifierService.cpp new file mode 100644 index 0000000000..e09a05f8fb --- /dev/null +++ b/dom/storage/StorageNotifierService.cpp @@ -0,0 +1,131 @@ +/* -*- 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 "StorageNotifierService.h" +#include "StorageUtils.h" +#include "mozilla/dom/StorageEvent.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/StaticPtr.h" +#include "nsThreadUtils.h" + +namespace mozilla { +namespace dom { + +namespace { + +// This boolean is used to avoid the creation of the service after been +// distroyed on shutdown. +bool gStorageShuttingDown = false; + +StaticRefPtr<StorageNotifierService> gStorageNotifierService; + +} // namespace + +/* static */ +StorageNotifierService* StorageNotifierService::GetOrCreate() { + MOZ_ASSERT(NS_IsMainThread()); + if (!gStorageNotifierService && !gStorageShuttingDown) { + gStorageNotifierService = new StorageNotifierService(); + ClearOnShutdown(&gStorageNotifierService); + } + + return gStorageNotifierService; +} + +StorageNotifierService::StorageNotifierService() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!gStorageNotifierService); +} + +StorageNotifierService::~StorageNotifierService() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!gStorageNotifierService); + gStorageShuttingDown = true; +} + +/* static */ +void StorageNotifierService::Broadcast(StorageEvent* aEvent, + const char16_t* aStorageType, + bool aPrivateBrowsing, + bool aImmediateDispatch) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<StorageNotifierService> service = gStorageNotifierService; + if (!service) { + return; + } + + RefPtr<StorageEvent> event = aEvent; + + for (const auto& observer : service->mObservers.ForwardRange()) { + // Enforce that the source storage area's private browsing state matches + // this window's state. These flag checks and their maintenance independent + // from the principal's OriginAttributes matter because chrome docshells + // that are part of private browsing windows can be private browsing without + // having their OriginAttributes set (because they have the system + // principal). + if (aPrivateBrowsing != observer->IsPrivateBrowsing()) { + continue; + } + + // No reasons to continue if the principal of the event doesn't match with + // the window's one. + if (!StorageUtils::PrincipalsEqual( + aEvent->GetPrincipal(), observer->GetEffectiveStoragePrincipal())) { + continue; + } + + const auto pinnedObserver = observer; + + RefPtr<Runnable> r = NS_NewRunnableFunction( + "StorageNotifierService::Broadcast", + [pinnedObserver, event, aStorageType, aPrivateBrowsing, + aImmediateDispatch]() { + // Check principals again. EffectiveStoragePrincipal may be changed + // when relaxed. + if (!aImmediateDispatch && + !StorageUtils::PrincipalsEqual( + event->GetPrincipal(), + pinnedObserver->GetEffectiveStoragePrincipal())) { + return; + } + + pinnedObserver->ObserveStorageNotification(event, aStorageType, + aPrivateBrowsing); + }); + + if (aImmediateDispatch) { + r->Run(); + } else { + nsCOMPtr<nsIEventTarget> et = pinnedObserver->GetEventTarget(); + if (et) { + et->Dispatch(r.forget()); + } + } + } +} + +void StorageNotifierService::Register(StorageNotificationObserver* aObserver) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aObserver); + MOZ_ASSERT(!mObservers.Contains(aObserver)); + + mObservers.AppendElement(aObserver); +} + +void StorageNotifierService::Unregister( + StorageNotificationObserver* aObserver) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aObserver); + + // No assertion about mObservers containing aObserver because window calls + // this method multiple times. + + mObservers.RemoveElement(aObserver); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/StorageNotifierService.h b/dom/storage/StorageNotifierService.h new file mode 100644 index 0000000000..e36bb08b0d --- /dev/null +++ b/dom/storage/StorageNotifierService.h @@ -0,0 +1,76 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_StorageNotifierService_h +#define mozilla_dom_StorageNotifierService_h + +#include "nsISupportsImpl.h" +#include "nsTObserverArray.h" + +class nsIEventTarget; +class nsIPrincipal; +class nsPIDOMWindowInner; + +namespace mozilla { +namespace dom { + +class StorageEvent; + +/** + * Enables the StorageNotifierService to check whether an observer is interested + * in receiving events for the given principal before calling the method, an + * optimization refactoring. + * + * Expected to only be implemented by nsGlobalWindowObserver or its succesor. + */ +class StorageNotificationObserver { + public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + virtual void ObserveStorageNotification(StorageEvent* aEvent, + const char16_t* aStorageType, + bool aPrivateBrowsing) = 0; + + virtual bool IsPrivateBrowsing() const = 0; + + virtual nsIPrincipal* GetEffectiveStoragePrincipal() const = 0; + + virtual nsIEventTarget* GetEventTarget() const = 0; +}; + +/** + * A specialized version of the observer service that uses the custom + * StorageNotificationObserver so that principal checks can happen in this class + * rather than in the nsIObserver::observe method where they used to happen. + * + * The only expected consumers are nsGlobalWindowInner instances via their + * nsGlobalWindowObserver helper that avoids being able to use the window as an + * nsIObserver. + */ +class StorageNotifierService final { + public: + NS_INLINE_DECL_REFCOUNTING(StorageNotifierService) + + static StorageNotifierService* GetOrCreate(); + + static void Broadcast(StorageEvent* aEvent, const char16_t* aStorageType, + bool aPrivateBrowsing, bool aImmediateDispatch); + + void Register(StorageNotificationObserver* aObserver); + + void Unregister(StorageNotificationObserver* aObserver); + + private: + StorageNotifierService(); + ~StorageNotifierService(); + + nsTObserverArray<RefPtr<StorageNotificationObserver>> mObservers; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_StorageNotifierService_h diff --git a/dom/storage/StorageObserver.cpp b/dom/storage/StorageObserver.cpp new file mode 100644 index 0000000000..e8eed3c4ef --- /dev/null +++ b/dom/storage/StorageObserver.cpp @@ -0,0 +1,498 @@ +/* -*- 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 "StorageObserver.h" + +#include "LocalStorageCache.h" +#include "StorageDBThread.h" +#include "StorageIPC.h" +#include "StorageUtils.h" + +#include "mozilla/BasePrincipal.h" +#include "nsIObserverService.h" +#include "nsIURI.h" +#include "nsIPermission.h" +#include "nsIIDNService.h" +#include "nsICookiePermission.h" + +#include "nsPrintfCString.h" +#include "nsXULAppAPI.h" +#include "nsEscape.h" +#include "nsNetCID.h" +#include "mozilla/dom/LocalStorageCommon.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "nsServiceManagerUtils.h" + +namespace mozilla { +namespace dom { + +using namespace StorageUtils; + +static const char kStartupTopic[] = "sessionstore-windows-restored"; +static const uint32_t kStartupDelay = 0; + +const char kTestingPref[] = "dom.storage.testing"; + +constexpr auto kPrivateBrowsingPattern = u"{ \"privateBrowsingId\": 1 }"_ns; + +NS_IMPL_ISUPPORTS(StorageObserver, nsIObserver, nsISupportsWeakReference) + +StorageObserver* StorageObserver::sSelf = nullptr; + +// static +nsresult StorageObserver::Init() { + if (sSelf) { + return NS_OK; + } + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (!obs) { + return NS_ERROR_UNEXPECTED; + } + + sSelf = new StorageObserver(); + NS_ADDREF(sSelf); + + // Chrome clear operations. + obs->AddObserver(sSelf, kStartupTopic, true); + obs->AddObserver(sSelf, "cookie-changed", true); + obs->AddObserver(sSelf, "perm-changed", true); + obs->AddObserver(sSelf, "last-pb-context-exited", true); + obs->AddObserver(sSelf, "clear-origin-attributes-data", true); + obs->AddObserver(sSelf, "extension:purge-localStorage", true); + obs->AddObserver(sSelf, "browser:purge-sessionStorage", true); + + // Shutdown + obs->AddObserver(sSelf, "profile-after-change", true); + if (XRE_IsParentProcess()) { + obs->AddObserver(sSelf, "profile-before-change", true); + } + + // Testing +#ifdef DOM_STORAGE_TESTS + Preferences::RegisterCallbackAndCall(TestingPrefChanged, kTestingPref); +#endif + + return NS_OK; +} + +// static +nsresult StorageObserver::Shutdown() { + if (!sSelf) { + return NS_ERROR_NOT_INITIALIZED; + } + + NS_RELEASE(sSelf); + return NS_OK; +} + +// static +void StorageObserver::TestingPrefChanged(const char* aPrefName, + void* aClosure) { + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (!obs) { + return; + } + + if (Preferences::GetBool(kTestingPref)) { + obs->AddObserver(sSelf, "domstorage-test-flush-force", true); + if (XRE_IsParentProcess()) { + // Only to forward to child process. + obs->AddObserver(sSelf, "domstorage-test-flushed", true); + } + obs->AddObserver(sSelf, "domstorage-test-reload", true); + } else { + obs->RemoveObserver(sSelf, "domstorage-test-flush-force"); + if (XRE_IsParentProcess()) { + // Only to forward to child process. + obs->RemoveObserver(sSelf, "domstorage-test-flushed"); + } + obs->RemoveObserver(sSelf, "domstorage-test-reload"); + } +} + +void StorageObserver::AddSink(StorageObserverSink* aObs) { + mSinks.AppendElement(aObs); +} + +void StorageObserver::RemoveSink(StorageObserverSink* aObs) { + mSinks.RemoveElement(aObs); +} + +void StorageObserver::Notify(const char* aTopic, + const nsAString& aOriginAttributesPattern, + const nsACString& aOriginScope) { + for (auto* sink : mSinks.ForwardRange()) { + sink->Observe(aTopic, aOriginAttributesPattern, aOriginScope); + } +} + +void StorageObserver::NoteBackgroundThread(const uint32_t aPrivateBrowsingId, + nsIEventTarget* aBackgroundThread) { + MOZ_ASSERT(aPrivateBrowsingId <= 1); + + mBackgroundThread[aPrivateBrowsingId] = aBackgroundThread; +} + +nsresult StorageObserver::GetOriginScope(const char16_t* aData, + nsACString& aOriginScope) { + nsresult rv; + + NS_ConvertUTF16toUTF8 domain(aData); + + nsAutoCString convertedDomain; + nsCOMPtr<nsIIDNService> converter = do_GetService(NS_IDNSERVICE_CONTRACTID); + if (converter) { + // Convert the domain name to the ACE format + rv = converter->ConvertUTF8toACE(domain, convertedDomain); + } else { + // In case the IDN service is not available, this is the best we can come + // up with! + rv = NS_EscapeURL(domain, esc_OnlyNonASCII | esc_AlwaysCopy, + convertedDomain, fallible); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCString originScope; + rv = CreateReversedDomain(convertedDomain, originScope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + aOriginScope = originScope; + return NS_OK; +} + +NS_IMETHODIMP +StorageObserver::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + nsresult rv; + + // Start the thread that opens the database. + if (!strcmp(aTopic, kStartupTopic)) { + MOZ_ASSERT(XRE_IsParentProcess()); + + if (NextGenLocalStorageEnabled()) { + return NS_OK; + } + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + obs->RemoveObserver(this, kStartupTopic); + + return NS_NewTimerWithObserver(getter_AddRefs(mDBThreadStartDelayTimer), + this, nsITimer::TYPE_ONE_SHOT, + kStartupDelay); + } + + // Timer callback used to start the database a short timer after startup + if (!strcmp(aTopic, NS_TIMER_CALLBACK_TOPIC)) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!NextGenLocalStorageEnabled()); + + nsCOMPtr<nsITimer> timer = do_QueryInterface(aSubject); + if (!timer) { + return NS_ERROR_UNEXPECTED; + } + + if (timer == mDBThreadStartDelayTimer) { + mDBThreadStartDelayTimer = nullptr; + + for (const uint32_t id : {0, 1}) { + StorageDBChild* storageChild = StorageDBChild::GetOrCreate(id); + if (NS_WARN_IF(!storageChild)) { + return NS_ERROR_FAILURE; + } + + storageChild->SendStartup(); + } + } + + return NS_OK; + } + + // Clear everything, caches + database + if (!strcmp(aTopic, "cookie-changed")) { + if (!u"cleared"_ns.Equals(aData)) { + return NS_OK; + } + + if (!NextGenLocalStorageEnabled()) { + for (const uint32_t id : {0, 1}) { + StorageDBChild* storageChild = StorageDBChild::GetOrCreate(id); + if (NS_WARN_IF(!storageChild)) { + return NS_ERROR_FAILURE; + } + + storageChild->AsyncClearAll(); + + if (XRE_IsParentProcess()) { + storageChild->SendClearAll(); + } + } + } + + Notify("cookie-cleared"); + + return NS_OK; + } + + // Clear from caches everything that has been stored + // while in session-only mode + if (!strcmp(aTopic, "perm-changed")) { + // Check for cookie permission change + nsCOMPtr<nsIPermission> perm(do_QueryInterface(aSubject)); + if (!perm) { + return NS_OK; + } + + nsAutoCString type; + perm->GetType(type); + if (type != "cookie"_ns) { + return NS_OK; + } + + uint32_t cap = 0; + perm->GetCapability(&cap); + if (!(cap & nsICookiePermission::ACCESS_SESSION) || + !u"deleted"_ns.Equals(nsDependentString(aData))) { + return NS_OK; + } + + nsCOMPtr<nsIPrincipal> principal; + perm->GetPrincipal(getter_AddRefs(principal)); + if (!principal) { + return NS_OK; + } + + nsAutoCString originSuffix; + BasePrincipal::Cast(principal)->OriginAttributesRef().CreateSuffix( + originSuffix); + + nsAutoCString host; + principal->GetHost(host); + if (host.IsEmpty()) { + return NS_OK; + } + + nsAutoCString originScope; + rv = CreateReversedDomain(host, originScope); + NS_ENSURE_SUCCESS(rv, rv); + + Notify("session-only-cleared", NS_ConvertUTF8toUTF16(originSuffix), + originScope); + + return NS_OK; + } + + if (!strcmp(aTopic, "extension:purge-localStorage")) { + if (NextGenLocalStorageEnabled()) { + return NS_OK; + } + + const char topic[] = "extension:purge-localStorage-caches"; + + if (aData) { + nsCString originScope; + + rv = GetOriginScope(aData, originScope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (XRE_IsParentProcess()) { + for (const uint32_t id : {0, 1}) { + StorageDBChild* storageChild = StorageDBChild::GetOrCreate(id); + if (NS_WARN_IF(!storageChild)) { + return NS_ERROR_FAILURE; + } + + storageChild->SendClearMatchingOrigin(originScope); + } + } + + Notify(topic, u""_ns, originScope); + } else { + for (const uint32_t id : {0, 1}) { + StorageDBChild* storageChild = StorageDBChild::GetOrCreate(id); + if (NS_WARN_IF(!storageChild)) { + return NS_ERROR_FAILURE; + } + + storageChild->AsyncClearAll(); + + if (XRE_IsParentProcess()) { + storageChild->SendClearAll(); + } + } + + Notify(topic); + } + + return NS_OK; + } + + if (!strcmp(aTopic, "browser:purge-sessionStorage")) { + if (aData) { + nsCString originScope; + rv = GetOriginScope(aData, originScope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + Notify(aTopic, u""_ns, originScope); + } else { + Notify(aTopic, u""_ns, ""_ns); + } + + return NS_OK; + } + + // Clear all private-browsing caches + if (!strcmp(aTopic, "last-pb-context-exited")) { + if (NextGenLocalStorageEnabled()) { + return NS_OK; + } + + // We get the notification in both processes (parent and content), but the + // clearing of the in-memory database should be triggered from the parent + // process only to avoid creation of redundant clearing operations. + // Also, if we create a new StorageDBChild instance late during content + // process shutdown, then it might be leaked in debug builds because it + // could happen that there is no chance to properly destroy it. + if (XRE_IsParentProcess()) { + // This doesn't use a loop with privateBrowsingId 0 and 1, since we only + // need to clear the in-memory database which is represented by + // privateBrowsingId 1. + static const uint32_t id = 1; + + StorageDBChild* storageChild = StorageDBChild::GetOrCreate(id); + if (NS_WARN_IF(!storageChild)) { + return NS_ERROR_FAILURE; + } + + OriginAttributesPattern pattern; + if (!pattern.Init(kPrivateBrowsingPattern)) { + NS_ERROR("Cannot parse origin attributes pattern"); + return NS_ERROR_FAILURE; + } + + storageChild->SendClearMatchingOriginAttributes(pattern); + } + + Notify("private-browsing-data-cleared", kPrivateBrowsingPattern); + + return NS_OK; + } + + // Clear data of the origins whose prefixes will match the suffix. + if (!strcmp(aTopic, "clear-origin-attributes-data")) { + MOZ_ASSERT(XRE_IsParentProcess()); + + if (NextGenLocalStorageEnabled()) { + return NS_OK; + } + + OriginAttributesPattern pattern; + if (!pattern.Init(nsDependentString(aData))) { + NS_ERROR("Cannot parse origin attributes pattern"); + return NS_ERROR_FAILURE; + } + + for (const uint32_t id : {0, 1}) { + StorageDBChild* storageChild = StorageDBChild::GetOrCreate(id); + if (NS_WARN_IF(!storageChild)) { + return NS_ERROR_FAILURE; + } + + storageChild->SendClearMatchingOriginAttributes(pattern); + } + + Notify("origin-attr-pattern-cleared", nsDependentString(aData)); + + return NS_OK; + } + + if (!strcmp(aTopic, "profile-after-change")) { + Notify("profile-change"); + + return NS_OK; + } + + if (!strcmp(aTopic, "profile-before-change")) { + MOZ_ASSERT(XRE_IsParentProcess()); + + if (NextGenLocalStorageEnabled()) { + return NS_OK; + } + + for (const uint32_t id : {0, 1}) { + if (mBackgroundThread[id]) { + bool done = false; + + RefPtr<StorageDBThread::ShutdownRunnable> shutdownRunnable = + new StorageDBThread::ShutdownRunnable(id, done); + MOZ_ALWAYS_SUCCEEDS(mBackgroundThread[id]->Dispatch( + shutdownRunnable, NS_DISPATCH_NORMAL)); + + MOZ_ALWAYS_TRUE(SpinEventLoopUntil([&]() { return done; })); + + mBackgroundThread[id] = nullptr; + } + } + + return NS_OK; + } + +#ifdef DOM_STORAGE_TESTS + if (!strcmp(aTopic, "domstorage-test-flush-force")) { + if (NextGenLocalStorageEnabled()) { + return NS_OK; + } + + for (const uint32_t id : {0, 1}) { + StorageDBChild* storageChild = StorageDBChild::GetOrCreate(id); + if (NS_WARN_IF(!storageChild)) { + return NS_ERROR_FAILURE; + } + + storageChild->SendAsyncFlush(); + } + + return NS_OK; + } + + if (!strcmp(aTopic, "domstorage-test-flushed")) { + if (NextGenLocalStorageEnabled()) { + return NS_OK; + } + + // Only used to propagate to IPC children + Notify("test-flushed"); + + return NS_OK; + } + + if (!strcmp(aTopic, "domstorage-test-reload")) { + if (NextGenLocalStorageEnabled()) { + return NS_OK; + } + + Notify("test-reload"); + + return NS_OK; + } +#endif + + NS_ERROR("Unexpected topic"); + return NS_ERROR_UNEXPECTED; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/StorageObserver.h b/dom/storage/StorageObserver.h new file mode 100644 index 0000000000..3d359d4ea4 --- /dev/null +++ b/dom/storage/StorageObserver.h @@ -0,0 +1,75 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_StorageObserver_h +#define mozilla_dom_StorageObserver_h + +#include "nsIObserver.h" +#include "nsITimer.h" +#include "nsWeakReference.h" +#include "nsTObserverArray.h" +#include "nsString.h" + +namespace mozilla { +namespace dom { + +class StorageObserver; + +// Main-thread interface implemented by legacy LocalStorageManager and current +// SessionStorageManager for direct consumption. Also implemented by legacy +// StorageDBParent and current SessionStorageObserverParent for propagation to +// content processes. +class StorageObserverSink { + public: + virtual ~StorageObserverSink() = default; + + private: + friend class StorageObserver; + virtual nsresult Observe(const char* aTopic, + const nsAString& aOriginAttributesPattern, + const nsACString& aOriginScope) = 0; +}; + +// Statically (through layout statics) initialized observer receiving and +// processing chrome clearing notifications, such as cookie deletion etc. +class StorageObserver : public nsIObserver, public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + static nsresult Init(); + static nsresult Shutdown(); + static StorageObserver* Self() { return sSelf; } + + void AddSink(StorageObserverSink* aObs); + void RemoveSink(StorageObserverSink* aObs); + void Notify(const char* aTopic, + const nsAString& aOriginAttributesPattern = u""_ns, + const nsACString& aOriginScope = ""_ns); + + void NoteBackgroundThread(uint32_t aPrivateBrowsingId, + nsIEventTarget* aBackgroundThread); + + private: + virtual ~StorageObserver() = default; + + nsresult GetOriginScope(const char16_t* aData, nsACString& aOriginScope); + + static void TestingPrefChanged(const char* aPrefName, void* aClosure); + + static StorageObserver* sSelf; + + nsCOMPtr<nsIEventTarget> mBackgroundThread[2]; + + // Weak references + nsTObserverArray<StorageObserverSink*> mSinks; + nsCOMPtr<nsITimer> mDBThreadStartDelayTimer; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_StorageObserver_h diff --git a/dom/storage/StorageUtils.cpp b/dom/storage/StorageUtils.cpp new file mode 100644 index 0000000000..8ff3203ae9 --- /dev/null +++ b/dom/storage/StorageUtils.cpp @@ -0,0 +1,115 @@ +/* -*- 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 "nsIURL.h" +#include "StorageUtils.h" + +#include "mozilla/OriginAttributes.h" +#include "nsDebug.h" +#include "nsIPrincipal.h" +#include "nsIURI.h" +#include "nsIURL.h" +#include "nsNetUtil.h" +#include "nsPrintfCString.h" + +namespace mozilla { +namespace dom { +namespace StorageUtils { + +bool PrincipalsEqual(nsIPrincipal* aObjectPrincipal, + nsIPrincipal* aSubjectPrincipal) { + if (!aSubjectPrincipal) { + return true; + } + + if (!aObjectPrincipal) { + return false; + } + + return aSubjectPrincipal->Equals(aObjectPrincipal); +} + +void ReverseString(const nsACString& aSource, nsACString& aResult) { + nsACString::const_iterator sourceBegin, sourceEnd; + aSource.BeginReading(sourceBegin); + aSource.EndReading(sourceEnd); + + aResult.SetLength(aSource.Length()); + auto destEnd = aResult.EndWriting(); + + while (sourceBegin != sourceEnd) { + *(--destEnd) = *sourceBegin; + ++sourceBegin; + } +} + +nsresult CreateReversedDomain(const nsACString& aAsciiDomain, + nsACString& aKey) { + if (aAsciiDomain.IsEmpty()) { + return NS_ERROR_NOT_AVAILABLE; + } + + ReverseString(aAsciiDomain, aKey); + + aKey.Append('.'); + return NS_OK; +} + +// This is only a compatibility code for schema version 0. Returns the 'scope' +// key in the schema version 0 format for the scope column. +nsCString Scheme0Scope(const nsACString& aOriginSuffix, + const nsACString& aOriginNoSuffix) { + nsCString result; + + OriginAttributes oa; + if (!aOriginSuffix.IsEmpty()) { + DebugOnly<bool> success = oa.PopulateFromSuffix(aOriginSuffix); + MOZ_ASSERT(success); + } + + if (oa.mInIsolatedMozBrowser) { + result.AppendInt(0); // This is the appId to be removed. + result.Append(':'); + result.Append(oa.mInIsolatedMozBrowser ? 't' : 'f'); + result.Append(':'); + } + + // If there is more than just appid and/or inbrowser stored in origin + // attributes, put it to the schema 0 scope as well. We must do that + // to keep the scope column unique (same resolution as schema 1 has + // with originAttributes and originKey columns) so that switch between + // schema 1 and 0 always works in both ways. + nsAutoCString remaining; + oa.mInIsolatedMozBrowser = false; + oa.CreateSuffix(remaining); + if (!remaining.IsEmpty()) { + MOZ_ASSERT(!aOriginSuffix.IsEmpty()); + + if (result.IsEmpty()) { + // Must contain the old prefix, otherwise we won't search for the whole + // origin attributes suffix. + result.AppendLiteral("0:f:"); + } + + // Append the whole origin attributes suffix despite we have already stored + // appid and inbrowser. We are only looking for it when the scope string + // starts with "$appid:$inbrowser:" (with whatever valid values). + // + // The OriginAttributes suffix is a string in a form like: + // "^addonId=101&userContextId=5" and it's ensured it always starts with '^' + // and never contains ':'. See OriginAttributes::CreateSuffix. + result.Append(aOriginSuffix); + result.Append(':'); + } + + result.Append(aOriginNoSuffix); + + return result; +} + +} // namespace StorageUtils +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/StorageUtils.h b/dom/storage/StorageUtils.h new file mode 100644 index 0000000000..d04aff5f2a --- /dev/null +++ b/dom/storage/StorageUtils.h @@ -0,0 +1,32 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_StorageUtils_h +#define mozilla_dom_StorageUtils_h + +#include "nsStringFwd.h" + +class nsIPrincipal; + +namespace mozilla { +namespace dom { +namespace StorageUtils { + +bool PrincipalsEqual(nsIPrincipal* aObjectPrincipal, + nsIPrincipal* aSubjectPrincipal); + +void ReverseString(const nsACString& aSource, nsACString& aResult); + +nsresult CreateReversedDomain(const nsACString& aAsciiDomain, nsACString& aKey); + +nsCString Scheme0Scope(const nsACString& aOriginSuffix, + const nsACString& aOriginNoSuffix); + +} // namespace StorageUtils +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_StorageUtils_h diff --git a/dom/storage/moz.build b/dom/storage/moz.build new file mode 100644 index 0000000000..2b9aef5043 --- /dev/null +++ b/dom/storage/moz.build @@ -0,0 +1,59 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Storage: localStorage & sessionStorage") + +EXPORTS.mozilla.dom += [ + "LocalStorage.h", + "LocalStorageManager.h", + "PartitionedLocalStorage.h", + "SessionStorageManager.h", + "Storage.h", + "StorageActivityService.h", + "StorageDBUpdater.h", + "StorageIPC.h", + "StorageNotifierService.h", + "StorageObserver.h", + "StorageUtils.h", +] + +UNIFIED_SOURCES += [ + "LocalStorage.cpp", + "LocalStorageCache.cpp", + "LocalStorageManager.cpp", + "PartitionedLocalStorage.cpp", + "SessionStorage.cpp", + "SessionStorageCache.cpp", + "SessionStorageManager.cpp", + "SessionStorageObserver.cpp", + "Storage.cpp", + "StorageActivityService.cpp", + "StorageDBThread.cpp", + "StorageDBUpdater.cpp", + "StorageIPC.cpp", + "StorageNotifierService.cpp", + "StorageObserver.cpp", + "StorageUtils.cpp", +] + +IPDL_SOURCES += [ + "PBackgroundLocalStorageCache.ipdl", + "PBackgroundSessionStorageCache.ipdl", + "PBackgroundSessionStorageManager.ipdl", + "PBackgroundStorage.ipdl", + "PSessionStorageObserver.ipdl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [ + "/dom/base", +] + +if CONFIG["ENABLE_TESTS"]: + DEFINES["DOM_STORAGE_TESTS"] = True |