blob: 8cf05cce1880128de59be70a99836f5a745b6d71 [file] [log] [blame]
/*
* Copyright (C) 2008, 2009, 2010, 2011 Apple Inc. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "ApplicationCacheStorage.h"
#include "ApplicationCache.h"
#include "ApplicationCacheGroup.h"
#include "ApplicationCacheHost.h"
#include "ApplicationCacheResource.h"
#include "SQLiteDatabaseTracker.h"
#include "SQLiteStatement.h"
#include "SQLiteTransaction.h"
#include "SecurityOrigin.h"
#include "SecurityOriginData.h"
#include <wtf/FileSystem.h>
#include <wtf/StdLibExtras.h>
#include <wtf/URL.h>
#include <wtf/UUID.h>
#include <wtf/text/CString.h>
#include <wtf/text/StringBuilder.h>
#include <wtf/text/StringConcatenateNumbers.h>
namespace WebCore {
template <class T>
class StorageIDJournal {
public:
~StorageIDJournal()
{
for (auto& record : m_records)
record.restore();
}
void add(T* resource, unsigned storageID)
{
m_records.append(Record(resource, storageID));
}
void commit()
{
m_records.clear();
}
private:
class Record {
public:
Record() : m_resource(nullptr), m_storageID(0) { }
Record(T* resource, unsigned storageID) : m_resource(resource), m_storageID(storageID) { }
void restore()
{
m_resource->setStorageID(m_storageID);
}
private:
T* m_resource;
unsigned m_storageID;
};
Vector<Record> m_records;
};
static unsigned urlHostHash(const URL& url)
{
StringView host = url.host();
if (host.is8Bit())
return AlreadyHashed::avoidDeletedValue(StringHasher::computeHashAndMaskTop8Bits(host.characters8(), host.length()));
return AlreadyHashed::avoidDeletedValue(StringHasher::computeHashAndMaskTop8Bits(host.characters16(), host.length()));
}
ApplicationCacheGroup* ApplicationCacheStorage::loadCacheGroup(const URL& manifestURL)
{
SQLiteTransactionInProgressAutoCounter transactionCounter;
openDatabase(false);
if (!m_database.isOpen())
return nullptr;
auto statement = m_database.prepareStatement("SELECT id, manifestURL, newestCache FROM CacheGroups WHERE newestCache IS NOT NULL AND manifestURL=?"_s);
if (!statement)
return nullptr;
statement->bindText(1, manifestURL.string());
int result = statement->step();
if (result == SQLITE_DONE)
return nullptr;
if (result != SQLITE_ROW) {
LOG_ERROR("Could not load cache group, error \"%s\"", m_database.lastErrorMsg());
return nullptr;
}
unsigned newestCacheStorageID = static_cast<unsigned>(statement->columnInt64(2));
auto cache = loadCache(newestCacheStorageID);
if (!cache)
return nullptr;
auto& group = *new ApplicationCacheGroup(*this, manifestURL);
group.setStorageID(static_cast<unsigned>(statement->columnInt64(0)));
group.setNewestCache(cache.releaseNonNull());
return &group;
}
ApplicationCacheGroup* ApplicationCacheStorage::findOrCreateCacheGroup(const URL& manifestURL)
{
ASSERT(!manifestURL.hasFragmentIdentifier());
auto result = m_cachesInMemory.add(manifestURL.string(), nullptr);
if (!result.isNewEntry) {
ASSERT(result.iterator->value);
return result.iterator->value;
}
// Look up the group in the database
auto* group = loadCacheGroup(manifestURL);
// If the group was not found we need to create it
if (!group) {
group = new ApplicationCacheGroup(*this, manifestURL);
m_cacheHostSet.add(urlHostHash(manifestURL));
}
result.iterator->value = group;
return group;
}
ApplicationCacheGroup* ApplicationCacheStorage::findInMemoryCacheGroup(const URL& manifestURL) const
{
return m_cachesInMemory.get(manifestURL.string());
}
void ApplicationCacheStorage::loadManifestHostHashes()
{
static bool hasLoadedHashes = false;
if (hasLoadedHashes)
return;
// We set this flag to true before the database has been opened
// to avoid trying to open the database over and over if it doesn't exist.
hasLoadedHashes = true;
SQLiteTransactionInProgressAutoCounter transactionCounter;
openDatabase(false);
if (!m_database.isOpen())
return;
// Fetch the host hashes.
auto statement = m_database.prepareStatement("SELECT manifestHostHash FROM CacheGroups"_s);
if (!statement)
return;
while (statement->step() == SQLITE_ROW)
m_cacheHostSet.add(static_cast<unsigned>(statement->columnInt64(0)));
}
ApplicationCacheGroup* ApplicationCacheStorage::cacheGroupForURL(const URL& url)
{
ASSERT(!url.hasFragmentIdentifier());
loadManifestHostHashes();
// Hash the host name and see if there's a manifest with the same host.
if (!m_cacheHostSet.contains(urlHostHash(url)))
return nullptr;
// Check if a cache already exists in memory.
for (const auto& group : m_cachesInMemory.values()) {
ASSERT(!group->isObsolete());
if (!protocolHostAndPortAreEqual(url, group->manifestURL()))
continue;
if (ApplicationCache* cache = group->newestCache()) {
ApplicationCacheResource* resource = cache->resourceForURL(url.string());
if (!resource)
continue;
if (resource->type() & ApplicationCacheResource::Foreign)
continue;
return group;
}
}
if (!m_database.isOpen())
return nullptr;
SQLiteTransactionInProgressAutoCounter transactionCounter;
// Check the database. Look for all cache groups with a newest cache.
auto statement = m_database.prepareStatement("SELECT id, manifestURL, newestCache FROM CacheGroups WHERE newestCache IS NOT NULL"_s);
if (!statement)
return nullptr;
int result;
while ((result = statement->step()) == SQLITE_ROW) {
URL manifestURL = URL({ }, statement->columnText(1));
if (m_cachesInMemory.contains(manifestURL.string()))
continue;
if (!protocolHostAndPortAreEqual(url, manifestURL))
continue;
// We found a cache group that matches. Now check if the newest cache has a resource with
// a matching URL.
unsigned newestCacheID = static_cast<unsigned>(statement->columnInt64(2));
auto cache = loadCache(newestCacheID);
if (!cache)
continue;
auto* resource = cache->resourceForURL(url.string());
if (!resource)
continue;
if (resource->type() & ApplicationCacheResource::Foreign)
continue;
auto& group = *new ApplicationCacheGroup(*this, manifestURL);
group.setStorageID(static_cast<unsigned>(statement->columnInt64(0)));
group.setNewestCache(cache.releaseNonNull());
m_cachesInMemory.set(group.manifestURL().string(), &group);
return &group;
}
if (result != SQLITE_DONE)
LOG_ERROR("Could not load cache group, error \"%s\"", m_database.lastErrorMsg());
return nullptr;
}
ApplicationCacheGroup* ApplicationCacheStorage::fallbackCacheGroupForURL(const URL& url)
{
SQLiteTransactionInProgressAutoCounter transactionCounter;
ASSERT(!url.hasFragmentIdentifier());
// Check if an appropriate cache already exists in memory.
for (auto* group : m_cachesInMemory.values()) {
ASSERT(!group->isObsolete());
if (ApplicationCache* cache = group->newestCache()) {
URL fallbackURL;
if (cache->isURLInOnlineAllowlist(url))
continue;
if (!cache->urlMatchesFallbackNamespace(url, &fallbackURL))
continue;
if (cache->resourceForURL(fallbackURL.string())->type() & ApplicationCacheResource::Foreign)
continue;
return group;
}
}
if (!m_database.isOpen())
return nullptr;
// Check the database. Look for all cache groups with a newest cache.
auto statement = m_database.prepareStatement("SELECT id, manifestURL, newestCache FROM CacheGroups WHERE newestCache IS NOT NULL"_s);
if (!statement)
return nullptr;
int result;
while ((result = statement->step()) == SQLITE_ROW) {
URL manifestURL = URL({ }, statement->columnText(1));
if (m_cachesInMemory.contains(manifestURL.string()))
continue;
// Fallback namespaces always have the same origin as manifest URL, so we can avoid loading caches that cannot match.
if (!protocolHostAndPortAreEqual(url, manifestURL))
continue;
// We found a cache group that matches. Now check if the newest cache has a resource with
// a matching fallback namespace.
unsigned newestCacheID = static_cast<unsigned>(statement->columnInt64(2));
auto cache = loadCache(newestCacheID);
URL fallbackURL;
if (cache->isURLInOnlineAllowlist(url))
continue;
if (!cache->urlMatchesFallbackNamespace(url, &fallbackURL))
continue;
if (cache->resourceForURL(fallbackURL.string())->type() & ApplicationCacheResource::Foreign)
continue;
auto& group = *new ApplicationCacheGroup(*this, manifestURL);
group.setStorageID(static_cast<unsigned>(statement->columnInt64(0)));
group.setNewestCache(cache.releaseNonNull());
m_cachesInMemory.set(group.manifestURL().string(), &group);
return &group;
}
if (result != SQLITE_DONE)
LOG_ERROR("Could not load cache group, error \"%s\"", m_database.lastErrorMsg());
return nullptr;
}
void ApplicationCacheStorage::cacheGroupDestroyed(ApplicationCacheGroup& group)
{
if (group.isObsolete()) {
ASSERT(!group.storageID());
ASSERT(m_cachesInMemory.get(group.manifestURL().string()) != &group);
return;
}
ASSERT(m_cachesInMemory.get(group.manifestURL().string()) == &group);
m_cachesInMemory.remove(group.manifestURL().string());
// If the cache group is half-created, we don't want it in the saved set (as it is not stored in database).
if (!group.storageID())
m_cacheHostSet.remove(urlHostHash(group.manifestURL()));
}
void ApplicationCacheStorage::cacheGroupMadeObsolete(ApplicationCacheGroup& group)
{
ASSERT(m_cachesInMemory.get(group.manifestURL().string()) == &group);
ASSERT(m_cacheHostSet.contains(urlHostHash(group.manifestURL())));
if (auto* newestCache = group.newestCache())
remove(newestCache);
m_cachesInMemory.remove(group.manifestURL().string());
m_cacheHostSet.remove(urlHostHash(group.manifestURL()));
}
void ApplicationCacheStorage::setMaximumSize(int64_t size)
{
m_maximumSize = size;
}
int64_t ApplicationCacheStorage::maximumSize() const
{
return m_maximumSize;
}
bool ApplicationCacheStorage::isMaximumSizeReached() const
{
return m_isMaximumSizeReached;
}
int64_t ApplicationCacheStorage::spaceNeeded(int64_t cacheToSave)
{
int64_t spaceNeeded = 0;
auto fileSize = FileSystem::fileSize(m_cacheFile);
if (!fileSize)
return 0;
int64_t currentSize = *fileSize + flatFileAreaSize();
// Determine the amount of free space we have available.
int64_t totalAvailableSize = 0;
if (m_maximumSize < currentSize) {
// The max size is smaller than the actual size of the app cache file.
// This can happen if the client previously imposed a larger max size
// value and the app cache file has already grown beyond the current
// max size value.
// The amount of free space is just the amount of free space inside
// the database file. Note that this is always 0 if SQLite is compiled
// with AUTO_VACUUM = 1.
totalAvailableSize = m_database.freeSpaceSize();
} else {
// The max size is the same or larger than the current size.
// The amount of free space available is the amount of free space
// inside the database file plus the amount we can grow until we hit
// the max size.
totalAvailableSize = (m_maximumSize - currentSize) + m_database.freeSpaceSize();
}
// The space needed to be freed in order to accommodate the failed cache is
// the size of the failed cache minus any already available free space.
spaceNeeded = cacheToSave - totalAvailableSize;
// The space needed value must be positive (or else the total already
// available free space would be larger than the size of the failed cache and
// saving of the cache should have never failed).
ASSERT(spaceNeeded);
return spaceNeeded;
}
void ApplicationCacheStorage::setDefaultOriginQuota(int64_t quota)
{
m_defaultOriginQuota = quota;
}
bool ApplicationCacheStorage::calculateQuotaForOrigin(const SecurityOrigin& origin, int64_t& quota)
{
SQLiteTransactionInProgressAutoCounter transactionCounter;
// If an Origin record doesn't exist, then the COUNT will be 0 and quota will be 0.
// Using the count to determine if a record existed or not is a safe way to determine
// if a quota of 0 is real, from the record, or from null.
auto statement = m_database.prepareStatement("SELECT COUNT(quota), quota FROM Origins WHERE origin=?"_s);
if (!statement)
return false;
statement->bindText(1, origin.data().databaseIdentifier());
int result = statement->step();
// Return the quota, or if it was null the default.
if (result == SQLITE_ROW) {
bool wasNoRecord = !statement->columnInt64(0);
quota = wasNoRecord ? m_defaultOriginQuota : statement->columnInt64(1);
return true;
}
LOG_ERROR("Could not get the quota of an origin, error \"%s\"", m_database.lastErrorMsg());
return false;
}
bool ApplicationCacheStorage::calculateUsageForOrigin(const SecurityOriginData& origin, int64_t& usage)
{
SQLiteTransactionInProgressAutoCounter transactionCounter;
// If an Origins record doesn't exist, then the SUM will be null,
// which will become 0, as expected, when converting to a number.
auto statement = m_database.prepareStatement("SELECT SUM(Caches.size)"
" FROM CacheGroups"
" INNER JOIN Origins ON CacheGroups.origin = Origins.origin"
" INNER JOIN Caches ON CacheGroups.id = Caches.cacheGroup"
" WHERE Origins.origin=?"_s);
if (!statement)
return false;
statement->bindText(1, origin.databaseIdentifier());
int result = statement->step();
if (result == SQLITE_ROW) {
usage = statement->columnInt64(0);
return true;
}
LOG_ERROR("Could not get the quota of an origin, error \"%s\"", m_database.lastErrorMsg());
return false;
}
bool ApplicationCacheStorage::calculateRemainingSizeForOriginExcludingCache(const SecurityOrigin& origin, ApplicationCache* cache, int64_t& remainingSize)
{
SQLiteTransactionInProgressAutoCounter transactionCounter;
openDatabase(false);
if (!m_database.isOpen())
return false;
// Remaining size = total origin quota - size of all caches with origin excluding the provided cache.
// Keep track of the number of caches so we can tell if the result was a calculation or not.
int64_t excludingCacheIdentifier = cache ? cache->storageID() : 0;
auto query = [excludingCacheIdentifier]() {
if (excludingCacheIdentifier) {
return "SELECT COUNT(Caches.size), Origins.quota - SUM(Caches.size)"
" FROM CacheGroups"
" INNER JOIN Origins ON CacheGroups.origin = Origins.origin"
" INNER JOIN Caches ON CacheGroups.id = Caches.cacheGroup"
" WHERE Origins.origin=?"
" AND Caches.id!=?"_s;
}
return "SELECT COUNT(Caches.size), Origins.quota - SUM(Caches.size)"
" FROM CacheGroups"
" INNER JOIN Origins ON CacheGroups.origin = Origins.origin"
" INNER JOIN Caches ON CacheGroups.id = Caches.cacheGroup"
" WHERE Origins.origin=?"_s;
}();
auto statement = m_database.prepareStatement(query);
if (!statement)
return false;
statement->bindText(1, origin.data().databaseIdentifier());
if (excludingCacheIdentifier != 0)
statement->bindInt64(2, excludingCacheIdentifier);
int result = statement->step();
// If the count was 0 that then we have to query the origin table directly
// for its quota. Otherwise we can use the calculated value.
if (result == SQLITE_ROW) {
int64_t numberOfCaches = statement->columnInt64(0);
if (numberOfCaches == 0)
calculateQuotaForOrigin(origin, remainingSize);
else
remainingSize = statement->columnInt64(1);
return true;
}
LOG_ERROR("Could not get the remaining size of an origin's quota, error \"%s\"", m_database.lastErrorMsg());
return false;
}
bool ApplicationCacheStorage::storeUpdatedQuotaForOrigin(const SecurityOrigin* origin, int64_t quota)
{
SQLiteTransactionInProgressAutoCounter transactionCounter;
openDatabase(true);
if (!m_database.isOpen())
return false;
if (!ensureOriginRecord(origin))
return false;
auto updateStatement = m_database.prepareStatement("UPDATE Origins SET quota=? WHERE origin=?"_s);
if (!updateStatement)
return false;
updateStatement->bindInt64(1, quota);
updateStatement->bindText(2, origin->data().databaseIdentifier());
return executeStatement(updateStatement.value());
}
bool ApplicationCacheStorage::executeSQLCommand(ASCIILiteral sql)
{
ASSERT(SQLiteDatabaseTracker::hasTransactionInProgress());
ASSERT(m_database.isOpen());
bool result = m_database.executeCommand(sql);
if (!result)
LOG_ERROR("Application Cache Storage: failed to execute statement \"%s\" error \"%s\"", sql.characters(), m_database.lastErrorMsg());
return result;
}
// Update the schemaVersion when the schema of any the Application Cache
// SQLite tables changes. This allows the database to be rebuilt when
// a new, incompatible change has been introduced to the database schema.
static const int schemaVersion = 7;
void ApplicationCacheStorage::verifySchemaVersion()
{
ASSERT(SQLiteDatabaseTracker::hasTransactionInProgress());
auto versionStatement = m_database.prepareStatement("PRAGMA user_version"_s);
int version = versionStatement ? versionStatement->columnInt(0) : 0;
if (version == schemaVersion)
return;
// Version will be 0 if we just created an empty file. Trying to delete tables would cause errors, because they don't exist yet.
if (version)
deleteTables();
// Update user version.
SQLiteTransaction setDatabaseVersion(m_database);
setDatabaseVersion.begin();
auto statement = m_database.prepareStatementSlow(makeString("PRAGMA user_version=%", schemaVersion));
if (!statement)
return;
executeStatement(statement.value());
setDatabaseVersion.commit();
}
void ApplicationCacheStorage::openDatabase(bool createIfDoesNotExist)
{
SQLiteTransactionInProgressAutoCounter transactionCounter;
if (m_database.isOpen())
return;
// The cache directory should never be null, but if it for some weird reason is we bail out.
if (m_cacheDirectory.isNull())
return;
m_cacheFile = FileSystem::pathByAppendingComponent(m_cacheDirectory, "ApplicationCache.db");
if (!createIfDoesNotExist && !FileSystem::fileExists(m_cacheFile))
return;
FileSystem::makeAllDirectories(m_cacheDirectory);
m_database.open(m_cacheFile);
if (!m_database.isOpen())
return;
verifySchemaVersion();
// Create tables
executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheGroups (id INTEGER PRIMARY KEY AUTOINCREMENT, "
"manifestHostHash INTEGER NOT NULL ON CONFLICT FAIL, manifestURL TEXT UNIQUE ON CONFLICT FAIL, newestCache INTEGER, origin TEXT)"_s);
executeSQLCommand("CREATE TABLE IF NOT EXISTS Caches (id INTEGER PRIMARY KEY AUTOINCREMENT, cacheGroup INTEGER, size INTEGER)"_s);
executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheWhitelistURLs (url TEXT NOT NULL ON CONFLICT FAIL, cache INTEGER NOT NULL ON CONFLICT FAIL)"_s);
executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheAllowsAllNetworkRequests (wildcard INTEGER NOT NULL ON CONFLICT FAIL, cache INTEGER NOT NULL ON CONFLICT FAIL)"_s);
executeSQLCommand("CREATE TABLE IF NOT EXISTS FallbackURLs (namespace TEXT NOT NULL ON CONFLICT FAIL, fallbackURL TEXT NOT NULL ON CONFLICT FAIL, "
"cache INTEGER NOT NULL ON CONFLICT FAIL)"_s);
executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheEntries (cache INTEGER NOT NULL ON CONFLICT FAIL, type INTEGER, resource INTEGER NOT NULL)"_s);
executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheResources (id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL ON CONFLICT FAIL, "
"statusCode INTEGER NOT NULL, responseURL TEXT NOT NULL, mimeType TEXT, textEncodingName TEXT, headers TEXT, data INTEGER NOT NULL ON CONFLICT FAIL)"_s);
executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheResourceData (id INTEGER PRIMARY KEY AUTOINCREMENT, data BLOB, path TEXT)"_s);
executeSQLCommand("CREATE TABLE IF NOT EXISTS DeletedCacheResources (id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT)"_s);
executeSQLCommand("CREATE TABLE IF NOT EXISTS Origins (origin TEXT UNIQUE ON CONFLICT IGNORE, quota INTEGER NOT NULL ON CONFLICT FAIL)"_s);
// When a cache is deleted, all its entries and its allowlist should be deleted.
executeSQLCommand("CREATE TRIGGER IF NOT EXISTS CacheDeleted AFTER DELETE ON Caches"
" FOR EACH ROW BEGIN"
" DELETE FROM CacheEntries WHERE cache = OLD.id;"
" DELETE FROM CacheWhitelistURLs WHERE cache = OLD.id;"
" DELETE FROM CacheAllowsAllNetworkRequests WHERE cache = OLD.id;"
" DELETE FROM FallbackURLs WHERE cache = OLD.id;"
" END"_s);
// When a cache entry is deleted, its resource should also be deleted.
executeSQLCommand("CREATE TRIGGER IF NOT EXISTS CacheEntryDeleted AFTER DELETE ON CacheEntries"
" FOR EACH ROW BEGIN"
" DELETE FROM CacheResources WHERE id = OLD.resource;"
" END"_s);
// When a cache resource is deleted, its data blob should also be deleted.
executeSQLCommand("CREATE TRIGGER IF NOT EXISTS CacheResourceDeleted AFTER DELETE ON CacheResources"
" FOR EACH ROW BEGIN"
" DELETE FROM CacheResourceData WHERE id = OLD.data;"
" END"_s);
// When a cache resource is deleted, if it contains a non-empty path, that path should
// be added to the DeletedCacheResources table so the flat file at that path can
// be deleted at a later time.
executeSQLCommand("CREATE TRIGGER IF NOT EXISTS CacheResourceDataDeleted AFTER DELETE ON CacheResourceData"
" FOR EACH ROW"
" WHEN OLD.path NOT NULL BEGIN"
" INSERT INTO DeletedCacheResources (path) values (OLD.path);"
" END"_s);
}
bool ApplicationCacheStorage::executeStatement(SQLiteStatement& statement)
{
ASSERT(SQLiteDatabaseTracker::hasTransactionInProgress());
bool result = statement.executeCommand();
if (!result)
LOG_ERROR("Application Cache Storage: failed to execute statement, error \"%s\"", m_database.lastErrorMsg());
return result;
}
bool ApplicationCacheStorage::store(ApplicationCacheGroup* group, GroupStorageIDJournal* journal)
{
ASSERT(SQLiteDatabaseTracker::hasTransactionInProgress());
ASSERT(group->storageID() == 0);
ASSERT(journal);
// For some reason, an app cache may be partially written to disk. In particular, there may be
// a cache group with an identical manifest URL and associated cache entries. We want to remove
// this cache group and its associated cache entries so that we can create it again (below) as
// a way to repair it.
deleteCacheGroupRecord(group->manifestURL().string());
auto statement = m_database.prepareStatement("INSERT INTO CacheGroups (manifestHostHash, manifestURL, origin) VALUES (?, ?, ?)"_s);
if (!statement)
return false;
statement->bindInt64(1, urlHostHash(group->manifestURL()));
statement->bindText(2, group->manifestURL().string());
statement->bindText(3, group->origin().data().databaseIdentifier());
if (!executeStatement(statement.value()))
return false;
unsigned groupStorageID = static_cast<unsigned>(m_database.lastInsertRowID());
if (!ensureOriginRecord(&group->origin()))
return false;
group->setStorageID(groupStorageID);
journal->add(group, 0);
return true;
}
bool ApplicationCacheStorage::store(ApplicationCache* cache, ResourceStorageIDJournal* storageIDJournal)
{
ASSERT(SQLiteDatabaseTracker::hasTransactionInProgress());
ASSERT(cache->storageID() == 0);
ASSERT(cache->group()->storageID() != 0);
ASSERT(storageIDJournal);
auto statement = m_database.prepareStatement("INSERT INTO Caches (cacheGroup, size) VALUES (?, ?)"_s);
if (!statement)
return false;
statement->bindInt64(1, cache->group()->storageID());
statement->bindInt64(2, cache->estimatedSizeInStorage());
if (!executeStatement(statement.value()))
return false;
unsigned cacheStorageID = static_cast<unsigned>(m_database.lastInsertRowID());
// Store all resources
for (auto& resource : cache->resources().values()) {
unsigned oldStorageID = resource->storageID();
if (!store(resource.get(), cacheStorageID))
return false;
// Storing the resource succeeded. Log its old storageID in case
// it needs to be restored later.
storageIDJournal->add(resource.get(), oldStorageID);
}
// Store the online allowlist
const Vector<URL>& onlineAllowlist = cache->onlineAllowlist();
{
for (auto& allowlistURL : onlineAllowlist) {
auto statement = m_database.prepareStatement("INSERT INTO CacheWhitelistURLs (url, cache) VALUES (?, ?)"_s);
if (!statement)
return false;
statement->bindText(1, allowlistURL.string());
statement->bindInt64(2, cacheStorageID);
if (!executeStatement(statement.value()))
return false;
}
}
// Store online allowlist wildcard flag.
{
auto statement = m_database.prepareStatement("INSERT INTO CacheAllowsAllNetworkRequests (wildcard, cache) VALUES (?, ?)"_s);
if (!statement)
return false;
statement->bindInt64(1, cache->allowsAllNetworkRequests());
statement->bindInt64(2, cacheStorageID);
if (!executeStatement(statement.value()))
return false;
}
// Store fallback URLs.
const FallbackURLVector& fallbackURLs = cache->fallbackURLs();
{
for (auto& fallbackURL : fallbackURLs) {
auto statement = m_database.prepareStatement("INSERT INTO FallbackURLs (namespace, fallbackURL, cache) VALUES (?, ?, ?)"_s);
if (!statement)
return false;
statement->bindText(1, fallbackURL.first.string());
statement->bindText(2, fallbackURL.second.string());
statement->bindInt64(3, cacheStorageID);
if (!executeStatement(statement.value()))
return false;
}
}
cache->setStorageID(cacheStorageID);
return true;
}
bool ApplicationCacheStorage::store(ApplicationCacheResource* resource, unsigned cacheStorageID)
{
ASSERT(SQLiteDatabaseTracker::hasTransactionInProgress());
ASSERT(cacheStorageID);
ASSERT(!resource->storageID());
openDatabase(true);
// openDatabase(true) could still fail, for example when cacheStorage is full or no longer available.
if (!m_database.isOpen())
return false;
// First, insert the data
auto dataStatement = m_database.prepareStatement("INSERT INTO CacheResourceData (data, path) VALUES (?, ?)"_s);
if (!dataStatement)
return false;
String fullPath;
if (!resource->path().isEmpty())
dataStatement->bindText(2, FileSystem::pathFileName(resource->path()));
else if (shouldStoreResourceAsFlatFile(resource)) {
// First, check to see if creating the flat file would violate the maximum total quota. We don't need
// to check the per-origin quota here, as it was already checked in storeNewestCache().
if (m_database.totalSize() + flatFileAreaSize() + static_cast<int64_t>(resource->data().size()) > m_maximumSize) {
m_isMaximumSizeReached = true;
return false;
}
String flatFileDirectory = FileSystem::pathByAppendingComponent(m_cacheDirectory, m_flatFileSubdirectoryName);
FileSystem::makeAllDirectories(flatFileDirectory);
String extension;
String fileName = resource->response().suggestedFilename();
size_t dotIndex = fileName.reverseFind('.');
if (dotIndex != notFound && dotIndex < (fileName.length() - 1))
extension = fileName.substring(dotIndex);
String path;
if (!writeDataToUniqueFileInDirectory(resource->data(), flatFileDirectory, path, extension))
return false;
fullPath = FileSystem::pathByAppendingComponent(flatFileDirectory, path);
resource->setPath(fullPath);
dataStatement->bindText(2, path);
} else {
if (resource->data().size())
dataStatement->bindBlob(1, resource->data().makeContiguous().get());
}
if (!dataStatement->executeCommand()) {
// Clean up the file which we may have written to:
if (!fullPath.isEmpty())
FileSystem::deleteFile(fullPath);
return false;
}
unsigned dataId = static_cast<unsigned>(m_database.lastInsertRowID());
// Then, insert the resource
// Serialize the headers
StringBuilder stringBuilder;
for (const auto& header : resource->response().httpHeaderFields()) {
stringBuilder.append(header.key);
stringBuilder.append(':');
stringBuilder.append(header.value);
stringBuilder.append('\n');
}
String headers = stringBuilder.toString();
auto resourceStatement = m_database.prepareStatement("INSERT INTO CacheResources (url, statusCode, responseURL, headers, data, mimeType, textEncodingName) VALUES (?, ?, ?, ?, ?, ?, ?)"_s);
if (!resourceStatement)
return false;
// The same ApplicationCacheResource are used in ApplicationCacheResource::size()
// to calculate the approximate size of an ApplicationCacheResource object. If
// you change the code below, please also change ApplicationCacheResource::size().
resourceStatement->bindText(1, resource->url().string());
resourceStatement->bindInt64(2, resource->response().httpStatusCode());
resourceStatement->bindText(3, resource->response().url().string());
resourceStatement->bindText(4, headers);
resourceStatement->bindInt64(5, dataId);
resourceStatement->bindText(6, resource->response().mimeType());
resourceStatement->bindText(7, resource->response().textEncodingName());
if (!executeStatement(resourceStatement.value()))
return false;
unsigned resourceId = static_cast<unsigned>(m_database.lastInsertRowID());
// Finally, insert the cache entry
auto entryStatement = m_database.prepareStatement("INSERT INTO CacheEntries (cache, type, resource) VALUES (?, ?, ?)"_s);
if (!entryStatement)
return false;
entryStatement->bindInt64(1, cacheStorageID);
entryStatement->bindInt64(2, resource->type());
entryStatement->bindInt64(3, resourceId);
if (!executeStatement(entryStatement.value()))
return false;
// Did we successfully write the resource data to a file? If so,
// release the resource's data and free up a potentially large amount
// of memory:
if (!fullPath.isEmpty())
resource->clear();
resource->setStorageID(resourceId);
return true;
}
bool ApplicationCacheStorage::storeUpdatedType(ApplicationCacheResource* resource, ApplicationCache* cache)
{
SQLiteTransactionInProgressAutoCounter transactionCounter;
ASSERT_UNUSED(cache, cache->storageID());
ASSERT(resource->storageID());
// First, insert the data
auto entryStatement = m_database.prepareStatement("UPDATE CacheEntries SET type=? WHERE resource=?"_s);
if (!entryStatement)
return false;
entryStatement->bindInt64(1, resource->type());
entryStatement->bindInt64(2, resource->storageID());
return executeStatement(entryStatement.value());
}
bool ApplicationCacheStorage::store(ApplicationCacheResource* resource, ApplicationCache* cache)
{
SQLiteTransactionInProgressAutoCounter transactionCounter;
ASSERT(cache->storageID());
openDatabase(true);
if (!m_database.isOpen())
return false;
m_isMaximumSizeReached = false;
m_database.setMaximumSize(m_maximumSize - flatFileAreaSize());
SQLiteTransaction storeResourceTransaction(m_database);
storeResourceTransaction.begin();
if (!store(resource, cache->storageID())) {
checkForMaxSizeReached();
return false;
}
// A resource was added to the cache. Update the total data size for the cache.
auto sizeUpdateStatement = m_database.prepareStatement("UPDATE Caches SET size=size+? WHERE id=?"_s);
if (!sizeUpdateStatement)
return false;
sizeUpdateStatement->bindInt64(1, resource->estimatedSizeInStorage());
sizeUpdateStatement->bindInt64(2, cache->storageID());
if (!executeStatement(sizeUpdateStatement.value()))
return false;
storeResourceTransaction.commit();
return true;
}
bool ApplicationCacheStorage::ensureOriginRecord(const SecurityOrigin* origin)
{
ASSERT(SQLiteDatabaseTracker::hasTransactionInProgress());
auto insertOriginStatement = m_database.prepareStatement("INSERT INTO Origins (origin, quota) VALUES (?, ?)"_s);
if (!insertOriginStatement)
return false;
insertOriginStatement->bindText(1, origin->data().databaseIdentifier());
insertOriginStatement->bindInt64(2, m_defaultOriginQuota);
if (!executeStatement(insertOriginStatement.value()))
return false;
return true;
}
bool ApplicationCacheStorage::checkOriginQuota(ApplicationCacheGroup* group, ApplicationCache* oldCache, ApplicationCache* newCache, int64_t& totalSpaceNeeded)
{
// Check if the oldCache with the newCache would reach the per-origin quota.
int64_t remainingSpaceInOrigin;
auto& origin = group->origin();
if (calculateRemainingSizeForOriginExcludingCache(origin, oldCache, remainingSpaceInOrigin)) {
if (remainingSpaceInOrigin < newCache->estimatedSizeInStorage()) {
int64_t quota;
if (calculateQuotaForOrigin(origin, quota)) {
totalSpaceNeeded = quota - remainingSpaceInOrigin + newCache->estimatedSizeInStorage();
return false;
}
ASSERT_NOT_REACHED();
totalSpaceNeeded = 0;
return false;
}
}
return true;
}
bool ApplicationCacheStorage::storeNewestCache(ApplicationCacheGroup& group, ApplicationCache* oldCache, FailureReason& failureReason)
{
openDatabase(true);
if (!m_database.isOpen())
return false;
m_isMaximumSizeReached = false;
m_database.setMaximumSize(m_maximumSize - flatFileAreaSize());
SQLiteTransaction storeCacheTransaction(m_database);
storeCacheTransaction.begin();
// Check if this would reach the per-origin quota.
int64_t totalSpaceNeededIgnored;
if (!checkOriginQuota(&group, oldCache, group.newestCache(), totalSpaceNeededIgnored)) {
failureReason = OriginQuotaReached;
return false;
}
GroupStorageIDJournal groupStorageIDJournal;
if (!group.storageID()) {
// Store the group
if (!store(&group, &groupStorageIDJournal)) {
checkForMaxSizeReached();
failureReason = isMaximumSizeReached() ? TotalQuotaReached : DiskOrOperationFailure;
return false;
}
}
ASSERT(group.newestCache());
ASSERT(!group.isObsolete());
ASSERT(!group.newestCache()->storageID());
// Log the storageID changes to the in-memory resource objects. The journal
// object will roll them back automatically in case a database operation
// fails and this method returns early.
ResourceStorageIDJournal resourceStorageIDJournal;
// Store the newest cache
if (!store(group.newestCache(), &resourceStorageIDJournal)) {
checkForMaxSizeReached();
failureReason = isMaximumSizeReached() ? TotalQuotaReached : DiskOrOperationFailure;
return false;
}
// Update the newest cache in the group.
auto statement = m_database.prepareStatement("UPDATE CacheGroups SET newestCache=? WHERE id=?"_s);
if (!statement) {
failureReason = DiskOrOperationFailure;
return false;
}
statement->bindInt64(1, group.newestCache()->storageID());
statement->bindInt64(2, group.storageID());
if (!executeStatement(statement.value())) {
failureReason = DiskOrOperationFailure;
return false;
}
groupStorageIDJournal.commit();
resourceStorageIDJournal.commit();
storeCacheTransaction.commit();
return true;
}
bool ApplicationCacheStorage::storeNewestCache(ApplicationCacheGroup& group)
{
// Ignore the reason for failing, just attempt the store.
FailureReason ignoredFailureReason;
return storeNewestCache(group, nullptr, ignoredFailureReason);
}
template<typename CharacterType>
static inline void parseHeader(const CharacterType* header, unsigned headerLength, ResourceResponse& response)
{
ASSERT(find(header, headerLength, ':') != notFound);
unsigned colonPosition = find(header, headerLength, ':');
// Save memory by putting the header names into atom strings so each is stored only once,
// even though the setHTTPHeaderField function does not require an atom string.
AtomString headerName { header, colonPosition };
String headerValue { header + colonPosition + 1, headerLength - colonPosition - 1 };
response.setHTTPHeaderField(headerName, headerValue);
}
static inline void parseHeaders(const String& headers, ResourceResponse& response)
{
unsigned startPos = 0;
size_t endPos;
while ((endPos = headers.find('\n', startPos)) != notFound) {
ASSERT(startPos != endPos);
if (headers.is8Bit())
parseHeader(headers.characters8() + startPos, endPos - startPos, response);
else
parseHeader(headers.characters16() + startPos, endPos - startPos, response);
startPos = endPos + 1;
}
if (startPos != headers.length()) {
if (headers.is8Bit())
parseHeader(headers.characters8(), headers.length(), response);
else
parseHeader(headers.characters16(), headers.length(), response);
}
}
RefPtr<ApplicationCache> ApplicationCacheStorage::loadCache(unsigned storageID)
{
ASSERT(SQLiteDatabaseTracker::hasTransactionInProgress());
auto cacheStatement = m_database.prepareStatement(
"SELECT url, statusCode, type, mimeType, textEncodingName, headers, CacheResourceData.data, CacheResourceData.path FROM CacheEntries INNER JOIN CacheResources ON CacheEntries.resource=CacheResources.id "
"INNER JOIN CacheResourceData ON CacheResourceData.id=CacheResources.data WHERE CacheEntries.cache=?"_s);
if (!cacheStatement) {
LOG_ERROR("Could not prepare cache statement, error \"%s\"", m_database.lastErrorMsg());
return nullptr;
}
cacheStatement->bindInt64(1, storageID);
auto cache = ApplicationCache::create();
String flatFileDirectory = FileSystem::pathByAppendingComponent(m_cacheDirectory, m_flatFileSubdirectoryName);
int result;
while ((result = cacheStatement->step()) == SQLITE_ROW) {
URL url({ }, cacheStatement->columnText(0));
int httpStatusCode = cacheStatement->columnInt(1);
unsigned type = static_cast<unsigned>(cacheStatement->columnInt64(2));
auto data = SharedBuffer::create(cacheStatement->columnBlob(6));
String path = cacheStatement->columnText(7);
long long size = 0;
if (path.isEmpty())
size = data->size();
else {
path = FileSystem::pathByAppendingComponent(flatFileDirectory, path);
size = FileSystem::fileSize(path).value_or(0);
}
String mimeType = cacheStatement->columnText(3);
String textEncodingName = cacheStatement->columnText(4);
ResourceResponse response(url, mimeType, size, textEncodingName);
response.setHTTPStatusCode(httpStatusCode);
String headers = cacheStatement->columnText(5);
parseHeaders(headers, response);
auto resource = ApplicationCacheResource::create(url, response, type, WTFMove(data), path);
if (type & ApplicationCacheResource::Manifest)
cache->setManifestResource(WTFMove(resource));
else
cache->addResource(WTFMove(resource));
}
if (result != SQLITE_DONE)
LOG_ERROR("Could not load cache resources, error \"%s\"", m_database.lastErrorMsg());
if (!cache->manifestResource()) {
LOG_ERROR("Could not load application cache because there was no manifest resource");
return nullptr;
}
// Load the online allowlist
auto allowlistStatement = m_database.prepareStatement("SELECT url FROM CacheWhitelistURLs WHERE cache=?"_s);
if (!allowlistStatement)
return nullptr;
allowlistStatement->bindInt64(1, storageID);
Vector<URL> allowlist;
while ((result = allowlistStatement->step()) == SQLITE_ROW)
allowlist.append(URL({ }, allowlistStatement->columnText(0)));
if (result != SQLITE_DONE)
LOG_ERROR("Could not load cache online allowlist, error \"%s\"", m_database.lastErrorMsg());
cache->setOnlineAllowlist(allowlist);
// Load online allowlist wildcard flag.
auto allowlistWildcardStatement = m_database.prepareStatement("SELECT wildcard FROM CacheAllowsAllNetworkRequests WHERE cache=?"_s);
if (!allowlistWildcardStatement)
return nullptr;
allowlistWildcardStatement->bindInt64(1, storageID);
result = allowlistWildcardStatement->step();
if (result != SQLITE_ROW)
LOG_ERROR("Could not load cache online allowlist wildcard flag, error \"%s\"", m_database.lastErrorMsg());
cache->setAllowsAllNetworkRequests(allowlistWildcardStatement->columnInt64(0));
if (allowlistWildcardStatement->step() != SQLITE_DONE)
LOG_ERROR("Too many rows for online allowlist wildcard flag");
// Load fallback URLs.
auto fallbackStatement = m_database.prepareStatement("SELECT namespace, fallbackURL FROM FallbackURLs WHERE cache=?"_s);
if (!fallbackStatement)
return nullptr;
fallbackStatement->bindInt64(1, storageID);
FallbackURLVector fallbackURLs;
while ((result = fallbackStatement->step()) == SQLITE_ROW)
fallbackURLs.append(std::make_pair(URL({ }, fallbackStatement->columnText(0)), URL({ }, fallbackStatement->columnText(1))));
if (result != SQLITE_DONE)
LOG_ERROR("Could not load fallback URLs, error \"%s\"", m_database.lastErrorMsg());
cache->setFallbackURLs(fallbackURLs);
cache->setStorageID(storageID);
return cache;
}
void ApplicationCacheStorage::remove(ApplicationCache* cache)
{
SQLiteTransactionInProgressAutoCounter transactionCounter;
if (!cache->storageID())
return;
openDatabase(false);
if (!m_database.isOpen())
return;
ASSERT(cache->group());
ASSERT(cache->group()->storageID());
// All associated data will be deleted by database triggers.
auto statement = m_database.prepareStatement("DELETE FROM Caches WHERE id=?"_s);
if (!statement)
return;
statement->bindInt64(1, cache->storageID());
executeStatement(statement.value());
cache->clearStorageID();
if (cache->group()->newestCache() == cache) {
// Currently, there are no triggers on the cache group, which is why the cache had to be removed separately above.
auto groupStatement = m_database.prepareStatement("DELETE FROM CacheGroups WHERE id=?"_s);
if (!groupStatement)
return;
groupStatement->bindInt64(1, cache->group()->storageID());
executeStatement(groupStatement.value());
cache->group()->clearStorageID();
}
checkForDeletedResources();
}
void ApplicationCacheStorage::empty()
{
SQLiteTransactionInProgressAutoCounter transactionCounter;
openDatabase(false);
if (!m_database.isOpen())
return;
// Clear cache groups, caches, cache resources, and origins.
executeSQLCommand("DELETE FROM CacheGroups"_s);
executeSQLCommand("DELETE FROM Caches"_s);
executeSQLCommand("DELETE FROM Origins"_s);
// Clear the storage IDs for the caches in memory.
// The caches will still work, but cached resources will not be saved to disk
// until a cache update process has been initiated.
for (auto* group : m_cachesInMemory.values())
group->clearStorageID();
checkForDeletedResources();
}
void ApplicationCacheStorage::deleteTables()
{
empty();
m_database.clearAllTables();
}
bool ApplicationCacheStorage::shouldStoreResourceAsFlatFile(ApplicationCacheResource* resource)
{
auto& type = resource->response().mimeType();
return startsWithLettersIgnoringASCIICase(type, "audio/") || startsWithLettersIgnoringASCIICase(type, "video/");
}
bool ApplicationCacheStorage::writeDataToUniqueFileInDirectory(FragmentedSharedBuffer& data, const String& directory, String& path, const String& fileExtension)
{
String fullPath;
do {
path = FileSystem::encodeForFileName(createCanonicalUUIDString()) + fileExtension;
// Guard against the above function being called on a platform which does not implement
// createCanonicalUUIDString().
ASSERT(!path.isEmpty());
if (path.isEmpty())
return false;
fullPath = FileSystem::pathByAppendingComponent(directory, path);
} while (FileSystem::parentPath(fullPath) != directory || FileSystem::fileExists(fullPath));
FileSystem::PlatformFileHandle handle = FileSystem::openFile(fullPath, FileSystem::FileOpenMode::Write);
if (!handle)
return false;
int64_t writtenBytes = 0;
data.forEachSegment([&](auto& segment) {
writtenBytes += FileSystem::writeToFile(handle, segment.data(), segment.size());
});
FileSystem::closeFile(handle);
if (writtenBytes != static_cast<int64_t>(data.size())) {
FileSystem::deleteFile(fullPath);
return false;
}
return true;
}
std::optional<Vector<URL>> ApplicationCacheStorage::manifestURLs()
{
SQLiteTransactionInProgressAutoCounter transactionCounter;
openDatabase(false);
if (!m_database.isOpen())
return std::nullopt;
auto selectURLs = m_database.prepareStatement("SELECT manifestURL FROM CacheGroups"_s);
if (!selectURLs)
return std::nullopt;
Vector<URL> urls;
while (selectURLs->step() == SQLITE_ROW)
urls.append(URL({ }, selectURLs->columnText(0)));
return urls;
}
bool ApplicationCacheStorage::deleteCacheGroupRecord(const String& manifestURL)
{
ASSERT(SQLiteDatabaseTracker::hasTransactionInProgress());
auto idStatement = m_database.prepareStatement("SELECT id FROM CacheGroups WHERE manifestURL=?"_s);
if (!idStatement)
return false;
idStatement->bindText(1, manifestURL);
int result = idStatement->step();
if (result != SQLITE_ROW)
return false;
int64_t groupId = idStatement->columnInt64(0);
auto cacheStatement = m_database.prepareStatement("DELETE FROM Caches WHERE cacheGroup=?"_s);
if (!cacheStatement)
return false;
auto groupStatement = m_database.prepareStatement("DELETE FROM CacheGroups WHERE id=?"_s);
if (!groupStatement)
return false;
cacheStatement->bindInt64(1, groupId);
executeStatement(cacheStatement.value());
groupStatement->bindInt64(1, groupId);
executeStatement(groupStatement.value());
return true;
}
bool ApplicationCacheStorage::deleteCacheGroup(const String& manifestURL)
{
SQLiteTransactionInProgressAutoCounter transactionCounter;
SQLiteTransaction deleteTransaction(m_database);
// Check to see if the group is in memory.
if (auto* group = m_cachesInMemory.get(manifestURL))
cacheGroupMadeObsolete(*group);
else {
// The cache group is not in memory, so remove it from the disk.
openDatabase(false);
if (!m_database.isOpen())
return false;
if (!deleteCacheGroupRecord(manifestURL)) {
LOG_ERROR("Could not delete cache group record, error \"%s\"", m_database.lastErrorMsg());
return false;
}
}
deleteTransaction.commit();
checkForDeletedResources();
return true;
}
void ApplicationCacheStorage::vacuumDatabaseFile()
{
SQLiteTransactionInProgressAutoCounter transactionCounter;
openDatabase(false);
if (!m_database.isOpen())
return;
m_database.runVacuumCommand();
}
void ApplicationCacheStorage::checkForMaxSizeReached()
{
if (m_database.lastError() == SQLITE_FULL)
m_isMaximumSizeReached = true;
}
void ApplicationCacheStorage::checkForDeletedResources()
{
openDatabase(false);
if (!m_database.isOpen())
return;
// Select only the paths in DeletedCacheResources that do not also appear in CacheResourceData:
auto selectPaths = m_database.prepareStatement("SELECT DeletedCacheResources.path "
"FROM DeletedCacheResources "
"LEFT JOIN CacheResourceData "
"ON DeletedCacheResources.path = CacheResourceData.path "
"WHERE (SELECT DeletedCacheResources.path == CacheResourceData.path) IS NULL"_s);
if (!selectPaths)
return;
if (selectPaths->step() != SQLITE_ROW)
return;
do {
String path = selectPaths->columnText(0);
if (path.isEmpty())
continue;
String flatFileDirectory = FileSystem::pathByAppendingComponent(m_cacheDirectory, m_flatFileSubdirectoryName);
String fullPath = FileSystem::pathByAppendingComponent(flatFileDirectory, path);
// Don't exit the flatFileDirectory! This should only happen if the "path" entry contains a directory
// component, but protect against it regardless.
if (FileSystem::parentPath(fullPath) != flatFileDirectory)
continue;
FileSystem::deleteFile(fullPath);
} while (selectPaths->step() == SQLITE_ROW);
executeSQLCommand("DELETE FROM DeletedCacheResources"_s);
}
long long ApplicationCacheStorage::flatFileAreaSize()
{
openDatabase(false);
if (!m_database.isOpen())
return 0;
auto selectPaths = m_database.prepareStatement("SELECT path FROM CacheResourceData WHERE path NOT NULL"_s);
if (!selectPaths) {
LOG_ERROR("Could not load flat file cache resource data, error \"%s\"", m_database.lastErrorMsg());
return 0;
}
long long totalSize = 0;
String flatFileDirectory = FileSystem::pathByAppendingComponent(m_cacheDirectory, m_flatFileSubdirectoryName);
while (selectPaths->step() == SQLITE_ROW) {
auto fullPath = FileSystem::pathByAppendingComponent(flatFileDirectory, selectPaths->columnText(0));
totalSize += FileSystem::fileSize(fullPath).value_or(0);
}
return totalSize;
}
HashSet<SecurityOriginData> ApplicationCacheStorage::originsWithCache()
{
auto urls = manifestURLs();
if (!urls)
return { };
// Multiple manifest URLs might share the same SecurityOrigin, so we might be creating extra, wasted origins here.
// The current schema doesn't allow for a more efficient way of building this list.
HashSet<SecurityOriginData> origins;
for (auto& url : *urls)
origins.add(SecurityOriginData::fromURL(url));
return origins;
}
void ApplicationCacheStorage::deleteAllEntries()
{
empty();
vacuumDatabaseFile();
}
void ApplicationCacheStorage::deleteAllCaches()
{
auto origins = originsWithCache();
for (auto& origin : origins)
deleteCacheForOrigin(origin);
vacuumDatabaseFile();
}
void ApplicationCacheStorage::deleteCacheForOrigin(const SecurityOriginData& securityOrigin)
{
auto urls = manifestURLs();
if (!urls) {
LOG_ERROR("Failed to retrieve ApplicationCache manifest URLs");
return;
}
URL originURL(URL(), securityOrigin.toString());
for (const auto& url : *urls) {
if (!protocolHostAndPortAreEqual(url, originURL))
continue;
if (auto* group = findInMemoryCacheGroup(url))
group->makeObsolete();
else
deleteCacheGroup(url.string());
}
}
int64_t ApplicationCacheStorage::diskUsageForOrigin(const SecurityOriginData& securityOrigin)
{
int64_t usage = 0;
calculateUsageForOrigin(securityOrigin, usage);
return usage;
}
ApplicationCacheStorage::ApplicationCacheStorage(const String& cacheDirectory, const String& flatFileSubdirectoryName)
: m_cacheDirectory(cacheDirectory)
, m_flatFileSubdirectoryName(flatFileSubdirectoryName)
{
}
} // namespace WebCore