/* -*- 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 #include "mozilla/ArrayUtils.h" #include "mozilla/Attributes.h" #include "mozilla/Sprintf.h" #include "mozilla/ThreadLocal.h" #include "mozilla/Unused.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" }; static NS_DEFINE_CID(kCacheServiceCID, NS_CACHESERVICE_CID); #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 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 &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 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 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 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(" \n" " Cache Directory:\n" " "); 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("\n" " \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 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 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 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 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 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 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 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 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 infoObj = entry->SecurityInfo(); nsCOMPtr 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 &statement; const char *sql; StatementSql (nsCOMPtr &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 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 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 obj = do_QueryReferent(iter.UserData()); if (obj) { auto appCache = static_cast(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 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); 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 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 in; NS_NewLocalFileInputStream(getter_AddRefs(in), binding->mDataFile, PR_RDONLY); if (!in) return NS_ERROR_UNEXPECTED; // respect |offset| param if (offset != 0) { nsCOMPtr 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 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 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 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 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 info = new nsOfflineCacheEntryInfo; if (!info) return NS_ERROR_OUT_OF_MEMORY; info->mRec = &rec; // XXX may want to list columns explicitly nsCOMPtr 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 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 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(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 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(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 cache = new nsApplicationCache(this, group, clientID); if (!cache) return NS_ERROR_OUT_OF_MEMORY; nsCOMPtr 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 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 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 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 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 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 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 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 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 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 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 serv = do_GetService(kCacheServiceCID); RefPtr cacheService = nsCacheService::GlobalInstance(); cacheService->RemoveCustomOfflineDevice(this); nsAutoCString clientID; aAppCache->GetClientID(clientID); MutexAutoLock lock(mLock); mCaches.Remove(clientID); return true; }