diff options
Diffstat (limited to 'netwerk/cache/nsDiskCacheDeviceSQL.cpp')
-rw-r--r-- | netwerk/cache/nsDiskCacheDeviceSQL.cpp | 2906 |
1 files changed, 2906 insertions, 0 deletions
diff --git a/netwerk/cache/nsDiskCacheDeviceSQL.cpp b/netwerk/cache/nsDiskCacheDeviceSQL.cpp new file mode 100644 index 000000000..56ece5887 --- /dev/null +++ b/netwerk/cache/nsDiskCacheDeviceSQL.cpp @@ -0,0 +1,2906 @@ +/* -*- Mode: C++; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cin: */ +/* 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 <inttypes.h> + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Attributes.h" +#include "mozilla/Sprintf.h" +#include "mozilla/ThreadLocal.h" + +#include "nsCache.h" +#include "nsDiskCache.h" +#include "nsDiskCacheDeviceSQL.h" +#include "nsCacheService.h" +#include "nsApplicationCache.h" + +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsIURI.h" +#include "nsAutoPtr.h" +#include "nsEscape.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsString.h" +#include "nsPrintfCString.h" +#include "nsCRT.h" +#include "nsArrayUtils.h" +#include "nsIArray.h" +#include "nsIVariant.h" +#include "nsILoadContextInfo.h" +#include "nsThreadUtils.h" +#include "nsISerializable.h" +#include "nsIInputStream.h" +#include "nsIOutputStream.h" +#include "nsSerializationHelper.h" + +#include "mozIStorageService.h" +#include "mozIStorageStatement.h" +#include "mozIStorageFunction.h" +#include "mozStorageHelper.h" + +#include "nsICacheVisitor.h" +#include "nsISeekableStream.h" + +#include "mozilla/Telemetry.h" + +#include "sqlite3.h" +#include "mozilla/storage.h" +#include "nsVariant.h" +#include "mozilla/BasePrincipal.h" + +using namespace mozilla; +using namespace mozilla::storage; +using mozilla::NeckoOriginAttributes; + +static const char OFFLINE_CACHE_DEVICE_ID[] = { "offline" }; + +#define LOG(args) CACHE_LOG_DEBUG(args) + +static uint32_t gNextTemporaryClientID = 0; + +/***************************************************************************** + * helpers + */ + +static nsresult +EnsureDir(nsIFile *dir) +{ + bool exists; + nsresult rv = dir->Exists(&exists); + if (NS_SUCCEEDED(rv) && !exists) + rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0700); + return rv; +} + +static bool +DecomposeCacheEntryKey(const nsCString *fullKey, + const char **cid, + const char **key, + nsCString &buf) +{ + buf = *fullKey; + + int32_t colon = buf.FindChar(':'); + if (colon == kNotFound) + { + NS_ERROR("Invalid key"); + return false; + } + buf.SetCharAt('\0', colon); + + *cid = buf.get(); + *key = buf.get() + colon + 1; + + return true; +} + +class AutoResetStatement +{ + public: + explicit AutoResetStatement(mozIStorageStatement *s) + : mStatement(s) {} + ~AutoResetStatement() { mStatement->Reset(); } + mozIStorageStatement *operator->() MOZ_NO_ADDREF_RELEASE_ON_RETURN { return mStatement; } + private: + mozIStorageStatement *mStatement; +}; + +class EvictionObserver +{ + public: + EvictionObserver(mozIStorageConnection *db, + nsOfflineCacheEvictionFunction *evictionFunction) + : mDB(db), mEvictionFunction(evictionFunction) + { + mEvictionFunction->Init(); + mDB->ExecuteSimpleSQL( + NS_LITERAL_CSTRING("CREATE TEMP TRIGGER cache_on_delete BEFORE DELETE" + " ON moz_cache FOR EACH ROW BEGIN SELECT" + " cache_eviction_observer(" + " OLD.ClientID, OLD.key, OLD.generation);" + " END;")); + } + + ~EvictionObserver() + { + mDB->ExecuteSimpleSQL( + NS_LITERAL_CSTRING("DROP TRIGGER cache_on_delete;")); + mEvictionFunction->Reset(); + } + + void Apply() { return mEvictionFunction->Apply(); } + + private: + mozIStorageConnection *mDB; + RefPtr<nsOfflineCacheEvictionFunction> mEvictionFunction; +}; + +#define DCACHE_HASH_MAX INT64_MAX +#define DCACHE_HASH_BITS 64 + +/** + * nsOfflineCache::Hash(const char * key) + * + * This algorithm of this method implies nsOfflineCacheRecords will be stored + * in a certain order on disk. If the algorithm changes, existing cache + * map files may become invalid, and therefore the kCurrentVersion needs + * to be revised. + */ +static uint64_t +DCacheHash(const char * key) +{ + // initval 0x7416f295 was chosen randomly + return (uint64_t(nsDiskCache::Hash(key, 0)) << 32) | nsDiskCache::Hash(key, 0x7416f295); +} + +/****************************************************************************** + * nsOfflineCacheEvictionFunction + */ + +NS_IMPL_ISUPPORTS(nsOfflineCacheEvictionFunction, mozIStorageFunction) + +// helper function for directly exposing the same data file binding +// path algorithm used in nsOfflineCacheBinding::Create +static nsresult +GetCacheDataFile(nsIFile *cacheDir, const char *key, + int generation, nsCOMPtr<nsIFile> &file) +{ + cacheDir->Clone(getter_AddRefs(file)); + if (!file) + return NS_ERROR_OUT_OF_MEMORY; + + uint64_t hash = DCacheHash(key); + + uint32_t dir1 = (uint32_t) (hash & 0x0F); + uint32_t dir2 = (uint32_t)((hash & 0xF0) >> 4); + + hash >>= 8; + + file->AppendNative(nsPrintfCString("%X", dir1)); + file->AppendNative(nsPrintfCString("%X", dir2)); + + char leaf[64]; + SprintfLiteral(leaf, "%014" PRIX64 "-%X", hash, generation); + return file->AppendNative(nsDependentCString(leaf)); +} + +namespace appcachedetail { + +typedef nsCOMArray<nsIFile> FileArray; +static MOZ_THREAD_LOCAL(FileArray*) tlsEvictionItems; + +} // appcachedetail + +NS_IMETHODIMP +nsOfflineCacheEvictionFunction::OnFunctionCall(mozIStorageValueArray *values, nsIVariant **_retval) +{ + LOG(("nsOfflineCacheEvictionFunction::OnFunctionCall\n")); + + *_retval = nullptr; + + uint32_t numEntries; + nsresult rv = values->GetNumEntries(&numEntries); + NS_ENSURE_SUCCESS(rv, rv); + NS_ASSERTION(numEntries == 3, "unexpected number of arguments"); + + uint32_t valueLen; + const char *clientID = values->AsSharedUTF8String(0, &valueLen); + const char *key = values->AsSharedUTF8String(1, &valueLen); + nsAutoCString fullKey(clientID); + fullKey.Append(':'); + fullKey.Append(key); + int generation = values->AsInt32(2); + + // If the key is currently locked, refuse to delete this row. + if (mDevice->IsLocked(fullKey)) { + NS_ADDREF(*_retval = new IntegerVariant(SQLITE_IGNORE)); + return NS_OK; + } + + nsCOMPtr<nsIFile> file; + rv = GetCacheDataFile(mDevice->CacheDirectory(), key, + generation, file); + if (NS_FAILED(rv)) + { + LOG(("GetCacheDataFile [key=%s generation=%d] failed [rv=%x]!\n", + key, generation, rv)); + return rv; + } + + appcachedetail::FileArray* items = appcachedetail::tlsEvictionItems.get(); + MOZ_ASSERT(items); + if (items) { + items->AppendObject(file); + } + + return NS_OK; +} + +nsOfflineCacheEvictionFunction::nsOfflineCacheEvictionFunction(nsOfflineCacheDevice * device) + : mDevice(device) +{ + mTLSInited = appcachedetail::tlsEvictionItems.init(); +} + +void nsOfflineCacheEvictionFunction::Init() +{ + if (mTLSInited) { + appcachedetail::tlsEvictionItems.set(new appcachedetail::FileArray()); + } +} + +void nsOfflineCacheEvictionFunction::Reset() +{ + if (!mTLSInited) { + return; + } + + appcachedetail::FileArray* items = appcachedetail::tlsEvictionItems.get(); + if (!items) { + return; + } + + appcachedetail::tlsEvictionItems.set(nullptr); + delete items; +} + +void +nsOfflineCacheEvictionFunction::Apply() +{ + LOG(("nsOfflineCacheEvictionFunction::Apply\n")); + + if (!mTLSInited) { + return; + } + + appcachedetail::FileArray* pitems = appcachedetail::tlsEvictionItems.get(); + if (!pitems) { + return; + } + + appcachedetail::FileArray items; + items.SwapElements(*pitems); + + for (int32_t i = 0; i < items.Count(); i++) { + if (MOZ_LOG_TEST(gCacheLog, LogLevel::Debug)) { + nsAutoCString path; + items[i]->GetNativePath(path); + LOG((" removing %s\n", path.get())); + } + + items[i]->Remove(false); + } +} + +class nsOfflineCacheDiscardCache : public Runnable +{ +public: + nsOfflineCacheDiscardCache(nsOfflineCacheDevice *device, + nsCString &group, + nsCString &clientID) + : mDevice(device) + , mGroup(group) + , mClientID(clientID) + { + } + + NS_IMETHOD Run() override + { + if (mDevice->IsActiveCache(mGroup, mClientID)) + { + mDevice->DeactivateGroup(mGroup); + } + + return mDevice->EvictEntries(mClientID.get()); + } + +private: + RefPtr<nsOfflineCacheDevice> mDevice; + nsCString mGroup; + nsCString mClientID; +}; + +/****************************************************************************** + * nsOfflineCacheDeviceInfo + */ + +class nsOfflineCacheDeviceInfo final : public nsICacheDeviceInfo +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSICACHEDEVICEINFO + + explicit nsOfflineCacheDeviceInfo(nsOfflineCacheDevice* device) + : mDevice(device) + {} + +private: + ~nsOfflineCacheDeviceInfo() {} + + nsOfflineCacheDevice* mDevice; +}; + +NS_IMPL_ISUPPORTS(nsOfflineCacheDeviceInfo, nsICacheDeviceInfo) + +NS_IMETHODIMP +nsOfflineCacheDeviceInfo::GetDescription(char **aDescription) +{ + *aDescription = NS_strdup("Offline cache device"); + return *aDescription ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +NS_IMETHODIMP +nsOfflineCacheDeviceInfo::GetUsageReport(char ** usageReport) +{ + nsAutoCString buffer; + buffer.AssignLiteral(" <tr>\n" + " <th>Cache Directory:</th>\n" + " <td>"); + nsIFile *cacheDir = mDevice->CacheDirectory(); + if (!cacheDir) + return NS_OK; + + nsAutoString path; + nsresult rv = cacheDir->GetPath(path); + if (NS_SUCCEEDED(rv)) + AppendUTF16toUTF8(path, buffer); + else + buffer.AppendLiteral("directory unavailable"); + + buffer.AppendLiteral("</td>\n" + " </tr>\n"); + + *usageReport = ToNewCString(buffer); + if (!*usageReport) + return NS_ERROR_OUT_OF_MEMORY; + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheDeviceInfo::GetEntryCount(uint32_t *aEntryCount) +{ + *aEntryCount = mDevice->EntryCount(); + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheDeviceInfo::GetTotalSize(uint32_t *aTotalSize) +{ + *aTotalSize = mDevice->CacheSize(); + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheDeviceInfo::GetMaximumSize(uint32_t *aMaximumSize) +{ + *aMaximumSize = mDevice->CacheCapacity(); + return NS_OK; +} + +/****************************************************************************** + * nsOfflineCacheBinding + */ + +class nsOfflineCacheBinding final : public nsISupports +{ + ~nsOfflineCacheBinding() {} + +public: + NS_DECL_THREADSAFE_ISUPPORTS + + static nsOfflineCacheBinding * + Create(nsIFile *cacheDir, const nsCString *key, int generation); + + enum { FLAG_NEW_ENTRY = 1 }; + + nsCOMPtr<nsIFile> mDataFile; + int mGeneration; + int mFlags; + + bool IsNewEntry() { return mFlags & FLAG_NEW_ENTRY; } + void MarkNewEntry() { mFlags |= FLAG_NEW_ENTRY; } + void ClearNewEntry() { mFlags &= ~FLAG_NEW_ENTRY; } +}; + +NS_IMPL_ISUPPORTS0(nsOfflineCacheBinding) + +nsOfflineCacheBinding * +nsOfflineCacheBinding::Create(nsIFile *cacheDir, + const nsCString *fullKey, + int generation) +{ + nsCOMPtr<nsIFile> file; + cacheDir->Clone(getter_AddRefs(file)); + if (!file) + return nullptr; + + nsAutoCString keyBuf; + const char *cid, *key; + if (!DecomposeCacheEntryKey(fullKey, &cid, &key, keyBuf)) + return nullptr; + + uint64_t hash = DCacheHash(key); + + uint32_t dir1 = (uint32_t) (hash & 0x0F); + uint32_t dir2 = (uint32_t)((hash & 0xF0) >> 4); + + hash >>= 8; + + // XXX we might want to create these directories up-front + + file->AppendNative(nsPrintfCString("%X", dir1)); + Unused << file->Create(nsIFile::DIRECTORY_TYPE, 00700); + + file->AppendNative(nsPrintfCString("%X", dir2)); + Unused << file->Create(nsIFile::DIRECTORY_TYPE, 00700); + + nsresult rv; + char leaf[64]; + + if (generation == -1) + { + file->AppendNative(NS_LITERAL_CSTRING("placeholder")); + + for (generation = 0; ; ++generation) + { + SprintfLiteral(leaf, "%014" PRIX64 "-%X", hash, generation); + + rv = file->SetNativeLeafName(nsDependentCString(leaf)); + if (NS_FAILED(rv)) + return nullptr; + rv = file->Create(nsIFile::NORMAL_FILE_TYPE, 00600); + if (NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS) + return nullptr; + if (NS_SUCCEEDED(rv)) + break; + } + } + else + { + SprintfLiteral(leaf, "%014" PRIX64 "-%X", hash, generation); + rv = file->AppendNative(nsDependentCString(leaf)); + if (NS_FAILED(rv)) + return nullptr; + } + + nsOfflineCacheBinding *binding = new nsOfflineCacheBinding; + if (!binding) + return nullptr; + + binding->mDataFile.swap(file); + binding->mGeneration = generation; + binding->mFlags = 0; + return binding; +} + +/****************************************************************************** + * nsOfflineCacheRecord + */ + +struct nsOfflineCacheRecord +{ + const char *clientID; + const char *key; + const uint8_t *metaData; + uint32_t metaDataLen; + int32_t generation; + int32_t dataSize; + int32_t fetchCount; + int64_t lastFetched; + int64_t lastModified; + int64_t expirationTime; +}; + +static nsCacheEntry * +CreateCacheEntry(nsOfflineCacheDevice *device, + const nsCString *fullKey, + const nsOfflineCacheRecord &rec) +{ + nsCacheEntry *entry; + + if (device->IsLocked(*fullKey)) { + return nullptr; + } + + nsresult rv = nsCacheEntry::Create(fullKey->get(), // XXX enable sharing + nsICache::STREAM_BASED, + nsICache::STORE_OFFLINE, + device, &entry); + if (NS_FAILED(rv)) + return nullptr; + + entry->SetFetchCount((uint32_t) rec.fetchCount); + entry->SetLastFetched(SecondsFromPRTime(rec.lastFetched)); + entry->SetLastModified(SecondsFromPRTime(rec.lastModified)); + entry->SetExpirationTime(SecondsFromPRTime(rec.expirationTime)); + entry->SetDataSize((uint32_t) rec.dataSize); + + entry->UnflattenMetaData((const char *) rec.metaData, rec.metaDataLen); + + // Restore security info, if present + const char* info = entry->GetMetaDataElement("security-info"); + if (info) { + nsCOMPtr<nsISupports> infoObj; + rv = NS_DeserializeObject(nsDependentCString(info), + getter_AddRefs(infoObj)); + if (NS_FAILED(rv)) { + delete entry; + return nullptr; + } + entry->SetSecurityInfo(infoObj); + } + + // create a binding object for this entry + nsOfflineCacheBinding *binding = + nsOfflineCacheBinding::Create(device->CacheDirectory(), + fullKey, + rec.generation); + if (!binding) + { + delete entry; + return nullptr; + } + entry->SetData(binding); + + return entry; +} + + +/****************************************************************************** + * nsOfflineCacheEntryInfo + */ + +class nsOfflineCacheEntryInfo final : public nsICacheEntryInfo +{ + ~nsOfflineCacheEntryInfo() {} + +public: + NS_DECL_ISUPPORTS + NS_DECL_NSICACHEENTRYINFO + + nsOfflineCacheRecord *mRec; +}; + +NS_IMPL_ISUPPORTS(nsOfflineCacheEntryInfo, nsICacheEntryInfo) + +NS_IMETHODIMP +nsOfflineCacheEntryInfo::GetClientID(char **result) +{ + *result = NS_strdup(mRec->clientID); + return *result ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +NS_IMETHODIMP +nsOfflineCacheEntryInfo::GetDeviceID(char ** deviceID) +{ + *deviceID = NS_strdup(OFFLINE_CACHE_DEVICE_ID); + return *deviceID ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +NS_IMETHODIMP +nsOfflineCacheEntryInfo::GetKey(nsACString &clientKey) +{ + clientKey.Assign(mRec->key); + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheEntryInfo::GetFetchCount(int32_t *aFetchCount) +{ + *aFetchCount = mRec->fetchCount; + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheEntryInfo::GetLastFetched(uint32_t *aLastFetched) +{ + *aLastFetched = SecondsFromPRTime(mRec->lastFetched); + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheEntryInfo::GetLastModified(uint32_t *aLastModified) +{ + *aLastModified = SecondsFromPRTime(mRec->lastModified); + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheEntryInfo::GetExpirationTime(uint32_t *aExpirationTime) +{ + *aExpirationTime = SecondsFromPRTime(mRec->expirationTime); + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheEntryInfo::IsStreamBased(bool *aStreamBased) +{ + *aStreamBased = true; + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheEntryInfo::GetDataSize(uint32_t *aDataSize) +{ + *aDataSize = mRec->dataSize; + return NS_OK; +} + + +/****************************************************************************** + * nsApplicationCacheNamespace + */ + +NS_IMPL_ISUPPORTS(nsApplicationCacheNamespace, nsIApplicationCacheNamespace) + +NS_IMETHODIMP +nsApplicationCacheNamespace::Init(uint32_t itemType, + const nsACString &namespaceSpec, + const nsACString &data) +{ + mItemType = itemType; + mNamespaceSpec = namespaceSpec; + mData = data; + return NS_OK; +} + +NS_IMETHODIMP +nsApplicationCacheNamespace::GetItemType(uint32_t *out) +{ + *out = mItemType; + return NS_OK; +} + +NS_IMETHODIMP +nsApplicationCacheNamespace::GetNamespaceSpec(nsACString &out) +{ + out = mNamespaceSpec; + return NS_OK; +} + +NS_IMETHODIMP +nsApplicationCacheNamespace::GetData(nsACString &out) +{ + out = mData; + return NS_OK; +} + +/****************************************************************************** + * nsApplicationCache + */ + +NS_IMPL_ISUPPORTS(nsApplicationCache, + nsIApplicationCache, + nsISupportsWeakReference) + +nsApplicationCache::nsApplicationCache() + : mDevice(nullptr) + , mValid(true) +{ +} + +nsApplicationCache::nsApplicationCache(nsOfflineCacheDevice *device, + const nsACString &group, + const nsACString &clientID) + : mDevice(device) + , mGroup(group) + , mClientID(clientID) + , mValid(true) +{ +} + +nsApplicationCache::~nsApplicationCache() +{ + if (!mDevice) + return; + + { + MutexAutoLock lock(mDevice->mLock); + mDevice->mCaches.Remove(mClientID); + } + + // If this isn't an active cache anymore, it can be destroyed. + if (mValid && !mDevice->IsActiveCache(mGroup, mClientID)) + Discard(); +} + +void +nsApplicationCache::MarkInvalid() +{ + mValid = false; +} + +NS_IMETHODIMP +nsApplicationCache::InitAsHandle(const nsACString &groupId, + const nsACString &clientId) +{ + NS_ENSURE_FALSE(mDevice, NS_ERROR_ALREADY_INITIALIZED); + NS_ENSURE_TRUE(mGroup.IsEmpty(), NS_ERROR_ALREADY_INITIALIZED); + + mGroup = groupId; + mClientID = clientId; + return NS_OK; +} + +NS_IMETHODIMP +nsApplicationCache::GetManifestURI(nsIURI **out) +{ + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), mGroup); + NS_ENSURE_SUCCESS(rv, rv); + + rv = uri->CloneIgnoringRef(out); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +nsApplicationCache::GetGroupID(nsACString &out) +{ + out = mGroup; + return NS_OK; +} + +NS_IMETHODIMP +nsApplicationCache::GetClientID(nsACString &out) +{ + out = mClientID; + return NS_OK; +} + +NS_IMETHODIMP +nsApplicationCache::GetProfileDirectory(nsIFile **out) +{ + if (mDevice->BaseDirectory()) + NS_ADDREF(*out = mDevice->BaseDirectory()); + else + *out = nullptr; + + return NS_OK; +} + +NS_IMETHODIMP +nsApplicationCache::GetActive(bool *out) +{ + NS_ENSURE_TRUE(mDevice, NS_ERROR_NOT_AVAILABLE); + + *out = mDevice->IsActiveCache(mGroup, mClientID); + return NS_OK; +} + +NS_IMETHODIMP +nsApplicationCache::Activate() +{ + NS_ENSURE_TRUE(mValid, NS_ERROR_NOT_AVAILABLE); + NS_ENSURE_TRUE(mDevice, NS_ERROR_NOT_AVAILABLE); + + mDevice->ActivateCache(mGroup, mClientID); + + if (mDevice->AutoShutdown(this)) + mDevice = nullptr; + + return NS_OK; +} + +NS_IMETHODIMP +nsApplicationCache::Discard() +{ + NS_ENSURE_TRUE(mValid, NS_ERROR_NOT_AVAILABLE); + NS_ENSURE_TRUE(mDevice, NS_ERROR_NOT_AVAILABLE); + + mValid = false; + + nsCOMPtr<nsIRunnable> ev = + new nsOfflineCacheDiscardCache(mDevice, mGroup, mClientID); + nsresult rv = nsCacheService::DispatchToCacheIOThread(ev); + return rv; +} + +NS_IMETHODIMP +nsApplicationCache::MarkEntry(const nsACString &key, + uint32_t typeBits) +{ + NS_ENSURE_TRUE(mValid, NS_ERROR_NOT_AVAILABLE); + NS_ENSURE_TRUE(mDevice, NS_ERROR_NOT_AVAILABLE); + + return mDevice->MarkEntry(mClientID, key, typeBits); +} + + +NS_IMETHODIMP +nsApplicationCache::UnmarkEntry(const nsACString &key, + uint32_t typeBits) +{ + NS_ENSURE_TRUE(mValid, NS_ERROR_NOT_AVAILABLE); + NS_ENSURE_TRUE(mDevice, NS_ERROR_NOT_AVAILABLE); + + return mDevice->UnmarkEntry(mClientID, key, typeBits); +} + +NS_IMETHODIMP +nsApplicationCache::GetTypes(const nsACString &key, + uint32_t *typeBits) +{ + NS_ENSURE_TRUE(mValid, NS_ERROR_NOT_AVAILABLE); + NS_ENSURE_TRUE(mDevice, NS_ERROR_NOT_AVAILABLE); + + return mDevice->GetTypes(mClientID, key, typeBits); +} + +NS_IMETHODIMP +nsApplicationCache::GatherEntries(uint32_t typeBits, + uint32_t * count, + char *** keys) +{ + NS_ENSURE_TRUE(mValid, NS_ERROR_NOT_AVAILABLE); + NS_ENSURE_TRUE(mDevice, NS_ERROR_NOT_AVAILABLE); + + return mDevice->GatherEntries(mClientID, typeBits, count, keys); +} + +NS_IMETHODIMP +nsApplicationCache::AddNamespaces(nsIArray *namespaces) +{ + NS_ENSURE_TRUE(mValid, NS_ERROR_NOT_AVAILABLE); + NS_ENSURE_TRUE(mDevice, NS_ERROR_NOT_AVAILABLE); + + if (!namespaces) + return NS_OK; + + mozStorageTransaction transaction(mDevice->mDB, false); + + uint32_t length; + nsresult rv = namespaces->GetLength(&length); + NS_ENSURE_SUCCESS(rv, rv); + + for (uint32_t i = 0; i < length; i++) { + nsCOMPtr<nsIApplicationCacheNamespace> ns = + do_QueryElementAt(namespaces, i); + if (ns) { + rv = mDevice->AddNamespace(mClientID, ns); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + rv = transaction.Commit(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +nsApplicationCache::GetMatchingNamespace(const nsACString &key, + nsIApplicationCacheNamespace **out) + +{ + NS_ENSURE_TRUE(mValid, NS_ERROR_NOT_AVAILABLE); + NS_ENSURE_TRUE(mDevice, NS_ERROR_NOT_AVAILABLE); + + return mDevice->GetMatchingNamespace(mClientID, key, out); +} + +NS_IMETHODIMP +nsApplicationCache::GetUsage(uint32_t *usage) +{ + NS_ENSURE_TRUE(mValid, NS_ERROR_NOT_AVAILABLE); + NS_ENSURE_TRUE(mDevice, NS_ERROR_NOT_AVAILABLE); + + return mDevice->GetUsage(mClientID, usage); +} + +/****************************************************************************** + * nsCloseDBEvent + *****************************************************************************/ + +class nsCloseDBEvent : public Runnable { +public: + explicit nsCloseDBEvent(mozIStorageConnection *aDB) + { + mDB = aDB; + } + + NS_IMETHOD Run() override + { + mDB->Close(); + return NS_OK; + } + +protected: + virtual ~nsCloseDBEvent() {} + +private: + nsCOMPtr<mozIStorageConnection> mDB; +}; + + + +/****************************************************************************** + * nsOfflineCacheDevice + */ + +NS_IMPL_ISUPPORTS0(nsOfflineCacheDevice) + +nsOfflineCacheDevice::nsOfflineCacheDevice() + : mDB(nullptr) + , mCacheCapacity(0) + , mDeltaCounter(0) + , mAutoShutdown(false) + , mLock("nsOfflineCacheDevice.lock") + , mActiveCaches(4) + , mLockedEntries(32) +{ +} + +nsOfflineCacheDevice::~nsOfflineCacheDevice() +{} + +/* static */ +bool +nsOfflineCacheDevice::GetStrictFileOriginPolicy() +{ + nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + + bool retval; + if (prefs && NS_SUCCEEDED(prefs->GetBoolPref("security.fileuri.strict_origin_policy", &retval))) + return retval; + + // As default value use true (be more strict) + return true; +} + +uint32_t +nsOfflineCacheDevice::CacheSize() +{ + NS_ENSURE_TRUE(Initialized(), 0); + + AutoResetStatement statement(mStatement_CacheSize); + + bool hasRows; + nsresult rv = statement->ExecuteStep(&hasRows); + NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hasRows, 0); + + return (uint32_t) statement->AsInt32(0); +} + +uint32_t +nsOfflineCacheDevice::EntryCount() +{ + NS_ENSURE_TRUE(Initialized(), 0); + + AutoResetStatement statement(mStatement_EntryCount); + + bool hasRows; + nsresult rv = statement->ExecuteStep(&hasRows); + NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hasRows, 0); + + return (uint32_t) statement->AsInt32(0); +} + +nsresult +nsOfflineCacheDevice::UpdateEntry(nsCacheEntry *entry) +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + // Decompose the key into "ClientID" and "Key" + nsAutoCString keyBuf; + const char *cid, *key; + + if (!DecomposeCacheEntryKey(entry->Key(), &cid, &key, keyBuf)) + return NS_ERROR_UNEXPECTED; + + // Store security info, if it is serializable + nsCOMPtr<nsISupports> infoObj = entry->SecurityInfo(); + nsCOMPtr<nsISerializable> serializable = do_QueryInterface(infoObj); + if (infoObj && !serializable) + return NS_ERROR_UNEXPECTED; + + if (serializable) { + nsCString info; + nsresult rv = NS_SerializeToString(serializable, info); + NS_ENSURE_SUCCESS(rv, rv); + + rv = entry->SetMetaDataElement("security-info", info.get()); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCString metaDataBuf; + uint32_t mdSize = entry->MetaDataSize(); + if (!metaDataBuf.SetLength(mdSize, fallible)) + return NS_ERROR_OUT_OF_MEMORY; + char *md = metaDataBuf.BeginWriting(); + entry->FlattenMetaData(md, mdSize); + + nsOfflineCacheRecord rec; + rec.metaData = (const uint8_t *) md; + rec.metaDataLen = mdSize; + rec.dataSize = entry->DataSize(); + rec.fetchCount = entry->FetchCount(); + rec.lastFetched = PRTimeFromSeconds(entry->LastFetched()); + rec.lastModified = PRTimeFromSeconds(entry->LastModified()); + rec.expirationTime = PRTimeFromSeconds(entry->ExpirationTime()); + + AutoResetStatement statement(mStatement_UpdateEntry); + + nsresult rv; + rv = statement->BindBlobByIndex(0, rec.metaData, rec.metaDataLen); + nsresult tmp = statement->BindInt32ByIndex(1, rec.dataSize); + if (NS_FAILED(tmp)) { + rv = tmp; + } + tmp = statement->BindInt32ByIndex(2, rec.fetchCount); + if (NS_FAILED(tmp)) { + rv = tmp; + } + tmp = statement->BindInt64ByIndex(3, rec.lastFetched); + if (NS_FAILED(tmp)) { + rv = tmp; + } + tmp = statement->BindInt64ByIndex(4, rec.lastModified); + if (NS_FAILED(tmp)) { + rv = tmp; + } + tmp = statement->BindInt64ByIndex(5, rec.expirationTime); + if (NS_FAILED(tmp)) { + rv = tmp; + } + tmp = statement->BindUTF8StringByIndex(6, nsDependentCString(cid)); + if (NS_FAILED(tmp)) { + rv = tmp; + } + tmp = statement->BindUTF8StringByIndex(7, nsDependentCString(key)); + if (NS_FAILED(tmp)) { + rv = tmp; + } + NS_ENSURE_SUCCESS(rv, rv); + + bool hasRows; + rv = statement->ExecuteStep(&hasRows); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(!hasRows, "UPDATE should not result in output"); + return rv; +} + +nsresult +nsOfflineCacheDevice::UpdateEntrySize(nsCacheEntry *entry, uint32_t newSize) +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + // Decompose the key into "ClientID" and "Key" + nsAutoCString keyBuf; + const char *cid, *key; + if (!DecomposeCacheEntryKey(entry->Key(), &cid, &key, keyBuf)) + return NS_ERROR_UNEXPECTED; + + AutoResetStatement statement(mStatement_UpdateEntrySize); + + nsresult rv = statement->BindInt32ByIndex(0, newSize); + nsresult tmp = statement->BindUTF8StringByIndex(1, nsDependentCString(cid)); + if (NS_FAILED(tmp)) { + rv = tmp; + } + tmp = statement->BindUTF8StringByIndex(2, nsDependentCString(key)); + if (NS_FAILED(tmp)) { + rv = tmp; + } + NS_ENSURE_SUCCESS(rv, rv); + + bool hasRows; + rv = statement->ExecuteStep(&hasRows); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(!hasRows, "UPDATE should not result in output"); + return rv; +} + +nsresult +nsOfflineCacheDevice::DeleteEntry(nsCacheEntry *entry, bool deleteData) +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + if (deleteData) + { + nsresult rv = DeleteData(entry); + if (NS_FAILED(rv)) + return rv; + } + + // Decompose the key into "ClientID" and "Key" + nsAutoCString keyBuf; + const char *cid, *key; + if (!DecomposeCacheEntryKey(entry->Key(), &cid, &key, keyBuf)) + return NS_ERROR_UNEXPECTED; + + AutoResetStatement statement(mStatement_DeleteEntry); + + nsresult rv = statement->BindUTF8StringByIndex(0, nsDependentCString(cid)); + nsresult rv2 = statement->BindUTF8StringByIndex(1, nsDependentCString(key)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_SUCCESS(rv2, rv2); + + bool hasRows; + rv = statement->ExecuteStep(&hasRows); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(!hasRows, "DELETE should not result in output"); + return rv; +} + +nsresult +nsOfflineCacheDevice::DeleteData(nsCacheEntry *entry) +{ + nsOfflineCacheBinding *binding = (nsOfflineCacheBinding *) entry->Data(); + NS_ENSURE_STATE(binding); + + return binding->mDataFile->Remove(false); +} + +/** + * nsCacheDevice implementation + */ + +// This struct is local to nsOfflineCacheDevice::Init, but ISO C++98 doesn't +// allow a template (mozilla::ArrayLength) to be instantiated based on a local +// type. Boo-urns! +struct StatementSql { + nsCOMPtr<mozIStorageStatement> &statement; + const char *sql; + StatementSql (nsCOMPtr<mozIStorageStatement> &aStatement, const char *aSql): + statement (aStatement), sql (aSql) {} +}; + +nsresult +nsOfflineCacheDevice::Init() +{ + MOZ_ASSERT(false, "Need to be initialized with sqlite"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult +nsOfflineCacheDevice::InitWithSqlite(mozIStorageService * ss) +{ + NS_ENSURE_TRUE(!mDB, NS_ERROR_ALREADY_INITIALIZED); + + // SetCacheParentDirectory must have been called + NS_ENSURE_TRUE(mCacheDirectory, NS_ERROR_UNEXPECTED); + + // make sure the cache directory exists + nsresult rv = EnsureDir(mCacheDirectory); + NS_ENSURE_SUCCESS(rv, rv); + + // build path to index file + nsCOMPtr<nsIFile> indexFile; + rv = mCacheDirectory->Clone(getter_AddRefs(indexFile)); + NS_ENSURE_SUCCESS(rv, rv); + rv = indexFile->AppendNative(NS_LITERAL_CSTRING("index.sqlite")); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_ASSERT(ss, "nsOfflineCacheDevice::InitWithSqlite called before nsCacheService::Init() ?"); + NS_ENSURE_TRUE(ss, NS_ERROR_UNEXPECTED); + + rv = ss->OpenDatabase(indexFile, getter_AddRefs(mDB)); + NS_ENSURE_SUCCESS(rv, rv); + + mInitThread = do_GetCurrentThread(); + + mDB->ExecuteSimpleSQL(NS_LITERAL_CSTRING("PRAGMA synchronous = OFF;")); + + // XXX ... other initialization steps + + // XXX in the future we may wish to verify the schema for moz_cache + // perhaps using "PRAGMA table_info" ? + + // build the table + // + // "Generation" is the data file generation number. + // + rv = mDB->ExecuteSimpleSQL( + NS_LITERAL_CSTRING("CREATE TABLE IF NOT EXISTS moz_cache (\n" + " ClientID TEXT,\n" + " Key TEXT,\n" + " MetaData BLOB,\n" + " Generation INTEGER,\n" + " DataSize INTEGER,\n" + " FetchCount INTEGER,\n" + " LastFetched INTEGER,\n" + " LastModified INTEGER,\n" + " ExpirationTime INTEGER,\n" + " ItemType INTEGER DEFAULT 0\n" + ");\n")); + NS_ENSURE_SUCCESS(rv, rv); + + // Databases from 1.9.0 don't have the ItemType column. Add the column + // here, but don't worry about failures (the column probably already exists) + mDB->ExecuteSimpleSQL( + NS_LITERAL_CSTRING("ALTER TABLE moz_cache ADD ItemType INTEGER DEFAULT 0")); + + // Create the table for storing cache groups. All actions on + // moz_cache_groups use the GroupID, so use it as the primary key. + rv = mDB->ExecuteSimpleSQL( + NS_LITERAL_CSTRING("CREATE TABLE IF NOT EXISTS moz_cache_groups (\n" + " GroupID TEXT PRIMARY KEY,\n" + " ActiveClientID TEXT\n" + ");\n")); + NS_ENSURE_SUCCESS(rv, rv); + + mDB->ExecuteSimpleSQL( + NS_LITERAL_CSTRING("ALTER TABLE moz_cache_groups " + "ADD ActivateTimeStamp INTEGER DEFAULT 0")); + + // ClientID: clientID joining moz_cache and moz_cache_namespaces + // tables. + // Data: Data associated with this namespace (e.g. a fallback URI + // for fallback entries). + // ItemType: the type of namespace. + rv = mDB->ExecuteSimpleSQL( + NS_LITERAL_CSTRING("CREATE TABLE IF NOT EXISTS" + " moz_cache_namespaces (\n" + " ClientID TEXT,\n" + " NameSpace TEXT,\n" + " Data TEXT,\n" + " ItemType INTEGER\n" + ");\n")); + NS_ENSURE_SUCCESS(rv, rv); + + // Databases from 1.9.0 have a moz_cache_index that should be dropped + rv = mDB->ExecuteSimpleSQL( + NS_LITERAL_CSTRING("DROP INDEX IF EXISTS moz_cache_index")); + NS_ENSURE_SUCCESS(rv, rv); + + // Key/ClientID pairs should be unique in the database. All queries + // against moz_cache use the Key (which is also the most unique), so + // use it as the primary key for this index. + rv = mDB->ExecuteSimpleSQL( + NS_LITERAL_CSTRING("CREATE UNIQUE INDEX IF NOT EXISTS " + " moz_cache_key_clientid_index" + " ON moz_cache (Key, ClientID);")); + NS_ENSURE_SUCCESS(rv, rv); + + // Used for ClientID lookups and to keep ClientID/NameSpace pairs unique. + rv = mDB->ExecuteSimpleSQL( + NS_LITERAL_CSTRING("CREATE UNIQUE INDEX IF NOT EXISTS" + " moz_cache_namespaces_clientid_index" + " ON moz_cache_namespaces (ClientID, NameSpace);")); + NS_ENSURE_SUCCESS(rv, rv); + + // Used for namespace lookups. + rv = mDB->ExecuteSimpleSQL( + NS_LITERAL_CSTRING("CREATE INDEX IF NOT EXISTS" + " moz_cache_namespaces_namespace_index" + " ON moz_cache_namespaces (NameSpace);")); + NS_ENSURE_SUCCESS(rv, rv); + + + mEvictionFunction = new nsOfflineCacheEvictionFunction(this); + if (!mEvictionFunction) return NS_ERROR_OUT_OF_MEMORY; + + rv = mDB->CreateFunction(NS_LITERAL_CSTRING("cache_eviction_observer"), 3, mEvictionFunction); + NS_ENSURE_SUCCESS(rv, rv); + + // create all (most) of our statements up front + StatementSql prepared[] = { + StatementSql ( mStatement_CacheSize, "SELECT Sum(DataSize) from moz_cache;" ), + StatementSql ( mStatement_ApplicationCacheSize, "SELECT Sum(DataSize) from moz_cache WHERE ClientID = ?;" ), + StatementSql ( mStatement_EntryCount, "SELECT count(*) from moz_cache;" ), + StatementSql ( mStatement_UpdateEntry, "UPDATE moz_cache SET MetaData = ?, DataSize = ?, FetchCount = ?, LastFetched = ?, LastModified = ?, ExpirationTime = ? WHERE ClientID = ? AND Key = ?;" ), + StatementSql ( mStatement_UpdateEntrySize, "UPDATE moz_cache SET DataSize = ? WHERE ClientID = ? AND Key = ?;" ), + StatementSql ( mStatement_DeleteEntry, "DELETE FROM moz_cache WHERE ClientID = ? AND Key = ?;" ), + StatementSql ( mStatement_FindEntry, "SELECT MetaData, Generation, DataSize, FetchCount, LastFetched, LastModified, ExpirationTime, ItemType FROM moz_cache WHERE ClientID = ? AND Key = ?;" ), + StatementSql ( mStatement_BindEntry, "INSERT INTO moz_cache (ClientID, Key, MetaData, Generation, DataSize, FetchCount, LastFetched, LastModified, ExpirationTime) VALUES(?,?,?,?,?,?,?,?,?);" ), + + StatementSql ( mStatement_MarkEntry, "UPDATE moz_cache SET ItemType = (ItemType | ?) WHERE ClientID = ? AND Key = ?;" ), + StatementSql ( mStatement_UnmarkEntry, "UPDATE moz_cache SET ItemType = (ItemType & ~?) WHERE ClientID = ? AND Key = ?;" ), + StatementSql ( mStatement_GetTypes, "SELECT ItemType FROM moz_cache WHERE ClientID = ? AND Key = ?;"), + StatementSql ( mStatement_CleanupUnmarked, "DELETE FROM moz_cache WHERE ClientID = ? AND Key = ? AND ItemType = 0;" ), + StatementSql ( mStatement_GatherEntries, "SELECT Key FROM moz_cache WHERE ClientID = ? AND (ItemType & ?) > 0;" ), + + StatementSql ( mStatement_ActivateClient, "INSERT OR REPLACE INTO moz_cache_groups (GroupID, ActiveClientID, ActivateTimeStamp) VALUES (?, ?, ?);" ), + StatementSql ( mStatement_DeactivateGroup, "DELETE FROM moz_cache_groups WHERE GroupID = ?;" ), + StatementSql ( mStatement_FindClient, "SELECT ClientID, ItemType FROM moz_cache WHERE Key = ? ORDER BY LastFetched DESC, LastModified DESC;" ), + + // Search for namespaces that match the URI. Use the <= operator + // to ensure that we use the index on moz_cache_namespaces. + StatementSql ( mStatement_FindClientByNamespace, "SELECT ns.ClientID, ns.ItemType FROM" + " moz_cache_namespaces AS ns JOIN moz_cache_groups AS groups" + " ON ns.ClientID = groups.ActiveClientID" + " WHERE ns.NameSpace <= ?1 AND ?1 GLOB ns.NameSpace || '*'" + " ORDER BY ns.NameSpace DESC, groups.ActivateTimeStamp DESC;"), + StatementSql ( mStatement_FindNamespaceEntry, "SELECT NameSpace, Data, ItemType FROM moz_cache_namespaces" + " WHERE ClientID = ?1" + " AND NameSpace <= ?2 AND ?2 GLOB NameSpace || '*'" + " ORDER BY NameSpace DESC;"), + StatementSql ( mStatement_InsertNamespaceEntry, "INSERT INTO moz_cache_namespaces (ClientID, NameSpace, Data, ItemType) VALUES(?, ?, ?, ?);"), + StatementSql ( mStatement_EnumerateApps, "SELECT GroupID, ActiveClientID FROM moz_cache_groups WHERE GroupID LIKE ?1;"), + StatementSql ( mStatement_EnumerateGroups, "SELECT GroupID, ActiveClientID FROM moz_cache_groups;"), + StatementSql ( mStatement_EnumerateGroupsTimeOrder, "SELECT GroupID, ActiveClientID FROM moz_cache_groups ORDER BY ActivateTimeStamp;") + }; + for (uint32_t i = 0; NS_SUCCEEDED(rv) && i < ArrayLength(prepared); ++i) + { + LOG(("Creating statement: %s\n", prepared[i].sql)); + + rv = mDB->CreateStatement(nsDependentCString(prepared[i].sql), + getter_AddRefs(prepared[i].statement)); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = InitActiveCaches(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +namespace { + +nsresult +GetGroupForCache(const nsCSubstring &clientID, nsCString &group) +{ + group.Assign(clientID); + group.Truncate(group.FindChar('|')); + NS_UnescapeURL(group); + + return NS_OK; +} + +} // namespace + +// static +nsresult +nsOfflineCacheDevice::BuildApplicationCacheGroupID(nsIURI *aManifestURL, + nsACString const &aOriginSuffix, + nsACString &_result) +{ + nsCOMPtr<nsIURI> newURI; + nsresult rv = aManifestURL->CloneIgnoringRef(getter_AddRefs(newURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString manifestSpec; + rv = newURI->GetAsciiSpec(manifestSpec); + NS_ENSURE_SUCCESS(rv, rv); + + _result.Assign(manifestSpec); + _result.Append('#'); + _result.Append(aOriginSuffix); + + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::InitActiveCaches() +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + MutexAutoLock lock(mLock); + + AutoResetStatement statement(mStatement_EnumerateGroups); + + bool hasRows; + nsresult rv = statement->ExecuteStep(&hasRows); + NS_ENSURE_SUCCESS(rv, rv); + + while (hasRows) + { + nsAutoCString group; + statement->GetUTF8String(0, group); + nsCString clientID; + statement->GetUTF8String(1, clientID); + + mActiveCaches.PutEntry(clientID); + mActiveCachesByGroup.Put(group, new nsCString(clientID)); + + rv = statement->ExecuteStep(&hasRows); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::Shutdown() +{ + NS_ENSURE_TRUE(mDB, NS_ERROR_NOT_INITIALIZED); + + { + MutexAutoLock lock(mLock); + for (auto iter = mCaches.Iter(); !iter.Done(); iter.Next()) { + nsCOMPtr<nsIApplicationCache> obj = do_QueryReferent(iter.UserData()); + if (obj) { + auto appCache = static_cast<nsApplicationCache*>(obj.get()); + appCache->MarkInvalid(); + } + } + } + + { + EvictionObserver evictionObserver(mDB, mEvictionFunction); + + // Delete all rows whose clientID is not an active clientID. + nsresult rv = mDB->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DELETE FROM moz_cache WHERE rowid IN" + " (SELECT moz_cache.rowid FROM" + " moz_cache LEFT OUTER JOIN moz_cache_groups ON" + " (moz_cache.ClientID = moz_cache_groups.ActiveClientID)" + " WHERE moz_cache_groups.GroupID ISNULL)")); + + if (NS_FAILED(rv)) + NS_WARNING("Failed to clean up unused application caches."); + else + evictionObserver.Apply(); + + // Delete all namespaces whose clientID is not an active clientID. + rv = mDB->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DELETE FROM moz_cache_namespaces WHERE rowid IN" + " (SELECT moz_cache_namespaces.rowid FROM" + " moz_cache_namespaces LEFT OUTER JOIN moz_cache_groups ON" + " (moz_cache_namespaces.ClientID = moz_cache_groups.ActiveClientID)" + " WHERE moz_cache_groups.GroupID ISNULL)")); + + if (NS_FAILED(rv)) + NS_WARNING("Failed to clean up namespaces."); + + mEvictionFunction = nullptr; + + mStatement_CacheSize = nullptr; + mStatement_ApplicationCacheSize = nullptr; + mStatement_EntryCount = nullptr; + mStatement_UpdateEntry = nullptr; + mStatement_UpdateEntrySize = nullptr; + mStatement_DeleteEntry = nullptr; + mStatement_FindEntry = nullptr; + mStatement_BindEntry = nullptr; + mStatement_ClearDomain = nullptr; + mStatement_MarkEntry = nullptr; + mStatement_UnmarkEntry = nullptr; + mStatement_GetTypes = nullptr; + mStatement_FindNamespaceEntry = nullptr; + mStatement_InsertNamespaceEntry = nullptr; + mStatement_CleanupUnmarked = nullptr; + mStatement_GatherEntries = nullptr; + mStatement_ActivateClient = nullptr; + mStatement_DeactivateGroup = nullptr; + mStatement_FindClient = nullptr; + mStatement_FindClientByNamespace = nullptr; + mStatement_EnumerateApps = nullptr; + mStatement_EnumerateGroups = nullptr; + mStatement_EnumerateGroupsTimeOrder = nullptr; + } + + // Close Database on the correct thread + bool isOnCurrentThread = true; + if (mInitThread) + mInitThread->IsOnCurrentThread(&isOnCurrentThread); + + if (!isOnCurrentThread) { + nsCOMPtr<nsIRunnable> ev = new nsCloseDBEvent(mDB); + + if (ev) { + mInitThread->Dispatch(ev, NS_DISPATCH_NORMAL); + } + } + else { + mDB->Close(); + } + + mDB = nullptr; + mInitThread = nullptr; + + return NS_OK; +} + +const char * +nsOfflineCacheDevice::GetDeviceID() +{ + return OFFLINE_CACHE_DEVICE_ID; +} + +nsCacheEntry * +nsOfflineCacheDevice::FindEntry(nsCString *fullKey, bool *collision) +{ + NS_ENSURE_TRUE(Initialized(), nullptr); + + mozilla::Telemetry::AutoTimer<mozilla::Telemetry::CACHE_OFFLINE_SEARCH_2> timer; + LOG(("nsOfflineCacheDevice::FindEntry [key=%s]\n", fullKey->get())); + + // SELECT * FROM moz_cache WHERE key = ? + + // Decompose the key into "ClientID" and "Key" + nsAutoCString keyBuf; + const char *cid, *key; + if (!DecomposeCacheEntryKey(fullKey, &cid, &key, keyBuf)) + return nullptr; + + AutoResetStatement statement(mStatement_FindEntry); + + nsresult rv = statement->BindUTF8StringByIndex(0, nsDependentCString(cid)); + nsresult rv2 = statement->BindUTF8StringByIndex(1, nsDependentCString(key)); + NS_ENSURE_SUCCESS(rv, nullptr); + NS_ENSURE_SUCCESS(rv2, nullptr); + + bool hasRows; + rv = statement->ExecuteStep(&hasRows); + if (NS_FAILED(rv) || !hasRows) + return nullptr; // entry not found + + nsOfflineCacheRecord rec; + statement->GetSharedBlob(0, &rec.metaDataLen, + (const uint8_t **) &rec.metaData); + rec.generation = statement->AsInt32(1); + rec.dataSize = statement->AsInt32(2); + rec.fetchCount = statement->AsInt32(3); + rec.lastFetched = statement->AsInt64(4); + rec.lastModified = statement->AsInt64(5); + rec.expirationTime = statement->AsInt64(6); + + LOG(("entry: [%u %d %d %d %lld %lld %lld]\n", + rec.metaDataLen, + rec.generation, + rec.dataSize, + rec.fetchCount, + rec.lastFetched, + rec.lastModified, + rec.expirationTime)); + + nsCacheEntry *entry = CreateCacheEntry(this, fullKey, rec); + + if (entry) + { + // make sure that the data file exists + nsOfflineCacheBinding *binding = (nsOfflineCacheBinding*)entry->Data(); + bool isFile; + rv = binding->mDataFile->IsFile(&isFile); + if (NS_FAILED(rv) || !isFile) + { + DeleteEntry(entry, false); + delete entry; + return nullptr; + } + + // lock the entry + Lock(*fullKey); + } + + return entry; +} + +nsresult +nsOfflineCacheDevice::DeactivateEntry(nsCacheEntry *entry) +{ + LOG(("nsOfflineCacheDevice::DeactivateEntry [key=%s]\n", + entry->Key()->get())); + + // This method is called to inform us that the nsCacheEntry object is going + // away. We should persist anything that needs to be persisted, or if the + // entry is doomed, we can go ahead and clear its storage. + + if (entry->IsDoomed()) + { + // remove corresponding row and file if they exist + + // the row should have been removed in DoomEntry... we could assert that + // that happened. otherwise, all we have to do here is delete the file + // on disk. + DeleteData(entry); + } + else if (((nsOfflineCacheBinding *)entry->Data())->IsNewEntry()) + { + // UPDATE the database row + + // Only new entries are updated, since offline cache is updated in + // transactions. New entries are those who is returned from + // BindEntry(). + + LOG(("nsOfflineCacheDevice::DeactivateEntry updating new entry\n")); + UpdateEntry(entry); + } else { + LOG(("nsOfflineCacheDevice::DeactivateEntry " + "skipping update since entry is not dirty\n")); + } + + // Unlock the entry + Unlock(*entry->Key()); + + delete entry; + + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::BindEntry(nsCacheEntry *entry) +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + LOG(("nsOfflineCacheDevice::BindEntry [key=%s]\n", entry->Key()->get())); + + NS_ENSURE_STATE(!entry->Data()); + + // This method is called to inform us that we have a new entry. The entry + // may collide with an existing entry in our DB, but if that happens we can + // assume that the entry is not being used. + + // INSERT the database row + + // XXX Assumption: if the row already exists, then FindEntry would have + // returned it. if that entry was doomed, then DoomEntry would have removed + // it from the table. so, we should always have to insert at this point. + + // Decompose the key into "ClientID" and "Key" + nsAutoCString keyBuf; + const char *cid, *key; + if (!DecomposeCacheEntryKey(entry->Key(), &cid, &key, keyBuf)) + return NS_ERROR_UNEXPECTED; + + // create binding, pick best generation number + RefPtr<nsOfflineCacheBinding> binding = + nsOfflineCacheBinding::Create(mCacheDirectory, entry->Key(), -1); + if (!binding) + return NS_ERROR_OUT_OF_MEMORY; + binding->MarkNewEntry(); + + nsOfflineCacheRecord rec; + rec.clientID = cid; + rec.key = key; + rec.metaData = nullptr; // don't write any metadata now. + rec.metaDataLen = 0; + rec.generation = binding->mGeneration; + rec.dataSize = 0; + rec.fetchCount = entry->FetchCount(); + rec.lastFetched = PRTimeFromSeconds(entry->LastFetched()); + rec.lastModified = PRTimeFromSeconds(entry->LastModified()); + rec.expirationTime = PRTimeFromSeconds(entry->ExpirationTime()); + + AutoResetStatement statement(mStatement_BindEntry); + + nsresult rv = statement->BindUTF8StringByIndex(0, nsDependentCString(rec.clientID)); + nsresult tmp = statement->BindUTF8StringByIndex(1, nsDependentCString(rec.key)); + if (NS_FAILED(tmp)) { + rv = tmp; + } + tmp = statement->BindBlobByIndex(2, rec.metaData, rec.metaDataLen); + if (NS_FAILED(tmp)) { + rv = tmp; + } + tmp = statement->BindInt32ByIndex(3, rec.generation); + if (NS_FAILED(tmp)) { + rv = tmp; + } + tmp = statement->BindInt32ByIndex(4, rec.dataSize); + if (NS_FAILED(tmp)) { + rv = tmp; + } + tmp = statement->BindInt32ByIndex(5, rec.fetchCount); + if (NS_FAILED(tmp)) { + rv = tmp; + } + tmp = statement->BindInt64ByIndex(6, rec.lastFetched); + if (NS_FAILED(tmp)) { + rv = tmp; + } + tmp = statement->BindInt64ByIndex(7, rec.lastModified); + if (NS_FAILED(tmp)) { + rv = tmp; + } + tmp = statement->BindInt64ByIndex(8, rec.expirationTime); + if (NS_FAILED(tmp)) { + rv = tmp; + } + NS_ENSURE_SUCCESS(rv, rv); + + bool hasRows; + rv = statement->ExecuteStep(&hasRows); + NS_ENSURE_SUCCESS(rv, rv); + NS_ASSERTION(!hasRows, "INSERT should not result in output"); + + entry->SetData(binding); + + // lock the entry + Lock(*entry->Key()); + + return NS_OK; +} + +void +nsOfflineCacheDevice::DoomEntry(nsCacheEntry *entry) +{ + LOG(("nsOfflineCacheDevice::DoomEntry [key=%s]\n", entry->Key()->get())); + + // This method is called to inform us that we should mark the entry to be + // deleted when it is no longer in use. + + // We can go ahead and delete the corresponding row in our table, + // but we must not delete the file on disk until we are deactivated. + // In another word, the file should be deleted if the entry had been + // deactivated. + + DeleteEntry(entry, !entry->IsActive()); +} + +nsresult +nsOfflineCacheDevice::OpenInputStreamForEntry(nsCacheEntry *entry, + nsCacheAccessMode mode, + uint32_t offset, + nsIInputStream **result) +{ + LOG(("nsOfflineCacheDevice::OpenInputStreamForEntry [key=%s]\n", + entry->Key()->get())); + + *result = nullptr; + + NS_ENSURE_TRUE(!offset || (offset < entry->DataSize()), NS_ERROR_INVALID_ARG); + + // return an input stream to the entry's data file. the stream + // may be read on a background thread. + + nsOfflineCacheBinding *binding = (nsOfflineCacheBinding *) entry->Data(); + NS_ENSURE_STATE(binding); + + nsCOMPtr<nsIInputStream> in; + NS_NewLocalFileInputStream(getter_AddRefs(in), binding->mDataFile, PR_RDONLY); + if (!in) + return NS_ERROR_UNEXPECTED; + + // respect |offset| param + if (offset != 0) + { + nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(in); + NS_ENSURE_TRUE(seekable, NS_ERROR_UNEXPECTED); + + seekable->Seek(nsISeekableStream::NS_SEEK_SET, offset); + } + + in.swap(*result); + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::OpenOutputStreamForEntry(nsCacheEntry *entry, + nsCacheAccessMode mode, + uint32_t offset, + nsIOutputStream **result) +{ + LOG(("nsOfflineCacheDevice::OpenOutputStreamForEntry [key=%s]\n", + entry->Key()->get())); + + *result = nullptr; + + NS_ENSURE_TRUE(offset <= entry->DataSize(), NS_ERROR_INVALID_ARG); + + // return an output stream to the entry's data file. we can assume + // that the output stream will only be used on the main thread. + + nsOfflineCacheBinding *binding = (nsOfflineCacheBinding *) entry->Data(); + NS_ENSURE_STATE(binding); + + nsCOMPtr<nsIOutputStream> out; + NS_NewLocalFileOutputStream(getter_AddRefs(out), binding->mDataFile, + PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, + 00600); + if (!out) + return NS_ERROR_UNEXPECTED; + + // respect |offset| param + nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(out); + NS_ENSURE_TRUE(seekable, NS_ERROR_UNEXPECTED); + if (offset != 0) + seekable->Seek(nsISeekableStream::NS_SEEK_SET, offset); + + // truncate the file at the given offset + seekable->SetEOF(); + + nsCOMPtr<nsIOutputStream> bufferedOut; + nsresult rv = + NS_NewBufferedOutputStream(getter_AddRefs(bufferedOut), out, 16 * 1024); + NS_ENSURE_SUCCESS(rv, rv); + + bufferedOut.swap(*result); + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::GetFileForEntry(nsCacheEntry *entry, nsIFile **result) +{ + LOG(("nsOfflineCacheDevice::GetFileForEntry [key=%s]\n", + entry->Key()->get())); + + nsOfflineCacheBinding *binding = (nsOfflineCacheBinding *) entry->Data(); + NS_ENSURE_STATE(binding); + + NS_IF_ADDREF(*result = binding->mDataFile); + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::OnDataSizeChange(nsCacheEntry *entry, int32_t deltaSize) +{ + LOG(("nsOfflineCacheDevice::OnDataSizeChange [key=%s delta=%d]\n", + entry->Key()->get(), deltaSize)); + + const int32_t DELTA_THRESHOLD = 1<<14; // 16k + + // called to notify us of an impending change in the total size of the + // specified entry. + + uint32_t oldSize = entry->DataSize(); + NS_ASSERTION(deltaSize >= 0 || int32_t(oldSize) + deltaSize >= 0, "oops"); + uint32_t newSize = int32_t(oldSize) + deltaSize; + UpdateEntrySize(entry, newSize); + + mDeltaCounter += deltaSize; // this may go negative + + if (mDeltaCounter >= DELTA_THRESHOLD) + { + if (CacheSize() > mCacheCapacity) { + // the entry will overrun the cache capacity, doom the entry + // and abort +#ifdef DEBUG + nsresult rv = +#endif + nsCacheService::DoomEntry(entry); + NS_ASSERTION(NS_SUCCEEDED(rv), "DoomEntry() failed."); + return NS_ERROR_ABORT; + } + + mDeltaCounter = 0; // reset counter + } + + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::Visit(nsICacheVisitor *visitor) +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + // called to enumerate the offline cache. + + nsCOMPtr<nsICacheDeviceInfo> deviceInfo = + new nsOfflineCacheDeviceInfo(this); + + bool keepGoing; + nsresult rv = visitor->VisitDevice(OFFLINE_CACHE_DEVICE_ID, deviceInfo, + &keepGoing); + if (NS_FAILED(rv)) + return rv; + + if (!keepGoing) + return NS_OK; + + // SELECT * from moz_cache; + + nsOfflineCacheRecord rec; + RefPtr<nsOfflineCacheEntryInfo> info = new nsOfflineCacheEntryInfo; + if (!info) + return NS_ERROR_OUT_OF_MEMORY; + info->mRec = &rec; + + // XXX may want to list columns explicitly + nsCOMPtr<mozIStorageStatement> statement; + rv = mDB->CreateStatement( + NS_LITERAL_CSTRING("SELECT * FROM moz_cache;"), + getter_AddRefs(statement)); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasRows; + for (;;) + { + rv = statement->ExecuteStep(&hasRows); + if (NS_FAILED(rv) || !hasRows) + break; + + statement->GetSharedUTF8String(0, nullptr, &rec.clientID); + statement->GetSharedUTF8String(1, nullptr, &rec.key); + statement->GetSharedBlob(2, &rec.metaDataLen, + (const uint8_t **) &rec.metaData); + rec.generation = statement->AsInt32(3); + rec.dataSize = statement->AsInt32(4); + rec.fetchCount = statement->AsInt32(5); + rec.lastFetched = statement->AsInt64(6); + rec.lastModified = statement->AsInt64(7); + rec.expirationTime = statement->AsInt64(8); + + bool keepGoing; + rv = visitor->VisitEntry(OFFLINE_CACHE_DEVICE_ID, info, &keepGoing); + if (NS_FAILED(rv) || !keepGoing) + break; + } + + info->mRec = nullptr; + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::EvictEntries(const char *clientID) +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + LOG(("nsOfflineCacheDevice::EvictEntries [cid=%s]\n", + clientID ? clientID : "")); + + // called to evict all entries matching the given clientID. + + // need trigger to fire user defined function after a row is deleted + // so we can delete the corresponding data file. + EvictionObserver evictionObserver(mDB, mEvictionFunction); + + nsCOMPtr<mozIStorageStatement> statement; + nsresult rv; + if (clientID) + { + rv = mDB->CreateStatement(NS_LITERAL_CSTRING("DELETE FROM moz_cache WHERE ClientID=?;"), + getter_AddRefs(statement)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = statement->BindUTF8StringByIndex(0, nsDependentCString(clientID)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = statement->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mDB->CreateStatement(NS_LITERAL_CSTRING("DELETE FROM moz_cache_groups WHERE ActiveClientID=?;"), + getter_AddRefs(statement)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = statement->BindUTF8StringByIndex(0, nsDependentCString(clientID)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = statement->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + // TODO - Should update internal hashtables. + // Low priority, since this API is not widely used. + } + else + { + rv = mDB->CreateStatement(NS_LITERAL_CSTRING("DELETE FROM moz_cache;"), + getter_AddRefs(statement)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = statement->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mDB->CreateStatement(NS_LITERAL_CSTRING("DELETE FROM moz_cache_groups;"), + getter_AddRefs(statement)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = statement->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + MutexAutoLock lock(mLock); + mCaches.Clear(); + mActiveCaches.Clear(); + mActiveCachesByGroup.Clear(); + } + + evictionObserver.Apply(); + + statement = nullptr; + // Also evict any namespaces associated with this clientID. + if (clientID) + { + rv = mDB->CreateStatement(NS_LITERAL_CSTRING("DELETE FROM moz_cache_namespaces WHERE ClientID=?"), + getter_AddRefs(statement)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = statement->BindUTF8StringByIndex(0, nsDependentCString(clientID)); + NS_ENSURE_SUCCESS(rv, rv); + } + else + { + rv = mDB->CreateStatement(NS_LITERAL_CSTRING("DELETE FROM moz_cache_namespaces;"), + getter_AddRefs(statement)); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = statement->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::MarkEntry(const nsCString &clientID, + const nsACString &key, + uint32_t typeBits) +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + LOG(("nsOfflineCacheDevice::MarkEntry [cid=%s, key=%s, typeBits=%d]\n", + clientID.get(), PromiseFlatCString(key).get(), typeBits)); + + AutoResetStatement statement(mStatement_MarkEntry); + nsresult rv = statement->BindInt32ByIndex(0, typeBits); + NS_ENSURE_SUCCESS(rv, rv); + rv = statement->BindUTF8StringByIndex(1, clientID); + NS_ENSURE_SUCCESS(rv, rv); + rv = statement->BindUTF8StringByIndex(2, key); + NS_ENSURE_SUCCESS(rv, rv); + + rv = statement->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::UnmarkEntry(const nsCString &clientID, + const nsACString &key, + uint32_t typeBits) +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + LOG(("nsOfflineCacheDevice::UnmarkEntry [cid=%s, key=%s, typeBits=%d]\n", + clientID.get(), PromiseFlatCString(key).get(), typeBits)); + + AutoResetStatement statement(mStatement_UnmarkEntry); + nsresult rv = statement->BindInt32ByIndex(0, typeBits); + NS_ENSURE_SUCCESS(rv, rv); + rv = statement->BindUTF8StringByIndex(1, clientID); + NS_ENSURE_SUCCESS(rv, rv); + rv = statement->BindUTF8StringByIndex(2, key); + NS_ENSURE_SUCCESS(rv, rv); + + rv = statement->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + // Remove the entry if it is now empty. + + EvictionObserver evictionObserver(mDB, mEvictionFunction); + + AutoResetStatement cleanupStatement(mStatement_CleanupUnmarked); + rv = cleanupStatement->BindUTF8StringByIndex(0, clientID); + NS_ENSURE_SUCCESS(rv, rv); + rv = cleanupStatement->BindUTF8StringByIndex(1, key); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cleanupStatement->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + evictionObserver.Apply(); + + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::GetMatchingNamespace(const nsCString &clientID, + const nsACString &key, + nsIApplicationCacheNamespace **out) +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + LOG(("nsOfflineCacheDevice::GetMatchingNamespace [cid=%s, key=%s]\n", + clientID.get(), PromiseFlatCString(key).get())); + + nsresult rv; + + AutoResetStatement statement(mStatement_FindNamespaceEntry); + + rv = statement->BindUTF8StringByIndex(0, clientID); + NS_ENSURE_SUCCESS(rv, rv); + rv = statement->BindUTF8StringByIndex(1, key); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasRows; + rv = statement->ExecuteStep(&hasRows); + NS_ENSURE_SUCCESS(rv, rv); + + *out = nullptr; + + bool found = false; + nsCString nsSpec; + int32_t nsType = 0; + nsCString nsData; + + while (hasRows) + { + int32_t itemType; + rv = statement->GetInt32(2, &itemType); + NS_ENSURE_SUCCESS(rv, rv); + + if (!found || itemType > nsType) + { + nsType = itemType; + + rv = statement->GetUTF8String(0, nsSpec); + NS_ENSURE_SUCCESS(rv, rv); + + rv = statement->GetUTF8String(1, nsData); + NS_ENSURE_SUCCESS(rv, rv); + + found = true; + } + + rv = statement->ExecuteStep(&hasRows); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (found) { + nsCOMPtr<nsIApplicationCacheNamespace> ns = + new nsApplicationCacheNamespace(); + if (!ns) + return NS_ERROR_OUT_OF_MEMORY; + rv = ns->Init(nsType, nsSpec, nsData); + NS_ENSURE_SUCCESS(rv, rv); + + ns.swap(*out); + } + + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::CacheOpportunistically(const nsCString &clientID, + const nsACString &key) +{ + // XXX: We should also be propagating this cache entry to other matching + // caches. See bug 444807. + + return MarkEntry(clientID, key, nsIApplicationCache::ITEM_OPPORTUNISTIC); +} + +nsresult +nsOfflineCacheDevice::GetTypes(const nsCString &clientID, + const nsACString &key, + uint32_t *typeBits) +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + LOG(("nsOfflineCacheDevice::GetTypes [cid=%s, key=%s]\n", + clientID.get(), PromiseFlatCString(key).get())); + + AutoResetStatement statement(mStatement_GetTypes); + nsresult rv = statement->BindUTF8StringByIndex(0, clientID); + NS_ENSURE_SUCCESS(rv, rv); + rv = statement->BindUTF8StringByIndex(1, key); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasRows; + rv = statement->ExecuteStep(&hasRows); + NS_ENSURE_SUCCESS(rv, rv); + + if (!hasRows) + return NS_ERROR_CACHE_KEY_NOT_FOUND; + + *typeBits = statement->AsInt32(0); + + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::GatherEntries(const nsCString &clientID, + uint32_t typeBits, + uint32_t *count, + char ***keys) +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + LOG(("nsOfflineCacheDevice::GatherEntries [cid=%s, typeBits=%X]\n", + clientID.get(), typeBits)); + + AutoResetStatement statement(mStatement_GatherEntries); + nsresult rv = statement->BindUTF8StringByIndex(0, clientID); + NS_ENSURE_SUCCESS(rv, rv); + + rv = statement->BindInt32ByIndex(1, typeBits); + NS_ENSURE_SUCCESS(rv, rv); + + return RunSimpleQuery(mStatement_GatherEntries, 0, count, keys); +} + +nsresult +nsOfflineCacheDevice::AddNamespace(const nsCString &clientID, + nsIApplicationCacheNamespace *ns) +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + nsCString namespaceSpec; + nsresult rv = ns->GetNamespaceSpec(namespaceSpec); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString data; + rv = ns->GetData(data); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t itemType; + rv = ns->GetItemType(&itemType); + NS_ENSURE_SUCCESS(rv, rv); + + LOG(("nsOfflineCacheDevice::AddNamespace [cid=%s, ns=%s, data=%s, type=%d]", + clientID.get(), namespaceSpec.get(), data.get(), itemType)); + + AutoResetStatement statement(mStatement_InsertNamespaceEntry); + + rv = statement->BindUTF8StringByIndex(0, clientID); + NS_ENSURE_SUCCESS(rv, rv); + + rv = statement->BindUTF8StringByIndex(1, namespaceSpec); + NS_ENSURE_SUCCESS(rv, rv); + + rv = statement->BindUTF8StringByIndex(2, data); + NS_ENSURE_SUCCESS(rv, rv); + + rv = statement->BindInt32ByIndex(3, itemType); + NS_ENSURE_SUCCESS(rv, rv); + + rv = statement->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::GetUsage(const nsACString &clientID, + uint32_t *usage) +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + LOG(("nsOfflineCacheDevice::GetUsage [cid=%s]\n", + PromiseFlatCString(clientID).get())); + + *usage = 0; + + AutoResetStatement statement(mStatement_ApplicationCacheSize); + + nsresult rv = statement->BindUTF8StringByIndex(0, clientID); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasRows; + rv = statement->ExecuteStep(&hasRows); + NS_ENSURE_SUCCESS(rv, rv); + + if (!hasRows) + return NS_OK; + + *usage = static_cast<uint32_t>(statement->AsInt32(0)); + + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::GetGroups(uint32_t *count, + char ***keys) +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + LOG(("nsOfflineCacheDevice::GetGroups")); + + return RunSimpleQuery(mStatement_EnumerateGroups, 0, count, keys); +} + +nsresult +nsOfflineCacheDevice::GetGroupsTimeOrdered(uint32_t *count, + char ***keys) +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + LOG(("nsOfflineCacheDevice::GetGroupsTimeOrder")); + + return RunSimpleQuery(mStatement_EnumerateGroupsTimeOrder, 0, count, keys); +} + +bool +nsOfflineCacheDevice::IsLocked(const nsACString &key) +{ + MutexAutoLock lock(mLock); + return mLockedEntries.GetEntry(key); +} + +void +nsOfflineCacheDevice::Lock(const nsACString &key) +{ + MutexAutoLock lock(mLock); + mLockedEntries.PutEntry(key); +} + +void +nsOfflineCacheDevice::Unlock(const nsACString &key) +{ + MutexAutoLock lock(mLock); + mLockedEntries.RemoveEntry(key); +} + +nsresult +nsOfflineCacheDevice::RunSimpleQuery(mozIStorageStatement * statement, + uint32_t resultIndex, + uint32_t * count, + char *** values) +{ + bool hasRows; + nsresult rv = statement->ExecuteStep(&hasRows); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray<nsCString> valArray; + while (hasRows) + { + uint32_t length; + valArray.AppendElement( + nsDependentCString(statement->AsSharedUTF8String(resultIndex, &length))); + + rv = statement->ExecuteStep(&hasRows); + NS_ENSURE_SUCCESS(rv, rv); + } + + *count = valArray.Length(); + char **ret = static_cast<char **>(moz_xmalloc(*count * sizeof(char*))); + if (!ret) return NS_ERROR_OUT_OF_MEMORY; + + for (uint32_t i = 0; i < *count; i++) { + ret[i] = NS_strdup(valArray[i].get()); + if (!ret[i]) { + NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(i, ret); + return NS_ERROR_OUT_OF_MEMORY; + } + } + + *values = ret; + + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::CreateApplicationCache(const nsACString &group, + nsIApplicationCache **out) +{ + *out = nullptr; + + nsCString clientID; + // Some characters are special in the clientID. Escape the groupID + // before putting it in to the client key. + if (!NS_Escape(nsCString(group), clientID, url_Path)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + PRTime now = PR_Now(); + + // Include the timestamp to guarantee uniqueness across runs, and + // the gNextTemporaryClientID for uniqueness within a second. + clientID.Append(nsPrintfCString("|%016lld|%d", + now / PR_USEC_PER_SEC, + gNextTemporaryClientID++)); + + nsCOMPtr<nsIApplicationCache> cache = new nsApplicationCache(this, + group, + clientID); + if (!cache) + return NS_ERROR_OUT_OF_MEMORY; + + nsCOMPtr<nsIWeakReference> weak = do_GetWeakReference(cache); + if (!weak) + return NS_ERROR_OUT_OF_MEMORY; + + MutexAutoLock lock(mLock); + mCaches.Put(clientID, weak); + + cache.swap(*out); + + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::GetApplicationCache(const nsACString &clientID, + nsIApplicationCache **out) +{ + MutexAutoLock lock(mLock); + return GetApplicationCache_Unlocked(clientID, out); +} + +nsresult +nsOfflineCacheDevice::GetApplicationCache_Unlocked(const nsACString &clientID, + nsIApplicationCache **out) +{ + *out = nullptr; + + nsCOMPtr<nsIApplicationCache> cache; + + nsWeakPtr weak; + if (mCaches.Get(clientID, getter_AddRefs(weak))) + cache = do_QueryReferent(weak); + + if (!cache) + { + nsCString group; + nsresult rv = GetGroupForCache(clientID, group); + NS_ENSURE_SUCCESS(rv, rv); + + if (group.IsEmpty()) { + return NS_OK; + } + + cache = new nsApplicationCache(this, group, clientID); + weak = do_GetWeakReference(cache); + if (!weak) + return NS_ERROR_OUT_OF_MEMORY; + + mCaches.Put(clientID, weak); + } + + cache.swap(*out); + + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::GetActiveCache(const nsACString &group, + nsIApplicationCache **out) +{ + *out = nullptr; + + MutexAutoLock lock(mLock); + + nsCString *clientID; + if (mActiveCachesByGroup.Get(group, &clientID)) + return GetApplicationCache_Unlocked(*clientID, out); + + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::DeactivateGroup(const nsACString &group) +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + nsCString *active = nullptr; + + AutoResetStatement statement(mStatement_DeactivateGroup); + nsresult rv = statement->BindUTF8StringByIndex(0, group); + NS_ENSURE_SUCCESS(rv, rv); + + rv = statement->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + MutexAutoLock lock(mLock); + + if (mActiveCachesByGroup.Get(group, &active)) + { + mActiveCaches.RemoveEntry(*active); + mActiveCachesByGroup.Remove(group); + active = nullptr; + } + + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::Evict(nsILoadContextInfo *aInfo) +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + NS_ENSURE_ARG(aInfo); + + nsresult rv; + + mozilla::OriginAttributes const *oa = aInfo->OriginAttributesPtr(); + + if (oa->mAppId == NECKO_NO_APP_ID && oa->mInIsolatedMozBrowser == false) { + nsCOMPtr<nsICacheService> serv = do_GetService(kCacheServiceCID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return nsCacheService::GlobalInstance()->EvictEntriesInternal(nsICache::STORE_OFFLINE); + } + + nsAutoCString jaridsuffix; + jaridsuffix.Append('%'); + + nsAutoCString suffix; + oa->CreateSuffix(suffix); + jaridsuffix.Append('#'); + jaridsuffix.Append(suffix); + + AutoResetStatement statement(mStatement_EnumerateApps); + rv = statement->BindUTF8StringByIndex(0, jaridsuffix); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasRows; + rv = statement->ExecuteStep(&hasRows); + NS_ENSURE_SUCCESS(rv, rv); + + while (hasRows) { + nsAutoCString group; + rv = statement->GetUTF8String(0, group); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString clientID; + rv = statement->GetUTF8String(1, clientID); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIRunnable> ev = + new nsOfflineCacheDiscardCache(this, group, clientID); + + rv = nsCacheService::DispatchToCacheIOThread(ev); + NS_ENSURE_SUCCESS(rv, rv); + + rv = statement->ExecuteStep(&hasRows); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +namespace { // anon + +class OriginMatch final : public mozIStorageFunction +{ + ~OriginMatch() {} + mozilla::OriginAttributesPattern const mPattern; + + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEFUNCTION + explicit OriginMatch(mozilla::OriginAttributesPattern const &aPattern) + : mPattern(aPattern) {} +}; + +NS_IMPL_ISUPPORTS(OriginMatch, mozIStorageFunction) + +NS_IMETHODIMP +OriginMatch::OnFunctionCall(mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) +{ + nsresult rv; + + nsAutoCString groupId; + rv = aFunctionArguments->GetUTF8String(0, groupId); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t hash = groupId.Find(NS_LITERAL_CSTRING("#")); + if (hash == kNotFound) { + // Just ignore... + return NS_OK; + } + + ++hash; + + nsDependentCSubstring suffix(groupId.BeginReading() + hash, groupId.Length() - hash); + + mozilla::NeckoOriginAttributes oa; + bool ok = oa.PopulateFromSuffix(suffix); + NS_ENSURE_TRUE(ok, NS_ERROR_UNEXPECTED); + + bool match = mPattern.Matches(oa); + + RefPtr<nsVariant> outVar(new nsVariant()); + rv = outVar->SetAsUint32(match ? 1 : 0); + NS_ENSURE_SUCCESS(rv, rv); + + outVar.forget(aResult); + return NS_OK; +} + +} // anon + +nsresult +nsOfflineCacheDevice::Evict(mozilla::OriginAttributesPattern const &aPattern) +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + nsresult rv; + + nsCOMPtr<mozIStorageFunction> function1(new OriginMatch(aPattern)); + rv = mDB->CreateFunction(NS_LITERAL_CSTRING("ORIGIN_MATCH"), 1, function1); + NS_ENSURE_SUCCESS(rv, rv); + + class AutoRemoveFunc { + public: + mozIStorageConnection* mDB; + explicit AutoRemoveFunc(mozIStorageConnection* aDB) : mDB(aDB) {} + ~AutoRemoveFunc() { + mDB->RemoveFunction(NS_LITERAL_CSTRING("ORIGIN_MATCH")); + } + }; + AutoRemoveFunc autoRemove(mDB); + + nsCOMPtr<mozIStorageStatement> statement; + rv = mDB->CreateStatement( + NS_LITERAL_CSTRING("SELECT GroupID, ActiveClientID FROM moz_cache_groups WHERE ORIGIN_MATCH(GroupID);"), + getter_AddRefs(statement)); + NS_ENSURE_SUCCESS(rv, rv); + + AutoResetStatement statementScope(statement); + + bool hasRows; + rv = statement->ExecuteStep(&hasRows); + NS_ENSURE_SUCCESS(rv, rv); + + while (hasRows) { + nsAutoCString group; + rv = statement->GetUTF8String(0, group); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString clientID; + rv = statement->GetUTF8String(1, clientID); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIRunnable> ev = + new nsOfflineCacheDiscardCache(this, group, clientID); + + rv = nsCacheService::DispatchToCacheIOThread(ev); + NS_ENSURE_SUCCESS(rv, rv); + + rv = statement->ExecuteStep(&hasRows); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +bool +nsOfflineCacheDevice::CanUseCache(nsIURI *keyURI, + const nsACString &clientID, + nsILoadContextInfo *loadContextInfo) +{ + { + MutexAutoLock lock(mLock); + if (!mActiveCaches.Contains(clientID)) + return false; + } + + nsAutoCString groupID; + nsresult rv = GetGroupForCache(clientID, groupID); + NS_ENSURE_SUCCESS(rv, false); + + nsCOMPtr<nsIURI> groupURI; + rv = NS_NewURI(getter_AddRefs(groupURI), groupID); + if (NS_FAILED(rv)) { + return false; + } + + // When we are choosing an initial cache to load the top + // level document from, the URL of that document must have + // the same origin as the manifest, according to the spec. + // The following check is here because explicit, fallback + // and dynamic entries might have origin different from the + // manifest origin. + if (!NS_SecurityCompareURIs(keyURI, groupURI, + GetStrictFileOriginPolicy())) { + return false; + } + + // Check the groupID we found is equal to groupID based + // on the load context demanding load from app cache. + // This is check of extended origin. + + nsAutoCString originSuffix; + loadContextInfo->OriginAttributesPtr()->CreateSuffix(originSuffix); + + nsAutoCString demandedGroupID; + rv = BuildApplicationCacheGroupID(groupURI, originSuffix, demandedGroupID); + NS_ENSURE_SUCCESS(rv, false); + + if (groupID != demandedGroupID) { + return false; + } + + return true; +} + + +nsresult +nsOfflineCacheDevice::ChooseApplicationCache(const nsACString &key, + nsILoadContextInfo *loadContextInfo, + nsIApplicationCache **out) +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + NS_ENSURE_ARG(loadContextInfo); + + nsresult rv; + + *out = nullptr; + + nsCOMPtr<nsIURI> keyURI; + rv = NS_NewURI(getter_AddRefs(keyURI), key); + NS_ENSURE_SUCCESS(rv, rv); + + // First try to find a matching cache entry. + AutoResetStatement statement(mStatement_FindClient); + rv = statement->BindUTF8StringByIndex(0, key); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasRows; + rv = statement->ExecuteStep(&hasRows); + NS_ENSURE_SUCCESS(rv, rv); + + while (hasRows) { + int32_t itemType; + rv = statement->GetInt32(1, &itemType); + NS_ENSURE_SUCCESS(rv, rv); + + if (!(itemType & nsIApplicationCache::ITEM_FOREIGN)) { + nsAutoCString clientID; + rv = statement->GetUTF8String(0, clientID); + NS_ENSURE_SUCCESS(rv, rv); + + if (CanUseCache(keyURI, clientID, loadContextInfo)) { + return GetApplicationCache(clientID, out); + } + } + + rv = statement->ExecuteStep(&hasRows); + NS_ENSURE_SUCCESS(rv, rv); + } + + // OK, we didn't find an exact match. Search for a client with a + // matching namespace. + + AutoResetStatement nsstatement(mStatement_FindClientByNamespace); + + rv = nsstatement->BindUTF8StringByIndex(0, key); + NS_ENSURE_SUCCESS(rv, rv); + + rv = nsstatement->ExecuteStep(&hasRows); + NS_ENSURE_SUCCESS(rv, rv); + + while (hasRows) + { + int32_t itemType; + rv = nsstatement->GetInt32(1, &itemType); + NS_ENSURE_SUCCESS(rv, rv); + + // Don't associate with a cache based solely on a whitelist entry + if (!(itemType & nsIApplicationCacheNamespace::NAMESPACE_BYPASS)) { + nsAutoCString clientID; + rv = nsstatement->GetUTF8String(0, clientID); + NS_ENSURE_SUCCESS(rv, rv); + + if (CanUseCache(keyURI, clientID, loadContextInfo)) { + return GetApplicationCache(clientID, out); + } + } + + rv = nsstatement->ExecuteStep(&hasRows); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult +nsOfflineCacheDevice::CacheOpportunistically(nsIApplicationCache* cache, + const nsACString &key) +{ + NS_ENSURE_ARG_POINTER(cache); + + nsresult rv; + + nsAutoCString clientID; + rv = cache->GetClientID(clientID); + NS_ENSURE_SUCCESS(rv, rv); + + return CacheOpportunistically(clientID, key); +} + +nsresult +nsOfflineCacheDevice::ActivateCache(const nsCSubstring &group, + const nsCSubstring &clientID) +{ + NS_ENSURE_TRUE(Initialized(), NS_ERROR_NOT_INITIALIZED); + + AutoResetStatement statement(mStatement_ActivateClient); + nsresult rv = statement->BindUTF8StringByIndex(0, group); + NS_ENSURE_SUCCESS(rv, rv); + rv = statement->BindUTF8StringByIndex(1, clientID); + NS_ENSURE_SUCCESS(rv, rv); + rv = statement->BindInt32ByIndex(2, SecondsFromPRTime(PR_Now())); + NS_ENSURE_SUCCESS(rv, rv); + + rv = statement->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + MutexAutoLock lock(mLock); + + nsCString *active; + if (mActiveCachesByGroup.Get(group, &active)) + { + mActiveCaches.RemoveEntry(*active); + mActiveCachesByGroup.Remove(group); + active = nullptr; + } + + if (!clientID.IsEmpty()) + { + mActiveCaches.PutEntry(clientID); + mActiveCachesByGroup.Put(group, new nsCString(clientID)); + } + + return NS_OK; +} + +bool +nsOfflineCacheDevice::IsActiveCache(const nsCSubstring &group, + const nsCSubstring &clientID) +{ + nsCString *active = nullptr; + MutexAutoLock lock(mLock); + return mActiveCachesByGroup.Get(group, &active) && *active == clientID; +} + +/** + * Preference accessors + */ + +void +nsOfflineCacheDevice::SetCacheParentDirectory(nsIFile *parentDir) +{ + if (Initialized()) + { + NS_ERROR("cannot switch cache directory once initialized"); + return; + } + + if (!parentDir) + { + mCacheDirectory = nullptr; + return; + } + + // ensure parent directory exists + nsresult rv = EnsureDir(parentDir); + if (NS_FAILED(rv)) + { + NS_WARNING("unable to create parent directory"); + return; + } + + mBaseDirectory = parentDir; + + // cache dir may not exist, but that's ok + nsCOMPtr<nsIFile> dir; + rv = parentDir->Clone(getter_AddRefs(dir)); + if (NS_FAILED(rv)) + return; + rv = dir->AppendNative(NS_LITERAL_CSTRING("OfflineCache")); + if (NS_FAILED(rv)) + return; + + mCacheDirectory = do_QueryInterface(dir); +} + +void +nsOfflineCacheDevice::SetCapacity(uint32_t capacity) +{ + mCacheCapacity = capacity * 1024; +} + +bool +nsOfflineCacheDevice::AutoShutdown(nsIApplicationCache * aAppCache) +{ + if (!mAutoShutdown) + return false; + + mAutoShutdown = false; + + Shutdown(); + + nsCOMPtr<nsICacheService> serv = do_GetService(kCacheServiceCID); + RefPtr<nsCacheService> cacheService = nsCacheService::GlobalInstance(); + cacheService->RemoveCustomOfflineDevice(this); + + nsAutoCString clientID; + aAppCache->GetClientID(clientID); + + MutexAutoLock lock(mLock); + mCaches.Remove(clientID); + + return true; +} |