diff options
Diffstat (limited to 'dom/storage')
-rw-r--r-- | dom/storage/DOMStorage.cpp | 301 | ||||
-rw-r--r-- | dom/storage/DOMStorage.h | 172 | ||||
-rw-r--r-- | dom/storage/DOMStorageCache.cpp | 807 | ||||
-rw-r--r-- | dom/storage/DOMStorageCache.h | 288 | ||||
-rw-r--r-- | dom/storage/DOMStorageDBThread.cpp | 1486 | ||||
-rw-r--r-- | dom/storage/DOMStorageDBThread.h | 407 | ||||
-rw-r--r-- | dom/storage/DOMStorageDBUpdater.cpp | 417 | ||||
-rw-r--r-- | dom/storage/DOMStorageDBUpdater.h | 22 | ||||
-rw-r--r-- | dom/storage/DOMStorageIPC.cpp | 770 | ||||
-rw-r--r-- | dom/storage/DOMStorageIPC.h | 218 | ||||
-rw-r--r-- | dom/storage/DOMStorageManager.cpp | 661 | ||||
-rw-r--r-- | dom/storage/DOMStorageManager.h | 155 | ||||
-rw-r--r-- | dom/storage/DOMStorageObserver.cpp | 355 | ||||
-rw-r--r-- | dom/storage/DOMStorageObserver.h | 67 | ||||
-rw-r--r-- | dom/storage/PStorage.ipdl | 45 | ||||
-rw-r--r-- | dom/storage/moz.build | 34 |
16 files changed, 6205 insertions, 0 deletions
diff --git a/dom/storage/DOMStorage.cpp b/dom/storage/DOMStorage.cpp new file mode 100644 index 000000000..026959dce --- /dev/null +++ b/dom/storage/DOMStorage.cpp @@ -0,0 +1,301 @@ +/* -*- 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 "DOMStorage.h" +#include "DOMStorageCache.h" +#include "DOMStorageManager.h" + +#include "nsIObserverService.h" +#include "nsIScriptSecurityManager.h" +#include "nsIPermissionManager.h" +#include "nsIPrincipal.h" +#include "nsICookiePermission.h" +#include "nsPIDOMWindow.h" + +#include "mozilla/dom/StorageBinding.h" +#include "mozilla/dom/StorageEvent.h" +#include "mozilla/dom/StorageEventBinding.h" +#include "mozilla/Services.h" +#include "mozilla/Preferences.h" +#include "mozilla/EnumSet.h" +#include "nsThreadUtils.h" +#include "nsContentUtils.h" +#include "nsServiceManagerUtils.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DOMStorage, mManager, mPrincipal, mWindow) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMStorage) +NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMStorage) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMStorage) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMStorage) + NS_INTERFACE_MAP_ENTRY(nsIDOMStorage) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END + +DOMStorage::DOMStorage(nsPIDOMWindowInner* aWindow, + DOMStorageManager* aManager, + DOMStorageCache* aCache, + const nsAString& aDocumentURI, + nsIPrincipal* aPrincipal, + bool aIsPrivate) +: mWindow(aWindow) +, mManager(aManager) +, mCache(aCache) +, mDocumentURI(aDocumentURI) +, mPrincipal(aPrincipal) +, mIsPrivate(aIsPrivate) +, mIsSessionOnly(false) +{ + mCache->Preload(); +} + +DOMStorage::~DOMStorage() +{ + mCache->KeepAlive(); +} + +/* virtual */ JSObject* +DOMStorage::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return StorageBinding::Wrap(aCx, this, aGivenProto); +} + +uint32_t +DOMStorage::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 +DOMStorage::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 +DOMStorage::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 +DOMStorage::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)) { + BroadcastChangeNotification(aKey, old, aData); + } +} + +void +DOMStorage::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)) { + BroadcastChangeNotification(aKey, old, NullString()); + } +} + +void +DOMStorage::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)) { + BroadcastChangeNotification(NullString(), NullString(), NullString()); + } +} + +namespace { + +class StorageNotifierRunnable : public Runnable +{ +public: + StorageNotifierRunnable(nsISupports* aSubject, const char16_t* aType) + : mSubject(aSubject), mType(aType) + { } + + NS_DECL_NSIRUNNABLE + +private: + nsCOMPtr<nsISupports> mSubject; + const char16_t* mType; +}; + +NS_IMETHODIMP +StorageNotifierRunnable::Run() +{ + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->NotifyObservers(mSubject, "dom-storage2-changed", mType); + } + return NS_OK; +} + +} // namespace + +void +DOMStorage::BroadcastChangeNotification(const nsSubstring& aKey, + const nsSubstring& aOldValue, + const nsSubstring& aNewValue) +{ + StorageEventInit dict; + dict.mBubbles = false; + dict.mCancelable = false; + dict.mKey = aKey; + dict.mNewValue = aNewValue; + dict.mOldValue = aOldValue; + dict.mStorageArea = this; + dict.mUrl = mDocumentURI; + + // Note, this DOM event should never reach JS. It is cloned later in + // nsGlobalWindow. + RefPtr<StorageEvent> event = + StorageEvent::Constructor(nullptr, NS_LITERAL_STRING("storage"), dict); + + RefPtr<StorageNotifierRunnable> r = + new StorageNotifierRunnable(event, + GetType() == LocalStorage + ? u"localStorage" + : u"sessionStorage"); + NS_DispatchToMainThread(r); +} + +static const char kPermissionType[] = "cookie"; +static const char kStorageEnabled[] = "dom.storage.enabled"; + +bool +DOMStorage::CanUseStorage(nsIPrincipal& aSubjectPrincipal) +{ + // This method is responsible for correct setting of mIsSessionOnly. + // It doesn't work with mIsPrivate flag at all, since it is checked + // regardless mIsSessionOnly flag in DOMStorageCache code. + + if (!mozilla::Preferences::GetBool(kStorageEnabled)) { + return false; + } + + nsContentUtils::StorageAccess access = + nsContentUtils::StorageAllowedForPrincipal(mPrincipal); + + if (access == nsContentUtils::StorageAccess::eDeny) { + return false; + } + + mIsSessionOnly = access <= nsContentUtils::StorageAccess::eSessionScoped; + return CanAccess(&aSubjectPrincipal); +} + +DOMStorage::StorageType +DOMStorage::GetType() const +{ + return mManager->Type(); +} + +nsIPrincipal* +DOMStorage::GetPrincipal() +{ + return mPrincipal; +} + +// Defined in DOMStorageManager.cpp +extern bool +PrincipalsEqual(nsIPrincipal* aObjectPrincipal, nsIPrincipal* aSubjectPrincipal); + +bool +DOMStorage::PrincipalEquals(nsIPrincipal* aPrincipal) +{ + return PrincipalsEqual(mPrincipal, aPrincipal); +} + +bool +DOMStorage::CanAccess(nsIPrincipal* aPrincipal) +{ + return !aPrincipal || aPrincipal->Subsumes(mPrincipal); +} + +void +DOMStorage::GetSupportedNames(nsTArray<nsString>& aKeys) +{ + if (!CanUseStorage(*nsContentUtils::SubjectPrincipal())) { + // return just an empty array + aKeys.Clear(); + return; + } + + mCache->GetKeys(this, aKeys); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/DOMStorage.h b/dom/storage/DOMStorage.h new file mode 100644 index 000000000..d956ac3ed --- /dev/null +++ b/dom/storage/DOMStorage.h @@ -0,0 +1,172 @@ +/* -*- 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 nsDOMStorage_h___ +#define nsDOMStorage_h___ + +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/Maybe.h" +#include "nsIDOMStorage.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWeakReference.h" +#include "nsWrapperCache.h" +#include "nsISupports.h" + +class nsIPrincipal; +class nsPIDOMWindowInner; + +namespace mozilla { +namespace dom { + +class DOMStorageManager; +class DOMStorageCache; + +class DOMStorage final + : public nsIDOMStorage + , public nsSupportsWeakReference + , public nsWrapperCache +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_AMBIGUOUS(DOMStorage, + nsIDOMStorage) + + enum StorageType { + LocalStorage = 1, + SessionStorage = 2 + }; + + StorageType GetType() const; + + DOMStorageManager* GetManager() const + { + return mManager; + } + + DOMStorageCache const* GetCache() const + { + return mCache; + } + + nsIPrincipal* GetPrincipal(); + bool PrincipalEquals(nsIPrincipal* aPrincipal); + bool CanAccess(nsIPrincipal* aPrincipal); + + DOMStorage(nsPIDOMWindowInner* aWindow, + DOMStorageManager* aManager, + DOMStorageCache* aCache, + const nsAString& aDocumentURI, + nsIPrincipal* aPrincipal, + bool aIsPrivate); + + // WebIDL + JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + nsPIDOMWindowInner* GetParentObject() const + { + return mWindow; + } + + uint32_t GetLength(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv); + + void Key(uint32_t aIndex, nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv); + + void GetItem(const nsAString& aKey, nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv); + + void GetSupportedNames(nsTArray<nsString>& aKeys); + + void NamedGetter(const nsAString& aKey, bool& aFound, nsAString& aResult, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) + { + GetItem(aKey, aResult, aSubjectPrincipal, aRv); + aFound = !aResult.IsVoid(); + } + + void SetItem(const nsAString& aKey, const nsAString& aValue, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv); + + void NamedSetter(const nsAString& aKey, const nsAString& aValue, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) + { + SetItem(aKey, aValue, aSubjectPrincipal, aRv); + } + + void RemoveItem(const nsAString& aKey, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv); + + void NamedDeleter(const nsAString& aKey, bool& aFound, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) + { + RemoveItem(aKey, aSubjectPrincipal, aRv); + + aFound = !aRv.ErrorCodeIs(NS_SUCCESS_DOM_NO_OPERATION); + } + + void Clear(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv); + + bool IsPrivate() const { return mIsPrivate; } + bool IsSessionOnly() const { return mIsSessionOnly; } + + bool IsForkOf(const DOMStorage* aOther) const + { + MOZ_ASSERT(aOther); + return mCache == aOther->mCache; + } + +protected: + // The method checks whether the caller can use a storage. + // CanUseStorage is called before any DOM initiated operation + // on a storage is about to happen and ensures that the storage's + // session-only flag is properly set according the current settings. + // It is an optimization since the privileges check and session only + // state determination are complex and share the code (comes hand in + // hand together). + bool CanUseStorage(nsIPrincipal& aSubjectPrincipal); + +private: + ~DOMStorage(); + + friend class DOMStorageManager; + friend class DOMStorageCache; + + nsCOMPtr<nsPIDOMWindowInner> mWindow; + RefPtr<DOMStorageManager> mManager; + RefPtr<DOMStorageCache> mCache; + nsString mDocumentURI; + + // Principal this DOMStorage (i.e. localStorage or sessionStorage) has + // been created for + nsCOMPtr<nsIPrincipal> mPrincipal; + + // Whether this storage is running in private-browsing window. + bool mIsPrivate : 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 mIsSessionOnly : 1; + + void BroadcastChangeNotification(const nsSubstring& aKey, + const nsSubstring& aOldValue, + const nsSubstring& aNewValue); +}; + +} // namespace dom +} // namespace mozilla + +#endif /* nsDOMStorage_h___ */ diff --git a/dom/storage/DOMStorageCache.cpp b/dom/storage/DOMStorageCache.cpp new file mode 100644 index 000000000..811f79fd3 --- /dev/null +++ b/dom/storage/DOMStorageCache.cpp @@ -0,0 +1,807 @@ +/* -*- 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 "DOMStorageCache.h" + +#include "DOMStorage.h" +#include "DOMStorageDBThread.h" +#include "DOMStorageIPC.h" +#include "DOMStorageManager.h" + +#include "nsAutoPtr.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 + +// static +DOMStorageDBBridge* DOMStorageCache::sDatabase = nullptr; +bool DOMStorageCache::sDatabaseDown = false; + +namespace { + +const uint32_t kDefaultSet = 0; +const uint32_t kPrivateSet = 1; +const uint32_t kSessionSet = 2; + +inline uint32_t +GetDataSetIndex(bool aPrivate, bool aSessionOnly) +{ + if (aPrivate) { + return kPrivateSet; + } + + if (aSessionOnly) { + return kSessionSet; + } + + return kDefaultSet; +} + +inline uint32_t +GetDataSetIndex(const DOMStorage* aStorage) +{ + return GetDataSetIndex(aStorage->IsPrivate(), aStorage->IsSessionOnly()); +} + +} // namespace + +// DOMStorageCacheBridge + +NS_IMPL_ADDREF(DOMStorageCacheBridge) + +// Since there is no consumer of return value of Release, we can turn this +// method to void to make implementation of asynchronous DOMStorageCache::Release +// much simpler. +NS_IMETHODIMP_(void) DOMStorageCacheBridge::Release(void) +{ + MOZ_ASSERT(int32_t(mRefCnt) > 0, "dup release"); + nsrefcnt count = --mRefCnt; + NS_LOG_RELEASE(this, count, "DOMStorageCacheBridge"); + if (0 == count) { + mRefCnt = 1; /* stabilize */ + /* enable this to find non-threadsafe destructors: */ + /* NS_ASSERT_OWNINGTHREAD(_class); */ + delete (this); + } +} + +// DOMStorageCache + +DOMStorageCache::DOMStorageCache(const nsACString* aOriginNoSuffix) +: mOriginNoSuffix(*aOriginNoSuffix) +, mMonitor("DOMStorageCache") +, mLoaded(false) +, mLoadResult(NS_OK) +, mInitialized(false) +, mPersistent(false) +, mSessionOnlyDataSetActive(false) +, mPreloadTelemetryRecorded(false) +{ + MOZ_COUNT_CTOR(DOMStorageCache); +} + +DOMStorageCache::~DOMStorageCache() +{ + if (mManager) { + mManager->DropCache(this); + } + + MOZ_COUNT_DTOR(DOMStorageCache); +} + +NS_IMETHODIMP_(void) +DOMStorageCache::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()) { + DOMStorageCacheBridge::Release(); + return; + } + + RefPtr<nsRunnableMethod<DOMStorageCacheBridge, void, false> > event = + NewNonOwningRunnableMethod(static_cast<DOMStorageCacheBridge*>(this), + &DOMStorageCacheBridge::Release); + + nsresult rv = NS_DispatchToMainThread(event); + if (NS_FAILED(rv)) { + NS_WARNING("DOMStorageCache::Release() on a non-main thread"); + DOMStorageCacheBridge::Release(); + } +} + +void +DOMStorageCache::Init(DOMStorageManager* aManager, + bool aPersistent, + nsIPrincipal* aPrincipal, + const nsACString& aQuotaOriginScope) +{ + if (mInitialized) { + return; + } + + mInitialized = true; + mPrincipal = aPrincipal; + BasePrincipal::Cast(aPrincipal)->OriginAttributesRef().CreateSuffix(mOriginSuffix); + mPersistent = aPersistent; + if (aQuotaOriginScope.IsEmpty()) { + mQuotaOriginScope = Origin(); + } else { + 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_LITERAL_CSTRING("^"))); + + mUsage = aManager->GetOriginUsage(mQuotaOriginScope); +} + +inline bool +DOMStorageCache::Persist(const DOMStorage* aStorage) const +{ + return mPersistent && + !aStorage->IsSessionOnly() && + !aStorage->IsPrivate(); +} + +const nsCString +DOMStorageCache::Origin() const +{ + return DOMStorageManager::CreateOrigin(mOriginSuffix, mOriginNoSuffix); +} + +DOMStorageCache::Data& +DOMStorageCache::DataSet(const DOMStorage* aStorage) +{ + uint32_t index = GetDataSetIndex(aStorage); + + if (index == kSessionSet && !mSessionOnlyDataSetActive) { + // Session only data set is demanded but not filled with + // current data set, copy to session only set now. + + WaitForPreload(Telemetry::LOCALDOMSTORAGE_SESSIONONLY_PRELOAD_BLOCKING_MS); + + Data& defaultSet = mData[kDefaultSet]; + Data& sessionSet = mData[kSessionSet]; + + for (auto iter = defaultSet.mKeys.Iter(); !iter.Done(); iter.Next()) { + sessionSet.mKeys.Put(iter.Key(), iter.UserData()); + } + + mSessionOnlyDataSetActive = true; + + // This updates sessionSet.mOriginQuotaUsage and also updates global usage + // for all session only data + ProcessUsageDelta(kSessionSet, defaultSet.mOriginQuotaUsage); + } + + return mData[index]; +} + +bool +DOMStorageCache::ProcessUsageDelta(const DOMStorage* aStorage, int64_t aDelta) +{ + return ProcessUsageDelta(GetDataSetIndex(aStorage), aDelta); +} + +bool +DOMStorageCache::ProcessUsageDelta(uint32_t aGetDataSetIndex, const int64_t aDelta) +{ + // Check if we are in a low disk space situation + if (aDelta > 0 && mManager && mManager->IsLowDiskSpace()) { + return false; + } + + // Check limit per this origin + Data& data = mData[aGetDataSetIndex]; + uint64_t newOriginUsage = data.mOriginQuotaUsage + aDelta; + if (aDelta > 0 && newOriginUsage > DOMStorageManager::GetQuota()) { + return false; + } + + // Now check eTLD+1 limit + if (mUsage && !mUsage->CheckAndSetETLD1UsageDelta(aGetDataSetIndex, aDelta)) { + return false; + } + + // Update size in our data set + data.mOriginQuotaUsage = newOriginUsage; + return true; +} + +void +DOMStorageCache::Preload() +{ + if (mLoaded || !mPersistent) { + return; + } + + if (!StartDatabase()) { + mLoaded = true; + mLoadResult = NS_ERROR_FAILURE; + return; + } + + sDatabase->AsyncPreload(this); +} + +namespace { + +// This class is passed to timer as a tick observer. It refers the cache +// and keeps it alive for a time. +class DOMStorageCacheHolder : public nsITimerCallback +{ + virtual ~DOMStorageCacheHolder() {} + + NS_DECL_ISUPPORTS + + NS_IMETHOD + Notify(nsITimer* aTimer) override + { + mCache = nullptr; + return NS_OK; + } + + RefPtr<DOMStorageCache> mCache; + +public: + explicit DOMStorageCacheHolder(DOMStorageCache* aCache) : mCache(aCache) {} +}; + +NS_IMPL_ISUPPORTS(DOMStorageCacheHolder, nsITimerCallback) + +} // namespace + +void +DOMStorageCache::KeepAlive() +{ + // Missing reference back to the manager means the cache is not responsible + // for its lifetime. Used for keeping sessionStorage live forever. + if (!mManager) { + return; + } + + if (!NS_IsMainThread()) { + // Timer and the holder must be initialized on the main thread. + NS_DispatchToMainThread(NewRunnableMethod(this, &DOMStorageCache::KeepAlive)); + return; + } + + nsCOMPtr<nsITimer> timer = do_CreateInstance("@mozilla.org/timer;1"); + if (!timer) { + return; + } + + RefPtr<DOMStorageCacheHolder> holder = new DOMStorageCacheHolder(this); + timer->InitWithCallback(holder, DOM_STORAGE_CACHE_KEEP_ALIVE_TIME_MS, + nsITimer::TYPE_ONE_SHOT); + + mKeepAliveTimer.swap(timer); +} + +namespace { + +// The AutoTimer provided by telemetry headers is only using static, +// i.e. compile time known ID, but here we know the ID only at run time. +// Hence a new class. +class TelemetryAutoTimer +{ +public: + explicit TelemetryAutoTimer(Telemetry::ID aId) + : id(aId), start(TimeStamp::Now()) {} + ~TelemetryAutoTimer() + { Telemetry::AccumulateDelta_impl<Telemetry::Millisecond>::compute(id, start); } +private: + Telemetry::ID id; + const TimeStamp start; +}; + +} // namespace + +void +DOMStorageCache::WaitForPreload(Telemetry::ID 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 + TelemetryAutoTimer 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. + sDatabase->SyncPreload(this); +} + +nsresult +DOMStorageCache::GetLength(const DOMStorage* 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 +DOMStorageCache::GetKey(const DOMStorage* 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 +DOMStorageCache::GetKeys(const DOMStorage* 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 +DOMStorageCache::GetItem(const DOMStorage* 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 +DOMStorageCache::SetItem(const DOMStorage* aStorage, const nsAString& aKey, + const nsString& aValue, nsString& aOld) +{ + // 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)) { + return NS_ERROR_DOM_QUOTA_REACHED; + } + + if (aValue == aOld && DOMStringIsNull(aValue) == DOMStringIsNull(aOld)) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + + data.mKeys.Put(aKey, aValue); + + if (Persist(aStorage)) { + if (!sDatabase) { + NS_ERROR("Writing to localStorage after the database has been shut down" + ", data lose!"); + return NS_ERROR_NOT_INITIALIZED; + } + + if (DOMStringIsNull(aOld)) { + return sDatabase->AsyncAddItem(this, aKey, aValue); + } + + return sDatabase->AsyncUpdateItem(this, aKey, aValue); + } + + return NS_OK; +} + +nsresult +DOMStorageCache::RemoveItem(const DOMStorage* aStorage, const nsAString& aKey, + nsString& aOld) +{ + 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); + data.mKeys.Remove(aKey); + + if (Persist(aStorage)) { + if (!sDatabase) { + NS_ERROR("Writing to localStorage after the database has been shut down" + ", data lose!"); + return NS_ERROR_NOT_INITIALIZED; + } + + return sDatabase->AsyncRemoveItem(this, aKey); + } + + return NS_OK; +} + +nsresult +DOMStorageCache::Clear(const DOMStorage* aStorage) +{ + 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); + data.mKeys.Clear(); + } + + if (Persist(aStorage) && (refresh || hadData)) { + if (!sDatabase) { + NS_ERROR("Writing to localStorage after the database has been shut down" + ", data lose!"); + return NS_ERROR_NOT_INITIALIZED; + } + + return sDatabase->AsyncClear(this); + } + + return hadData ? NS_OK : NS_SUCCESS_DOM_NO_OPERATION; +} + +void +DOMStorageCache::CloneFrom(const DOMStorageCache* aThat) +{ + // This will never be called on anything else than SessionStorage. + // This means mData will never be touched on any other thread than + // the main thread and it never went through the loading process. + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mPersistent); + MOZ_ASSERT(!(bool)aThat->mLoaded); + + mLoaded = false; + mInitialized = aThat->mInitialized; + mPersistent = false; + mSessionOnlyDataSetActive = aThat->mSessionOnlyDataSetActive; + + for (uint32_t i = 0; i < kDataSetCount; ++i) { + for (auto it = aThat->mData[i].mKeys.ConstIter(); !it.Done(); it.Next()) { + mData[i].mKeys.Put(it.Key(), it.UserData()); + } + ProcessUsageDelta(i, aThat->mData[i].mOriginQuotaUsage); + } +} + +// Defined in DOMStorageManager.cpp +extern bool +PrincipalsEqual(nsIPrincipal* aObjectPrincipal, nsIPrincipal* aSubjectPrincipal); + +bool +DOMStorageCache::CheckPrincipal(nsIPrincipal* aPrincipal) const +{ + return PrincipalsEqual(mPrincipal, aPrincipal); +} + +void +DOMStorageCache::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 & kUnloadPrivate) { + mData[kPrivateSet].mKeys.Clear(); + ProcessUsageDelta(kPrivateSet, -mData[kPrivateSet].mOriginQuotaUsage); + } + + if (aUnloadFlags & kUnloadSession) { + mData[kSessionSet].mKeys.Clear(); + ProcessUsageDelta(kSessionSet, -mData[kSessionSet].mOriginQuotaUsage); + mSessionOnlyDataSetActive = false; + } + +#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 +} + +// DOMStorageCacheBridge + +uint32_t +DOMStorageCache::LoadedCount() +{ + MonitorAutoLock monitor(mMonitor); + Data& data = mData[kDefaultSet]; + return data.mKeys.Count(); +} + +bool +DOMStorageCache::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 +DOMStorageCache::LoadDone(nsresult aRv) +{ + // Keep the preloaded cache alive for a time + KeepAlive(); + + MonitorAutoLock monitor(mMonitor); + mLoadResult = aRv; + mLoaded = true; + monitor.Notify(); +} + +void +DOMStorageCache::LoadWait() +{ + MonitorAutoLock monitor(mMonitor); + while (!mLoaded) { + monitor.Wait(); + } +} + +// DOMStorageUsage + +DOMStorageUsage::DOMStorageUsage(const nsACString& aOriginScope) + : mOriginScope(aOriginScope) +{ + mUsage[kDefaultSet] = mUsage[kPrivateSet] = mUsage[kSessionSet] = 0LL; +} + +namespace { + +class LoadUsageRunnable : public Runnable +{ +public: + LoadUsageRunnable(int64_t* aUsage, const int64_t aDelta) + : mTarget(aUsage) + , mDelta(aDelta) + {} + +private: + int64_t* mTarget; + int64_t mDelta; + + NS_IMETHOD Run() override { *mTarget = mDelta; return NS_OK; } +}; + +} // namespace + +void +DOMStorageUsage::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 +DOMStorageUsage::CheckAndSetETLD1UsageDelta(uint32_t aDataSetIndex, const int64_t aDelta) +{ + MOZ_ASSERT(NS_IsMainThread()); + + int64_t newUsage = mUsage[aDataSetIndex] + aDelta; + if (aDelta > 0 && newUsage > DOMStorageManager::GetQuota()) { + return false; + } + + mUsage[aDataSetIndex] = newUsage; + return true; +} + + +// static +DOMStorageDBBridge* +DOMStorageCache::StartDatabase() +{ + if (sDatabase || sDatabaseDown) { + // When sDatabaseDown is at true, sDatabase is null. + // Checking sDatabaseDown flag here prevents reinitialization of + // the database after shutdown. + return sDatabase; + } + + if (XRE_IsParentProcess()) { + nsAutoPtr<DOMStorageDBThread> db(new DOMStorageDBThread()); + + nsresult rv = db->Init(); + if (NS_FAILED(rv)) { + return nullptr; + } + + sDatabase = db.forget(); + } else { + // Use DOMLocalStorageManager::Ensure in case we're called from + // DOMSessionStorageManager's initializer and we haven't yet initialized the + // local storage manager. + RefPtr<DOMStorageDBChild> db = new DOMStorageDBChild( + DOMLocalStorageManager::Ensure()); + + nsresult rv = db->Init(); + if (NS_FAILED(rv)) { + return nullptr; + } + + db.forget(&sDatabase); + } + + return sDatabase; +} + +// static +DOMStorageDBBridge* +DOMStorageCache::GetDatabase() +{ + return sDatabase; +} + +// static +nsresult +DOMStorageCache::StopDatabase() +{ + if (!sDatabase) { + return NS_OK; + } + + sDatabaseDown = true; + + nsresult rv = sDatabase->Shutdown(); + if (XRE_IsParentProcess()) { + delete sDatabase; + } else { + DOMStorageDBChild* child = static_cast<DOMStorageDBChild*>(sDatabase); + NS_RELEASE(child); + } + + sDatabase = nullptr; + return rv; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/DOMStorageCache.h b/dom/storage/DOMStorageCache.h new file mode 100644 index 000000000..01cf6b3ea --- /dev/null +++ b/dom/storage/DOMStorageCache.h @@ -0,0 +1,288 @@ +/* -*- 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 nsDOMStorageCache_h___ +#define nsDOMStorageCache_h___ + +#include "nsIPrincipal.h" +#include "nsITimer.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 DOMStorage; +class DOMStorageUsage; +class DOMStorageManager; +class DOMStorageDBBridge; + +// Interface class on which only the database or IPC may call. +// Used to populate the cache with DB data. +class DOMStorageCacheBridge +{ +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 ~DOMStorageCacheBridge() {} + + 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 DOMStorageCache : public DOMStorageCacheBridge +{ +public: + NS_IMETHOD_(void) Release(void); + + // Note: We pass aOriginNoSuffix through the ctor here, because + // DOMStorageCacheHashKey's ctor is creating this class and + // accepts reversed-origin-no-suffix as an argument - the hashing key. + explicit DOMStorageCache(const nsACString* aOriginNoSuffix); + +protected: + virtual ~DOMStorageCache(); + +public: + void Init(DOMStorageManager* aManager, bool aPersistent, nsIPrincipal* aPrincipal, + const nsACString& aQuotaOriginScope); + + // Copies all data from the other storage. + void CloneFrom(const DOMStorageCache* aThat); + + // Starts async preload of this cache if it persistent and not loaded. + void Preload(); + + // Keeps the cache alive (i.e. present in the manager's hash table) for a time. + void KeepAlive(); + + // The set of methods that are invoked by DOM storage web API. + // We are passing the DOMStorage object just to let the cache + // read properties like mPrivate, mPrincipal and mSessionOnly. + // Get* methods return error when load from the database has failed. + nsresult GetLength(const DOMStorage* aStorage, uint32_t* aRetval); + nsresult GetKey(const DOMStorage* aStorage, uint32_t index, nsAString& aRetval); + nsresult GetItem(const DOMStorage* aStorage, const nsAString& aKey, nsAString& aRetval); + nsresult SetItem(const DOMStorage* aStorage, const nsAString& aKey, const nsString& aValue, nsString& aOld); + nsresult RemoveItem(const DOMStorage* aStorage, const nsAString& aKey, nsString& aOld); + nsresult Clear(const DOMStorage* aStorage); + + void GetKeys(const DOMStorage* aStorage, nsTArray<nsString>& aKeys); + + // Whether the principal equals principal the cache was created for + bool CheckPrincipal(nsIPrincipal* aPrincipal) const; + nsIPrincipal* Principal() const { return mPrincipal; } + + // Starts the database engine thread or the IPC bridge + static DOMStorageDBBridge* StartDatabase(); + static DOMStorageDBBridge* GetDatabase(); + + // Stops the thread and flushes all uncommited data + static nsresult StopDatabase(); + + // DOMStorageCacheBridge + + virtual const nsCString Origin() const; + virtual const nsCString& OriginNoSuffix() const { return mOriginNoSuffix; } + virtual const nsCString& OriginSuffix() const { return mOriginSuffix; } + virtual bool Loaded() { return mLoaded; } + virtual uint32_t LoadedCount(); + virtual bool LoadItem(const nsAString& aKey, const nsString& aValue); + virtual void LoadDone(nsresult aRv); + virtual void LoadWait(); + + // 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, private, session + static const uint32_t kDataSetCount = 3; + +private: + // API to clear the cache data, this is invoked by chrome operations + // like cookie deletion. + friend class DOMStorageManager; + + static const uint32_t kUnloadDefault = 1 << 0; + static const uint32_t kUnloadPrivate = 1 << 1; + static const uint32_t kUnloadSession = 1 << 2; + static const uint32_t kUnloadComplete = + kUnloadDefault | kUnloadPrivate | 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::ID aTelemetryID); + + // Helper to get one of the 3 data sets (regular, private, session) + Data& DataSet(const DOMStorage* aStorage); + + // Whether the storage change is about to persist + bool Persist(const DOMStorage* 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. + bool ProcessUsageDelta(uint32_t aGetDataSetIndex, const int64_t aDelta); + bool ProcessUsageDelta(const DOMStorage* aStorage, const int64_t aDelta); + +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<DOMStorageManager> mManager; + + // Reference to the usage counter object we check on for eTLD+1 quota limit. + // Obtained from the manager during initialization (Init method). + RefPtr<DOMStorageUsage> mUsage; + + // Timer that holds this cache alive for a while after it has been preloaded. + nsCOMPtr<nsITimer> mKeepAliveTimer; + + // Principal the cache has been initially created for, this is used only + // for sessionStorage access checks since sessionStorage objects are strictly + // scoped by a principal. localStorage objects on the other hand are scoped by + // origin only. + nsCOMPtr<nsIPrincipal> mPrincipal; + + // 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; + + // 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; + + // - False when the session-only data set was never used. + // - True after access to session-only data has been made for the first time. + // We also fill session-only data set with the default one at that moment. + // Drops back to false when session-only data are cleared from chrome. + bool mSessionOnlyDataSetActive : 1; + + // Whether we have already captured state of the cache preload on our first access. + bool mPreloadTelemetryRecorded : 1; + + // DOMStorageDBThread on the parent or single process, + // DOMStorageDBChild on the child process. + static DOMStorageDBBridge* sDatabase; + + // False until we shut the database down. + static bool sDatabaseDown; +}; + +// DOMStorageUsage +// Infrastructure to manage and check eTLD+1 quota +class DOMStorageUsageBridge +{ +public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DOMStorageUsageBridge) + + virtual const nsCString& OriginScope() = 0; + virtual void LoadUsage(const int64_t aUsage) = 0; + +protected: + // Protected destructor, to discourage deletion outside of Release(): + virtual ~DOMStorageUsageBridge() {} +}; + +class DOMStorageUsage : public DOMStorageUsageBridge +{ +public: + explicit DOMStorageUsage(const nsACString& aOriginScope); + + bool CheckAndSetETLD1UsageDelta(uint32_t aDataSetIndex, int64_t aUsageDelta); + +private: + virtual const nsCString& OriginScope() { return mOriginScope; } + virtual void LoadUsage(const int64_t aUsage); + + nsCString mOriginScope; + int64_t mUsage[DOMStorageCache::kDataSetCount]; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/storage/DOMStorageDBThread.cpp b/dom/storage/DOMStorageDBThread.cpp new file mode 100644 index 000000000..183be5c5c --- /dev/null +++ b/dom/storage/DOMStorageDBThread.cpp @@ -0,0 +1,1486 @@ +/* -*- 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 "DOMStorageDBThread.h" +#include "DOMStorageDBUpdater.h" +#include "DOMStorageCache.h" +#include "DOMStorageManager.h" + +#include "nsIEffectiveTLDService.h" +#include "nsDirectoryServiceUtils.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsThreadUtils.h" +#include "nsProxyRelease.h" +#include "mozStorageCID.h" +#include "mozStorageHelper.h" +#include "mozIStorageService.h" +#include "mozIStorageBindingParamsArray.h" +#include "mozIStorageBindingParams.h" +#include "mozIStorageValueArray.h" +#include "mozIStorageFunction.h" +#include "mozilla/BasePrincipal.h" +#include "nsIObserverService.h" +#include "nsVariant.h" +#include "mozilla/IOInterposer.h" +#include "mozilla/Services.h" +#include "mozilla/Tokenizer.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 1 + +namespace mozilla { +namespace dom { + +namespace { // anon + +// 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(DOMStorageCacheBridge* aCache) +{ + nsCString result; + + nsCString suffix = aCache->OriginSuffix(); + + PrincipalOriginAttributes oa; + if (!suffix.IsEmpty()) { + DebugOnly<bool> success = oa.PopulateFromSuffix(suffix); + MOZ_ASSERT(success); + } + + if (oa.mAppId != nsIScriptSecurityManager::NO_APP_ID || oa.mInIsolatedMozBrowser) { + result.AppendInt(oa.mAppId); + 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.mAppId = 0; + oa.mInIsolatedMozBrowser = false; + oa.CreateSuffix(remaining); + if (!remaining.IsEmpty()) { + MOZ_ASSERT(!suffix.IsEmpty()); + + if (result.IsEmpty()) { + // Must contain the old prefix, otherwise we won't search for the whole + // origin attributes suffix. + result.Append(NS_LITERAL_CSTRING("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(suffix); + result.Append(':'); + } + + result.Append(aCache->OriginNoSuffix()); + + return result; +} + +} // anon + + +DOMStorageDBBridge::DOMStorageDBBridge() +{ +} + + +DOMStorageDBThread::DOMStorageDBThread() +: mThread(nullptr) +, mThreadObserver(new ThreadObserver()) +, mStopIOThread(false) +, mWALModeEnabled(false) +, mDBReady(false) +, mStatus(NS_OK) +, mWorkerStatements(mWorkerConnection) +, mReaderStatements(mReaderConnection) +, mDirtyEpoch(0) +, mFlushImmediately(false) +, mPriorityCounter(0) +{ +} + +nsresult +DOMStorageDBThread::Init() +{ + nsresult rv; + + // Need to determine location on the main thread, since + // NS_GetSpecialDirectory access the atom table that can + // be accessed only on the main thread. + rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mDatabaseFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mDatabaseFile->Append(NS_LITERAL_STRING("webappsstore.sqlite")); + NS_ENSURE_SUCCESS(rv, rv); + + // Ensure mozIStorageService init on the main thread first. + nsCOMPtr<mozIStorageService> service = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); + 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, &DOMStorageDBThread::ThreadFunc, this, + PR_PRIORITY_LOW, PR_GLOBAL_THREAD, PR_JOINABLE_THREAD, + 262144); + if (!mThread) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +nsresult +DOMStorageDBThread::Shutdown() +{ + 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 +DOMStorageDBThread::SyncPreload(DOMStorageCacheBridge* aCache, bool aForceSync) +{ + PROFILER_LABEL_FUNC(js::ProfileEntry::Category::STORAGE); + 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 +DOMStorageDBThread::AsyncFlush() +{ + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); + mFlushImmediately = true; + monitor.Notify(); +} + +bool +DOMStorageDBThread::ShouldPreloadOrigin(const nsACString& aOrigin) +{ + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); + return mOriginsHavingData.Contains(aOrigin); +} + +void +DOMStorageDBThread::GetOriginsHavingData(InfallibleTArray<nsCString>* aOrigins) +{ + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); + for (auto iter = mOriginsHavingData.Iter(); !iter.Done(); iter.Next()) { + aOrigins->AppendElement(iter.Get()->GetKey()); + } +} + +nsresult +DOMStorageDBThread::InsertDBOp(DOMStorageDBThread::DBOperation* aOperation) +{ + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); + + // Sentinel to don't forget to delete the operation when we exit early. + nsAutoPtr<DOMStorageDBThread::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; + } + MOZ_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. + opScope.forget(); + + // 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. + opScope.forget(); + + ScheduleFlush(); + break; + } + + return NS_OK; +} + +void +DOMStorageDBThread::SetHigherPriority() +{ + ++mPriorityCounter; + PR_SetThreadPriority(mThread, PR_PRIORITY_URGENT); +} + +void +DOMStorageDBThread::SetDefaultPriority() +{ + if (--mPriorityCounter <= 0) { + PR_SetThreadPriority(mThread, PR_PRIORITY_LOW); + } +} + +void +DOMStorageDBThread::ThreadFunc(void* aArg) +{ + PR_SetCurrentThreadName("localStorage DB"); + mozilla::IOInterposer::RegisterCurrentThread(); + + DOMStorageDBThread* thread = static_cast<DOMStorageDBThread*>(aArg); + thread->ThreadFunc(); + mozilla::IOInterposer::UnregisterCurrentThread(); +} + +void +DOMStorageDBThread::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); + } + + if (MOZ_UNLIKELY(TimeUntilFlush() == 0)) { + // 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())) { + nsAutoPtr<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)) { + lockMonitor.Wait(TimeUntilFlush()); + } + } // thread loop + + mStatus = ShutdownDatabase(); + + if (threadInternal) { + threadInternal->SetObserver(nullptr); + } +} + + +NS_IMPL_ISUPPORTS(DOMStorageDBThread::ThreadObserver, nsIThreadObserver) + +NS_IMETHODIMP +DOMStorageDBThread::ThreadObserver::OnDispatchedEvent(nsIThreadInternal *thread) +{ + MonitorAutoLock lock(mMonitor); + mHasPendingEvents = true; + lock.Notify(); + return NS_OK; +} + +NS_IMETHODIMP +DOMStorageDBThread::ThreadObserver::OnProcessNextEvent(nsIThreadInternal *thread, + bool mayWait) +{ + return NS_OK; +} + +NS_IMETHODIMP +DOMStorageDBThread::ThreadObserver::AfterProcessNextEvent(nsIThreadInternal *thread, + bool eventWasProcessed) +{ + return NS_OK; +} + + +extern void +ReverseString(const nsCSubstring& aSource, nsCSubstring& aResult); + +nsresult +DOMStorageDBThread::OpenDatabaseConnection() +{ + nsresult rv; + + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<mozIStorageService> service + = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + 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)); + } + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +DOMStorageDBThread::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); + + rv = TryJournalMode(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +DOMStorageDBThread::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 = DOMStorageDBUpdater::Update(mWorkerConnection); + if (NS_FAILED(rv)) { + // 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 totaly broken and incosistent database. + mDBReady = true; + + // List scopes having any stored data + nsCOMPtr<mozIStorageStatement> stmt; + // Note: result of this select must match DOMStorageManager::CreateOrigin() + rv = mWorkerConnection->CreateStatement(NS_LITERAL_CSTRING( + "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 +DOMStorageDBThread::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 +DOMStorageDBThread::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 +DOMStorageDBThread::ConfigureWALBehavior() +{ + // Get the DB's page size + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = mWorkerConnection->CreateStatement(NS_LITERAL_CSTRING( + 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 +DOMStorageDBThread::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 +DOMStorageDBThread::ScheduleFlush() +{ + if (mDirtyEpoch) { + return; // Already scheduled + } + + mDirtyEpoch = PR_IntervalNow() | 1; // Must be non-zero to indicate we are scheduled + + // Wake the monitor from indefinite sleep... + (mThreadObserver->GetMonitor()).Notify(); +} + +void +DOMStorageDBThread::UnscheduleFlush() +{ + // We are just about to do the flush, drop flags + mFlushImmediately = false; + mDirtyEpoch = 0; +} + +PRIntervalTime +DOMStorageDBThread::TimeUntilFlush() +{ + if (mFlushImmediately) { + return 0; // Do it now regardless the timeout. + } + + static_assert(PR_INTERVAL_NO_TIMEOUT != 0, + "PR_INTERVAL_NO_TIMEOUT must be non-zero"); + + if (!mDirtyEpoch) { + return PR_INTERVAL_NO_TIMEOUT; // No pending task... + } + + static const PRIntervalTime kMaxAge = PR_MillisecondsToInterval(FLUSHING_INTERVAL_MS); + + PRIntervalTime now = PR_IntervalNow() | 1; + PRIntervalTime age = now - mDirtyEpoch; + if (age > kMaxAge) { + return 0; // It is time. + } + + return kMaxAge - age; // Time left, this is used to sleep the monitor +} + +void +DOMStorageDBThread::NotifyFlushCompletion() +{ +#ifdef DOM_STORAGE_TESTS + if (!NS_IsMainThread()) { + RefPtr<nsRunnableMethod<DOMStorageDBThread, void, false> > event = + NewNonOwningRunnableMethod(this, &DOMStorageDBThread::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() {} + + 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); + + PrincipalOriginAttributes 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 + +// DOMStorageDBThread::DBOperation + +DOMStorageDBThread::DBOperation::DBOperation(const OperationType aType, + DOMStorageCacheBridge* 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(DOMStorageDBThread::DBOperation); +} + +DOMStorageDBThread::DBOperation::DBOperation(const OperationType aType, + DOMStorageUsageBridge* aUsage) +: mType(aType) +, mUsage(aUsage) +{ + MOZ_ASSERT(mType == opGetUsage); + MOZ_COUNT_CTOR(DOMStorageDBThread::DBOperation); +} + +DOMStorageDBThread::DBOperation::DBOperation(const OperationType aType, + const nsACString& aOriginNoSuffix) +: mType(aType) +, mCache(nullptr) +, mOrigin(aOriginNoSuffix) +{ + MOZ_ASSERT(mType == opClearMatchingOrigin); + MOZ_COUNT_CTOR(DOMStorageDBThread::DBOperation); +} + +DOMStorageDBThread::DBOperation::DBOperation(const OperationType aType, + const OriginAttributesPattern& aOriginNoSuffix) +: mType(aType) +, mCache(nullptr) +, mOriginPattern(aOriginNoSuffix) +{ + MOZ_ASSERT(mType == opClearMatchingOriginAttributes); + MOZ_COUNT_CTOR(DOMStorageDBThread::DBOperation); +} + +DOMStorageDBThread::DBOperation::~DBOperation() +{ + MOZ_COUNT_DTOR(DOMStorageDBThread::DBOperation); +} + +const nsCString +DOMStorageDBThread::DBOperation::OriginNoSuffix() const +{ + if (mCache) { + return mCache->OriginNoSuffix(); + } + + return EmptyCString(); +} + +const nsCString +DOMStorageDBThread::DBOperation::OriginSuffix() const +{ + if (mCache) { + return mCache->OriginSuffix(); + } + + return EmptyCString(); +} + +const nsCString +DOMStorageDBThread::DBOperation::Origin() const +{ + if (mCache) { + return mCache->Origin(); + } + + return mOrigin; +} + +const nsCString +DOMStorageDBThread::DBOperation::Target() const +{ + switch (mType) { + case opAddItem: + case opUpdateItem: + case opRemoveItem: + return Origin() + NS_LITERAL_CSTRING("|") + NS_ConvertUTF16toUTF8(mKey); + + default: + return Origin(); + } +} + +void +DOMStorageDBThread::DBOperation::PerformAndFinalize(DOMStorageDBThread* aThread) +{ + Finalize(Perform(aThread)); +} + +nsresult +DOMStorageDBThread::DBOperation::Perform(DOMStorageDBThread* aThread) +{ + nsresult rv; + + switch (mType) { + case opPreload: + case opPreloadUrgent: + { + // Already loaded? + if (mCache->Loaded()) { + break; + } + + StatementCache* statements; + if (MOZ_UNLIKELY(NS_IsMainThread())) { + 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(NS_LITERAL_CSTRING("originAttributes"), + mCache->OriginSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("originKey"), + mCache->OriginNoSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("offset"), + 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: + { + 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(NS_LITERAL_CSTRING("usageOrigin"), + mUsage->OriginScope()); + NS_ENSURE_SUCCESS(rv, rv); + + 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(NS_LITERAL_CSTRING("originAttributes"), + mCache->OriginSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("originKey"), + mCache->OriginNoSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + // Filling the 'scope' column just for downgrade compatibility reasons + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("scope"), + Scheme0Scope(mCache)); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key"), + mKey); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("value"), + 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(NS_LITERAL_CSTRING("originAttributes"), + mCache->OriginSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("originKey"), + mCache->OriginNoSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key"), + 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(NS_LITERAL_CSTRING("originAttributes"), + mCache->OriginSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("originKey"), + 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(NS_LITERAL_CSTRING("scope"), + mOrigin + NS_LITERAL_CSTRING("*")); + 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( + NS_LITERAL_CSTRING("ORIGIN_ATTRS_PATTERN_MATCH"), 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( + NS_LITERAL_CSTRING("ORIGIN_ATTRS_PATTERN_MATCH")); + + 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 +DOMStorageDBThread::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; + } +} + +// DOMStorageDBThread::PendingOperations + +DOMStorageDBThread::PendingOperations::PendingOperations() +: mFlushFailureCount(0) +{ +} + +bool +DOMStorageDBThread::PendingOperations::HasTasks() const +{ + return !!mUpdates.Count() || !!mClears.Count(); +} + +namespace { + +bool OriginPatternMatches(const nsACString& aOriginSuffix, const OriginAttributesPattern& aPattern) +{ + PrincipalOriginAttributes oa; + DebugOnly<bool> rv = oa.PopulateFromSuffix(aOriginSuffix); + MOZ_ASSERT(rv); + return aPattern.Matches(oa); +} + +} // namespace + +bool +DOMStorageDBThread::PendingOperations::CheckForCoalesceOpportunity(DBOperation* aNewOp, + DBOperation::OperationType aPendingType, + DBOperation::OperationType aNewType) +{ + if (aNewOp->Type() != aNewType) { + return false; + } + + DOMStorageDBThread::DBOperation* pendingTask; + if (!mUpdates.Get(aNewOp->Target(), &pendingTask)) { + return false; + } + + if (pendingTask->Type() != aPendingType) { + return false; + } + + return true; +} + +void +DOMStorageDBThread::PendingOperations::Add(DOMStorageDBThread::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()) { + nsAutoPtr<DBOperation>& 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 +DOMStorageDBThread::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(iter.Data().forget()); + } + mClears.Clear(); + + for (auto iter = mUpdates.Iter(); !iter.Done(); iter.Next()) { + mExecList.AppendElement(iter.Data().forget()); + } + mUpdates.Clear(); + + return !!mExecList.Length(); +} + +nsresult +DOMStorageDBThread::PendingOperations::Execute(DOMStorageDBThread* aThread) +{ + // Called outside the lock + + mozStorageTransaction transaction(aThread->mWorkerConnection, false); + + nsresult rv; + + for (uint32_t i = 0; i < mExecList.Length(); ++i) { + DOMStorageDBThread::DBOperation* 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 +DOMStorageDBThread::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, + DOMStorageDBThread::DBOperation* aPendingOperation) +{ + if (aPendingOperation->Type() == DOMStorageDBThread::DBOperation::opClearAll) { + return true; + } + + if (aPendingOperation->Type() == DOMStorageDBThread::DBOperation::opClear && + aOriginNoSuffix == aPendingOperation->OriginNoSuffix() && + aOriginSuffix == aPendingOperation->OriginSuffix()) { + return true; + } + + if (aPendingOperation->Type() == DOMStorageDBThread::DBOperation::opClearMatchingOrigin && + StringBeginsWith(aOriginNoSuffix, aPendingOperation->Origin())) { + return true; + } + + if (aPendingOperation->Type() == DOMStorageDBThread::DBOperation::opClearMatchingOriginAttributes && + OriginPatternMatches(aOriginSuffix, aPendingOperation->OriginPattern())) { + return true; + } + + return false; +} + +} // namespace + +bool +DOMStorageDBThread::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])) { + return true; + } + } + + return false; +} + +namespace { + +bool +FindPendingUpdateForOrigin(const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix, + DOMStorageDBThread::DBOperation* aPendingOperation) +{ + if ((aPendingOperation->Type() == DOMStorageDBThread::DBOperation::opAddItem || + aPendingOperation->Type() == DOMStorageDBThread::DBOperation::opUpdateItem || + aPendingOperation->Type() == DOMStorageDBThread::DBOperation::opRemoveItem) && + aOriginNoSuffix == aPendingOperation->OriginNoSuffix() && + aOriginSuffix == aPendingOperation->OriginSuffix()) { + return true; + } + + return false; +} + +} // namespace + +bool +DOMStorageDBThread::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])) { + return true; + } + } + + return false; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/DOMStorageDBThread.h b/dom/storage/DOMStorageDBThread.h new file mode 100644 index 000000000..0efaebaa3 --- /dev/null +++ b/dom/storage/DOMStorageDBThread.h @@ -0,0 +1,407 @@ +/* -*- 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 DOMStorageDBThread_h___ +#define DOMStorageDBThread_h___ + +#include "prthread.h" +#include "prinrval.h" +#include "nsTArray.h" +#include "mozilla/Atomics.h" +#include "mozilla/Monitor.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/storage/StatementCache.h" +#include "nsAutoPtr.h" +#include "nsString.h" +#include "nsCOMPtr.h" +#include "nsClassHashtable.h" +#include "nsIFile.h" +#include "nsIThreadInternal.h" + +class mozIStorageConnection; + +namespace mozilla { +namespace dom { + +class DOMStorageCacheBridge; +class DOMStorageUsageBridge; +class DOMStorageUsage; + +typedef mozilla::storage::StatementCache<mozIStorageStatement> StatementCache; + +// Interface used by the cache to post operations to the asynchronous +// database thread or process. +class DOMStorageDBBridge +{ +public: + DOMStorageDBBridge(); + virtual ~DOMStorageDBBridge() {} + + // 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(DOMStorageCacheBridge* 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(DOMStorageUsageBridge* 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(DOMStorageCacheBridge* aCache, bool aForceSync = false) = 0; + + // Called when an existing key is modified in the storage, schedules update to the database + virtual nsresult AsyncAddItem(DOMStorageCacheBridge* 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(DOMStorageCacheBridge* 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(DOMStorageCacheBridge* aCache, const nsAString& aKey) = 0; + + // Called when the whole storage is cleared by the DOM API, schedules delete of the scope + virtual nsresult AsyncClear(DOMStorageCacheBridge* 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; + + // Get the complete list of scopes having data + virtual void GetOriginsHavingData(InfallibleTArray<nsCString>* aOrigins) = 0; +}; + +// 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 (DOMStorageCache) communicating though +// DOMStorageCacheBridge interface class +class DOMStorageDBThread final : public DOMStorageDBBridge +{ +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, + DOMStorageCacheBridge* aCache = nullptr, + const nsAString& aKey = EmptyString(), + const nsAString& aValue = EmptyString()); + DBOperation(const OperationType aType, + DOMStorageUsageBridge* 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(DOMStorageDBThread* 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(DOMStorageDBThread* aThread); + + friend class PendingOperations; + OperationType mType; + RefPtr<DOMStorageCacheBridge> mCache; + RefPtr<DOMStorageUsageBridge> 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, retuns result, but doesn't + // handle it in any way in case of a failure + nsresult Execute(DOMStorageDBThread* 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<nsAutoPtr<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("DOMStorageThreadMonitor") + { + } + + bool HasPendingEvents() { + mMonitor.AssertCurrentThreadOwns(); + return mHasPendingEvents; + } + void ClearPendingEvents() { + mMonitor.AssertCurrentThreadOwns(); + mHasPendingEvents = false; + } + Monitor& GetMonitor() { return mMonitor; } + + private: + virtual ~ThreadObserver() {} + bool mHasPendingEvents; + // The monitor we drive the thread with + Monitor mMonitor; + }; + +public: + DOMStorageDBThread(); + virtual ~DOMStorageDBThread() {} + + virtual nsresult Init(); + virtual nsresult Shutdown(); + + virtual void AsyncPreload(DOMStorageCacheBridge* aCache, bool aPriority = false) + { InsertDBOp(new DBOperation(aPriority ? DBOperation::opPreloadUrgent : DBOperation::opPreload, aCache)); } + + virtual void SyncPreload(DOMStorageCacheBridge* aCache, bool aForce = false); + + virtual void AsyncGetUsage(DOMStorageUsageBridge * aUsage) + { InsertDBOp(new DBOperation(DBOperation::opGetUsage, aUsage)); } + + virtual nsresult AsyncAddItem(DOMStorageCacheBridge* aCache, const nsAString& aKey, const nsAString& aValue) + { return InsertDBOp(new DBOperation(DBOperation::opAddItem, aCache, aKey, aValue)); } + + virtual nsresult AsyncUpdateItem(DOMStorageCacheBridge* aCache, const nsAString& aKey, const nsAString& aValue) + { return InsertDBOp(new DBOperation(DBOperation::opUpdateItem, aCache, aKey, aValue)); } + + virtual nsresult AsyncRemoveItem(DOMStorageCacheBridge* aCache, const nsAString& aKey) + { return InsertDBOp(new DBOperation(DBOperation::opRemoveItem, aCache, aKey)); } + + virtual nsresult AsyncClear(DOMStorageCacheBridge* 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); + virtual void GetOriginsHavingData(InfallibleTArray<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 + PRIntervalTime 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; + + // 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: + // - PR_INTERVAL_NO_TIMEOUT when no pending tasks are scheduled + // - larger then zero when tasks have been scheduled, but it is + // still not time to perform the flush ; it is actual interval + // time to wait until the flush has to happen + // - 0 when it is time to do the flush + PRIntervalTime 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 /* DOMStorageDBThread_h___ */ diff --git a/dom/storage/DOMStorageDBUpdater.cpp b/dom/storage/DOMStorageDBUpdater.cpp new file mode 100644 index 000000000..8e3f07d7c --- /dev/null +++ b/dom/storage/DOMStorageDBUpdater.cpp @@ -0,0 +1,417 @@ +/* -*- 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 "DOMStorageManager.h" + +#include "mozIStorageBindingParamsArray.h" +#include "mozIStorageBindingParams.h" +#include "mozIStorageValueArray.h" +#include "mozIStorageFunction.h" +#include "mozilla/BasePrincipal.h" +#include "nsVariant.h" +#include "mozilla/Services.h" +#include "mozilla/Tokenizer.h" + +// Current version of the database schema +#define CURRENT_SCHEMA_VERSION 1 + +namespace mozilla { +namespace dom { + +extern void +ReverseString(const nsCSubstring& aSource, nsCSubstring& aResult); + +namespace { + +class nsReverseStringSQLFunction final : public mozIStorageFunction +{ + ~nsReverseStringSQLFunction() {} + + 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. + 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 { + PrincipalOriginAttributes attrs(appId, 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() {} + + 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(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; +} + +} // namespace + +namespace DOMStorageDBUpdater { + +nsresult CreateSchema1Tables(mozIStorageConnection *aWorkerConnection) +{ + nsresult rv; + + rv = aWorkerConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "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(NS_LITERAL_CSTRING( + "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(NS_LITERAL_CSTRING("scope_key_index"), + &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(NS_LITERAL_CSTRING("webappsstore2"), + &webappsstore2Exists); + NS_ENSURE_SUCCESS(rv, rv); + rv = aWorkerConnection->TableExists(NS_LITERAL_CSTRING("webappsstore"), + &webappsstoreExists); + NS_ENSURE_SUCCESS(rv, rv); + rv = aWorkerConnection->TableExists(NS_LITERAL_CSTRING("moz_webappsstore"), + &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(NS_LITERAL_CSTRING( + "CREATE TABLE IF NOT EXISTS webappsstore2 (" + "scope TEXT, " + "key TEXT, " + "value TEXT, " + "secure INTEGER, " + "owner TEXT)")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aWorkerConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "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(NS_LITERAL_CSTRING("REVERSESTRING"), 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( + NS_LITERAL_CSTRING("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( + NS_LITERAL_CSTRING("DROP TABLE webappsstore")); + 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( + NS_LITERAL_CSTRING("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( + NS_LITERAL_CSTRING("DROP TABLE moz_webappsstore")); + NS_ENSURE_SUCCESS(rv, rv); + } + + aWorkerConnection->RemoveFunction(NS_LITERAL_CSTRING("REVERSESTRING")); + + // 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(NS_LITERAL_CSTRING( + "DROP INDEX IF EXISTS webappsstore2.origin_key_index")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aWorkerConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP INDEX IF EXISTS webappsstore2.scope_key_index")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aWorkerConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE webappsstore2 RENAME TO webappsstore2_old")); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<mozIStorageFunction> oaSuffixFunc( + new GetOriginParticular(GetOriginParticular::ORIGIN_ATTRIBUTES_SUFFIX)); + rv = aWorkerConnection->CreateFunction(NS_LITERAL_CSTRING("GET_ORIGIN_SUFFIX"), 1, oaSuffixFunc); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<mozIStorageFunction> originKeyFunc( + new GetOriginParticular(GetOriginParticular::ORIGIN_KEY)); + rv = aWorkerConnection->CreateFunction(NS_LITERAL_CSTRING("GET_ORIGIN_KEY"), 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(NS_LITERAL_CSTRING( + "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( + NS_LITERAL_CSTRING("DROP TABLE webappsstore2_old")); + NS_ENSURE_SUCCESS(rv, rv); + + aWorkerConnection->RemoveFunction(NS_LITERAL_CSTRING("GET_ORIGIN_SUFFIX")); + aWorkerConnection->RemoveFunction(NS_LITERAL_CSTRING("GET_ORIGIN_KEY")); + + rv = aWorkerConnection->SetSchemaVersion(1); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_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( + NS_LITERAL_CSTRING("VACUUM")); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +} // namespace DOMStorageDBUpdater +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/DOMStorageDBUpdater.h b/dom/storage/DOMStorageDBUpdater.h new file mode 100644 index 000000000..6ab4622bc --- /dev/null +++ b/dom/storage/DOMStorageDBUpdater.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 DOMStorageDBUpdater_h___ +#define DOMStorageDBUpdater_h___ + +namespace mozilla { +namespace dom { + +namespace DOMStorageDBUpdater { + +nsresult Update(mozIStorageConnection *aWorkerConnection); + +} // DOMStorageDBUpdater + +} // dom +} // mozilla + +#endif diff --git a/dom/storage/DOMStorageIPC.cpp b/dom/storage/DOMStorageIPC.cpp new file mode 100644 index 000000000..a8cd745f1 --- /dev/null +++ b/dom/storage/DOMStorageIPC.cpp @@ -0,0 +1,770 @@ +/* -*- 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 "DOMStorageIPC.h" + +#include "DOMStorageManager.h" + +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/Unused.h" +#include "nsIDiskSpaceWatcher.h" +#include "nsThreadUtils.h" + +namespace mozilla { +namespace dom { + +// ---------------------------------------------------------------------------- +// Child +// ---------------------------------------------------------------------------- + +NS_IMPL_ADDREF(DOMStorageDBChild) + +NS_IMETHODIMP_(MozExternalRefCountType) DOMStorageDBChild::Release(void) +{ + NS_PRECONDITION(0 != mRefCnt, "dup release"); + nsrefcnt count = --mRefCnt; + NS_LOG_RELEASE(this, count, "DOMStorageDBChild"); + if (count == 1 && mIPCOpen) { + Send__delete__(this); + return 0; + } + if (count == 0) { + mRefCnt = 1; + delete this; + return 0; + } + return count; +} + +void +DOMStorageDBChild::AddIPDLReference() +{ + MOZ_ASSERT(!mIPCOpen, "Attempting to retain multiple IPDL references"); + mIPCOpen = true; + AddRef(); +} + +void +DOMStorageDBChild::ReleaseIPDLReference() +{ + MOZ_ASSERT(mIPCOpen, "Attempting to release non-existent IPDL reference"); + mIPCOpen = false; + Release(); +} + +DOMStorageDBChild::DOMStorageDBChild(DOMLocalStorageManager* aManager) + : mManager(aManager) + , mStatus(NS_OK) + , mIPCOpen(false) +{ +} + +DOMStorageDBChild::~DOMStorageDBChild() +{ +} + +nsTHashtable<nsCStringHashKey>& +DOMStorageDBChild::OriginsHavingData() +{ + if (!mOriginsHavingData) { + mOriginsHavingData = new nsTHashtable<nsCStringHashKey>; + } + + return *mOriginsHavingData; +} + +nsresult +DOMStorageDBChild::Init() +{ + ContentChild* child = ContentChild::GetSingleton(); + AddIPDLReference(); + child->SendPStorageConstructor(this); + return NS_OK; +} + +nsresult +DOMStorageDBChild::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 +DOMStorageDBChild::AsyncPreload(DOMStorageCacheBridge* 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 +DOMStorageDBChild::AsyncGetUsage(DOMStorageUsageBridge* aUsage) +{ + if (mIPCOpen) { + SendAsyncGetUsage(aUsage->OriginScope()); + } +} + +void +DOMStorageDBChild::SyncPreload(DOMStorageCacheBridge* 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. + InfallibleTArray<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 +DOMStorageDBChild::AsyncAddItem(DOMStorageCacheBridge* 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 +DOMStorageDBChild::AsyncUpdateItem(DOMStorageCacheBridge* 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 +DOMStorageDBChild::AsyncRemoveItem(DOMStorageCacheBridge* aCache, + const nsAString& aKey) +{ + if (NS_FAILED(mStatus) || !mIPCOpen) { + return mStatus; + } + + SendAsyncRemoveItem(aCache->OriginSuffix(), aCache->OriginNoSuffix(), + nsString(aKey)); + return NS_OK; +} + +nsresult +DOMStorageDBChild::AsyncClear(DOMStorageCacheBridge* aCache) +{ + if (NS_FAILED(mStatus) || !mIPCOpen) { + return mStatus; + } + + SendAsyncClear(aCache->OriginSuffix(), aCache->OriginNoSuffix()); + OriginsHavingData().RemoveEntry(aCache->Origin()); + return NS_OK; +} + +bool +DOMStorageDBChild::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); +} + +bool +DOMStorageDBChild::RecvObserve(const nsCString& aTopic, + const nsString& aOriginAttributesPattern, + const nsCString& aOriginScope) +{ + DOMStorageObserver::Self()->Notify( + aTopic.get(), aOriginAttributesPattern, aOriginScope); + return true; +} + +bool +DOMStorageDBChild::RecvOriginsHavingData(nsTArray<nsCString>&& aOrigins) +{ + for (uint32_t i = 0; i < aOrigins.Length(); ++i) { + OriginsHavingData().PutEntry(aOrigins[i]); + } + + return true; +} + +bool +DOMStorageDBChild::RecvLoadItem(const nsCString& aOriginSuffix, + const nsCString& aOriginNoSuffix, + const nsString& aKey, + const nsString& aValue) +{ + DOMStorageCache* aCache = mManager->GetCache(aOriginSuffix, aOriginNoSuffix); + if (aCache) { + aCache->LoadItem(aKey, aValue); + } + + return true; +} + +bool +DOMStorageDBChild::RecvLoadDone(const nsCString& aOriginSuffix, + const nsCString& aOriginNoSuffix, + const nsresult& aRv) +{ + DOMStorageCache* 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<DOMStorageCacheBridge*>(aCache)); + } + + return true; +} + +bool +DOMStorageDBChild::RecvLoadUsage(const nsCString& aOriginNoSuffix, const int64_t& aUsage) +{ + RefPtr<DOMStorageUsageBridge> scopeUsage = mManager->GetOriginUsage(aOriginNoSuffix); + scopeUsage->LoadUsage(aUsage); + return true; +} + +bool +DOMStorageDBChild::RecvError(const nsresult& aRv) +{ + mStatus = aRv; + return true; +} + +// ---------------------------------------------------------------------------- +// Parent +// ---------------------------------------------------------------------------- + +NS_IMPL_ADDREF(DOMStorageDBParent) +NS_IMPL_RELEASE(DOMStorageDBParent) + +void +DOMStorageDBParent::AddIPDLReference() +{ + MOZ_ASSERT(!mIPCOpen, "Attempting to retain multiple IPDL references"); + mIPCOpen = true; + AddRef(); +} + +void +DOMStorageDBParent::ReleaseIPDLReference() +{ + MOZ_ASSERT(mIPCOpen, "Attempting to release non-existent IPDL reference"); + mIPCOpen = false; + Release(); +} + +namespace { + +class SendInitialChildDataRunnable : public Runnable +{ +public: + explicit SendInitialChildDataRunnable(DOMStorageDBParent* aParent) + : mParent(aParent) + {} + +private: + NS_IMETHOD Run() override + { + if (!mParent->IPCOpen()) { + return NS_OK; + } + + DOMStorageDBBridge* db = DOMStorageCache::GetDatabase(); + if (db) { + InfallibleTArray<nsCString> scopes; + db->GetOriginsHavingData(&scopes); + mozilla::Unused << mParent->SendOriginsHavingData(scopes); + } + + // We need to check if the device is in a low disk space situation, so + // we can forbid in that case any write in localStorage. + nsCOMPtr<nsIDiskSpaceWatcher> diskSpaceWatcher = + do_GetService("@mozilla.org/toolkit/disk-space-watcher;1"); + if (!diskSpaceWatcher) { + return NS_OK; + } + + bool lowDiskSpace = false; + diskSpaceWatcher->GetIsDiskFull(&lowDiskSpace); + + if (lowDiskSpace) { + mozilla::Unused << mParent->SendObserve( + nsDependentCString("low-disk-space"), EmptyString(), EmptyCString()); + } + + return NS_OK; + } + + RefPtr<DOMStorageDBParent> mParent; +}; + +} // namespace + +DOMStorageDBParent::DOMStorageDBParent() +: mIPCOpen(false) +{ + DOMStorageObserver* observer = DOMStorageObserver::Self(); + if (observer) { + observer->AddSink(this); + } + + // We are always open by IPC only + AddIPDLReference(); + + // Cannot send directly from here since the channel + // is not completely built at this moment. + RefPtr<SendInitialChildDataRunnable> r = + new SendInitialChildDataRunnable(this); + NS_DispatchToCurrentThread(r); +} + +DOMStorageDBParent::~DOMStorageDBParent() +{ + DOMStorageObserver* observer = DOMStorageObserver::Self(); + if (observer) { + observer->RemoveSink(this); + } +} + +DOMStorageDBParent::CacheParentBridge* +DOMStorageDBParent::NewCache(const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix) +{ + return new CacheParentBridge(this, aOriginSuffix, aOriginNoSuffix); +} + +void +DOMStorageDBParent::ActorDestroy(ActorDestroyReason aWhy) +{ + // Implement me! Bug 1005169 +} + +bool +DOMStorageDBParent::RecvAsyncPreload(const nsCString& aOriginSuffix, + const nsCString& aOriginNoSuffix, + const bool& aPriority) +{ + DOMStorageDBBridge* db = DOMStorageCache::StartDatabase(); + if (!db) { + return false; + } + + db->AsyncPreload(NewCache(aOriginSuffix, aOriginNoSuffix), aPriority); + return true; +} + +bool +DOMStorageDBParent::RecvAsyncGetUsage(const nsCString& aOriginNoSuffix) +{ + DOMStorageDBBridge* db = DOMStorageCache::StartDatabase(); + if (!db) { + return false; + } + + // The object releases it self in LoadUsage method + RefPtr<UsageParentBridge> usage = new UsageParentBridge(this, aOriginNoSuffix); + db->AsyncGetUsage(usage); + return true; +} + +namespace { + +// We need another implementation of DOMStorageCacheBridge 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 DOMStorageCacheBridge +{ +public: + SyncLoadCacheHelper(const nsCString& aOriginSuffix, + const nsCString& aOriginNoSuffix, + uint32_t aAlreadyLoadedCount, + InfallibleTArray<nsString>* aKeys, + InfallibleTArray<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 + { + return DOMStorageManager::CreateOrigin(mSuffix, mOrigin); + } + virtual const nsCString& OriginNoSuffix() const { return mOrigin; } + virtual const nsCString& OriginSuffix() const { return mSuffix; } + virtual bool Loaded() { return mLoaded; } + virtual uint32_t LoadedCount() { return mLoadedCount; } + virtual bool LoadItem(const nsAString& aKey, const nsString& aValue) + { + // 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) + { + // 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() + { + // Called on the main thread, exits after LoadDone() call + MonitorAutoLock monitor(mMonitor); + while (!mLoaded) { + monitor.Wait(); + } + } + +private: + Monitor mMonitor; + nsCString mSuffix, mOrigin; + InfallibleTArray<nsString>* mKeys; + InfallibleTArray<nsString>* mValues; + nsresult* mRv; + bool mLoaded; + uint32_t mLoadedCount; +}; + +} // namespace + +bool +DOMStorageDBParent::RecvPreload(const nsCString& aOriginSuffix, + const nsCString& aOriginNoSuffix, + const uint32_t& aAlreadyLoadedCount, + InfallibleTArray<nsString>* aKeys, + InfallibleTArray<nsString>* aValues, + nsresult* aRv) +{ + DOMStorageDBBridge* db = DOMStorageCache::StartDatabase(); + if (!db) { + return false; + } + + RefPtr<SyncLoadCacheHelper> cache( + new SyncLoadCacheHelper(aOriginSuffix, aOriginNoSuffix, aAlreadyLoadedCount, aKeys, aValues, aRv)); + + db->SyncPreload(cache, true); + return true; +} + +bool +DOMStorageDBParent::RecvAsyncAddItem(const nsCString& aOriginSuffix, + const nsCString& aOriginNoSuffix, + const nsString& aKey, + const nsString& aValue) +{ + DOMStorageDBBridge* db = DOMStorageCache::StartDatabase(); + if (!db) { + return false; + } + + nsresult rv = db->AsyncAddItem(NewCache(aOriginSuffix, aOriginNoSuffix), aKey, aValue); + if (NS_FAILED(rv) && mIPCOpen) { + mozilla::Unused << SendError(rv); + } + + return true; +} + +bool +DOMStorageDBParent::RecvAsyncUpdateItem(const nsCString& aOriginSuffix, + const nsCString& aOriginNoSuffix, + const nsString& aKey, + const nsString& aValue) +{ + DOMStorageDBBridge* db = DOMStorageCache::StartDatabase(); + if (!db) { + return false; + } + + nsresult rv = db->AsyncUpdateItem(NewCache(aOriginSuffix, aOriginNoSuffix), aKey, aValue); + if (NS_FAILED(rv) && mIPCOpen) { + mozilla::Unused << SendError(rv); + } + + return true; +} + +bool +DOMStorageDBParent::RecvAsyncRemoveItem(const nsCString& aOriginSuffix, + const nsCString& aOriginNoSuffix, + const nsString& aKey) +{ + DOMStorageDBBridge* db = DOMStorageCache::StartDatabase(); + if (!db) { + return false; + } + + nsresult rv = db->AsyncRemoveItem(NewCache(aOriginSuffix, aOriginNoSuffix), aKey); + if (NS_FAILED(rv) && mIPCOpen) { + mozilla::Unused << SendError(rv); + } + + return true; +} + +bool +DOMStorageDBParent::RecvAsyncClear(const nsCString& aOriginSuffix, + const nsCString& aOriginNoSuffix) +{ + DOMStorageDBBridge* db = DOMStorageCache::StartDatabase(); + if (!db) { + return false; + } + + nsresult rv = db->AsyncClear(NewCache(aOriginSuffix, aOriginNoSuffix)); + if (NS_FAILED(rv) && mIPCOpen) { + mozilla::Unused << SendError(rv); + } + + return true; +} + +bool +DOMStorageDBParent::RecvAsyncFlush() +{ + DOMStorageDBBridge* db = DOMStorageCache::GetDatabase(); + if (!db) { + return false; + } + + db->AsyncFlush(); + return true; +} + +// DOMStorageObserverSink + +nsresult +DOMStorageDBParent::Observe(const char* aTopic, + const nsAString& aOriginAttributesPattern, + const nsACString& aOriginScope) +{ + if (mIPCOpen) { + mozilla::Unused << SendObserve(nsDependentCString(aTopic), + nsString(aOriginAttributesPattern), + nsCString(aOriginScope)); + } + + return NS_OK; +} + +namespace { + +// Results must be sent back on the main thread +class LoadRunnable : public Runnable +{ +public: + enum TaskType { + loadItem, + loadDone + }; + + LoadRunnable(DOMStorageDBParent* aParent, + TaskType aType, + const nsACString& aOriginSuffix, + const nsACString& aOriginNoSuffix, + const nsAString& aKey = EmptyString(), + const nsAString& aValue = EmptyString()) + : mParent(aParent) + , mType(aType) + , mSuffix(aOriginSuffix) + , mOrigin(aOriginNoSuffix) + , mKey(aKey) + , mValue(aValue) + { } + + LoadRunnable(DOMStorageDBParent* aParent, + TaskType aType, + const nsACString& aOriginSuffix, + const nsACString& aOriginNoSuffix, + nsresult aRv) + : mParent(aParent) + , mType(aType) + , mSuffix(aOriginSuffix) + , mOrigin(aOriginNoSuffix) + , mRv(aRv) + { } + +private: + RefPtr<DOMStorageDBParent> 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; + } + + return NS_OK; + } +}; + +} // namespace + +// DOMStorageDBParent::CacheParentBridge + +const nsCString +DOMStorageDBParent::CacheParentBridge::Origin() const +{ + return DOMStorageManager::CreateOrigin(mOriginSuffix, mOriginNoSuffix); +} + +bool +DOMStorageDBParent::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); + NS_DispatchToMainThread(r); + return true; +} + +void +DOMStorageDBParent::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); + NS_DispatchToMainThread(r); +} + +void +DOMStorageDBParent::CacheParentBridge::LoadWait() +{ + // Should never be called on this implementation + MOZ_ASSERT(false); +} + +// DOMStorageDBParent::UsageParentBridge + +namespace { + +class UsageRunnable : public Runnable +{ +public: + UsageRunnable(DOMStorageDBParent* aParent, const nsACString& aOriginScope, const int64_t& aUsage) + : mParent(aParent) + , mOriginScope(aOriginScope) + , mUsage(aUsage) + {} + +private: + NS_IMETHOD Run() override + { + if (!mParent->IPCOpen()) { + return NS_OK; + } + + mozilla::Unused << mParent->SendLoadUsage(mOriginScope, mUsage); + return NS_OK; + } + + RefPtr<DOMStorageDBParent> mParent; + nsCString mOriginScope; + int64_t mUsage; +}; + +} // namespace + +void +DOMStorageDBParent::UsageParentBridge::LoadUsage(const int64_t aUsage) +{ + RefPtr<UsageRunnable> r = new UsageRunnable(mParent, mOriginScope, aUsage); + NS_DispatchToMainThread(r); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/DOMStorageIPC.h b/dom/storage/DOMStorageIPC.h new file mode 100644 index 000000000..b3508a588 --- /dev/null +++ b/dom/storage/DOMStorageIPC.h @@ -0,0 +1,218 @@ +/* -*- 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 nsDOMStorageIPC_h___ +#define nsDOMStorageIPC_h___ + +#include "mozilla/dom/PStorageChild.h" +#include "mozilla/dom/PStorageParent.h" +#include "DOMStorageDBThread.h" +#include "DOMStorageCache.h" +#include "DOMStorageObserver.h" +#include "mozilla/Mutex.h" +#include "nsAutoPtr.h" + +namespace mozilla { + +class OriginAttributesPattern; + +namespace dom { + +class DOMLocalStorageManager; + +// 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 DOMStorageDBChild final : public DOMStorageDBBridge + , public PStorageChild +{ + virtual ~DOMStorageDBChild(); + +public: + explicit DOMStorageDBChild(DOMLocalStorageManager* aManager); + + NS_IMETHOD_(MozExternalRefCountType) AddRef(void); + NS_IMETHOD_(MozExternalRefCountType) Release(void); + + void AddIPDLReference(); + void ReleaseIPDLReference(); + + virtual nsresult Init(); + virtual nsresult Shutdown(); + + virtual void AsyncPreload(DOMStorageCacheBridge* aCache, bool aPriority = false); + virtual void AsyncGetUsage(DOMStorageUsageBridge* aUsage); + + virtual void SyncPreload(DOMStorageCacheBridge* aCache, bool aForceSync = false); + + virtual nsresult AsyncAddItem(DOMStorageCacheBridge* aCache, const nsAString& aKey, const nsAString& aValue); + virtual nsresult AsyncUpdateItem(DOMStorageCacheBridge* aCache, const nsAString& aKey, const nsAString& aValue); + virtual nsresult AsyncRemoveItem(DOMStorageCacheBridge* aCache, const nsAString& aKey); + virtual nsresult AsyncClear(DOMStorageCacheBridge* aCache); + + virtual void AsyncClearAll() + { + if (mOriginsHavingData) { + mOriginsHavingData->Clear(); /* NO-OP on the child process otherwise */ + } + } + + virtual void AsyncClearMatchingOrigin(const nsACString& aOriginNoSuffix) + { /* NO-OP on the child process */ } + + virtual void AsyncClearMatchingOriginAttributes(const OriginAttributesPattern& aPattern) + { /* NO-OP on the child process */ } + + virtual void AsyncFlush() + { SendAsyncFlush(); } + + virtual bool ShouldPreloadOrigin(const nsACString& aOriginNoSuffix); + virtual void GetOriginsHavingData(InfallibleTArray<nsCString>* aOrigins) + { NS_NOTREACHED("Not implemented for child process"); } + +private: + bool RecvObserve(const nsCString& aTopic, + const nsString& aOriginAttributesPattern, + const nsCString& aOriginScope); + bool RecvLoadItem(const nsCString& aOriginSuffix, + const nsCString& aOriginNoSuffix, + const nsString& aKey, + const nsString& aValue); + bool RecvLoadDone(const nsCString& aOriginSuffix, + const nsCString& aOriginNoSuffix, + const nsresult& aRv); + bool RecvOriginsHavingData(nsTArray<nsCString>&& aOrigins); + bool RecvLoadUsage(const nsCString& aOriginNoSuffix, + const int64_t& aUsage); + bool RecvError(const nsresult& aRv); + + nsTHashtable<nsCStringHashKey>& OriginsHavingData(); + + ThreadSafeAutoRefCnt mRefCnt; + NS_DECL_OWNINGTHREAD + + // Held to get caches to forward answers to. + RefPtr<DOMLocalStorageManager> mManager; + + // Origins having data hash, for optimization purposes only + nsAutoPtr<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<DOMStorageCacheBridge>> mLoadingCaches; + + // Status of the remote database + nsresult mStatus; + + bool mIPCOpen; +}; + + +// Receives async requests from child processes and is responsible +// to send back responses from the DB thread. Exposes as a fake +// DOMStorageCache consumer. +// Also responsible for forwardning all chrome operation notifications +// such as cookie cleaning etc to the child process. +class DOMStorageDBParent final : public PStorageParent + , public DOMStorageObserverSink +{ + virtual ~DOMStorageDBParent(); + +public: + DOMStorageDBParent(); + + 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 DOMStorageCacheBridge { + public: + CacheParentBridge(DOMStorageDBParent* aParentDB, + const nsACString& aOriginSuffix, + const nsACString& aOriginNoSuffix) + : mParent(aParentDB) + , mOriginSuffix(aOriginSuffix), mOriginNoSuffix(aOriginNoSuffix) + , mLoaded(false), mLoadedCount(0) {} + virtual ~CacheParentBridge() {} + + // DOMStorageCacheBridge + virtual const nsCString Origin() const; + virtual const nsCString& OriginNoSuffix() const + { return mOriginNoSuffix; } + virtual const nsCString& OriginSuffix() const + { return mOriginSuffix; } + virtual bool Loaded() + { return mLoaded; } + virtual uint32_t LoadedCount() + { return mLoadedCount; } + + virtual bool LoadItem(const nsAString& aKey, const nsString& aValue); + virtual void LoadDone(nsresult aRv); + virtual void LoadWait(); + + private: + RefPtr<DOMStorageDBParent> mParent; + nsCString mOriginSuffix, mOriginNoSuffix; + bool mLoaded; + uint32_t mLoadedCount; + }; + + // Fake usage class receiving async callbacks from DB thread + class UsageParentBridge : public DOMStorageUsageBridge + { + public: + UsageParentBridge(DOMStorageDBParent* aParentDB, const nsACString& aOriginScope) + : mParent(aParentDB), mOriginScope(aOriginScope) {} + virtual ~UsageParentBridge() {} + + // DOMStorageUsageBridge + virtual const nsCString& OriginScope() { return mOriginScope; } + virtual void LoadUsage(const int64_t usage); + + private: + RefPtr<DOMStorageDBParent> mParent; + nsCString mOriginScope; + }; + +private: + // IPC + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + bool RecvAsyncPreload(const nsCString& aOriginSuffix, const nsCString& aOriginNoSuffix, const bool& aPriority) override; + bool RecvPreload(const nsCString& aOriginSuffix, const nsCString& aOriginNoSuffix, const uint32_t& aAlreadyLoadedCount, + InfallibleTArray<nsString>* aKeys, InfallibleTArray<nsString>* aValues, + nsresult* aRv) override; + bool RecvAsyncGetUsage(const nsCString& aOriginNoSuffix) override; + bool RecvAsyncAddItem(const nsCString& aOriginSuffix, const nsCString& aOriginNoSuffix, const nsString& aKey, const nsString& aValue) override; + bool RecvAsyncUpdateItem(const nsCString& aOriginSuffix, const nsCString& aOriginNoSuffix, const nsString& aKey, const nsString& aValue) override; + bool RecvAsyncRemoveItem(const nsCString& aOriginSuffix, const nsCString& aOriginNoSuffix, const nsString& aKey) override; + bool RecvAsyncClear(const nsCString& aOriginSuffix, const nsCString& aOriginNoSuffix) override; + bool RecvAsyncFlush() override; + + // DOMStorageObserverSink + virtual nsresult Observe(const char* aTopic, const nsAString& aOriginAttrPattern, const nsACString& aOriginScope) override; + +private: + CacheParentBridge* NewCache(const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix); + + ThreadSafeAutoRefCnt mRefCnt; + NS_DECL_OWNINGTHREAD + + // True when IPC channel is open and Send*() methods are OK to use. + bool mIPCOpen; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/storage/DOMStorageManager.cpp b/dom/storage/DOMStorageManager.cpp new file mode 100644 index 000000000..156e846ba --- /dev/null +++ b/dom/storage/DOMStorageManager.cpp @@ -0,0 +1,661 @@ +/* -*- 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 "DOMStorageManager.h" +#include "DOMStorage.h" +#include "DOMStorageDBThread.h" + +#include "nsIScriptSecurityManager.h" +#include "nsIEffectiveTLDService.h" + +#include "nsNetUtil.h" +#include "nsNetCID.h" +#include "nsIURL.h" +#include "nsPrintfCString.h" +#include "nsXULAppAPI.h" +#include "nsThreadUtils.h" +#include "nsIObserverService.h" +#include "mozilla/Services.h" +#include "mozilla/Preferences.h" + +// Only allow relatively small amounts of data since performance of +// the synchronous IO is very bad. +// We are enforcing simple per-origin quota only. +#define DEFAULT_QUOTA_LIMIT (5 * 1024) + +namespace mozilla { +namespace dom { + +namespace { + +int32_t gQuotaLimit = DEFAULT_QUOTA_LIMIT; + +} // namespace + +DOMLocalStorageManager* +DOMLocalStorageManager::sSelf = nullptr; + +// static +uint32_t +DOMStorageManager::GetQuota() +{ + static bool preferencesInitialized = false; + if (!preferencesInitialized) { + mozilla::Preferences::AddIntVarCache(&gQuotaLimit, "dom.storage.default_quota", + DEFAULT_QUOTA_LIMIT); + preferencesInitialized = true; + } + + return gQuotaLimit * 1024; // pref is in kBs +} + +void +ReverseString(const nsCSubstring& aSource, nsCSubstring& aResult) +{ + nsACString::const_iterator sourceBegin, sourceEnd; + aSource.BeginReading(sourceBegin); + aSource.EndReading(sourceEnd); + + aResult.SetLength(aSource.Length()); + nsACString::iterator destEnd; + aResult.EndWriting(destEnd); + + 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; +} + +bool +PrincipalsEqual(nsIPrincipal* aObjectPrincipal, nsIPrincipal* aSubjectPrincipal) +{ + if (!aSubjectPrincipal) { + return true; + } + + if (!aObjectPrincipal) { + return false; + } + + return aSubjectPrincipal->Equals(aObjectPrincipal); +} + +NS_IMPL_ISUPPORTS(DOMStorageManager, + nsIDOMStorageManager) + +DOMStorageManager::DOMStorageManager(DOMStorage::StorageType aType) + : mCaches(8) + , mType(aType) + , mLowDiskSpace(false) +{ + DOMStorageObserver* observer = DOMStorageObserver::Self(); + NS_ASSERTION(observer, "No DOMStorageObserver, cannot observe private data delete notifications!"); + + if (observer) { + observer->AddSink(this); + } +} + +DOMStorageManager::~DOMStorageManager() +{ + DOMStorageObserver* observer = DOMStorageObserver::Self(); + if (observer) { + observer->RemoveSink(this); + } +} + +namespace { + +nsresult +AppendOriginNoSuffix(nsIPrincipal* aPrincipal, + nsACString& aKey) +{ + nsresult rv; + + nsCOMPtr<nsIURI> uri; + rv = aPrincipal->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + if (!uri) { + return NS_ERROR_UNEXPECTED; + } + + nsAutoCString domainOrigin; + rv = uri->GetAsciiHost(domainOrigin); + NS_ENSURE_SUCCESS(rv, rv); + + if (domainOrigin.IsEmpty()) { + // For the file:/// protocol use the exact directory as domain. + bool isScheme = false; + if (NS_SUCCEEDED(uri->SchemeIs("file", &isScheme)) && isScheme) { + nsCOMPtr<nsIURL> url = do_QueryInterface(uri, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = url->GetDirectory(domainOrigin); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // Append reversed domain + nsAutoCString reverseDomain; + rv = CreateReversedDomain(domainOrigin, reverseDomain); + if (NS_FAILED(rv)) { + return rv; + } + + aKey.Append(reverseDomain); + + // Append scheme + nsAutoCString scheme; + rv = uri->GetScheme(scheme); + NS_ENSURE_SUCCESS(rv, rv); + + aKey.Append(':'); + aKey.Append(scheme); + + // Append port if any + int32_t port = NS_GetRealPort(uri); + if (port != -1) { + aKey.Append(nsPrintfCString(":%d", port)); + } + + return NS_OK; +} + +nsresult +CreateQuotaDBKey(nsIPrincipal* aPrincipal, + nsACString& aKey) +{ + nsresult rv; + + nsCOMPtr<nsIEffectiveTLDService> eTLDService(do_GetService( + NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> uri; + rv = aPrincipal->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(uri, NS_ERROR_UNEXPECTED); + + nsAutoCString eTLDplusOne; + rv = eTLDService->GetBaseDomain(uri, 0, eTLDplusOne); + if (NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS == rv) { + // XXX bug 357323 - what to do for localhost/file exactly? + rv = uri->GetAsciiHost(eTLDplusOne); + } + NS_ENSURE_SUCCESS(rv, rv); + + aKey.Truncate(); + BasePrincipal::Cast(aPrincipal)->OriginAttributesRef().CreateSuffix(aKey); + + nsAutoCString subdomainsDBKey; + CreateReversedDomain(eTLDplusOne, subdomainsDBKey); + + aKey.Append(':'); + aKey.Append(subdomainsDBKey); + + return NS_OK; +} + +} // namespace + +// static +nsCString +DOMStorageManager::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; +} + +DOMStorageCache* +DOMStorageManager::GetCache(const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix) +{ + CacheOriginHashtable* table = mCaches.LookupOrAdd(aOriginSuffix); + DOMStorageCacheHashKey* entry = table->GetEntry(aOriginNoSuffix); + if (!entry) { + return nullptr; + } + + return entry->cache(); +} + +already_AddRefed<DOMStorageUsage> +DOMStorageManager::GetOriginUsage(const nsACString& aOriginNoSuffix) +{ + RefPtr<DOMStorageUsage> usage; + if (mUsages.Get(aOriginNoSuffix, &usage)) { + return usage.forget(); + } + + usage = new DOMStorageUsage(aOriginNoSuffix); + + if (mType == LocalStorage) { + DOMStorageDBBridge* db = DOMStorageCache::StartDatabase(); + if (db) { + db->AsyncGetUsage(usage); + } + } + + mUsages.Put(aOriginNoSuffix, usage); + + return usage.forget(); +} + +already_AddRefed<DOMStorageCache> +DOMStorageManager::PutCache(const nsACString& aOriginSuffix, + const nsACString& aOriginNoSuffix, + nsIPrincipal* aPrincipal) +{ + CacheOriginHashtable* table = mCaches.LookupOrAdd(aOriginSuffix); + DOMStorageCacheHashKey* entry = table->PutEntry(aOriginNoSuffix); + RefPtr<DOMStorageCache> cache = entry->cache(); + + nsAutoCString quotaOrigin; + CreateQuotaDBKey(aPrincipal, quotaOrigin); + + switch (mType) { + case SessionStorage: + // Lifetime handled by the manager, don't persist + entry->HardRef(); + cache->Init(this, false, aPrincipal, quotaOrigin); + break; + + case LocalStorage: + // Lifetime handled by the cache, do persist + cache->Init(this, true, aPrincipal, quotaOrigin); + break; + + default: + MOZ_ASSERT(false); + } + + return cache.forget(); +} + +void +DOMStorageManager::DropCache(DOMStorageCache* aCache) +{ + if (!NS_IsMainThread()) { + NS_WARNING("DOMStorageManager::DropCache called on a non-main thread, shutting down?"); + } + + CacheOriginHashtable* table = mCaches.LookupOrAdd(aCache->OriginSuffix()); + table->RemoveEntry(aCache->OriginNoSuffix()); +} + +nsresult +DOMStorageManager::GetStorageInternal(bool aCreate, + mozIDOMWindow* aWindow, + nsIPrincipal* aPrincipal, + const nsAString& aDocumentURI, + bool aPrivate, + nsIDOMStorage** aRetval) +{ + nsresult rv; + + nsAutoCString originAttrSuffix; + BasePrincipal::Cast(aPrincipal)->OriginAttributesRef().CreateSuffix(originAttrSuffix); + + nsAutoCString originKey; + rv = AppendOriginNoSuffix(aPrincipal, originKey); + if (NS_FAILED(rv)) { + return NS_ERROR_NOT_AVAILABLE; + } + + RefPtr<DOMStorageCache> cache = GetCache(originAttrSuffix, originKey); + + // Get or create a cache for the given scope + if (!cache) { + if (!aCreate) { + *aRetval = nullptr; + return NS_OK; + } + + if (!aRetval) { + // This is a demand to just preload the cache, if the scope has + // no data stored, bypass creation and preload of the cache. + DOMStorageDBBridge* db = DOMStorageCache::GetDatabase(); + if (db) { + if (!db->ShouldPreloadOrigin(DOMStorageManager::CreateOrigin(originAttrSuffix, originKey))) { + return NS_OK; + } + } else { + if (originKey.EqualsLiteral("knalb.:about")) { + return NS_OK; + } + } + } + + // There is always a single instance of a cache per scope + // in a single instance of a DOM storage manager. + cache = PutCache(originAttrSuffix, originKey, aPrincipal); + } else if (mType == SessionStorage) { + if (!cache->CheckPrincipal(aPrincipal)) { + return NS_ERROR_DOM_SECURITY_ERR; + } + } + + if (aRetval) { + nsCOMPtr<nsPIDOMWindowInner> inner = nsPIDOMWindowInner::From(aWindow); + + nsCOMPtr<nsIDOMStorage> storage = new DOMStorage( + inner, this, cache, aDocumentURI, aPrincipal, aPrivate); + storage.forget(aRetval); + } + + return NS_OK; +} + +NS_IMETHODIMP +DOMStorageManager::PrecacheStorage(nsIPrincipal* aPrincipal) +{ + return GetStorageInternal(true, nullptr, aPrincipal, EmptyString(), false, + nullptr); +} + +NS_IMETHODIMP +DOMStorageManager::CreateStorage(mozIDOMWindow* aWindow, + nsIPrincipal* aPrincipal, + const nsAString& aDocumentURI, + bool aPrivate, + nsIDOMStorage** aRetval) +{ + return GetStorageInternal(true, aWindow, aPrincipal, aDocumentURI, aPrivate, + aRetval); +} + +NS_IMETHODIMP +DOMStorageManager::GetStorage(mozIDOMWindow* aWindow, + nsIPrincipal* aPrincipal, + bool aPrivate, + nsIDOMStorage** aRetval) +{ + return GetStorageInternal(false, aWindow, aPrincipal, EmptyString(), aPrivate, + aRetval); +} + +NS_IMETHODIMP +DOMStorageManager::CloneStorage(nsIDOMStorage* aStorage) +{ + if (mType != SessionStorage) { + // Cloning is supported only for sessionStorage + return NS_ERROR_NOT_IMPLEMENTED; + } + + RefPtr<DOMStorage> storage = static_cast<DOMStorage*>(aStorage); + if (!storage) { + return NS_ERROR_UNEXPECTED; + } + + const DOMStorageCache* origCache = storage->GetCache(); + + DOMStorageCache* existingCache = GetCache(origCache->OriginSuffix(), + origCache->OriginNoSuffix()); + if (existingCache) { + // Do not replace an existing sessionStorage. + return NS_ERROR_NOT_AVAILABLE; + } + + // Since this manager is sessionStorage manager, PutCache hard references + // the cache in our hashtable. + RefPtr<DOMStorageCache> newCache = PutCache(origCache->OriginSuffix(), + origCache->OriginNoSuffix(), + origCache->Principal()); + + newCache->CloneFrom(origCache); + return NS_OK; +} + +NS_IMETHODIMP +DOMStorageManager::CheckStorage(nsIPrincipal* aPrincipal, + nsIDOMStorage* aStorage, + bool* aRetval) +{ + nsresult rv; + + RefPtr<DOMStorage> storage = static_cast<DOMStorage*>(aStorage); + if (!storage) { + return NS_ERROR_UNEXPECTED; + } + + *aRetval = false; + + if (!aPrincipal) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsAutoCString suffix; + BasePrincipal::Cast(aPrincipal)->OriginAttributesRef().CreateSuffix(suffix); + + nsAutoCString origin; + rv = AppendOriginNoSuffix(aPrincipal, origin); + if (NS_FAILED(rv)) { + return rv; + } + + DOMStorageCache* cache = GetCache(suffix, origin); + if (cache != storage->GetCache()) { + return NS_OK; + } + + if (!storage->PrincipalEquals(aPrincipal)) { + return NS_OK; + } + + *aRetval = true; + return NS_OK; +} + +// Obsolete nsIDOMStorageManager methods + +NS_IMETHODIMP +DOMStorageManager::GetLocalStorageForPrincipal(nsIPrincipal* aPrincipal, + const nsAString& aDocumentURI, + bool aPrivate, + nsIDOMStorage** aRetval) +{ + if (mType != LocalStorage) { + return NS_ERROR_UNEXPECTED; + } + + return CreateStorage(nullptr, aPrincipal, aDocumentURI, aPrivate, aRetval); +} + +void +DOMStorageManager::ClearCaches(uint32_t aUnloadFlags, + const OriginAttributesPattern& aPattern, + const nsACString& aOriginScope) +{ + for (auto iter1 = mCaches.Iter(); !iter1.Done(); iter1.Next()) { + PrincipalOriginAttributes 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.Data(); + + for (auto iter2 = table->Iter(); !iter2.Done(); iter2.Next()) { + DOMStorageCache* cache = iter2.Get()->cache(); + + if (aOriginScope.IsEmpty() || + StringBeginsWith(cache->OriginNoSuffix(), aOriginScope)) { + cache->UnloadItems(aUnloadFlags); + } + } + } +} + +nsresult +DOMStorageManager::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") || + !strcmp(aTopic, "extension:purge-localStorage-caches")) { + ClearCaches(DOMStorageCache::kUnloadComplete, pattern, EmptyCString()); + return NS_OK; + } + + // Clear from caches everything that has been stored + // while in session-only mode + if (!strcmp(aTopic, "session-only-cleared")) { + ClearCaches(DOMStorageCache::kUnloadSession, pattern, aOriginScope); + return NS_OK; + } + + // Clear everything (including so and pb data) from caches and database + // for the gived domain and subdomains. + if (!strcmp(aTopic, "domain-data-cleared")) { + ClearCaches(DOMStorageCache::kUnloadComplete, pattern, aOriginScope); + return NS_OK; + } + + // Clear all private-browsing caches + if (!strcmp(aTopic, "private-browsing-data-cleared")) { + ClearCaches(DOMStorageCache::kUnloadPrivate, pattern, EmptyCString()); + return NS_OK; + } + + // Clear localStorage data beloging to an origin pattern + if (!strcmp(aTopic, "origin-attr-pattern-cleared")) { + // sessionStorage is expected to stay + if (mType == SessionStorage) { + return NS_OK; + } + + ClearCaches(DOMStorageCache::kUnloadComplete, pattern, EmptyCString()); + return NS_OK; + } + + if (!strcmp(aTopic, "profile-change")) { + // For case caches are still referenced - clear them completely + ClearCaches(DOMStorageCache::kUnloadComplete, pattern, EmptyCString()); + mCaches.Clear(); + return NS_OK; + } + + if (!strcmp(aTopic, "low-disk-space")) { + if (mType == LocalStorage) { + mLowDiskSpace = true; + } + + return NS_OK; + } + + if (!strcmp(aTopic, "no-low-disk-space")) { + if (mType == LocalStorage) { + mLowDiskSpace = false; + } + + return NS_OK; + } + +#ifdef DOM_STORAGE_TESTS + if (!strcmp(aTopic, "test-reload")) { + if (mType != LocalStorage) { + return NS_OK; + } + + // This immediately completely reloads all caches from the database. + ClearCaches(DOMStorageCache::kTestReload, pattern, EmptyCString()); + 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; +} + +// DOMLocalStorageManager + +DOMLocalStorageManager::DOMLocalStorageManager() + : DOMStorageManager(LocalStorage) +{ + 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 + DOMStorageCache::StartDatabase(); + } +} + +DOMLocalStorageManager::~DOMLocalStorageManager() +{ + sSelf = nullptr; +} + +DOMLocalStorageManager* +DOMLocalStorageManager::Ensure() +{ + 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; +} + +// DOMSessionStorageManager + +DOMSessionStorageManager::DOMSessionStorageManager() + : DOMStorageManager(SessionStorage) +{ + if (!XRE_IsParentProcess()) { + // Do this only on the child process. The thread IPC bridge + // is also used to communicate chrome observer notifications. + DOMStorageCache::StartDatabase(); + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/DOMStorageManager.h b/dom/storage/DOMStorageManager.h new file mode 100644 index 000000000..666e16a6f --- /dev/null +++ b/dom/storage/DOMStorageManager.h @@ -0,0 +1,155 @@ +/* -*- 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 nsDOMStorageManager_h__ +#define nsDOMStorageManager_h__ + +#include "nsIDOMStorageManager.h" +#include "DOMStorageObserver.h" + +#include "DOMStorageCache.h" +#include "mozilla/dom/DOMStorage.h" + +#include "nsTHashtable.h" +#include "nsDataHashtable.h" +#include "nsClassHashtable.h" +#include "nsHashKeys.h" + +namespace mozilla { + +class OriginAttributesPattern; + +namespace dom { + +const DOMStorage::StorageType SessionStorage = DOMStorage::SessionStorage; +const DOMStorage::StorageType LocalStorage = DOMStorage::LocalStorage; + +class DOMStorageManager : public nsIDOMStorageManager + , public DOMStorageObserverSink +{ + NS_DECL_ISUPPORTS + NS_DECL_NSIDOMSTORAGEMANAGER + +public: + virtual DOMStorage::StorageType Type() { return mType; } + + // Reads the preference for DOM storage quota + static uint32_t GetQuota(); + // Gets (but not ensures) cache for the given scope + DOMStorageCache* GetCache(const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix); + // Returns object keeping usage cache for the scope. + already_AddRefed<DOMStorageUsage> GetOriginUsage(const nsACString& aOriginNoSuffix); + + static nsCString CreateOrigin(const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix); + +protected: + explicit DOMStorageManager(DOMStorage::StorageType aType); + virtual ~DOMStorageManager(); + +private: + // DOMStorageObserverSink, handler to various chrome clearing notification + virtual nsresult Observe(const char* aTopic, + const nsAString& aOriginAttributesPattern, + const nsACString& aOriginScope) override; + + // Since nsTHashtable doesn't like multiple inheritance, we have to aggregate + // DOMStorageCache into the entry. + class DOMStorageCacheHashKey : public nsCStringHashKey + { + public: + explicit DOMStorageCacheHashKey(const nsACString* aKey) + : nsCStringHashKey(aKey) + , mCache(new DOMStorageCache(aKey)) + {} + + DOMStorageCacheHashKey(const DOMStorageCacheHashKey& aOther) + : nsCStringHashKey(aOther) + { + NS_ERROR("Shouldn't be called"); + } + + DOMStorageCache* cache() { return mCache; } + // Keep the cache referenced forever, used for sessionStorage. + void HardRef() { mCacheRef = mCache; } + + private: + // weak ref only since cache references its manager. + DOMStorageCache* mCache; + // hard ref when this is sessionStorage to keep it alive forever. + RefPtr<DOMStorageCache> 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<DOMStorageCache> PutCache(const nsACString& aOriginSuffix, + const nsACString& aOriginNoSuffix, + nsIPrincipal* aPrincipal); + + // Helper for creation of DOM storage objects + nsresult GetStorageInternal(bool aCreate, + mozIDOMWindow* aWindow, + nsIPrincipal* aPrincipal, + const nsAString& aDocumentURI, + bool aPrivate, + nsIDOMStorage** aRetval); + + // Suffix->origin->cache map + typedef nsTHashtable<DOMStorageCacheHashKey> CacheOriginHashtable; + nsClassHashtable<nsCStringHashKey, CacheOriginHashtable> mCaches; + + const DOMStorage::StorageType mType; + + // If mLowDiskSpace is true it indicates a low device storage situation and + // so no localStorage writes are allowed. sessionStorage writes are still + // allowed. + bool mLowDiskSpace; + bool IsLowDiskSpace() const { return mLowDiskSpace; }; + + void ClearCaches(uint32_t aUnloadFlags, + const OriginAttributesPattern& aPattern, + const nsACString& aKeyPrefix); + +protected: + // Keeps usage cache objects for eTLD+1 scopes we have touched. + nsDataHashtable<nsCStringHashKey, RefPtr<DOMStorageUsage> > mUsages; + + friend class DOMStorageCache; + // Releases cache since it is no longer referrered by any DOMStorage object. + virtual void DropCache(DOMStorageCache* aCache); +}; + +// Derived classes to allow two different contract ids, one for localStorage and +// one for sessionStorage management. localStorage manager is used as service +// scoped to the application while sessionStorage managers are instantiated by each +// top doc shell in the application since sessionStorages are isolated per top level +// browsing context. The code may easily by shared by both. + +class DOMLocalStorageManager final : public DOMStorageManager +{ +public: + DOMLocalStorageManager(); + virtual ~DOMLocalStorageManager(); + + // Global getter of localStorage manager service + static DOMLocalStorageManager* Self() { return sSelf; } + + // Like Self, but creates an instance if we're not yet initialized. + static DOMLocalStorageManager* Ensure(); + +private: + static DOMLocalStorageManager* sSelf; +}; + +class DOMSessionStorageManager final : public DOMStorageManager +{ +public: + DOMSessionStorageManager(); +}; + +} // namespace dom +} // namespace mozilla + +#endif /* nsDOMStorageManager_h__ */ diff --git a/dom/storage/DOMStorageObserver.cpp b/dom/storage/DOMStorageObserver.cpp new file mode 100644 index 000000000..a2b3f1da8 --- /dev/null +++ b/dom/storage/DOMStorageObserver.cpp @@ -0,0 +1,355 @@ +/* -*- 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 "DOMStorageObserver.h" + +#include "DOMStorageDBThread.h" +#include "DOMStorageCache.h" + +#include "mozilla/BasePrincipal.h" +#include "nsIObserverService.h" +#include "nsIURI.h" +#include "nsIURL.h" +#include "nsIScriptSecurityManager.h" +#include "nsIPermission.h" +#include "nsIIDNService.h" +#include "nsICookiePermission.h" + +#include "nsPrintfCString.h" +#include "nsXULAppAPI.h" +#include "nsEscape.h" +#include "nsNetCID.h" +#include "mozilla/Services.h" +#include "nsServiceManagerUtils.h" + +namespace mozilla { +namespace dom { + +static const char kStartupTopic[] = "sessionstore-windows-restored"; +static const uint32_t kStartupDelay = 0; + +NS_IMPL_ISUPPORTS(DOMStorageObserver, + nsIObserver, + nsISupportsWeakReference) + +DOMStorageObserver* DOMStorageObserver::sSelf = nullptr; + +extern nsresult +CreateReversedDomain(const nsACString& aAsciiDomain, nsACString& aKey); + +// static +nsresult +DOMStorageObserver::Init() +{ + if (sSelf) { + return NS_OK; + } + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (!obs) { + return NS_ERROR_UNEXPECTED; + } + + sSelf = new DOMStorageObserver(); + 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, "browser:purge-domain-data", true); + obs->AddObserver(sSelf, "last-pb-context-exited", true); + obs->AddObserver(sSelf, "clear-origin-attributes-data", true); + obs->AddObserver(sSelf, "extension:purge-localStorage", true); + + // Shutdown + obs->AddObserver(sSelf, "profile-after-change", true); + obs->AddObserver(sSelf, "profile-before-change", true); + obs->AddObserver(sSelf, "xpcom-shutdown", true); + + // Observe low device storage notifications. + obs->AddObserver(sSelf, "disk-space-watcher", true); + +#ifdef DOM_STORAGE_TESTS + // Testing + 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); +#endif + + return NS_OK; +} + +// static +nsresult +DOMStorageObserver::Shutdown() +{ + if (!sSelf) { + return NS_ERROR_NOT_INITIALIZED; + } + + NS_RELEASE(sSelf); + return NS_OK; +} + +void +DOMStorageObserver::AddSink(DOMStorageObserverSink* aObs) +{ + mSinks.AppendElement(aObs); +} + +void +DOMStorageObserver::RemoveSink(DOMStorageObserverSink* aObs) +{ + mSinks.RemoveElement(aObs); +} + +void +DOMStorageObserver::Notify(const char* aTopic, + const nsAString& aOriginAttributesPattern, + const nsACString& aOriginScope) +{ + for (uint32_t i = 0; i < mSinks.Length(); ++i) { + DOMStorageObserverSink* sink = mSinks[i]; + sink->Observe(aTopic, aOriginAttributesPattern, aOriginScope); + } +} + +NS_IMETHODIMP +DOMStorageObserver::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) +{ + nsresult rv; + + // Start the thread that opens the database. + if (!strcmp(aTopic, kStartupTopic)) { + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + obs->RemoveObserver(this, kStartupTopic); + + mDBThreadStartDelayTimer = do_CreateInstance(NS_TIMER_CONTRACTID); + if (!mDBThreadStartDelayTimer) { + return NS_ERROR_UNEXPECTED; + } + + mDBThreadStartDelayTimer->Init(this, nsITimer::TYPE_ONE_SHOT, kStartupDelay); + + return NS_OK; + } + + // Timer callback used to start the database a short timer after startup + if (!strcmp(aTopic, NS_TIMER_CALLBACK_TOPIC)) { + nsCOMPtr<nsITimer> timer = do_QueryInterface(aSubject); + if (!timer) { + return NS_ERROR_UNEXPECTED; + } + + if (timer == mDBThreadStartDelayTimer) { + mDBThreadStartDelayTimer = nullptr; + + DOMStorageDBBridge* db = DOMStorageCache::StartDatabase(); + NS_ENSURE_TRUE(db, NS_ERROR_FAILURE); + } + + return NS_OK; + } + + // Clear everything, caches + database + if (!strcmp(aTopic, "cookie-changed")) { + if (!NS_LITERAL_STRING("cleared").Equals(aData)) { + return NS_OK; + } + + DOMStorageDBBridge* db = DOMStorageCache::StartDatabase(); + NS_ENSURE_TRUE(db, NS_ERROR_FAILURE); + + db->AsyncClearAll(); + + 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 != NS_LITERAL_CSTRING("cookie")) { + return NS_OK; + } + + uint32_t cap = 0; + perm->GetCapability(&cap); + if (!(cap & nsICookiePermission::ACCESS_SESSION) || + !NS_LITERAL_STRING("deleted").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); + + nsCOMPtr<nsIURI> origin; + principal->GetURI(getter_AddRefs(origin)); + if (!origin) { + return NS_OK; + } + + nsAutoCString host; + origin->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")) { + DOMStorageDBBridge* db = DOMStorageCache::StartDatabase(); + NS_ENSURE_TRUE(db, NS_ERROR_FAILURE); + + db->AsyncClearAll(); + + Notify("extension:purge-localStorage-caches"); + + return NS_OK; + } + + // Clear everything (including so and pb data) from caches and database + // for the gived domain and subdomains. + if (!strcmp(aTopic, "browser:purge-domain-data")) { + // Convert the domain name to the ACE format + nsAutoCString aceDomain; + nsCOMPtr<nsIIDNService> converter = do_GetService(NS_IDNSERVICE_CONTRACTID); + if (converter) { + rv = converter->ConvertUTF8toACE(NS_ConvertUTF16toUTF8(aData), aceDomain); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // In case the IDN service is not available, this is the best we can come up with! + rv = NS_EscapeURL(NS_ConvertUTF16toUTF8(aData), + esc_OnlyNonASCII | esc_AlwaysCopy, + aceDomain, + fallible); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsAutoCString originScope; + rv = CreateReversedDomain(aceDomain, originScope); + NS_ENSURE_SUCCESS(rv, rv); + + DOMStorageDBBridge* db = DOMStorageCache::StartDatabase(); + NS_ENSURE_TRUE(db, NS_ERROR_FAILURE); + + db->AsyncClearMatchingOrigin(originScope); + + Notify("domain-data-cleared", EmptyString(), originScope); + + return NS_OK; + } + + // Clear all private-browsing caches + if (!strcmp(aTopic, "last-pb-context-exited")) { + Notify("private-browsing-data-cleared"); + + return NS_OK; + } + + // Clear data of the origins whose prefixes will match the suffix. + if (!strcmp(aTopic, "clear-origin-attributes-data")) { + OriginAttributesPattern pattern; + if (!pattern.Init(nsDependentString(aData))) { + NS_ERROR("Cannot parse origin attributes pattern"); + return NS_ERROR_FAILURE; + } + + DOMStorageDBBridge* db = DOMStorageCache::StartDatabase(); + NS_ENSURE_TRUE(db, NS_ERROR_FAILURE); + + db->AsyncClearMatchingOriginAttributes(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") || + !strcmp(aTopic, "xpcom-shutdown")) { + rv = DOMStorageCache::StopDatabase(); + if (NS_FAILED(rv)) { + NS_WARNING("Error while stopping DOMStorage DB background thread"); + } + + return NS_OK; + } + + if (!strcmp(aTopic, "disk-space-watcher")) { + if (NS_LITERAL_STRING("full").Equals(aData)) { + Notify("low-disk-space"); + } else if (NS_LITERAL_STRING("free").Equals(aData)) { + Notify("no-low-disk-space"); + } + + return NS_OK; + } + +#ifdef DOM_STORAGE_TESTS + if (!strcmp(aTopic, "domstorage-test-flush-force")) { + DOMStorageDBBridge* db = DOMStorageCache::GetDatabase(); + if (db) { + db->AsyncFlush(); + } + + return NS_OK; + } + + if (!strcmp(aTopic, "domstorage-test-flushed")) { + // Only used to propagate to IPC children + Notify("test-flushed"); + + return NS_OK; + } + + if (!strcmp(aTopic, "domstorage-test-reload")) { + Notify("test-reload"); + + return NS_OK; + } +#endif + + NS_ERROR("Unexpected topic"); + return NS_ERROR_UNEXPECTED; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/DOMStorageObserver.h b/dom/storage/DOMStorageObserver.h new file mode 100644 index 000000000..2efce96a3 --- /dev/null +++ b/dom/storage/DOMStorageObserver.h @@ -0,0 +1,67 @@ +/* -*- 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 nsIDOMStorageObserver_h__ +#define nsIDOMStorageObserver_h__ + +#include "nsIObserver.h" +#include "nsITimer.h" +#include "nsWeakReference.h" +#include "nsTArray.h" +#include "nsString.h" + +namespace mozilla { +namespace dom { + +class DOMStorageObserver; + +// Implementers are DOMStorageManager and DOMStorageDBParent to forward to +// child processes. +class DOMStorageObserverSink +{ +public: + virtual ~DOMStorageObserverSink() {} + +private: + friend class DOMStorageObserver; + virtual nsresult Observe(const char* aTopic, + const nsAString& aOriginAttributesPattern, + const nsACString& aOriginScope) = 0; +}; + +// Statically (though layout statics) initialized observer receiving and processing +// chrome clearing notifications, such as cookie deletion etc. +class DOMStorageObserver : public nsIObserver + , public nsSupportsWeakReference +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + static nsresult Init(); + static nsresult Shutdown(); + static DOMStorageObserver* Self() { return sSelf; } + + void AddSink(DOMStorageObserverSink* aObs); + void RemoveSink(DOMStorageObserverSink* aObs); + void Notify(const char* aTopic, + const nsAString& aOriginAttributesPattern = EmptyString(), + const nsACString& aOriginScope = EmptyCString()); + +private: + virtual ~DOMStorageObserver() {} + + static DOMStorageObserver* sSelf; + + // Weak references + nsTArray<DOMStorageObserverSink*> mSinks; + nsCOMPtr<nsITimer> mDBThreadStartDelayTimer; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/storage/PStorage.ipdl b/dom/storage/PStorage.ipdl new file mode 100644 index 000000000..22b85f795 --- /dev/null +++ b/dom/storage/PStorage.ipdl @@ -0,0 +1,45 @@ +/* -*- 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 { + +/* This protocol bridges async access to the database thread running on the parent process + * and caches running on the child process. + */ +nested(upto inside_cpow) sync protocol PStorage +{ + manager PContent; + +parent: + async __delete__(); + + nested(inside_cpow) 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(); + +child: + 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/moz.build b/dom/storage/moz.build new file mode 100644 index 000000000..47c2fb85f --- /dev/null +++ b/dom/storage/moz.build @@ -0,0 +1,34 @@ +# -*- 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/. + +EXPORTS.mozilla.dom += [ + 'DOMStorage.h', + 'DOMStorageIPC.h', +] + +UNIFIED_SOURCES += [ + 'DOMStorage.cpp', + 'DOMStorageCache.cpp', + 'DOMStorageDBThread.cpp', + 'DOMStorageDBUpdater.cpp', + 'DOMStorageIPC.cpp', + 'DOMStorageManager.cpp', + 'DOMStorageObserver.cpp', +] + +IPDL_SOURCES += [ + 'PStorage.ipdl', +] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' +LOCAL_INCLUDES += [ + '/dom/base', +] + +if CONFIG['ENABLE_TESTS']: + DEFINES['DOM_STORAGE_TESTS'] = True |