diff -r 000000000000 -r 6474c204b198 dom/quota/QuotaManager.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dom/quota/QuotaManager.cpp Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,4020 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "QuotaManager.h" + +#include "mozIApplicationClearPrivateDataParams.h" +#include "nsIBinaryInputStream.h" +#include "nsIBinaryOutputStream.h" +#include "nsIFile.h" +#include "nsIObserverService.h" +#include "nsIOfflineStorage.h" +#include "nsIPrincipal.h" +#include "nsIQuotaRequest.h" +#include "nsIRunnable.h" +#include "nsISimpleEnumerator.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsIScriptSecurityManager.h" +#include "nsITimer.h" +#include "nsIURI.h" +#include "nsIUsageCallback.h" + +#include +#include "GeckoProfiler.h" +#include "mozilla/Atomics.h" +#include "mozilla/CondVar.h" +#include "mozilla/dom/asmjscache/AsmJSCache.h" +#include "mozilla/dom/file/FileService.h" +#include "mozilla/dom/indexedDB/Client.h" +#include "mozilla/Mutex.h" +#include "mozilla/LazyIdleThread.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsCRTGlue.h" +#include "nsDirectoryServiceUtils.h" +#include "nsNetUtil.h" +#include "nsScriptSecurityManager.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" +#include "xpcpublic.h" + +#include "AcquireListener.h" +#include "CheckQuotaHelper.h" +#include "OriginCollection.h" +#include "OriginOrPatternString.h" +#include "QuotaObject.h" +#include "StorageMatcher.h" +#include "UsageInfo.h" +#include "Utilities.h" + +// The amount of time, in milliseconds, that our IO thread will stay alive +// after the last event it processes. +#define DEFAULT_THREAD_TIMEOUT_MS 30000 + +// The amount of time, in milliseconds, that we will wait for active storage +// transactions on shutdown before aborting them. +#define DEFAULT_SHUTDOWN_TIMER_MS 30000 + +// Preference that users can set to override DEFAULT_QUOTA_MB +#define PREF_STORAGE_QUOTA "dom.indexedDB.warningQuota" + +// Preference that users can set to override temporary storage smart limit +// calculation. +#define PREF_FIXED_LIMIT "dom.quotaManager.temporaryStorage.fixedLimit" +#define PREF_CHUNK_SIZE "dom.quotaManager.temporaryStorage.chunkSize" + +// Preference that is used to enable testing features +#define PREF_TESTING_FEATURES "dom.quotaManager.testing" + +// profile-before-change, when we need to shut down quota manager +#define PROFILE_BEFORE_CHANGE_OBSERVER_ID "profile-before-change" + +// The name of the file that we use to load/save the last access time of an +// origin. +#define METADATA_FILE_NAME ".metadata" + +#define PERMISSION_DEFAUT_PERSISTENT_STORAGE "default-persistent-storage" + +#define KB * 1024ULL +#define MB * 1024ULL KB +#define GB * 1024ULL MB + +USING_QUOTA_NAMESPACE +using namespace mozilla::dom; +using mozilla::dom::file::FileService; + +static_assert( + static_cast(StorageType::Persistent) == + static_cast(PERSISTENCE_TYPE_PERSISTENT), + "Enum values should match."); + +static_assert( + static_cast(StorageType::Temporary) == + static_cast(PERSISTENCE_TYPE_TEMPORARY), + "Enum values should match."); + +BEGIN_QUOTA_NAMESPACE + +// A struct that contains the information corresponding to a pending or +// running operation that requires synchronization (e.g. opening a db, +// clearing dbs for an origin, etc). +struct SynchronizedOp +{ + SynchronizedOp(const OriginOrPatternString& aOriginOrPattern, + Nullable aPersistenceType, + const nsACString& aId); + + ~SynchronizedOp(); + + // Test whether this SynchronizedOp needs to wait for the given op. + bool + MustWaitFor(const SynchronizedOp& aOp); + + void + DelayRunnable(nsIRunnable* aRunnable); + + void + DispatchDelayedRunnables(); + + const OriginOrPatternString mOriginOrPattern; + Nullable mPersistenceType; + nsCString mId; + nsRefPtr mListener; + nsTArray > mDelayedRunnables; + ArrayCluster mStorages; +}; + +class CollectOriginsHelper MOZ_FINAL : public nsRunnable +{ +public: + CollectOriginsHelper(mozilla::Mutex& aMutex, uint64_t aMinSizeToBeFreed); + + NS_IMETHOD + Run(); + + // Blocks the current thread until origins are collected on the main thread. + // The returned value contains an aggregate size of those origins. + int64_t + BlockAndReturnOriginsForEviction(nsTArray& aOriginInfos); + +private: + ~CollectOriginsHelper() + { } + + uint64_t mMinSizeToBeFreed; + + mozilla::Mutex& mMutex; + mozilla::CondVar mCondVar; + + // The members below are protected by mMutex. + nsTArray mOriginInfos; + uint64_t mSizeToBeFreed; + bool mWaiting; +}; + +// Responsible for clearing the storage files for a particular origin on the +// IO thread. Created when nsIQuotaManager::ClearStoragesForURI is called. +// Runs three times, first on the main thread, next on the IO thread, and then +// finally again on the main thread. While on the IO thread the runnable will +// actually remove the origin's storage files and the directory that contains +// them before dispatching itself back to the main thread. When back on the main +// thread the runnable will notify the QuotaManager that the job has been +// completed. +class OriginClearRunnable MOZ_FINAL : public nsRunnable, + public AcquireListener +{ + enum CallbackState { + // Not yet run. + Pending = 0, + + // Running on the main thread in the callback for OpenAllowed. + OpenAllowed, + + // Running on the IO thread. + IO, + + // Running on the main thread after all work is done. + Complete + }; + +public: + NS_DECL_ISUPPORTS_INHERITED + + OriginClearRunnable(const OriginOrPatternString& aOriginOrPattern, + Nullable aPersistenceType) + : mOriginOrPattern(aOriginOrPattern), + mPersistenceType(aPersistenceType), + mCallbackState(Pending) + { } + + NS_IMETHOD + Run(); + + // AcquireListener override + virtual nsresult + OnExclusiveAccessAcquired() MOZ_OVERRIDE; + + void + AdvanceState() + { + switch (mCallbackState) { + case Pending: + mCallbackState = OpenAllowed; + return; + case OpenAllowed: + mCallbackState = IO; + return; + case IO: + mCallbackState = Complete; + return; + default: + NS_NOTREACHED("Can't advance past Complete!"); + } + } + + static void + InvalidateOpenedStorages(nsTArray >& aStorages, + void* aClosure); + + void + DeleteFiles(QuotaManager* aQuotaManager, + PersistenceType aPersistenceType); + +private: + OriginOrPatternString mOriginOrPattern; + Nullable mPersistenceType; + CallbackState mCallbackState; +}; + +// Responsible for calculating the amount of space taken up by storages of a +// certain origin. Created when nsIQuotaManager::GetUsageForURI is called. +// May be canceled with nsIQuotaRequest::Cancel. Runs three times, first +// on the main thread, next on the IO thread, and then finally again on the main +// thread. While on the IO thread the runnable will calculate the size of all +// files in the origin's directory before dispatching itself back to the main +// thread. When on the main thread the runnable will call the callback and then +// notify the QuotaManager that the job has been completed. +class AsyncUsageRunnable MOZ_FINAL : public UsageInfo, + public nsRunnable, + public nsIQuotaRequest +{ + enum CallbackState { + // Not yet run. + Pending = 0, + + // Running on the main thread in the callback for OpenAllowed. + OpenAllowed, + + // Running on the IO thread. + IO, + + // Running on the main thread after all work is done. + Complete, + + // Running on the main thread after skipping the work + Shortcut + }; + +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIQUOTAREQUEST + + AsyncUsageRunnable(uint32_t aAppId, + bool aInMozBrowserOnly, + const nsACString& aGroup, + const OriginOrPatternString& aOrigin, + nsIURI* aURI, + nsIUsageCallback* aCallback); + + NS_IMETHOD + Run(); + + void + AdvanceState() + { + switch (mCallbackState) { + case Pending: + mCallbackState = OpenAllowed; + return; + case OpenAllowed: + mCallbackState = IO; + return; + case IO: + mCallbackState = Complete; + return; + default: + NS_NOTREACHED("Can't advance past Complete!"); + } + } + + nsresult + TakeShortcut(); + +private: + // Run calls the RunInternal method and makes sure that we always dispatch + // to the main thread in case of an error. + inline nsresult + RunInternal(); + + nsresult + AddToUsage(QuotaManager* aQuotaManager, + PersistenceType aPersistenceType); + + nsCOMPtr mURI; + nsCOMPtr mCallback; + uint32_t mAppId; + nsCString mGroup; + OriginOrPatternString mOrigin; + CallbackState mCallbackState; + bool mInMozBrowserOnly; +}; + +class ResetOrClearRunnable MOZ_FINAL : public nsRunnable, + public AcquireListener +{ + enum CallbackState { + // Not yet run. + Pending = 0, + + // Running on the main thread in the callback for OpenAllowed. + OpenAllowed, + + // Running on the IO thread. + IO, + + // Running on the main thread after all work is done. + Complete + }; + +public: + NS_DECL_ISUPPORTS_INHERITED + + ResetOrClearRunnable(bool aClear) + : mCallbackState(Pending), + mClear(aClear) + { } + + NS_IMETHOD + Run(); + + // AcquireListener override + virtual nsresult + OnExclusiveAccessAcquired() MOZ_OVERRIDE; + + void + AdvanceState() + { + switch (mCallbackState) { + case Pending: + mCallbackState = OpenAllowed; + return; + case OpenAllowed: + mCallbackState = IO; + return; + case IO: + mCallbackState = Complete; + return; + default: + NS_NOTREACHED("Can't advance past Complete!"); + } + } + + static void + InvalidateOpenedStorages(nsTArray >& aStorages, + void* aClosure); + + void + DeleteFiles(QuotaManager* aQuotaManager, + PersistenceType aPersistenceType); + +private: + CallbackState mCallbackState; + bool mClear; +}; + +// Responsible for finalizing eviction of certian origins (storage files have +// been already cleared, we just need to release IO thread only objects and +// allow next synchronized ops for evicted origins). Created when +// QuotaManager::FinalizeOriginEviction is called. Runs three times, first +// on the main thread, next on the IO thread, and then finally again on the main +// thread. While on the IO thread the runnable will release IO thread only +// objects before dispatching itself back to the main thread. When back on the +// main thread the runnable will call QuotaManager::AllowNextSynchronizedOp. +// The runnable can also run in a shortened mode (runs only twice). +class FinalizeOriginEvictionRunnable MOZ_FINAL : public nsRunnable +{ + enum CallbackState { + // Not yet run. + Pending = 0, + + // Running on the main thread in the callback for OpenAllowed. + OpenAllowed, + + // Running on the IO thread. + IO, + + // Running on the main thread after IO work is done. + Complete + }; + +public: + FinalizeOriginEvictionRunnable(nsTArray& aOrigins) + : mCallbackState(Pending) + { + mOrigins.SwapElements(aOrigins); + } + + NS_IMETHOD + Run(); + + void + AdvanceState() + { + switch (mCallbackState) { + case Pending: + mCallbackState = OpenAllowed; + return; + case OpenAllowed: + mCallbackState = IO; + return; + case IO: + mCallbackState = Complete; + return; + default: + MOZ_ASSUME_UNREACHABLE("Can't advance past Complete!"); + } + } + + nsresult + Dispatch(); + + nsresult + RunImmediately(); + +private: + CallbackState mCallbackState; + nsTArray mOrigins; +}; + +bool +IsOnIOThread() +{ + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "Must have a manager here!"); + + bool currentThread; + return NS_SUCCEEDED(quotaManager->IOThread()-> + IsOnCurrentThread(¤tThread)) && currentThread; +} + +void +AssertIsOnIOThread() +{ + NS_ASSERTION(IsOnIOThread(), "Running on the wrong thread!"); +} + +void +AssertCurrentThreadOwnsQuotaMutex() +{ +#ifdef DEBUG + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "Must have a manager here!"); + + quotaManager->AssertCurrentThreadOwnsQuotaMutex(); +#endif +} + +END_QUOTA_NAMESPACE + +namespace { + +// Amount of space that storages may use by default in megabytes. +static const int32_t kDefaultQuotaMB = 50; + + +QuotaManager* gInstance = nullptr; +mozilla::Atomic gShutdown(false); + +int32_t gStorageQuotaMB = kDefaultQuotaMB; + +// Constants for temporary storage limit computing. +static const int32_t kDefaultFixedLimitKB = -1; +static const uint32_t kDefaultChunkSizeKB = 10 * 1024; +int32_t gFixedLimitKB = kDefaultFixedLimitKB; +uint32_t gChunkSizeKB = kDefaultChunkSizeKB; + +bool gTestingEnabled = false; + +// A callback runnable used by the TransactionPool when it's safe to proceed +// with a SetVersion/DeleteDatabase/etc. +class WaitForTransactionsToFinishRunnable MOZ_FINAL : public nsRunnable +{ +public: + WaitForTransactionsToFinishRunnable(SynchronizedOp* aOp) + : mOp(aOp), mCountdown(1) + { + NS_ASSERTION(mOp, "Why don't we have a runnable?"); + NS_ASSERTION(mOp->mStorages.IsEmpty(), "We're here too early!"); + NS_ASSERTION(mOp->mListener, + "What are we supposed to do when we're done?"); + NS_ASSERTION(mCountdown, "Wrong countdown!"); + } + + NS_IMETHOD + Run(); + + void + AddRun() + { + mCountdown++; + } + +private: + // The QuotaManager holds this alive. + SynchronizedOp* mOp; + uint32_t mCountdown; +}; + +class WaitForLockedFilesToFinishRunnable MOZ_FINAL : public nsRunnable +{ +public: + WaitForLockedFilesToFinishRunnable() + : mBusy(true) + { } + + NS_IMETHOD + Run(); + + bool + IsBusy() const + { + return mBusy; + } + +private: + bool mBusy; +}; + +class SaveOriginAccessTimeRunnable MOZ_FINAL : public nsRunnable +{ +public: + SaveOriginAccessTimeRunnable(const nsACString& aOrigin, int64_t aTimestamp) + : mOrigin(aOrigin), mTimestamp(aTimestamp) + { } + + NS_IMETHOD + Run(); + +private: + nsCString mOrigin; + int64_t mTimestamp; +}; + +struct MOZ_STACK_CLASS RemoveQuotaInfo +{ + RemoveQuotaInfo(PersistenceType aPersistenceType, const nsACString& aPattern) + : persistenceType(aPersistenceType), pattern(aPattern) + { } + + PersistenceType persistenceType; + nsCString pattern; +}; + +struct MOZ_STACK_CLASS InactiveOriginsInfo +{ + InactiveOriginsInfo(OriginCollection& aCollection, + nsTArray& aOrigins) + : collection(aCollection), origins(aOrigins) + { } + + OriginCollection& collection; + nsTArray& origins; +}; + +bool +IsMainProcess() +{ + return XRE_GetProcessType() == GeckoProcessType_Default; +} + +void +SanitizeOriginString(nsCString& aOrigin) +{ + // We want profiles to be platform-independent so we always need to replace + // the same characters on every platform. Windows has the most extensive set + // of illegal characters so we use its FILE_ILLEGAL_CHARACTERS and + // FILE_PATH_SEPARATOR. + static const char kReplaceChars[] = CONTROL_CHARACTERS "/:*?\"<>|\\"; + +#ifdef XP_WIN + NS_ASSERTION(!strcmp(kReplaceChars, + FILE_ILLEGAL_CHARACTERS FILE_PATH_SEPARATOR), + "Illegal file characters have changed!"); +#endif + + aOrigin.ReplaceChar(kReplaceChars, '+'); +} + +nsresult +EnsureDirectory(nsIFile* aDirectory, bool* aCreated) +{ + AssertIsOnIOThread(); + + nsresult rv = aDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755); + if (rv == NS_ERROR_FILE_ALREADY_EXISTS) { + bool isDirectory; + rv = aDirectory->IsDirectory(&isDirectory); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(isDirectory, NS_ERROR_UNEXPECTED); + + *aCreated = false; + } + else { + NS_ENSURE_SUCCESS(rv, rv); + + *aCreated = true; + } + + return NS_OK; +} + +nsresult +CreateDirectoryUpgradeStamp(nsIFile* aDirectory) +{ + AssertIsOnIOThread(); + + nsCOMPtr metadataFile; + nsresult rv = aDirectory->Clone(getter_AddRefs(metadataFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = metadataFile->Append(NS_LITERAL_STRING(METADATA_FILE_NAME)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = metadataFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +GetDirectoryMetadataStream(nsIFile* aDirectory, bool aUpdate, + nsIBinaryOutputStream** aStream) +{ + AssertIsOnIOThread(); + + nsCOMPtr metadataFile; + nsresult rv = aDirectory->Clone(getter_AddRefs(metadataFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = metadataFile->Append(NS_LITERAL_STRING(METADATA_FILE_NAME)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr outputStream; + if (aUpdate) { + bool exists; + rv = metadataFile->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!exists) { + *aStream = nullptr; + return NS_OK; + } + + nsCOMPtr stream; + rv = NS_NewLocalFileStream(getter_AddRefs(stream), metadataFile); + NS_ENSURE_SUCCESS(rv, rv); + + outputStream = do_QueryInterface(stream); + NS_ENSURE_TRUE(outputStream, NS_ERROR_FAILURE); + } + else { + rv = NS_NewLocalFileOutputStream(getter_AddRefs(outputStream), + metadataFile); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr binaryStream = + do_CreateInstance("@mozilla.org/binaryoutputstream;1"); + NS_ENSURE_TRUE(binaryStream, NS_ERROR_FAILURE); + + rv = binaryStream->SetOutputStream(outputStream); + NS_ENSURE_SUCCESS(rv, rv); + + binaryStream.forget(aStream); + return NS_OK; +} + +nsresult +CreateDirectoryMetadata(nsIFile* aDirectory, int64_t aTimestamp, + const nsACString& aGroup, const nsACString& aOrigin) +{ + AssertIsOnIOThread(); + + nsCOMPtr stream; + nsresult rv = + GetDirectoryMetadataStream(aDirectory, false, getter_AddRefs(stream)); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(stream, "This shouldn't be null!"); + + rv = stream->Write64(aTimestamp); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stream->WriteStringZ(PromiseFlatCString(aGroup).get()); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stream->WriteStringZ(PromiseFlatCString(aOrigin).get()); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +GetDirectoryMetadata(nsIFile* aDirectory, int64_t* aTimestamp, + nsACString& aGroup, nsACString& aOrigin) +{ + AssertIsOnIOThread(); + + nsCOMPtr metadataFile; + nsresult rv = aDirectory->Clone(getter_AddRefs(metadataFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = metadataFile->Append(NS_LITERAL_STRING(METADATA_FILE_NAME)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr stream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), metadataFile); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr bufferedStream; + rv = NS_NewBufferedInputStream(getter_AddRefs(bufferedStream), stream, 512); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr binaryStream = + do_CreateInstance("@mozilla.org/binaryinputstream;1"); + NS_ENSURE_TRUE(binaryStream, NS_ERROR_FAILURE); + + rv = binaryStream->SetInputStream(bufferedStream); + NS_ENSURE_SUCCESS(rv, rv); + + uint64_t timestamp; + rv = binaryStream->Read64(×tamp); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString group; + rv = binaryStream->ReadCString(group); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString origin; + rv = binaryStream->ReadCString(origin); + NS_ENSURE_SUCCESS(rv, rv); + + *aTimestamp = timestamp; + aGroup = group; + aOrigin = origin; + return NS_OK; +} + +nsresult +MaybeUpgradeOriginDirectory(nsIFile* aDirectory) +{ + AssertIsOnIOThread(); + NS_ASSERTION(aDirectory, "Null pointer!"); + + nsCOMPtr metadataFile; + nsresult rv = aDirectory->Clone(getter_AddRefs(metadataFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = metadataFile->Append(NS_LITERAL_STRING(METADATA_FILE_NAME)); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists; + rv = metadataFile->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!exists) { + // Directory structure upgrade needed. + // Move all files to IDB specific directory. + + nsString idbDirectoryName; + rv = Client::TypeToText(Client::IDB, idbDirectoryName); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr idbDirectory; + rv = aDirectory->Clone(getter_AddRefs(idbDirectory)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = idbDirectory->Append(idbDirectoryName); + NS_ENSURE_SUCCESS(rv, rv); + + rv = idbDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755); + if (rv == NS_ERROR_FILE_ALREADY_EXISTS) { + NS_WARNING("IDB directory already exists!"); + + bool isDirectory; + rv = idbDirectory->IsDirectory(&isDirectory); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(isDirectory, NS_ERROR_UNEXPECTED); + } + else { + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr entries; + rv = aDirectory->GetDirectoryEntries(getter_AddRefs(entries)); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasMore; + while (NS_SUCCEEDED((rv = entries->HasMoreElements(&hasMore))) && hasMore) { + nsCOMPtr entry; + rv = entries->GetNext(getter_AddRefs(entry)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr file = do_QueryInterface(entry); + NS_ENSURE_TRUE(file, NS_NOINTERFACE); + + nsString leafName; + rv = file->GetLeafName(leafName); + NS_ENSURE_SUCCESS(rv, rv); + + if (!leafName.Equals(idbDirectoryName)) { + rv = file->MoveTo(idbDirectory, EmptyString()); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + rv = metadataFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +// This method computes and returns our best guess for the temporary storage +// limit (in bytes), based on the amount of space users have free on their hard +// drive and on given temporary storage usage (also in bytes). +nsresult +GetTemporaryStorageLimit(nsIFile* aDirectory, uint64_t aCurrentUsage, + uint64_t* aLimit) +{ + // Check for free space on device where temporary storage directory lives. + int64_t bytesAvailable; + nsresult rv = aDirectory->GetDiskSpaceAvailable(&bytesAvailable); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(bytesAvailable >= 0, "Negative bytes available?!"); + + uint64_t availableKB = + static_cast((bytesAvailable + aCurrentUsage) / 1024); + + // Grow/shrink in gChunkSizeKB units, deliberately, so that in the common case + // we don't shrink temporary storage and evict origin data every time we + // initialize. + availableKB = (availableKB / gChunkSizeKB) * gChunkSizeKB; + + // Allow temporary storage to consume up to half the available space. + uint64_t resultKB = availableKB * .50; + + *aLimit = resultKB * 1024; + return NS_OK; +} + +} // anonymous namespace + +QuotaManager::QuotaManager() +: mCurrentWindowIndex(BAD_TLS_INDEX), + mQuotaMutex("QuotaManager.mQuotaMutex"), + mTemporaryStorageLimit(0), + mTemporaryStorageUsage(0), + mTemporaryStorageInitialized(false), + mStorageAreaInitialized(false) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(!gInstance, "More than one instance!"); +} + +QuotaManager::~QuotaManager() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(!gInstance || gInstance == this, "Different instances!"); + gInstance = nullptr; +} + +// static +QuotaManager* +QuotaManager::GetOrCreate() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + if (IsShuttingDown()) { + NS_ERROR("Calling GetOrCreate() after shutdown!"); + return nullptr; + } + + if (!gInstance) { + nsRefPtr instance(new QuotaManager()); + + nsresult rv = instance->Init(); + NS_ENSURE_SUCCESS(rv, nullptr); + + nsCOMPtr obs = mozilla::services::GetObserverService(); + NS_ENSURE_TRUE(obs, nullptr); + + // We need this callback to know when to shut down all our threads. + rv = obs->AddObserver(instance, PROFILE_BEFORE_CHANGE_OBSERVER_ID, false); + NS_ENSURE_SUCCESS(rv, nullptr); + + // The observer service will hold our last reference, don't AddRef here. + gInstance = instance; + } + + return gInstance; +} + +// static +QuotaManager* +QuotaManager::Get() +{ + // Does not return an owning reference. + return gInstance; +} + +// static +QuotaManager* +QuotaManager::FactoryCreate() +{ + // Returns a raw pointer that carries an owning reference! Lame, but the + // singleton factory macros force this. + QuotaManager* quotaManager = GetOrCreate(); + NS_IF_ADDREF(quotaManager); + return quotaManager; +} + +// static +bool +QuotaManager::IsShuttingDown() +{ + return gShutdown; +} + +nsresult +QuotaManager::Init() +{ + // We need a thread-local to hold the current window. + NS_ASSERTION(mCurrentWindowIndex == BAD_TLS_INDEX, "Huh?"); + + if (PR_NewThreadPrivateIndex(&mCurrentWindowIndex, nullptr) != PR_SUCCESS) { + NS_ERROR("PR_NewThreadPrivateIndex failed, QuotaManager disabled"); + mCurrentWindowIndex = BAD_TLS_INDEX; + return NS_ERROR_FAILURE; + } + + nsresult rv; + if (IsMainProcess()) { + nsCOMPtr baseDir; + rv = NS_GetSpecialDirectory(NS_APP_INDEXEDDB_PARENT_DIR, + getter_AddRefs(baseDir)); + if (NS_FAILED(rv)) { + rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(baseDir)); + } + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr indexedDBDir; + rv = baseDir->Clone(getter_AddRefs(indexedDBDir)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = indexedDBDir->Append(NS_LITERAL_STRING("indexedDB")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = indexedDBDir->GetPath(mIndexedDBPath); + NS_ENSURE_SUCCESS(rv, rv); + + rv = baseDir->Append(NS_LITERAL_STRING("storage")); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr persistentStorageDir; + rv = baseDir->Clone(getter_AddRefs(persistentStorageDir)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = persistentStorageDir->Append(NS_LITERAL_STRING("persistent")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = persistentStorageDir->GetPath(mPersistentStoragePath); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr temporaryStorageDir; + rv = baseDir->Clone(getter_AddRefs(temporaryStorageDir)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = temporaryStorageDir->Append(NS_LITERAL_STRING("temporary")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = temporaryStorageDir->GetPath(mTemporaryStoragePath); + NS_ENSURE_SUCCESS(rv, rv); + + // Make a lazy thread for any IO we need (like clearing or enumerating the + // contents of storage directories). + mIOThread = new LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS, + NS_LITERAL_CSTRING("Storage I/O"), + LazyIdleThread::ManualShutdown); + + // Make a timer here to avoid potential failures later. We don't actually + // initialize the timer until shutdown. + mShutdownTimer = do_CreateInstance(NS_TIMER_CONTRACTID); + NS_ENSURE_TRUE(mShutdownTimer, NS_ERROR_FAILURE); + } + + if (NS_FAILED(Preferences::AddIntVarCache(&gStorageQuotaMB, + PREF_STORAGE_QUOTA, + kDefaultQuotaMB))) { + NS_WARNING("Unable to respond to quota pref changes!"); + } + + if (NS_FAILED(Preferences::AddIntVarCache(&gFixedLimitKB, PREF_FIXED_LIMIT, + kDefaultFixedLimitKB)) || + NS_FAILED(Preferences::AddUintVarCache(&gChunkSizeKB, + PREF_CHUNK_SIZE, + kDefaultChunkSizeKB))) { + NS_WARNING("Unable to respond to temp storage pref changes!"); + } + + if (NS_FAILED(Preferences::AddBoolVarCache(&gTestingEnabled, + PREF_TESTING_FEATURES, false))) { + NS_WARNING("Unable to respond to testing pref changes!"); + } + + static_assert(Client::IDB == 0 && Client::ASMJS == 1 && Client::TYPE_MAX == 2, + "Fix the registration!"); + + NS_ASSERTION(mClients.Capacity() == Client::TYPE_MAX, + "Should be using an auto array with correct capacity!"); + + // Register IndexedDB + mClients.AppendElement(new indexedDB::Client()); + mClients.AppendElement(asmjscache::CreateClient()); + + return NS_OK; +} + +void +QuotaManager::InitQuotaForOrigin(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + uint64_t aLimitBytes, + uint64_t aUsageBytes, + int64_t aAccessTime) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aLimitBytes > 0 || + aPersistenceType == PERSISTENCE_TYPE_TEMPORARY); + MOZ_ASSERT(aUsageBytes <= aLimitBytes || + aPersistenceType == PERSISTENCE_TYPE_TEMPORARY); + + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aGroup, &pair)) { + pair = new GroupInfoPair(); + mGroupInfoPairs.Put(aGroup, pair); + // The hashtable is now responsible to delete the GroupInfoPair. + } + + nsRefPtr groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + if (!groupInfo) { + groupInfo = new GroupInfo(aPersistenceType, aGroup); + pair->LockedSetGroupInfo(groupInfo); + } + + nsRefPtr originInfo = + new OriginInfo(groupInfo, aOrigin, aLimitBytes, aUsageBytes, aAccessTime); + groupInfo->LockedAddOriginInfo(originInfo); +} + +void +QuotaManager::DecreaseUsageForOrigin(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + int64_t aSize) +{ + AssertIsOnIOThread(); + + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aGroup, &pair)) { + return; + } + + nsRefPtr groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + if (!groupInfo) { + return; + } + + nsRefPtr originInfo = groupInfo->LockedGetOriginInfo(aOrigin); + if (originInfo) { + originInfo->LockedDecreaseUsage(aSize); + } +} + +void +QuotaManager::UpdateOriginAccessTime(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aGroup, &pair)) { + return; + } + + nsRefPtr groupInfo = + pair->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY); + if (!groupInfo) { + return; + } + + nsRefPtr originInfo = groupInfo->LockedGetOriginInfo(aOrigin); + if (originInfo) { + int64_t timestamp = PR_Now(); + originInfo->LockedUpdateAccessTime(timestamp); + + if (!groupInfo->IsForTemporaryStorage()) { + return; + } + + MutexAutoUnlock autoUnlock(mQuotaMutex); + + SaveOriginAccessTime(aOrigin, timestamp); + } +} + +// static +PLDHashOperator +QuotaManager::RemoveQuotaCallback(const nsACString& aKey, + nsAutoPtr& aValue, + void* aUserArg) +{ + NS_ASSERTION(!aKey.IsEmpty(), "Empty key!"); + NS_ASSERTION(aValue, "Null pointer!"); + + nsRefPtr groupInfo = + aValue->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY); + if (groupInfo) { + groupInfo->LockedRemoveOriginInfos(); + } + + return PL_DHASH_REMOVE; +} + +void +QuotaManager::RemoveQuota() +{ + MutexAutoLock lock(mQuotaMutex); + + mGroupInfoPairs.Enumerate(RemoveQuotaCallback, nullptr); + + NS_ASSERTION(mTemporaryStorageUsage == 0, "Should be zero!"); +} + +// static +PLDHashOperator +QuotaManager::RemoveQuotaForPersistenceTypeCallback( + const nsACString& aKey, + nsAutoPtr& aValue, + void* aUserArg) +{ + NS_ASSERTION(!aKey.IsEmpty(), "Empty key!"); + NS_ASSERTION(aValue, "Null pointer!"); + NS_ASSERTION(aUserArg, "Null pointer!"); + + PersistenceType& persistenceType = *static_cast(aUserArg); + + if (persistenceType == PERSISTENCE_TYPE_TEMPORARY) { + nsRefPtr groupInfo = + aValue->LockedGetGroupInfo(persistenceType); + if (groupInfo) { + groupInfo->LockedRemoveOriginInfos(); + } + } + + aValue->LockedClearGroupInfo(persistenceType); + + return aValue->LockedHasGroupInfos() ? PL_DHASH_NEXT : PL_DHASH_REMOVE; +} + +void +QuotaManager::RemoveQuotaForPersistenceType(PersistenceType aPersistenceType) +{ + MutexAutoLock lock(mQuotaMutex); + + mGroupInfoPairs.Enumerate(RemoveQuotaForPersistenceTypeCallback, + &aPersistenceType); + + NS_ASSERTION(aPersistenceType == PERSISTENCE_TYPE_PERSISTENT || + mTemporaryStorageUsage == 0, "Should be zero!"); +} + +// static +PLDHashOperator +QuotaManager::RemoveQuotaForPatternCallback(const nsACString& aKey, + nsAutoPtr& aValue, + void* aUserArg) +{ + NS_ASSERTION(!aKey.IsEmpty(), "Empty key!"); + NS_ASSERTION(aValue, "Null pointer!"); + NS_ASSERTION(aUserArg, "Null pointer!"); + + RemoveQuotaInfo* info = static_cast(aUserArg); + + nsRefPtr groupInfo = + aValue->LockedGetGroupInfo(info->persistenceType); + if (groupInfo) { + groupInfo->LockedRemoveOriginInfosForPattern(info->pattern); + + if (!groupInfo->LockedHasOriginInfos()) { + aValue->LockedClearGroupInfo(info->persistenceType); + + if (!aValue->LockedHasGroupInfos()) { + return PL_DHASH_REMOVE; + } + } + } + + return PL_DHASH_NEXT; +} + +void +QuotaManager::RemoveQuotaForPattern(PersistenceType aPersistenceType, + const nsACString& aPattern) +{ + NS_ASSERTION(!aPattern.IsEmpty(), "Empty pattern!"); + + RemoveQuotaInfo info(aPersistenceType, aPattern); + + MutexAutoLock lock(mQuotaMutex); + + mGroupInfoPairs.Enumerate(RemoveQuotaForPatternCallback, &info); +} + +already_AddRefed +QuotaManager::GetQuotaObject(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + nsIFile* aFile) +{ + NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!"); + + nsString path; + nsresult rv = aFile->GetPath(path); + NS_ENSURE_SUCCESS(rv, nullptr); + + int64_t fileSize; + + bool exists; + rv = aFile->Exists(&exists); + NS_ENSURE_SUCCESS(rv, nullptr); + + if (exists) { + rv = aFile->GetFileSize(&fileSize); + NS_ENSURE_SUCCESS(rv, nullptr); + } + else { + fileSize = 0; + } + + nsRefPtr result; + { + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aGroup, &pair)) { + return nullptr; + } + + nsRefPtr groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + + if (!groupInfo) { + return nullptr; + } + + nsRefPtr originInfo = groupInfo->LockedGetOriginInfo(aOrigin); + + if (!originInfo) { + return nullptr; + } + + // We need this extra raw pointer because we can't assign to the smart + // pointer directly since QuotaObject::AddRef would try to acquire the same + // mutex. + QuotaObject* quotaObject; + if (!originInfo->mQuotaObjects.Get(path, "aObject)) { + // Create a new QuotaObject. + quotaObject = new QuotaObject(originInfo, path, fileSize); + + // Put it to the hashtable. The hashtable is not responsible to delete + // the QuotaObject. + originInfo->mQuotaObjects.Put(path, quotaObject); + } + + // Addref the QuotaObject and move the ownership to the result. This must + // happen before we unlock! + result = quotaObject->LockedAddRef(); + } + + // The caller becomes the owner of the QuotaObject, that is, the caller is + // is responsible to delete it when the last reference is removed. + return result.forget(); +} + +already_AddRefed +QuotaManager::GetQuotaObject(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + const nsAString& aPath) +{ + nsresult rv; + nsCOMPtr file = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, nullptr); + + rv = file->InitWithPath(aPath); + NS_ENSURE_SUCCESS(rv, nullptr); + + return GetQuotaObject(aPersistenceType, aGroup, aOrigin, file); +} + +bool +QuotaManager::RegisterStorage(nsIOfflineStorage* aStorage) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(aStorage, "Null pointer!"); + + // Don't allow any new storages to be created after shutdown. + if (IsShuttingDown()) { + return false; + } + + // Add this storage to its origin info if it exists, create it otherwise. + const nsACString& origin = aStorage->Origin(); + ArrayCluster* cluster; + if (!mLiveStorages.Get(origin, &cluster)) { + cluster = new ArrayCluster(); + mLiveStorages.Put(origin, cluster); + + UpdateOriginAccessTime(aStorage->Type(), aStorage->Group(), origin); + } + (*cluster)[aStorage->GetClient()->GetType()].AppendElement(aStorage); + + return true; +} + +void +QuotaManager::UnregisterStorage(nsIOfflineStorage* aStorage) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(aStorage, "Null pointer!"); + + // Remove this storage from its origin array, maybe remove the array if it + // is then empty. + const nsACString& origin = aStorage->Origin(); + ArrayCluster* cluster; + if (mLiveStorages.Get(origin, &cluster) && + (*cluster)[aStorage->GetClient()->GetType()].RemoveElement(aStorage)) { + if (cluster->IsEmpty()) { + mLiveStorages.Remove(origin); + + UpdateOriginAccessTime(aStorage->Type(), aStorage->Group(), origin); + } + return; + } + NS_ERROR("Didn't know anything about this storage!"); +} + +void +QuotaManager::OnStorageClosed(nsIOfflineStorage* aStorage) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(aStorage, "Null pointer!"); + + // Check through the list of SynchronizedOps to see if any are waiting for + // this storage to close before proceeding. + SynchronizedOp* op = + FindSynchronizedOp(aStorage->Origin(), + Nullable(aStorage->Type()), + aStorage->Id()); + if (op) { + Client::Type clientType = aStorage->GetClient()->GetType(); + + // This storage is in the scope of this SynchronizedOp. Remove it + // from the list if necessary. + if (op->mStorages[clientType].RemoveElement(aStorage)) { + // Now set up the helper if there are no more live storages. + NS_ASSERTION(op->mListener, + "How did we get rid of the listener before removing the " + "last storage?"); + if (op->mStorages[clientType].IsEmpty()) { + // At this point, all storages are closed, so no new transactions + // can be started. There may, however, still be outstanding + // transactions that have not completed. We need to wait for those + // before we dispatch the helper. + if (NS_FAILED(RunSynchronizedOp(aStorage, op))) { + NS_WARNING("Failed to run synchronized op!"); + } + } + } + } +} + +void +QuotaManager::AbortCloseStoragesForWindow(nsPIDOMWindow* aWindow) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(aWindow, "Null pointer!"); + + FileService* service = FileService::Get(); + + StorageMatcher > liveStorages; + liveStorages.Find(mLiveStorages); + + for (uint32_t i = 0; i < Client::TYPE_MAX; i++) { + nsRefPtr& client = mClients[i]; + bool utilized = service && client->IsFileServiceUtilized(); + bool activated = client->IsTransactionServiceActivated(); + + nsTArray& array = liveStorages[i]; + for (uint32_t j = 0; j < array.Length(); j++) { + nsIOfflineStorage*& storage = array[j]; + + if (storage->IsOwned(aWindow)) { + if (NS_FAILED(storage->Close())) { + NS_WARNING("Failed to close storage for dying window!"); + } + + if (utilized) { + service->AbortLockedFilesForStorage(storage); + } + + if (activated) { + client->AbortTransactionsForStorage(storage); + } + } + } + } +} + +bool +QuotaManager::HasOpenTransactions(nsPIDOMWindow* aWindow) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(aWindow, "Null pointer!"); + + FileService* service = FileService::Get(); + + nsAutoPtr > > liveStorages; + + for (uint32_t i = 0; i < Client::TYPE_MAX; i++) { + nsRefPtr& client = mClients[i]; + bool utilized = service && client->IsFileServiceUtilized(); + bool activated = client->IsTransactionServiceActivated(); + + if (utilized || activated) { + if (!liveStorages) { + liveStorages = new StorageMatcher >(); + liveStorages->Find(mLiveStorages); + } + + nsTArray& storages = liveStorages->ArrayAt(i); + for (uint32_t j = 0; j < storages.Length(); j++) { + nsIOfflineStorage*& storage = storages[j]; + + if (storage->IsOwned(aWindow) && + ((utilized && service->HasLockedFilesForStorage(storage)) || + (activated && client->HasTransactionsForStorage(storage)))) { + return true; + } + } + } + } + + return false; +} + +nsresult +QuotaManager::WaitForOpenAllowed(const OriginOrPatternString& aOriginOrPattern, + Nullable aPersistenceType, + const nsACString& aId, nsIRunnable* aRunnable) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(!aOriginOrPattern.IsEmpty() || aOriginOrPattern.IsNull(), + "Empty pattern!"); + NS_ASSERTION(aRunnable, "Null pointer!"); + + nsAutoPtr op(new SynchronizedOp(aOriginOrPattern, + aPersistenceType, aId)); + + // See if this runnable needs to wait. + bool delayed = false; + for (uint32_t index = mSynchronizedOps.Length(); index > 0; index--) { + nsAutoPtr& existingOp = mSynchronizedOps[index - 1]; + if (op->MustWaitFor(*existingOp)) { + existingOp->DelayRunnable(aRunnable); + delayed = true; + break; + } + } + + // Otherwise, dispatch it immediately. + if (!delayed) { + nsresult rv = NS_DispatchToCurrentThread(aRunnable); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Adding this to the synchronized ops list will block any additional + // ops from proceeding until this one is done. + mSynchronizedOps.AppendElement(op.forget()); + + return NS_OK; +} + +void +QuotaManager::AddSynchronizedOp(const OriginOrPatternString& aOriginOrPattern, + Nullable aPersistenceType) +{ + nsAutoPtr op(new SynchronizedOp(aOriginOrPattern, + aPersistenceType, + EmptyCString())); + +#ifdef DEBUG + for (uint32_t index = mSynchronizedOps.Length(); index > 0; index--) { + nsAutoPtr& existingOp = mSynchronizedOps[index - 1]; + NS_ASSERTION(!op->MustWaitFor(*existingOp), "What?"); + } +#endif + + mSynchronizedOps.AppendElement(op.forget()); +} + +void +QuotaManager::AllowNextSynchronizedOp( + const OriginOrPatternString& aOriginOrPattern, + Nullable aPersistenceType, + const nsACString& aId) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(!aOriginOrPattern.IsEmpty() || aOriginOrPattern.IsNull(), + "Empty origin/pattern!"); + + uint32_t count = mSynchronizedOps.Length(); + for (uint32_t index = 0; index < count; index++) { + nsAutoPtr& op = mSynchronizedOps[index]; + if (op->mOriginOrPattern.IsOrigin() == aOriginOrPattern.IsOrigin() && + op->mOriginOrPattern == aOriginOrPattern && + op->mPersistenceType == aPersistenceType) { + if (op->mId == aId) { + NS_ASSERTION(op->mStorages.IsEmpty(), "How did this happen?"); + + op->DispatchDelayedRunnables(); + + mSynchronizedOps.RemoveElementAt(index); + return; + } + + // If one or the other is for an origin clear, we should have matched + // solely on origin. + NS_ASSERTION(!op->mId.IsEmpty() && !aId.IsEmpty(), + "Why didn't we match earlier?"); + } + } + + NS_NOTREACHED("Why didn't we find a SynchronizedOp?"); +} + +nsresult +QuotaManager::GetDirectoryForOrigin(PersistenceType aPersistenceType, + const nsACString& aASCIIOrigin, + nsIFile** aDirectory) const +{ + nsresult rv; + nsCOMPtr directory = + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = directory->InitWithPath(GetStoragePath(aPersistenceType)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString originSanitized(aASCIIOrigin); + SanitizeOriginString(originSanitized); + + rv = directory->Append(NS_ConvertASCIItoUTF16(originSanitized)); + NS_ENSURE_SUCCESS(rv, rv); + + directory.forget(aDirectory); + return NS_OK; +} + +nsresult +QuotaManager::InitializeOrigin(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + bool aTrackQuota, + int64_t aAccessTime, + nsIFile* aDirectory) +{ + AssertIsOnIOThread(); + + nsresult rv; + + bool temporaryStorage = aPersistenceType == PERSISTENCE_TYPE_TEMPORARY; + if (!temporaryStorage) { + rv = MaybeUpgradeOriginDirectory(aDirectory); + NS_ENSURE_SUCCESS(rv, rv); + } + + // We need to initialize directories of all clients if they exists and also + // get the total usage to initialize the quota. + nsAutoPtr usageInfo; + if (aTrackQuota) { + usageInfo = new UsageInfo(); + } + + nsCOMPtr entries; + rv = aDirectory->GetDirectoryEntries(getter_AddRefs(entries)); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasMore; + while (NS_SUCCEEDED((rv = entries->HasMoreElements(&hasMore))) && hasMore) { + nsCOMPtr entry; + rv = entries->GetNext(getter_AddRefs(entry)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr file = do_QueryInterface(entry); + NS_ENSURE_TRUE(file, NS_NOINTERFACE); + + nsString leafName; + rv = file->GetLeafName(leafName); + NS_ENSURE_SUCCESS(rv, rv); + + if (leafName.EqualsLiteral(METADATA_FILE_NAME) || + leafName.EqualsLiteral(DSSTORE_FILE_NAME)) { + continue; + } + + bool isDirectory; + rv = file->IsDirectory(&isDirectory); + NS_ENSURE_SUCCESS(rv, rv); + + if (!isDirectory) { + NS_WARNING("Unknown file found!"); + return NS_ERROR_UNEXPECTED; + } + + Client::Type clientType; + rv = Client::TypeFromText(leafName, clientType); + if (NS_FAILED(rv)) { + NS_WARNING("Unknown directory found!"); + return NS_ERROR_UNEXPECTED; + } + + rv = mClients[clientType]->InitOrigin(aPersistenceType, aGroup, aOrigin, + usageInfo); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (aTrackQuota) { + uint64_t quotaMaxBytes; + uint64_t totalUsageBytes = usageInfo->TotalUsage(); + + if (temporaryStorage) { + // Temporary storage has no limit for origin usage (there's a group and + // the global limit though). + quotaMaxBytes = 0; + } + else { + quotaMaxBytes = GetStorageQuotaMB() * 1024 * 1024; + if (totalUsageBytes > quotaMaxBytes) { + NS_WARNING("Origin is already using more storage than allowed!"); + return NS_ERROR_UNEXPECTED; + } + } + + InitQuotaForOrigin(aPersistenceType, aGroup, aOrigin, quotaMaxBytes, + totalUsageBytes, aAccessTime); + } + + return NS_OK; +} + +nsresult +QuotaManager::MaybeUpgradeIndexedDBDirectory() +{ + AssertIsOnIOThread(); + + if (mStorageAreaInitialized) { + return NS_OK; + } + + nsresult rv; + + nsCOMPtr indexedDBDir = + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = indexedDBDir->InitWithPath(mIndexedDBPath); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists; + rv = indexedDBDir->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!exists) { + // Nothing to upgrade. + mStorageAreaInitialized = true; + + return NS_OK; + } + + bool isDirectory; + rv = indexedDBDir->IsDirectory(&isDirectory); + NS_ENSURE_SUCCESS(rv, rv); + + if (!isDirectory) { + NS_WARNING("indexedDB entry is not a directory!"); + return NS_OK; + } + + nsCOMPtr persistentStorageDir = + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = persistentStorageDir->InitWithPath(mPersistentStoragePath); + NS_ENSURE_SUCCESS(rv, rv); + + rv = persistentStorageDir->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (exists) { + NS_WARNING("indexedDB directory shouldn't exist after the upgrade!"); + return NS_OK; + } + + nsCOMPtr storageDir; + rv = persistentStorageDir->GetParent(getter_AddRefs(storageDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsString persistentStorageName; + rv = persistentStorageDir->GetLeafName(persistentStorageName); + NS_ENSURE_SUCCESS(rv, rv); + + // MoveTo() is atomic if the move happens on the same volume which should + // be our case, so even if we crash in the middle of the operation nothing + // breaks next time we try to initialize. + // However there's a theoretical possibility that the indexedDB directory + // is on different volume, but it should be rare enough that we don't have + // to worry about it. + rv = indexedDBDir->MoveTo(storageDir, persistentStorageName); + NS_ENSURE_SUCCESS(rv, rv); + + mStorageAreaInitialized = true; + + return NS_OK; +} + +nsresult +QuotaManager::EnsureOriginIsInitialized(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + bool aTrackQuota, + nsIFile** aDirectory) +{ + AssertIsOnIOThread(); + + nsresult rv = MaybeUpgradeIndexedDBDirectory(); + NS_ENSURE_SUCCESS(rv, rv); + + // Get directory for this origin and persistence type. + nsCOMPtr directory; + rv = GetDirectoryForOrigin(aPersistenceType, aOrigin, + getter_AddRefs(directory)); + NS_ENSURE_SUCCESS(rv, rv); + + if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + if (mInitializedOrigins.Contains(aOrigin)) { + NS_ADDREF(*aDirectory = directory); + return NS_OK; + } + + bool created; + rv = EnsureDirectory(directory, &created); + NS_ENSURE_SUCCESS(rv, rv); + + if (created) { + rv = CreateDirectoryUpgradeStamp(directory); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = InitializeOrigin(aPersistenceType, aGroup, aOrigin, aTrackQuota, 0, + directory); + NS_ENSURE_SUCCESS(rv, rv); + + mInitializedOrigins.AppendElement(aOrigin); + + directory.forget(aDirectory); + return NS_OK; + } + + NS_ASSERTION(aPersistenceType == PERSISTENCE_TYPE_TEMPORARY, "Huh?"); + NS_ASSERTION(aTrackQuota, "Huh?"); + + if (!mTemporaryStorageInitialized) { + nsCOMPtr parentDirectory; + rv = directory->GetParent(getter_AddRefs(parentDirectory)); + NS_ENSURE_SUCCESS(rv, rv); + + bool created; + rv = EnsureDirectory(parentDirectory, &created); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr entries; + rv = parentDirectory->GetDirectoryEntries(getter_AddRefs(entries)); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasMore; + while (NS_SUCCEEDED((rv = entries->HasMoreElements(&hasMore))) && hasMore) { + nsCOMPtr entry; + rv = entries->GetNext(getter_AddRefs(entry)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr childDirectory = do_QueryInterface(entry); + NS_ENSURE_TRUE(childDirectory, NS_NOINTERFACE); + + bool isDirectory; + rv = childDirectory->IsDirectory(&isDirectory); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(isDirectory, NS_ERROR_UNEXPECTED); + + int64_t timestamp; + nsCString group; + nsCString origin; + rv = GetDirectoryMetadata(childDirectory, ×tamp, group, origin); + NS_ENSURE_SUCCESS(rv, rv); + + rv = InitializeOrigin(aPersistenceType, group, origin, aTrackQuota, + timestamp, childDirectory); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to initialize origin!"); + + // We have to cleanup partially initialized quota for temporary storage. + RemoveQuotaForPersistenceType(aPersistenceType); + + return rv; + } + } + + if (gFixedLimitKB >= 0) { + mTemporaryStorageLimit = gFixedLimitKB * 1024; + } + else { + rv = GetTemporaryStorageLimit(parentDirectory, mTemporaryStorageUsage, + &mTemporaryStorageLimit); + NS_ENSURE_SUCCESS(rv, rv); + } + + mTemporaryStorageInitialized = true; + + CheckTemporaryStorageLimits(); + } + + bool created; + rv = EnsureDirectory(directory, &created); + NS_ENSURE_SUCCESS(rv, rv); + + if (created) { + int64_t timestamp = PR_Now(); + + rv = CreateDirectoryMetadata(directory, timestamp, aGroup, aOrigin); + NS_ENSURE_SUCCESS(rv, rv); + + rv = InitializeOrigin(aPersistenceType, aGroup, aOrigin, aTrackQuota, + timestamp, directory); + NS_ENSURE_SUCCESS(rv, rv); + } + + directory.forget(aDirectory); + return NS_OK; +} + +void +QuotaManager::OriginClearCompleted( + PersistenceType aPersistenceType, + const OriginOrPatternString& aOriginOrPattern) +{ + AssertIsOnIOThread(); + + if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + if (aOriginOrPattern.IsOrigin()) { + mInitializedOrigins.RemoveElement(aOriginOrPattern); + } + else { + for (uint32_t index = mInitializedOrigins.Length(); index > 0; index--) { + if (PatternMatchesOrigin(aOriginOrPattern, + mInitializedOrigins[index - 1])) { + mInitializedOrigins.RemoveElementAt(index - 1); + } + } + } + } + + for (uint32_t index = 0; index < Client::TYPE_MAX; index++) { + mClients[index]->OnOriginClearCompleted(aPersistenceType, aOriginOrPattern); + } +} + +void +QuotaManager::ResetOrClearCompleted() +{ + AssertIsOnIOThread(); + + mInitializedOrigins.Clear(); + mTemporaryStorageInitialized = false; + + ReleaseIOThreadObjects(); +} + +already_AddRefed +QuotaManager::GetClient(Client::Type aClientType) +{ + nsRefPtr client = mClients.SafeElementAt(aClientType); + return client.forget(); +} + +uint64_t +QuotaManager::GetGroupLimit() const +{ + MOZ_ASSERT(mTemporaryStorageInitialized); + + // To avoid one group evicting all the rest, limit the amount any one group + // can use to 20%. To prevent individual sites from using exorbitant amounts + // of storage where there is a lot of free space, cap the group limit to 2GB. + uint64_t x = std::min(mTemporaryStorageLimit * .20, 2 GB); + + // In low-storage situations, make an exception (while not exceeding the total + // storage limit). + return std::min(mTemporaryStorageLimit, + std::max(x, 10 MB)); +} + +// static +uint32_t +QuotaManager::GetStorageQuotaMB() +{ + return uint32_t(std::max(gStorageQuotaMB, 0)); +} + +// static +void +QuotaManager::GetStorageId(PersistenceType aPersistenceType, + const nsACString& aOrigin, + Client::Type aClientType, + const nsAString& aName, + nsACString& aDatabaseId) +{ + nsAutoCString str; + str.AppendInt(aPersistenceType); + str.Append('*'); + str.Append(aOrigin); + str.Append('*'); + str.AppendInt(aClientType); + str.Append('*'); + str.Append(NS_ConvertUTF16toUTF8(aName)); + + aDatabaseId = str; +} + +// static +nsresult +QuotaManager::GetInfoFromURI(nsIURI* aURI, + uint32_t aAppId, + bool aInMozBrowser, + nsACString* aGroup, + nsACString* aASCIIOrigin, + StoragePrivilege* aPrivilege, + PersistenceType* aDefaultPersistenceType) +{ + NS_ASSERTION(aURI, "Null uri!"); + + nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager(); + NS_ENSURE_TRUE(secMan, NS_ERROR_FAILURE); + + nsCOMPtr principal; + nsresult rv = secMan->GetAppCodebasePrincipal(aURI, aAppId, aInMozBrowser, + getter_AddRefs(principal)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = GetInfoFromPrincipal(principal, aGroup, aASCIIOrigin, aPrivilege, + aDefaultPersistenceType); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +// static +nsresult +QuotaManager::GetInfoFromPrincipal(nsIPrincipal* aPrincipal, + nsACString* aGroup, + nsACString* aASCIIOrigin, + StoragePrivilege* aPrivilege, + PersistenceType* aDefaultPersistenceType) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(aPrincipal, "Don't hand me a null principal!"); + + if (nsContentUtils::IsSystemPrincipal(aPrincipal)) { + GetInfoForChrome(aGroup, aASCIIOrigin, aPrivilege, aDefaultPersistenceType); + return NS_OK; + } + + bool isNullPrincipal; + nsresult rv = aPrincipal->GetIsNullPrincipal(&isNullPrincipal); + NS_ENSURE_SUCCESS(rv, rv); + + if (isNullPrincipal) { + NS_WARNING("IndexedDB not supported from this principal!"); + return NS_ERROR_FAILURE; + } + + nsCString origin; + rv = aPrincipal->GetOrigin(getter_Copies(origin)); + NS_ENSURE_SUCCESS(rv, rv); + + if (origin.EqualsLiteral("chrome")) { + NS_WARNING("Non-chrome principal can't use chrome origin!"); + return NS_ERROR_FAILURE; + } + + nsCString jarPrefix; + if (aGroup || aASCIIOrigin) { + rv = aPrincipal->GetJarPrefix(jarPrefix); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (aGroup) { + nsCString baseDomain; + rv = aPrincipal->GetBaseDomain(baseDomain); + if (NS_FAILED(rv)) { + // A hack for JetPack. + + nsCOMPtr uri; + rv = aPrincipal->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + bool isIndexedDBURI = false; + rv = uri->SchemeIs("indexedDB", &isIndexedDBURI); + NS_ENSURE_SUCCESS(rv, rv); + + if (isIndexedDBURI) { + rv = NS_OK; + } + } + NS_ENSURE_SUCCESS(rv, rv); + + if (baseDomain.IsEmpty()) { + aGroup->Assign(jarPrefix + origin); + } + else { + aGroup->Assign(jarPrefix + baseDomain); + } + } + + if (aASCIIOrigin) { + aASCIIOrigin->Assign(jarPrefix + origin); + } + + if (aPrivilege) { + *aPrivilege = Content; + } + + if (aDefaultPersistenceType) { + *aDefaultPersistenceType = PERSISTENCE_TYPE_PERSISTENT; + } + + return NS_OK; +} + +// static +nsresult +QuotaManager::GetInfoFromWindow(nsPIDOMWindow* aWindow, + nsACString* aGroup, + nsACString* aASCIIOrigin, + StoragePrivilege* aPrivilege, + PersistenceType* aDefaultPersistenceType) +{ + NS_ASSERTION(NS_IsMainThread(), + "We're about to touch a window off the main thread!"); + NS_ASSERTION(aWindow, "Don't hand me a null window!"); + + nsCOMPtr sop = do_QueryInterface(aWindow); + NS_ENSURE_TRUE(sop, NS_ERROR_FAILURE); + + nsCOMPtr principal = sop->GetPrincipal(); + NS_ENSURE_TRUE(principal, NS_ERROR_FAILURE); + + nsresult rv = GetInfoFromPrincipal(principal, aGroup, aASCIIOrigin, + aPrivilege, aDefaultPersistenceType); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +// static +void +QuotaManager::GetInfoForChrome(nsACString* aGroup, + nsACString* aASCIIOrigin, + StoragePrivilege* aPrivilege, + PersistenceType* aDefaultPersistenceType) +{ + NS_ASSERTION(nsContentUtils::IsCallerChrome(), "Only for chrome!"); + + static const char kChromeOrigin[] = "chrome"; + + if (aGroup) { + aGroup->AssignLiteral(kChromeOrigin); + } + if (aASCIIOrigin) { + aASCIIOrigin->AssignLiteral(kChromeOrigin); + } + if (aPrivilege) { + *aPrivilege = Chrome; + } + if (aDefaultPersistenceType) { + *aDefaultPersistenceType = PERSISTENCE_TYPE_PERSISTENT; + } +} + +NS_IMPL_ISUPPORTS(QuotaManager, nsIQuotaManager, nsIObserver) + +NS_IMETHODIMP +QuotaManager::GetUsageForURI(nsIURI* aURI, + nsIUsageCallback* aCallback, + uint32_t aAppId, + bool aInMozBrowserOnly, + uint8_t aOptionalArgCount, + nsIQuotaRequest** _retval) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + NS_ENSURE_ARG_POINTER(aURI); + NS_ENSURE_ARG_POINTER(aCallback); + + // This only works from the main process. + NS_ENSURE_TRUE(IsMainProcess(), NS_ERROR_NOT_AVAILABLE); + + if (!aOptionalArgCount) { + aAppId = nsIScriptSecurityManager::NO_APP_ID; + } + + // Figure out which origin we're dealing with. + nsCString group; + nsCString origin; + nsresult rv = GetInfoFromURI(aURI, aAppId, aInMozBrowserOnly, &group, &origin, + nullptr, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + OriginOrPatternString oops = OriginOrPatternString::FromOrigin(origin); + + nsRefPtr runnable = + new AsyncUsageRunnable(aAppId, aInMozBrowserOnly, group, oops, aURI, + aCallback); + + // Put the computation runnable in the queue. + rv = WaitForOpenAllowed(oops, Nullable(), EmptyCString(), + runnable); + NS_ENSURE_SUCCESS(rv, rv); + + runnable->AdvanceState(); + + runnable.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +QuotaManager::Clear() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + if (!gTestingEnabled) { + NS_WARNING("Testing features are not enabled!"); + return NS_OK; + } + + OriginOrPatternString oops = OriginOrPatternString::FromNull(); + + nsRefPtr runnable = new ResetOrClearRunnable(true); + + // Put the clear runnable in the queue. + nsresult rv = + WaitForOpenAllowed(oops, Nullable(), EmptyCString(), + runnable); + NS_ENSURE_SUCCESS(rv, rv); + + runnable->AdvanceState(); + + // Give the runnable some help by invalidating any storages in the way. + StorageMatcher > matches; + matches.Find(mLiveStorages); + + for (uint32_t index = 0; index < matches.Length(); index++) { + // We need to grab references to any live storages here to prevent them + // from dying while we invalidate them. + nsCOMPtr storage = matches[index]; + storage->Invalidate(); + } + + // After everything has been invalidated the helper should be dispatched to + // the end of the event queue. + return NS_OK; +} + +NS_IMETHODIMP +QuotaManager::ClearStoragesForURI(nsIURI* aURI, + uint32_t aAppId, + bool aInMozBrowserOnly, + const nsACString& aPersistenceType, + uint8_t aOptionalArgCount) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + NS_ENSURE_ARG_POINTER(aURI); + + // This only works from the main process. + NS_ENSURE_TRUE(IsMainProcess(), NS_ERROR_NOT_AVAILABLE); + + if (!aOptionalArgCount) { + aAppId = nsIScriptSecurityManager::NO_APP_ID; + } + + // Figure out which origin we're dealing with. + nsCString origin; + nsresult rv = GetInfoFromURI(aURI, aAppId, aInMozBrowserOnly, nullptr, &origin, + nullptr, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString pattern; + GetOriginPatternString(aAppId, aInMozBrowserOnly, origin, pattern); + + Nullable persistenceType; + rv = NullablePersistenceTypeFromText(aPersistenceType, &persistenceType); + NS_ENSURE_SUCCESS(rv, rv); + + // If there is a pending or running clear operation for this origin, return + // immediately. + if (IsClearOriginPending(pattern, persistenceType)) { + return NS_OK; + } + + OriginOrPatternString oops = OriginOrPatternString::FromPattern(pattern); + + // Queue up the origin clear runnable. + nsRefPtr runnable = + new OriginClearRunnable(oops, persistenceType); + + rv = WaitForOpenAllowed(oops, persistenceType, EmptyCString(), runnable); + NS_ENSURE_SUCCESS(rv, rv); + + runnable->AdvanceState(); + + // Give the runnable some help by invalidating any storages in the way. + StorageMatcher > matches; + matches.Find(mLiveStorages, pattern); + + for (uint32_t index = 0; index < matches.Length(); index++) { + if (persistenceType.IsNull() || + matches[index]->Type() == persistenceType.Value()) { + // We need to grab references to any live storages here to prevent them + // from dying while we invalidate them. + nsCOMPtr storage = matches[index]; + storage->Invalidate(); + } + } + + // After everything has been invalidated the helper should be dispatched to + // the end of the event queue. + return NS_OK; +} + +NS_IMETHODIMP +QuotaManager::Reset() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + if (!gTestingEnabled) { + NS_WARNING("Testing features are not enabled!"); + return NS_OK; + } + + OriginOrPatternString oops = OriginOrPatternString::FromNull(); + + nsRefPtr runnable = new ResetOrClearRunnable(false); + + // Put the reset runnable in the queue. + nsresult rv = + WaitForOpenAllowed(oops, Nullable(), EmptyCString(), + runnable); + NS_ENSURE_SUCCESS(rv, rv); + + runnable->AdvanceState(); + + // Give the runnable some help by invalidating any storages in the way. + StorageMatcher > matches; + matches.Find(mLiveStorages); + + for (uint32_t index = 0; index < matches.Length(); index++) { + // We need to grab references to any live storages here to prevent them + // from dying while we invalidate them. + nsCOMPtr storage = matches[index]; + storage->Invalidate(); + } + + // After everything has been invalidated the helper should be dispatched to + // the end of the event queue. + return NS_OK; +} + +NS_IMETHODIMP +QuotaManager::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + if (!strcmp(aTopic, PROFILE_BEFORE_CHANGE_OBSERVER_ID)) { + // Setting this flag prevents the service from being recreated and prevents + // further storagess from being created. + if (gShutdown.exchange(true)) { + NS_ERROR("Shutdown more than once?!"); + } + + if (IsMainProcess()) { + FileService* service = FileService::Get(); + if (service) { + // This should only wait for storages registered in this manager + // to complete. Other storages may still have running locked files. + // If the necko service (thread pool) gets the shutdown notification + // first then the sync loop won't be processed at all, otherwise it will + // lock the main thread until all storages registered in this manager + // are finished. + + nsTArray indexes; + for (uint32_t index = 0; index < Client::TYPE_MAX; index++) { + if (mClients[index]->IsFileServiceUtilized()) { + indexes.AppendElement(index); + } + } + + StorageMatcher > > liveStorages; + liveStorages.Find(mLiveStorages, &indexes); + + if (!liveStorages.IsEmpty()) { + nsRefPtr runnable = + new WaitForLockedFilesToFinishRunnable(); + + service->WaitForStoragesToComplete(liveStorages, runnable); + + nsIThread* thread = NS_GetCurrentThread(); + while (runnable->IsBusy()) { + if (!NS_ProcessNextEvent(thread)) { + NS_ERROR("Failed to process next event!"); + break; + } + } + } + } + + // Kick off the shutdown timer. + if (NS_FAILED(mShutdownTimer->Init(this, DEFAULT_SHUTDOWN_TIMER_MS, + nsITimer::TYPE_ONE_SHOT))) { + NS_WARNING("Failed to initialize shutdown timer!"); + } + + // Each client will spin the event loop while we wait on all the threads + // to close. Our timer may fire during that loop. + for (uint32_t index = 0; index < Client::TYPE_MAX; index++) { + mClients[index]->ShutdownTransactionService(); + } + + // Cancel the timer regardless of whether it actually fired. + if (NS_FAILED(mShutdownTimer->Cancel())) { + NS_WARNING("Failed to cancel shutdown timer!"); + } + + // Give clients a chance to cleanup IO thread only objects. + nsCOMPtr runnable = + NS_NewRunnableMethod(this, &QuotaManager::ReleaseIOThreadObjects); + if (!runnable) { + NS_WARNING("Failed to create runnable!"); + } + + if (NS_FAILED(mIOThread->Dispatch(runnable, NS_DISPATCH_NORMAL))) { + NS_WARNING("Failed to dispatch runnable!"); + } + + // Make sure to join with our IO thread. + if (NS_FAILED(mIOThread->Shutdown())) { + NS_WARNING("Failed to shutdown IO thread!"); + } + } + + return NS_OK; + } + + if (!strcmp(aTopic, NS_TIMER_CALLBACK_TOPIC)) { + NS_ASSERTION(IsMainProcess(), "Should only happen in the main process!"); + + NS_WARNING("Some storage operations are taking longer than expected " + "during shutdown and will be aborted!"); + + // Grab all live storages, for all origins. + StorageMatcher > liveStorages; + liveStorages.Find(mLiveStorages); + + // Invalidate them all. + if (!liveStorages.IsEmpty()) { + uint32_t count = liveStorages.Length(); + for (uint32_t index = 0; index < count; index++) { + liveStorages[index]->Invalidate(); + } + } + + return NS_OK; + } + + if (!strcmp(aTopic, TOPIC_WEB_APP_CLEAR_DATA)) { + nsCOMPtr params = + do_QueryInterface(aSubject); + NS_ENSURE_TRUE(params, NS_ERROR_UNEXPECTED); + + uint32_t appId; + nsresult rv = params->GetAppId(&appId); + NS_ENSURE_SUCCESS(rv, rv); + + bool browserOnly; + rv = params->GetBrowserOnly(&browserOnly); + NS_ENSURE_SUCCESS(rv, rv); + + rv = ClearStoragesForApp(appId, browserOnly); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + NS_NOTREACHED("Unknown topic!"); + return NS_ERROR_UNEXPECTED; +} + +void +QuotaManager::SetCurrentWindowInternal(nsPIDOMWindow* aWindow) +{ + NS_ASSERTION(mCurrentWindowIndex != BAD_TLS_INDEX, + "Should have a valid TLS storage index!"); + + if (aWindow) { + NS_ASSERTION(!PR_GetThreadPrivate(mCurrentWindowIndex), + "Somebody forgot to clear the current window!"); + PR_SetThreadPrivate(mCurrentWindowIndex, aWindow); + } + else { + // We cannot assert PR_GetThreadPrivate(mCurrentWindowIndex) here because + // there are some cases where we did not already have a window. + PR_SetThreadPrivate(mCurrentWindowIndex, nullptr); + } +} + +void +QuotaManager::CancelPromptsForWindowInternal(nsPIDOMWindow* aWindow) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + nsRefPtr helper; + + MutexAutoLock autoLock(mQuotaMutex); + + if (mCheckQuotaHelpers.Get(aWindow, getter_AddRefs(helper))) { + helper->Cancel(); + } +} + +bool +QuotaManager::LockedQuotaIsLifted() +{ + mQuotaMutex.AssertCurrentThreadOwns(); + + NS_ASSERTION(mCurrentWindowIndex != BAD_TLS_INDEX, + "Should have a valid TLS storage index!"); + + nsPIDOMWindow* window = + static_cast(PR_GetThreadPrivate(mCurrentWindowIndex)); + + // Quota is not enforced in chrome contexts (e.g. for components and JSMs) + // so we must have a window here. + NS_ASSERTION(window, "Why don't we have a Window here?"); + + bool createdHelper = false; + + nsRefPtr helper; + if (!mCheckQuotaHelpers.Get(window, getter_AddRefs(helper))) { + helper = new CheckQuotaHelper(window, mQuotaMutex); + createdHelper = true; + + mCheckQuotaHelpers.Put(window, helper); + + // Unlock while calling out to XPCOM (code behind the dispatch method needs + // to acquire its own lock which can potentially lead to a deadlock and it + // also calls an observer that can do various stuff like IO, so it's better + // to not hold our mutex while that happens). + { + MutexAutoUnlock autoUnlock(mQuotaMutex); + + nsresult rv = NS_DispatchToMainThread(helper); + NS_ENSURE_SUCCESS(rv, false); + } + + // Relocked. If any other threads hit the quota limit on the same Window, + // they are using the helper we created here and are now blocking in + // PromptAndReturnQuotaDisabled. + } + + bool result = helper->PromptAndReturnQuotaIsDisabled(); + + // If this thread created the helper and added it to the hash, this thread + // must remove it. + if (createdHelper) { + mCheckQuotaHelpers.Remove(window); + } + + return result; +} + +uint64_t +QuotaManager::LockedCollectOriginsForEviction( + uint64_t aMinSizeToBeFreed, + nsTArray& aOriginInfos) +{ + mQuotaMutex.AssertCurrentThreadOwns(); + + nsRefPtr helper = + new CollectOriginsHelper(mQuotaMutex, aMinSizeToBeFreed); + + // Unlock while calling out to XPCOM (see the detailed comment in + // LockedQuotaIsLifted) + { + MutexAutoUnlock autoUnlock(mQuotaMutex); + + if (NS_FAILED(NS_DispatchToMainThread(helper))) { + NS_WARNING("Failed to dispatch to the main thread!"); + } + } + + return helper->BlockAndReturnOriginsForEviction(aOriginInfos); +} + +void +QuotaManager::LockedRemoveQuotaForOrigin(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin) +{ + mQuotaMutex.AssertCurrentThreadOwns(); + + GroupInfoPair* pair; + mGroupInfoPairs.Get(aGroup, &pair); + + if (!pair) { + return; + } + + nsRefPtr groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + if (groupInfo) { + groupInfo->LockedRemoveOriginInfo(aOrigin); + + if (!groupInfo->LockedHasOriginInfos()) { + pair->LockedClearGroupInfo(aPersistenceType); + + if (!pair->LockedHasGroupInfos()) { + mGroupInfoPairs.Remove(aGroup); + } + } + } +} + +nsresult +QuotaManager::AcquireExclusiveAccess(const nsACString& aPattern, + Nullable aPersistenceType, + nsIOfflineStorage* aStorage, + AcquireListener* aListener, + WaitingOnStoragesCallback aCallback, + void* aClosure) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(aListener, "Need a listener!"); + + // Find the right SynchronizedOp. + SynchronizedOp* op = + FindSynchronizedOp(aPattern, aPersistenceType, + aStorage ? aStorage->Id() : EmptyCString()); + + NS_ASSERTION(op, "We didn't find a SynchronizedOp?"); + NS_ASSERTION(!op->mListener, "SynchronizedOp already has a listener?!?"); + + nsTArray > liveStorages; + + if (aStorage) { + // We need to wait for the storages to go away. + // Hold on to all storage objects that represent the same storage file + // (except the one that is requesting this version change). + + Client::Type clientType = aStorage->GetClient()->GetType(); + + StorageMatcher > matches; + matches.Find(mLiveStorages, aPattern, clientType); + + if (!matches.IsEmpty()) { + // Grab all storages that are not yet closed but whose storage id match + // the one we're looking for. + for (uint32_t index = 0; index < matches.Length(); index++) { + nsIOfflineStorage*& storage = matches[index]; + if (!storage->IsClosed() && + storage != aStorage && + storage->Id() == aStorage->Id()) { + liveStorages.AppendElement(storage); + } + } + } + + if (!liveStorages.IsEmpty()) { + NS_ASSERTION(op->mStorages[clientType].IsEmpty(), + "How do we already have storages here?"); + op->mStorages[clientType].AppendElements(liveStorages); + } + } + else { + StorageMatcher > matches; + if (aPattern.IsVoid()) { + matches.Find(mLiveStorages); + } + else { + matches.Find(mLiveStorages, aPattern); + } + + if (!matches.IsEmpty()) { + // We want *all* storages, even those that are closed, when we're going to + // clear the origin. + matches.AppendElementsTo(liveStorages); + + NS_ASSERTION(op->mStorages.IsEmpty(), + "How do we already have storages here?"); + matches.SwapElements(op->mStorages); + } + } + + op->mListener = aListener; + + if (!liveStorages.IsEmpty()) { + // Give our callback the storages so it can decide what to do with them. + aCallback(liveStorages, aClosure); + + NS_ASSERTION(liveStorages.IsEmpty(), + "Should have done something with the array!"); + + if (aStorage) { + // Wait for those storages to close. + return NS_OK; + } + } + + // If we're trying to open a storage and nothing blocks it, or if we're + // clearing an origin, then go ahead and schedule the op. + nsresult rv = RunSynchronizedOp(aStorage, op); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +QuotaManager::RunSynchronizedOp(nsIOfflineStorage* aStorage, + SynchronizedOp* aOp) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(aOp, "Null pointer!"); + NS_ASSERTION(aOp->mListener, "No listener on this op!"); + NS_ASSERTION(!aStorage || + aOp->mStorages[aStorage->GetClient()->GetType()].IsEmpty(), + "This op isn't ready to run!"); + + ArrayCluster storages; + + uint32_t startIndex; + uint32_t endIndex; + + if (aStorage) { + Client::Type clientType = aStorage->GetClient()->GetType(); + + storages[clientType].AppendElement(aStorage); + + startIndex = clientType; + endIndex = clientType + 1; + } + else { + aOp->mStorages.SwapElements(storages); + + startIndex = 0; + endIndex = Client::TYPE_MAX; + } + + nsRefPtr runnable = + new WaitForTransactionsToFinishRunnable(aOp); + + // Ask the file service to call us back when it's done with this storage. + FileService* service = FileService::Get(); + + if (service) { + // Have to copy here in case a transaction service needs a list too. + nsTArray > array; + + for (uint32_t index = startIndex; index < endIndex; index++) { + if (!storages[index].IsEmpty() && + mClients[index]->IsFileServiceUtilized()) { + array.AppendElements(storages[index]); + } + } + + if (!array.IsEmpty()) { + runnable->AddRun(); + + service->WaitForStoragesToComplete(array, runnable); + } + } + + // Ask each transaction service to call us back when they're done with this + // storage. + for (uint32_t index = startIndex; index < endIndex; index++) { + nsRefPtr& client = mClients[index]; + if (!storages[index].IsEmpty() && client->IsTransactionServiceActivated()) { + runnable->AddRun(); + + client->WaitForStoragesToComplete(storages[index], runnable); + } + } + + nsresult rv = runnable->Run(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +SynchronizedOp* +QuotaManager::FindSynchronizedOp(const nsACString& aPattern, + Nullable aPersistenceType, + const nsACString& aId) +{ + for (uint32_t index = 0; index < mSynchronizedOps.Length(); index++) { + const nsAutoPtr& currentOp = mSynchronizedOps[index]; + if (PatternMatchesOrigin(aPattern, currentOp->mOriginOrPattern) && + (currentOp->mPersistenceType.IsNull() || + currentOp->mPersistenceType == aPersistenceType) && + (currentOp->mId.IsEmpty() || currentOp->mId == aId)) { + return currentOp; + } + } + + return nullptr; +} + +nsresult +QuotaManager::ClearStoragesForApp(uint32_t aAppId, bool aBrowserOnly) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(aAppId != nsIScriptSecurityManager::UNKNOWN_APP_ID, + "Bad appId!"); + + // This only works from the main process. + NS_ENSURE_TRUE(IsMainProcess(), NS_ERROR_NOT_AVAILABLE); + + nsAutoCString pattern; + GetOriginPatternStringMaybeIgnoreBrowser(aAppId, aBrowserOnly, pattern); + + // Clear both temporary and persistent storages. + Nullable persistenceType; + + // If there is a pending or running clear operation for this app, return + // immediately. + if (IsClearOriginPending(pattern, persistenceType)) { + return NS_OK; + } + + OriginOrPatternString oops = OriginOrPatternString::FromPattern(pattern); + + // Queue up the origin clear runnable. + nsRefPtr runnable = + new OriginClearRunnable(oops, persistenceType); + + nsresult rv = + WaitForOpenAllowed(oops, persistenceType, EmptyCString(), runnable); + NS_ENSURE_SUCCESS(rv, rv); + + runnable->AdvanceState(); + + // Give the runnable some help by invalidating any storages in the way. + StorageMatcher > matches; + matches.Find(mLiveStorages, pattern); + + for (uint32_t index = 0; index < matches.Length(); index++) { + // We need to grab references here to prevent the storage from dying while + // we invalidate it. + nsCOMPtr storage = matches[index]; + storage->Invalidate(); + } + + return NS_OK; +} + +// static +PLDHashOperator +QuotaManager::GetOriginsExceedingGroupLimit(const nsACString& aKey, + GroupInfoPair* aValue, + void* aUserArg) +{ + NS_ASSERTION(!aKey.IsEmpty(), "Empty key!"); + NS_ASSERTION(aValue, "Null pointer!"); + + nsRefPtr groupInfo = + aValue->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY); + if (groupInfo) { + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "Shouldn't be null!"); + + if (groupInfo->mUsage > quotaManager->GetGroupLimit()) { + nsTArray* doomedOriginInfos = + static_cast*>(aUserArg); + + nsTArray >& originInfos = groupInfo->mOriginInfos; + originInfos.Sort(OriginInfoLRUComparator()); + + uint64_t usage = groupInfo->mUsage; + for (uint32_t i = 0; i < originInfos.Length(); i++) { + OriginInfo* originInfo = originInfos[i]; + + doomedOriginInfos->AppendElement(originInfo); + usage -= originInfo->mUsage; + + if (usage <= quotaManager->GetGroupLimit()) { + break; + } + } + } + } + + return PL_DHASH_NEXT; +} + +// static +PLDHashOperator +QuotaManager::GetAllTemporaryStorageOrigins(const nsACString& aKey, + GroupInfoPair* aValue, + void* aUserArg) +{ + NS_ASSERTION(!aKey.IsEmpty(), "Empty key!"); + NS_ASSERTION(aValue, "Null pointer!"); + + nsRefPtr groupInfo = + aValue->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY); + if (groupInfo) { + nsTArray* originInfos = + static_cast*>(aUserArg); + + originInfos->AppendElements(groupInfo->mOriginInfos); + } + + return PL_DHASH_NEXT; +} + +void +QuotaManager::CheckTemporaryStorageLimits() +{ + AssertIsOnIOThread(); + + nsTArray doomedOriginInfos; + { + MutexAutoLock lock(mQuotaMutex); + + mGroupInfoPairs.EnumerateRead(GetOriginsExceedingGroupLimit, + &doomedOriginInfos); + + uint64_t usage = 0; + for (uint32_t index = 0; index < doomedOriginInfos.Length(); index++) { + usage += doomedOriginInfos[index]->mUsage; + } + + if (mTemporaryStorageUsage - usage > mTemporaryStorageLimit) { + nsTArray originInfos; + + mGroupInfoPairs.EnumerateRead(GetAllTemporaryStorageOrigins, + &originInfos); + + for (uint32_t index = originInfos.Length(); index > 0; index--) { + if (doomedOriginInfos.Contains(originInfos[index - 1])) { + originInfos.RemoveElementAt(index - 1); + } + } + + originInfos.Sort(OriginInfoLRUComparator()); + + for (uint32_t i = 0; i < originInfos.Length(); i++) { + if (mTemporaryStorageUsage - usage <= mTemporaryStorageLimit) { + originInfos.TruncateLength(i); + break; + } + + usage += originInfos[i]->mUsage; + } + + doomedOriginInfos.AppendElements(originInfos); + } + } + + for (uint32_t index = 0; index < doomedOriginInfos.Length(); index++) { + DeleteTemporaryFilesForOrigin(doomedOriginInfos[index]->mOrigin); + } + + nsTArray doomedOrigins; + { + MutexAutoLock lock(mQuotaMutex); + + for (uint32_t index = 0; index < doomedOriginInfos.Length(); index++) { + OriginInfo* doomedOriginInfo = doomedOriginInfos[index]; + + nsCString group = doomedOriginInfo->mGroupInfo->mGroup; + nsCString origin = doomedOriginInfo->mOrigin; + LockedRemoveQuotaForOrigin(PERSISTENCE_TYPE_TEMPORARY, group, origin); + +#ifdef DEBUG + doomedOriginInfos[index] = nullptr; +#endif + + doomedOrigins.AppendElement(origin); + } + } + + for (uint32_t index = 0; index < doomedOrigins.Length(); index++) { + OriginClearCompleted( + PERSISTENCE_TYPE_TEMPORARY, + OriginOrPatternString::FromOrigin(doomedOrigins[index])); + } +} + +// static +PLDHashOperator +QuotaManager::AddTemporaryStorageOrigins( + const nsACString& aKey, + ArrayCluster* aValue, + void* aUserArg) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(!aKey.IsEmpty(), "Empty key!"); + NS_ASSERTION(aValue, "Null pointer!"); + NS_ASSERTION(aUserArg, "Null pointer!"); + + OriginCollection& collection = *static_cast(aUserArg); + + if (collection.ContainsOrigin(aKey)) { + return PL_DHASH_NEXT; + } + + for (uint32_t i = 0; i < Client::TYPE_MAX; i++) { + nsTArray& array = (*aValue)[i]; + for (uint32_t j = 0; j < array.Length(); j++) { + nsIOfflineStorage*& storage = array[j]; + if (storage->Type() == PERSISTENCE_TYPE_TEMPORARY) { + collection.AddOrigin(aKey); + return PL_DHASH_NEXT; + } + } + } + + return PL_DHASH_NEXT; +} + +// static +PLDHashOperator +QuotaManager::GetInactiveTemporaryStorageOrigins(const nsACString& aKey, + GroupInfoPair* aValue, + void* aUserArg) +{ + NS_ASSERTION(!aKey.IsEmpty(), "Empty key!"); + NS_ASSERTION(aValue, "Null pointer!"); + NS_ASSERTION(aUserArg, "Null pointer!"); + + nsRefPtr groupInfo = + aValue->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY); + if (groupInfo) { + InactiveOriginsInfo* info = static_cast(aUserArg); + + nsTArray >& originInfos = groupInfo->mOriginInfos; + + for (uint32_t i = 0; i < originInfos.Length(); i++) { + OriginInfo* originInfo = originInfos[i]; + + if (!info->collection.ContainsOrigin(originInfo->mOrigin)) { + NS_ASSERTION(!originInfo->mQuotaObjects.Count(), + "Inactive origin shouldn't have open files!"); + info->origins.AppendElement(originInfo); + } + } + } + + return PL_DHASH_NEXT; +} + +uint64_t +QuotaManager::CollectOriginsForEviction(uint64_t aMinSizeToBeFreed, + nsTArray& aOriginInfos) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + // Collect active origins first. + OriginCollection originCollection; + + // Add patterns and origins that have running or pending synchronized ops. + // (add patterns first to reduce redundancy in the origin collection). + uint32_t index; + for (index = 0; index < mSynchronizedOps.Length(); index++) { + nsAutoPtr& op = mSynchronizedOps[index]; + if (op->mPersistenceType.IsNull() || + op->mPersistenceType.Value() == PERSISTENCE_TYPE_TEMPORARY) { + if (op->mOriginOrPattern.IsPattern() && + !originCollection.ContainsPattern(op->mOriginOrPattern)) { + originCollection.AddPattern(op->mOriginOrPattern); + } + } + } + + for (index = 0; index < mSynchronizedOps.Length(); index++) { + nsAutoPtr& op = mSynchronizedOps[index]; + if (op->mPersistenceType.IsNull() || + op->mPersistenceType.Value() == PERSISTENCE_TYPE_TEMPORARY) { + if (op->mOriginOrPattern.IsOrigin() && + !originCollection.ContainsOrigin(op->mOriginOrPattern)) { + originCollection.AddOrigin(op->mOriginOrPattern); + } + } + } + + // Add origins that have live temporary storages. + mLiveStorages.EnumerateRead(AddTemporaryStorageOrigins, &originCollection); + + // Enumerate inactive origins. This must be protected by the mutex. + nsTArray inactiveOrigins; + { + InactiveOriginsInfo info(originCollection, inactiveOrigins); + MutexAutoLock lock(mQuotaMutex); + mGroupInfoPairs.EnumerateRead(GetInactiveTemporaryStorageOrigins, &info); + } + + // We now have a list of all inactive origins. So it's safe to sort the list + // and calculate available size without holding the lock. + + // Sort by the origin access time. + inactiveOrigins.Sort(OriginInfoLRUComparator()); + + // Create a list of inactive and the least recently used origins + // whose aggregate size is greater or equals the minimal size to be freed. + uint64_t sizeToBeFreed = 0; + for(index = 0; index < inactiveOrigins.Length(); index++) { + if (sizeToBeFreed >= aMinSizeToBeFreed) { + inactiveOrigins.TruncateLength(index); + break; + } + + sizeToBeFreed += inactiveOrigins[index]->mUsage; + } + + if (sizeToBeFreed >= aMinSizeToBeFreed) { + // Success, add synchronized ops for these origins, so any other + // operations for them will be delayed (until origin eviction is finalized). + + for(index = 0; index < inactiveOrigins.Length(); index++) { + OriginOrPatternString oops = + OriginOrPatternString::FromOrigin(inactiveOrigins[index]->mOrigin); + + AddSynchronizedOp(oops, + Nullable(PERSISTENCE_TYPE_TEMPORARY)); + } + + inactiveOrigins.SwapElements(aOriginInfos); + return sizeToBeFreed; + } + + return 0; +} + +void +QuotaManager::DeleteTemporaryFilesForOrigin(const nsACString& aOrigin) +{ + nsCOMPtr directory; + nsresult rv = GetDirectoryForOrigin(PERSISTENCE_TYPE_TEMPORARY, aOrigin, + getter_AddRefs(directory)); + NS_ENSURE_SUCCESS_VOID(rv); + + rv = directory->Remove(true); + if (rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST && + rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) { + // This should never fail if we've closed all storage connections + // correctly... + NS_ERROR("Failed to remove directory!"); + } +} + +void +QuotaManager::FinalizeOriginEviction(nsTArray& aOrigins) +{ + NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!"); + + nsRefPtr runnable = + new FinalizeOriginEvictionRunnable(aOrigins); + + nsresult rv = IsOnIOThread() ? runnable->RunImmediately() + : runnable->Dispatch(); + NS_ENSURE_SUCCESS_VOID(rv); +} + +void +QuotaManager::SaveOriginAccessTime(const nsACString& aOrigin, + int64_t aTimestamp) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + if (QuotaManager::IsShuttingDown()) { + return; + } + + nsRefPtr runnable = + new SaveOriginAccessTimeRunnable(aOrigin, aTimestamp); + + if (NS_FAILED(mIOThread->Dispatch(runnable, NS_DISPATCH_NORMAL))) { + NS_WARNING("Failed to dispatch runnable!"); + } +} + +void +QuotaManager::GetOriginPatternString(uint32_t aAppId, + MozBrowserPatternFlag aBrowserFlag, + const nsACString& aOrigin, + nsAutoCString& _retval) +{ + NS_ASSERTION(aAppId != nsIScriptSecurityManager::UNKNOWN_APP_ID, + "Bad appId!"); + NS_ASSERTION(aOrigin.IsEmpty() || aBrowserFlag != IgnoreMozBrowser, + "Bad args!"); + + if (aOrigin.IsEmpty()) { + _retval.Truncate(); + + _retval.AppendInt(aAppId); + _retval.Append('+'); + + if (aBrowserFlag != IgnoreMozBrowser) { + if (aBrowserFlag == MozBrowser) { + _retval.Append('t'); + } + else { + _retval.Append('f'); + } + _retval.Append('+'); + } + + return; + } + +#ifdef DEBUG + if (aAppId != nsIScriptSecurityManager::NO_APP_ID || + aBrowserFlag == MozBrowser) { + nsAutoCString pattern; + GetOriginPatternString(aAppId, aBrowserFlag, EmptyCString(), pattern); + NS_ASSERTION(PatternMatchesOrigin(pattern, aOrigin), + "Origin doesn't match parameters!"); + } +#endif + + _retval = aOrigin; +} + +SynchronizedOp::SynchronizedOp(const OriginOrPatternString& aOriginOrPattern, + Nullable aPersistenceType, + const nsACString& aId) +: mOriginOrPattern(aOriginOrPattern), mPersistenceType(aPersistenceType), + mId(aId) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + MOZ_COUNT_CTOR(SynchronizedOp); +} + +SynchronizedOp::~SynchronizedOp() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + MOZ_COUNT_DTOR(SynchronizedOp); +} + +bool +SynchronizedOp::MustWaitFor(const SynchronizedOp& aExistingOp) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + if (aExistingOp.mOriginOrPattern.IsNull() || mOriginOrPattern.IsNull()) { + return true; + } + + bool match; + + if (aExistingOp.mOriginOrPattern.IsOrigin()) { + if (mOriginOrPattern.IsOrigin()) { + match = aExistingOp.mOriginOrPattern.Equals(mOriginOrPattern); + } + else { + match = PatternMatchesOrigin(mOriginOrPattern, aExistingOp.mOriginOrPattern); + } + } + else if (mOriginOrPattern.IsOrigin()) { + match = PatternMatchesOrigin(aExistingOp.mOriginOrPattern, mOriginOrPattern); + } + else { + match = PatternMatchesOrigin(mOriginOrPattern, aExistingOp.mOriginOrPattern) || + PatternMatchesOrigin(aExistingOp.mOriginOrPattern, mOriginOrPattern); + } + + // If the origins don't match, the second can proceed. + if (!match) { + return false; + } + + // If the origins match but the persistence types are different, the second + // can proceed. + if (!aExistingOp.mPersistenceType.IsNull() && !mPersistenceType.IsNull() && + aExistingOp.mPersistenceType.Value() != mPersistenceType.Value()) { + return false; + } + + // If the origins and the ids match, the second must wait. + if (aExistingOp.mId == mId) { + return true; + } + + // Waiting is required if either one corresponds to an origin clearing + // (an empty Id). + if (aExistingOp.mId.IsEmpty() || mId.IsEmpty()) { + return true; + } + + // Otherwise, things for the same origin but different storages can proceed + // independently. + return false; +} + +void +SynchronizedOp::DelayRunnable(nsIRunnable* aRunnable) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(mDelayedRunnables.IsEmpty() || mId.IsEmpty(), + "Only ClearOrigin operations can delay multiple runnables!"); + + mDelayedRunnables.AppendElement(aRunnable); +} + +void +SynchronizedOp::DispatchDelayedRunnables() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(!mListener, "Any listener should be gone by now!"); + + uint32_t count = mDelayedRunnables.Length(); + for (uint32_t index = 0; index < count; index++) { + NS_DispatchToCurrentThread(mDelayedRunnables[index]); + } + + mDelayedRunnables.Clear(); +} + +CollectOriginsHelper::CollectOriginsHelper(mozilla::Mutex& aMutex, + uint64_t aMinSizeToBeFreed) +: mMinSizeToBeFreed(aMinSizeToBeFreed), + mMutex(aMutex), + mCondVar(aMutex, "CollectOriginsHelper::mCondVar"), + mSizeToBeFreed(0), + mWaiting(true) +{ + MOZ_ASSERT(!NS_IsMainThread(), "Wrong thread!"); + mMutex.AssertCurrentThreadOwns(); +} + +int64_t +CollectOriginsHelper::BlockAndReturnOriginsForEviction( + nsTArray& aOriginInfos) +{ + MOZ_ASSERT(!NS_IsMainThread(), "Wrong thread!"); + mMutex.AssertCurrentThreadOwns(); + + while (mWaiting) { + mCondVar.Wait(); + } + + mOriginInfos.SwapElements(aOriginInfos); + return mSizeToBeFreed; +} + +NS_IMETHODIMP +CollectOriginsHelper::Run() +{ + MOZ_ASSERT(NS_IsMainThread(), "Wrong thread!"); + + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "Shouldn't be null!"); + + // We use extra stack vars here to avoid race detector warnings (the same + // memory accessed with and without the lock held). + nsTArray originInfos; + uint64_t sizeToBeFreed = + quotaManager->CollectOriginsForEviction(mMinSizeToBeFreed, originInfos); + + MutexAutoLock lock(mMutex); + + NS_ASSERTION(mWaiting, "Huh?!"); + + mOriginInfos.SwapElements(originInfos); + mSizeToBeFreed = sizeToBeFreed; + mWaiting = false; + mCondVar.Notify(); + + return NS_OK; +} + +nsresult +OriginClearRunnable::OnExclusiveAccessAcquired() +{ + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "This should never fail!"); + + nsresult rv = quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +// static +void +OriginClearRunnable::InvalidateOpenedStorages( + nsTArray >& aStorages, + void* aClosure) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + nsTArray > storages; + storages.SwapElements(aStorages); + + for (uint32_t index = 0; index < storages.Length(); index++) { + storages[index]->Invalidate(); + } +} + +void +OriginClearRunnable::DeleteFiles(QuotaManager* aQuotaManager, + PersistenceType aPersistenceType) +{ + AssertIsOnIOThread(); + NS_ASSERTION(aQuotaManager, "Don't pass me null!"); + + nsresult rv; + + nsCOMPtr directory = + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS_VOID(rv); + + rv = directory->InitWithPath(aQuotaManager->GetStoragePath(aPersistenceType)); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr entries; + if (NS_FAILED(directory->GetDirectoryEntries(getter_AddRefs(entries))) || + !entries) { + return; + } + + nsCString originSanitized(mOriginOrPattern); + SanitizeOriginString(originSanitized); + + bool hasMore; + while (NS_SUCCEEDED((rv = entries->HasMoreElements(&hasMore))) && hasMore) { + nsCOMPtr entry; + rv = entries->GetNext(getter_AddRefs(entry)); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr file = do_QueryInterface(entry); + NS_ASSERTION(file, "Don't know what this is!"); + + bool isDirectory; + rv = file->IsDirectory(&isDirectory); + NS_ENSURE_SUCCESS_VOID(rv); + + if (!isDirectory) { + NS_WARNING("Something in the IndexedDB directory that doesn't belong!"); + continue; + } + + nsString leafName; + rv = file->GetLeafName(leafName); + NS_ENSURE_SUCCESS_VOID(rv); + + // Skip storages for other apps. + if (!PatternMatchesOrigin(originSanitized, + NS_ConvertUTF16toUTF8(leafName))) { + continue; + } + + if (NS_FAILED(file->Remove(true))) { + // This should never fail if we've closed all storage connections + // correctly... + NS_ERROR("Failed to remove directory!"); + } + } + + aQuotaManager->RemoveQuotaForPattern(aPersistenceType, mOriginOrPattern); + + aQuotaManager->OriginClearCompleted(aPersistenceType, mOriginOrPattern); +} + +NS_IMPL_ISUPPORTS_INHERITED0(OriginClearRunnable, nsRunnable) + +NS_IMETHODIMP +OriginClearRunnable::Run() +{ + PROFILER_LABEL("Quota", "OriginClearRunnable::Run"); + + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "This should never fail!"); + + switch (mCallbackState) { + case Pending: { + NS_NOTREACHED("Should never get here without being dispatched!"); + return NS_ERROR_UNEXPECTED; + } + + case OpenAllowed: { + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + AdvanceState(); + + // Now we have to wait until the thread pool is done with all of the + // storages we care about. + nsresult rv = + quotaManager->AcquireExclusiveAccess(mOriginOrPattern, mPersistenceType, + this, InvalidateOpenedStorages, + nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + case IO: { + AssertIsOnIOThread(); + + AdvanceState(); + + if (mPersistenceType.IsNull()) { + DeleteFiles(quotaManager, PERSISTENCE_TYPE_PERSISTENT); + DeleteFiles(quotaManager, PERSISTENCE_TYPE_TEMPORARY); + } else { + DeleteFiles(quotaManager, mPersistenceType.Value()); + } + + // Now dispatch back to the main thread. + if (NS_FAILED(NS_DispatchToMainThread(this, NS_DISPATCH_NORMAL))) { + NS_WARNING("Failed to dispatch to main thread!"); + return NS_ERROR_FAILURE; + } + + return NS_OK; + } + + case Complete: { + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + // Tell the QuotaManager that we're done. + quotaManager->AllowNextSynchronizedOp(mOriginOrPattern, mPersistenceType, + EmptyCString()); + + return NS_OK; + } + + default: + NS_ERROR("Unknown state value!"); + return NS_ERROR_UNEXPECTED; + } + + NS_NOTREACHED("Should never get here!"); + return NS_ERROR_UNEXPECTED; +} + +AsyncUsageRunnable::AsyncUsageRunnable(uint32_t aAppId, + bool aInMozBrowserOnly, + const nsACString& aGroup, + const OriginOrPatternString& aOrigin, + nsIURI* aURI, + nsIUsageCallback* aCallback) +: mURI(aURI), + mCallback(aCallback), + mAppId(aAppId), + mGroup(aGroup), + mOrigin(aOrigin), + mCallbackState(Pending), + mInMozBrowserOnly(aInMozBrowserOnly) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(aURI, "Null pointer!"); + NS_ASSERTION(!aGroup.IsEmpty(), "Empty group!"); + NS_ASSERTION(aOrigin.IsOrigin(), "Expect origin only here!"); + NS_ASSERTION(!aOrigin.IsEmpty(), "Empty origin!"); + NS_ASSERTION(aCallback, "Null pointer!"); +} + +nsresult +AsyncUsageRunnable::TakeShortcut() +{ + NS_ASSERTION(mCallbackState == Pending, "Huh?"); + + nsresult rv = NS_DispatchToCurrentThread(this); + NS_ENSURE_SUCCESS(rv, rv); + + mCallbackState = Shortcut; + return NS_OK; +} + +nsresult +AsyncUsageRunnable::RunInternal() +{ + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "This should never fail!"); + + nsresult rv; + + switch (mCallbackState) { + case Pending: { + NS_NOTREACHED("Should never get here without being dispatched!"); + return NS_ERROR_UNEXPECTED; + } + + case OpenAllowed: { + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + AdvanceState(); + + rv = quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to dispatch to the IO thread!"); + } + + return NS_OK; + } + + case IO: { + AssertIsOnIOThread(); + + AdvanceState(); + + // Add all the persistent storage files we care about. + rv = AddToUsage(quotaManager, PERSISTENCE_TYPE_PERSISTENT); + NS_ENSURE_SUCCESS(rv, rv); + + // Add all the temporary storage files we care about. + rv = AddToUsage(quotaManager, PERSISTENCE_TYPE_TEMPORARY); + NS_ENSURE_SUCCESS(rv, rv); + + // Run dispatches us back to the main thread. + return NS_OK; + } + + case Complete: // Fall through + case Shortcut: { + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + // Call the callback unless we were canceled. + if (!mCanceled) { + mCallback->OnUsageResult(mURI, TotalUsage(), FileUsage(), mAppId, + mInMozBrowserOnly); + } + + // Clean up. + mURI = nullptr; + mCallback = nullptr; + + // And tell the QuotaManager that we're done. + if (mCallbackState == Complete) { + quotaManager->AllowNextSynchronizedOp(mOrigin, + Nullable(), + EmptyCString()); + } + + return NS_OK; + } + + default: + NS_ERROR("Unknown state value!"); + return NS_ERROR_UNEXPECTED; + } + + NS_NOTREACHED("Should never get here!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult +AsyncUsageRunnable::AddToUsage(QuotaManager* aQuotaManager, + PersistenceType aPersistenceType) +{ + AssertIsOnIOThread(); + + nsCOMPtr directory; + nsresult rv = aQuotaManager->GetDirectoryForOrigin(aPersistenceType, mOrigin, + getter_AddRefs(directory)); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists; + rv = directory->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + // If the directory exists then enumerate all the files inside, adding up + // the sizes to get the final usage statistic. + if (exists && !mCanceled) { + bool initialized; + + if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + initialized = aQuotaManager->mInitializedOrigins.Contains(mOrigin); + + if (!initialized) { + rv = MaybeUpgradeOriginDirectory(directory); + NS_ENSURE_SUCCESS(rv, rv); + } + } + else { + NS_ASSERTION(aPersistenceType == PERSISTENCE_TYPE_TEMPORARY, "Huh?"); + initialized = aQuotaManager->mTemporaryStorageInitialized; + } + + nsCOMPtr entries; + rv = directory->GetDirectoryEntries(getter_AddRefs(entries)); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasMore; + while (NS_SUCCEEDED((rv = entries->HasMoreElements(&hasMore))) && + hasMore && !mCanceled) { + nsCOMPtr entry; + rv = entries->GetNext(getter_AddRefs(entry)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr file = do_QueryInterface(entry); + NS_ENSURE_TRUE(file, NS_NOINTERFACE); + + nsString leafName; + rv = file->GetLeafName(leafName); + NS_ENSURE_SUCCESS(rv, rv); + + if (leafName.EqualsLiteral(METADATA_FILE_NAME) || + leafName.EqualsLiteral(DSSTORE_FILE_NAME)) { + continue; + } + + if (!initialized) { + bool isDirectory; + rv = file->IsDirectory(&isDirectory); + NS_ENSURE_SUCCESS(rv, rv); + + if (!isDirectory) { + NS_WARNING("Unknown file found!"); + return NS_ERROR_UNEXPECTED; + } + } + + Client::Type clientType; + rv = Client::TypeFromText(leafName, clientType); + if (NS_FAILED(rv)) { + NS_WARNING("Unknown directory found!"); + if (!initialized) { + return NS_ERROR_UNEXPECTED; + } + continue; + } + + nsRefPtr& client = aQuotaManager->mClients[clientType]; + + if (initialized) { + rv = client->GetUsageForOrigin(aPersistenceType, mGroup, mOrigin, this); + } + else { + rv = client->InitOrigin(aPersistenceType, mGroup, mOrigin, this); + } + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return NS_OK; +} + +NS_IMPL_ISUPPORTS_INHERITED(AsyncUsageRunnable, nsRunnable, nsIQuotaRequest) + +NS_IMETHODIMP +AsyncUsageRunnable::Run() +{ + PROFILER_LABEL("Quota", "AsyncUsageRunnable::Run"); + + nsresult rv = RunInternal(); + + if (!NS_IsMainThread()) { + if (NS_FAILED(rv)) { + ResetUsage(); + } + + if (NS_FAILED(NS_DispatchToMainThread(this, NS_DISPATCH_NORMAL))) { + NS_WARNING("Failed to dispatch to main thread!"); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +AsyncUsageRunnable::Cancel() +{ + if (mCanceled.exchange(true)) { + NS_WARNING("Canceled more than once?!"); + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +nsresult +ResetOrClearRunnable::OnExclusiveAccessAcquired() +{ + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "This should never fail!"); + + nsresult rv = quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +// static +void +ResetOrClearRunnable::InvalidateOpenedStorages( + nsTArray >& aStorages, + void* aClosure) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + nsTArray > storages; + storages.SwapElements(aStorages); + + for (uint32_t index = 0; index < storages.Length(); index++) { + storages[index]->Invalidate(); + } +} + +void +ResetOrClearRunnable::DeleteFiles(QuotaManager* aQuotaManager, + PersistenceType aPersistenceType) +{ + AssertIsOnIOThread(); + NS_ASSERTION(aQuotaManager, "Don't pass me null!"); + + nsresult rv; + + nsCOMPtr directory = + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS_VOID(rv); + + rv = directory->InitWithPath(aQuotaManager->GetStoragePath(aPersistenceType)); + NS_ENSURE_SUCCESS_VOID(rv); + + rv = directory->Remove(true); + if (rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST && + rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) { + // This should never fail if we've closed all storage connections + // correctly... + NS_ERROR("Failed to remove directory!"); + } +} + +NS_IMPL_ISUPPORTS_INHERITED0(ResetOrClearRunnable, nsRunnable) + +NS_IMETHODIMP +ResetOrClearRunnable::Run() +{ + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "This should never fail!"); + + switch (mCallbackState) { + case Pending: { + NS_NOTREACHED("Should never get here without being dispatched!"); + return NS_ERROR_UNEXPECTED; + } + + case OpenAllowed: { + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + AdvanceState(); + + // Now we have to wait until the thread pool is done with all of the + // storages we care about. + nsresult rv = + quotaManager->AcquireExclusiveAccess(NullCString(), + Nullable(), this, + InvalidateOpenedStorages, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + case IO: { + AssertIsOnIOThread(); + + AdvanceState(); + + if (mClear) { + DeleteFiles(quotaManager, PERSISTENCE_TYPE_PERSISTENT); + DeleteFiles(quotaManager, PERSISTENCE_TYPE_TEMPORARY); + } + + quotaManager->RemoveQuota(); + quotaManager->ResetOrClearCompleted(); + + // Now dispatch back to the main thread. + if (NS_FAILED(NS_DispatchToMainThread(this, NS_DISPATCH_NORMAL))) { + NS_WARNING("Failed to dispatch to main thread!"); + return NS_ERROR_FAILURE; + } + + return NS_OK; + } + + case Complete: { + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + // Tell the QuotaManager that we're done. + quotaManager->AllowNextSynchronizedOp(OriginOrPatternString::FromNull(), + Nullable(), + EmptyCString()); + + return NS_OK; + } + + default: + NS_ERROR("Unknown state value!"); + return NS_ERROR_UNEXPECTED; + } + + NS_NOTREACHED("Should never get here!"); + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP +FinalizeOriginEvictionRunnable::Run() +{ + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "This should never fail!"); + + nsresult rv; + + switch (mCallbackState) { + case Pending: { + NS_NOTREACHED("Should never get here without being dispatched!"); + return NS_ERROR_UNEXPECTED; + } + + case OpenAllowed: { + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + AdvanceState(); + + rv = quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to dispatch to the IO thread!"); + } + + return NS_OK; + } + + case IO: { + AssertIsOnIOThread(); + + AdvanceState(); + + for (uint32_t index = 0; index < mOrigins.Length(); index++) { + quotaManager->OriginClearCompleted( + PERSISTENCE_TYPE_TEMPORARY, + OriginOrPatternString::FromOrigin(mOrigins[index])); + } + + if (NS_FAILED(NS_DispatchToMainThread(this, NS_DISPATCH_NORMAL))) { + NS_WARNING("Failed to dispatch to main thread!"); + return NS_ERROR_FAILURE; + } + + return NS_OK; + } + + case Complete: { + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + for (uint32_t index = 0; index < mOrigins.Length(); index++) { + quotaManager->AllowNextSynchronizedOp( + OriginOrPatternString::FromOrigin(mOrigins[index]), + Nullable(PERSISTENCE_TYPE_TEMPORARY), + EmptyCString()); + } + + return NS_OK; + } + + default: + NS_ERROR("Unknown state value!"); + return NS_ERROR_UNEXPECTED; + } + + NS_NOTREACHED("Should never get here!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult +FinalizeOriginEvictionRunnable::Dispatch() +{ + NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(mCallbackState == Pending, "Huh?"); + + mCallbackState = OpenAllowed; + return NS_DispatchToMainThread(this); +} + +nsresult +FinalizeOriginEvictionRunnable::RunImmediately() +{ + AssertIsOnIOThread(); + NS_ASSERTION(mCallbackState == Pending, "Huh?"); + + mCallbackState = IO; + return this->Run(); +} + +NS_IMETHODIMP +WaitForTransactionsToFinishRunnable::Run() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(mOp, "Null op!"); + NS_ASSERTION(mOp->mListener, "Nothing to run!"); + NS_ASSERTION(mCountdown, "Wrong countdown!"); + + if (--mCountdown) { + return NS_OK; + } + + // Don't hold the listener alive longer than necessary. + nsRefPtr listener; + listener.swap(mOp->mListener); + + mOp = nullptr; + + nsresult rv = listener->OnExclusiveAccessAcquired(); + NS_ENSURE_SUCCESS(rv, rv); + + // The listener is responsible for calling + // QuotaManager::AllowNextSynchronizedOp. + return NS_OK; +} + +NS_IMETHODIMP +WaitForLockedFilesToFinishRunnable::Run() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + mBusy = false; + + return NS_OK; +} + +NS_IMETHODIMP +SaveOriginAccessTimeRunnable::Run() +{ + AssertIsOnIOThread(); + + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "This should never fail!"); + + nsCOMPtr directory; + nsresult rv = + quotaManager->GetDirectoryForOrigin(PERSISTENCE_TYPE_TEMPORARY, mOrigin, + getter_AddRefs(directory)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr stream; + rv = GetDirectoryMetadataStream(directory, true, getter_AddRefs(stream)); + NS_ENSURE_SUCCESS(rv, rv); + + // The origin directory may not exist anymore. + if (stream) { + rv = stream->Write64(mTimestamp); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +}