/* | |
* Copyright (C) 2008, 2009 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" | |
#if ENABLE(OFFLINE_WEB_APPLICATIONS) | |
#include "ApplicationCache.h" | |
#include "ApplicationCacheHost.h" | |
#include "ApplicationCacheGroup.h" | |
#include "ApplicationCacheResource.h" | |
#include "CString.h" | |
#include "FileSystem.h" | |
#include "KURL.h" | |
#include "SQLiteStatement.h" | |
#include "SQLiteTransaction.h" | |
#include <wtf/StdLibExtras.h> | |
#include <wtf/StringExtras.h> | |
using namespace std; | |
namespace WebCore { | |
template <class T> | |
class StorageIDJournal { | |
public: | |
~StorageIDJournal() | |
{ | |
size_t size = m_records.size(); | |
for (size_t i = 0; i < size; ++i) | |
m_records[i].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(0), 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 KURL& url) | |
{ | |
unsigned hostStart = url.hostStart(); | |
unsigned hostEnd = url.hostEnd(); | |
return AlreadyHashed::avoidDeletedValue(StringImpl::computeHash(url.string().characters() + hostStart, hostEnd - hostStart)); | |
} | |
ApplicationCacheGroup* ApplicationCacheStorage::loadCacheGroup(const KURL& manifestURL) | |
{ | |
openDatabase(false); | |
if (!m_database.isOpen()) | |
return 0; | |
SQLiteStatement statement(m_database, "SELECT id, manifestURL, newestCache FROM CacheGroups WHERE newestCache IS NOT NULL AND manifestURL=?"); | |
if (statement.prepare() != SQLResultOk) | |
return 0; | |
statement.bindText(1, manifestURL); | |
int result = statement.step(); | |
if (result == SQLResultDone) | |
return 0; | |
if (result != SQLResultRow) { | |
LOG_ERROR("Could not load cache group, error \"%s\"", m_database.lastErrorMsg()); | |
return 0; | |
} | |
unsigned newestCacheStorageID = static_cast<unsigned>(statement.getColumnInt64(2)); | |
RefPtr<ApplicationCache> cache = loadCache(newestCacheStorageID); | |
if (!cache) | |
return 0; | |
ApplicationCacheGroup* group = new ApplicationCacheGroup(manifestURL); | |
group->setStorageID(static_cast<unsigned>(statement.getColumnInt64(0))); | |
group->setNewestCache(cache.release()); | |
return group; | |
} | |
ApplicationCacheGroup* ApplicationCacheStorage::findOrCreateCacheGroup(const KURL& manifestURL) | |
{ | |
ASSERT(!manifestURL.hasFragmentIdentifier()); | |
std::pair<CacheGroupMap::iterator, bool> result = m_cachesInMemory.add(manifestURL, 0); | |
if (!result.second) { | |
ASSERT(result.first->second); | |
return result.first->second; | |
} | |
// Look up the group in the database | |
ApplicationCacheGroup* group = loadCacheGroup(manifestURL); | |
// If the group was not found we need to create it | |
if (!group) { | |
group = new ApplicationCacheGroup(manifestURL); | |
m_cacheHostSet.add(urlHostHash(manifestURL)); | |
} | |
result.first->second = group; | |
return group; | |
} | |
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; | |
openDatabase(false); | |
if (!m_database.isOpen()) | |
return; | |
// Fetch the host hashes. | |
SQLiteStatement statement(m_database, "SELECT manifestHostHash FROM CacheGroups"); | |
if (statement.prepare() != SQLResultOk) | |
return; | |
int result; | |
while ((result = statement.step()) == SQLResultRow) | |
m_cacheHostSet.add(static_cast<unsigned>(statement.getColumnInt64(0))); | |
} | |
ApplicationCacheGroup* ApplicationCacheStorage::cacheGroupForURL(const KURL& 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 0; | |
// Check if a cache already exists in memory. | |
CacheGroupMap::const_iterator end = m_cachesInMemory.end(); | |
for (CacheGroupMap::const_iterator it = m_cachesInMemory.begin(); it != end; ++it) { | |
ApplicationCacheGroup* group = it->second; | |
ASSERT(!group->isObsolete()); | |
if (!protocolHostAndPortAreEqual(url, group->manifestURL())) | |
continue; | |
if (ApplicationCache* cache = group->newestCache()) { | |
ApplicationCacheResource* resource = cache->resourceForURL(url); | |
if (!resource) | |
continue; | |
if (resource->type() & ApplicationCacheResource::Foreign) | |
continue; | |
return group; | |
} | |
} | |
if (!m_database.isOpen()) | |
return 0; | |
// Check the database. Look for all cache groups with a newest cache. | |
SQLiteStatement statement(m_database, "SELECT id, manifestURL, newestCache FROM CacheGroups WHERE newestCache IS NOT NULL"); | |
if (statement.prepare() != SQLResultOk) | |
return 0; | |
int result; | |
while ((result = statement.step()) == SQLResultRow) { | |
KURL manifestURL = KURL(ParsedURLString, statement.getColumnText(1)); | |
if (m_cachesInMemory.contains(manifestURL)) | |
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.getColumnInt64(2)); | |
RefPtr<ApplicationCache> cache = loadCache(newestCacheID); | |
if (!cache) | |
continue; | |
ApplicationCacheResource* resource = cache->resourceForURL(url); | |
if (!resource) | |
continue; | |
if (resource->type() & ApplicationCacheResource::Foreign) | |
continue; | |
ApplicationCacheGroup* group = new ApplicationCacheGroup(manifestURL); | |
group->setStorageID(static_cast<unsigned>(statement.getColumnInt64(0))); | |
group->setNewestCache(cache.release()); | |
m_cachesInMemory.set(group->manifestURL(), group); | |
return group; | |
} | |
if (result != SQLResultDone) | |
LOG_ERROR("Could not load cache group, error \"%s\"", m_database.lastErrorMsg()); | |
return 0; | |
} | |
ApplicationCacheGroup* ApplicationCacheStorage::fallbackCacheGroupForURL(const KURL& url) | |
{ | |
ASSERT(!url.hasFragmentIdentifier()); | |
// Check if an appropriate cache already exists in memory. | |
CacheGroupMap::const_iterator end = m_cachesInMemory.end(); | |
for (CacheGroupMap::const_iterator it = m_cachesInMemory.begin(); it != end; ++it) { | |
ApplicationCacheGroup* group = it->second; | |
ASSERT(!group->isObsolete()); | |
if (ApplicationCache* cache = group->newestCache()) { | |
KURL fallbackURL; | |
if (!cache->urlMatchesFallbackNamespace(url, &fallbackURL)) | |
continue; | |
if (cache->resourceForURL(fallbackURL)->type() & ApplicationCacheResource::Foreign) | |
continue; | |
return group; | |
} | |
} | |
if (!m_database.isOpen()) | |
return 0; | |
// Check the database. Look for all cache groups with a newest cache. | |
SQLiteStatement statement(m_database, "SELECT id, manifestURL, newestCache FROM CacheGroups WHERE newestCache IS NOT NULL"); | |
if (statement.prepare() != SQLResultOk) | |
return 0; | |
int result; | |
while ((result = statement.step()) == SQLResultRow) { | |
KURL manifestURL = KURL(ParsedURLString, statement.getColumnText(1)); | |
if (m_cachesInMemory.contains(manifestURL)) | |
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.getColumnInt64(2)); | |
RefPtr<ApplicationCache> cache = loadCache(newestCacheID); | |
KURL fallbackURL; | |
if (!cache->urlMatchesFallbackNamespace(url, &fallbackURL)) | |
continue; | |
if (cache->resourceForURL(fallbackURL)->type() & ApplicationCacheResource::Foreign) | |
continue; | |
ApplicationCacheGroup* group = new ApplicationCacheGroup(manifestURL); | |
group->setStorageID(static_cast<unsigned>(statement.getColumnInt64(0))); | |
group->setNewestCache(cache.release()); | |
m_cachesInMemory.set(group->manifestURL(), group); | |
return group; | |
} | |
if (result != SQLResultDone) | |
LOG_ERROR("Could not load cache group, error \"%s\"", m_database.lastErrorMsg()); | |
return 0; | |
} | |
void ApplicationCacheStorage::cacheGroupDestroyed(ApplicationCacheGroup* group) | |
{ | |
if (group->isObsolete()) { | |
ASSERT(!group->storageID()); | |
ASSERT(m_cachesInMemory.get(group->manifestURL()) != group); | |
return; | |
} | |
ASSERT(m_cachesInMemory.get(group->manifestURL()) == group); | |
m_cachesInMemory.remove(group->manifestURL()); | |
// 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()) == group); | |
ASSERT(m_cacheHostSet.contains(urlHostHash(group->manifestURL()))); | |
if (ApplicationCache* newestCache = group->newestCache()) | |
remove(newestCache); | |
m_cachesInMemory.remove(group->manifestURL()); | |
m_cacheHostSet.remove(urlHostHash(group->manifestURL())); | |
} | |
void ApplicationCacheStorage::setCacheDirectory(const String& cacheDirectory) | |
{ | |
ASSERT(m_cacheDirectory.isNull()); | |
ASSERT(!cacheDirectory.isNull()); | |
m_cacheDirectory = cacheDirectory; | |
} | |
const String& ApplicationCacheStorage::cacheDirectory() const | |
{ | |
return m_cacheDirectory; | |
} | |
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; | |
long long fileSize = 0; | |
if (!getFileSize(m_cacheFile, fileSize)) | |
return 0; | |
int64_t currentSize = fileSize; | |
// 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; | |
} | |
bool ApplicationCacheStorage::executeSQLCommand(const String& sql) | |
{ | |
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.utf8().data(), m_database.lastErrorMsg()); | |
return result; | |
} | |
static const int schemaVersion = 5; | |
void ApplicationCacheStorage::verifySchemaVersion() | |
{ | |
int version = SQLiteStatement(m_database, "PRAGMA user_version").getColumnInt(0); | |
if (version == schemaVersion) | |
return; | |
m_database.clearAllTables(); | |
// Update user version. | |
SQLiteTransaction setDatabaseVersion(m_database); | |
setDatabaseVersion.begin(); | |
char userVersionSQL[32]; | |
int unusedNumBytes = snprintf(userVersionSQL, sizeof(userVersionSQL), "PRAGMA user_version=%d", schemaVersion); | |
ASSERT_UNUSED(unusedNumBytes, static_cast<int>(sizeof(userVersionSQL)) >= unusedNumBytes); | |
SQLiteStatement statement(m_database, userVersionSQL); | |
if (statement.prepare() != SQLResultOk) | |
return; | |
executeStatement(statement); | |
setDatabaseVersion.commit(); | |
} | |
void ApplicationCacheStorage::openDatabase(bool createIfDoesNotExist) | |
{ | |
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 = pathByAppendingComponent(m_cacheDirectory, "ApplicationCache.db"); | |
if (!createIfDoesNotExist && !fileExists(m_cacheFile)) | |
return; | |
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)"); | |
executeSQLCommand("CREATE TABLE IF NOT EXISTS Caches (id INTEGER PRIMARY KEY AUTOINCREMENT, cacheGroup INTEGER, size INTEGER)"); | |
executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheWhitelistURLs (url TEXT NOT NULL ON CONFLICT FAIL, cache INTEGER NOT NULL ON CONFLICT FAIL)"); | |
executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheAllowsAllNetworkRequests (wildcard INTEGER NOT NULL ON CONFLICT FAIL, cache INTEGER NOT NULL ON CONFLICT FAIL)"); | |
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)"); | |
executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheEntries (cache INTEGER NOT NULL ON CONFLICT FAIL, type INTEGER, resource INTEGER NOT NULL)"); | |
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)"); | |
executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheResourceData (id INTEGER PRIMARY KEY AUTOINCREMENT, data BLOB)"); | |
// When a cache is deleted, all its entries and its whitelist 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"); | |
// 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"); | |
// 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"); | |
} | |
bool ApplicationCacheStorage::executeStatement(SQLiteStatement& statement) | |
{ | |
bool result = statement.executeCommand(); | |
if (!result) | |
LOG_ERROR("Application Cache Storage: failed to execute statement \"%s\" error \"%s\"", | |
statement.query().utf8().data(), m_database.lastErrorMsg()); | |
return result; | |
} | |
bool ApplicationCacheStorage::store(ApplicationCacheGroup* group, GroupStorageIDJournal* journal) | |
{ | |
ASSERT(group->storageID() == 0); | |
ASSERT(journal); | |
SQLiteStatement statement(m_database, "INSERT INTO CacheGroups (manifestHostHash, manifestURL) VALUES (?, ?)"); | |
if (statement.prepare() != SQLResultOk) | |
return false; | |
statement.bindInt64(1, urlHostHash(group->manifestURL())); | |
statement.bindText(2, group->manifestURL()); | |
if (!executeStatement(statement)) | |
return false; | |
group->setStorageID(static_cast<unsigned>(m_database.lastInsertRowID())); | |
journal->add(group, 0); | |
return true; | |
} | |
bool ApplicationCacheStorage::store(ApplicationCache* cache, ResourceStorageIDJournal* storageIDJournal) | |
{ | |
ASSERT(cache->storageID() == 0); | |
ASSERT(cache->group()->storageID() != 0); | |
ASSERT(storageIDJournal); | |
SQLiteStatement statement(m_database, "INSERT INTO Caches (cacheGroup, size) VALUES (?, ?)"); | |
if (statement.prepare() != SQLResultOk) | |
return false; | |
statement.bindInt64(1, cache->group()->storageID()); | |
statement.bindInt64(2, cache->estimatedSizeInStorage()); | |
if (!executeStatement(statement)) | |
return false; | |
unsigned cacheStorageID = static_cast<unsigned>(m_database.lastInsertRowID()); | |
// Store all resources | |
{ | |
ApplicationCache::ResourceMap::const_iterator end = cache->end(); | |
for (ApplicationCache::ResourceMap::const_iterator it = cache->begin(); it != end; ++it) { | |
unsigned oldStorageID = it->second->storageID(); | |
if (!store(it->second.get(), cacheStorageID)) | |
return false; | |
// Storing the resource succeeded. Log its old storageID in case | |
// it needs to be restored later. | |
storageIDJournal->add(it->second.get(), oldStorageID); | |
} | |
} | |
// Store the online whitelist | |
const Vector<KURL>& onlineWhitelist = cache->onlineWhitelist(); | |
{ | |
size_t whitelistSize = onlineWhitelist.size(); | |
for (size_t i = 0; i < whitelistSize; ++i) { | |
SQLiteStatement statement(m_database, "INSERT INTO CacheWhitelistURLs (url, cache) VALUES (?, ?)"); | |
statement.prepare(); | |
statement.bindText(1, onlineWhitelist[i]); | |
statement.bindInt64(2, cacheStorageID); | |
if (!executeStatement(statement)) | |
return false; | |
} | |
} | |
// Store online whitelist wildcard flag. | |
{ | |
SQLiteStatement statement(m_database, "INSERT INTO CacheAllowsAllNetworkRequests (wildcard, cache) VALUES (?, ?)"); | |
statement.prepare(); | |
statement.bindInt64(1, cache->allowsAllNetworkRequests()); | |
statement.bindInt64(2, cacheStorageID); | |
if (!executeStatement(statement)) | |
return false; | |
} | |
// Store fallback URLs. | |
const FallbackURLVector& fallbackURLs = cache->fallbackURLs(); | |
{ | |
size_t fallbackCount = fallbackURLs.size(); | |
for (size_t i = 0; i < fallbackCount; ++i) { | |
SQLiteStatement statement(m_database, "INSERT INTO FallbackURLs (namespace, fallbackURL, cache) VALUES (?, ?, ?)"); | |
statement.prepare(); | |
statement.bindText(1, fallbackURLs[i].first); | |
statement.bindText(2, fallbackURLs[i].second); | |
statement.bindInt64(3, cacheStorageID); | |
if (!executeStatement(statement)) | |
return false; | |
} | |
} | |
cache->setStorageID(cacheStorageID); | |
return true; | |
} | |
bool ApplicationCacheStorage::store(ApplicationCacheResource* resource, unsigned cacheStorageID) | |
{ | |
ASSERT(cacheStorageID); | |
ASSERT(!resource->storageID()); | |
openDatabase(true); | |
// First, insert the data | |
SQLiteStatement dataStatement(m_database, "INSERT INTO CacheResourceData (data) VALUES (?)"); | |
if (dataStatement.prepare() != SQLResultOk) | |
return false; | |
if (resource->data()->size()) | |
dataStatement.bindBlob(1, resource->data()->data(), resource->data()->size()); | |
if (!dataStatement.executeCommand()) | |
return false; | |
unsigned dataId = static_cast<unsigned>(m_database.lastInsertRowID()); | |
// Then, insert the resource | |
// Serialize the headers | |
Vector<UChar> stringBuilder; | |
HTTPHeaderMap::const_iterator end = resource->response().httpHeaderFields().end(); | |
for (HTTPHeaderMap::const_iterator it = resource->response().httpHeaderFields().begin(); it!= end; ++it) { | |
stringBuilder.append(it->first.characters(), it->first.length()); | |
stringBuilder.append((UChar)':'); | |
stringBuilder.append(it->second.characters(), it->second.length()); | |
stringBuilder.append((UChar)'\n'); | |
} | |
String headers = String::adopt(stringBuilder); | |
SQLiteStatement resourceStatement(m_database, "INSERT INTO CacheResources (url, statusCode, responseURL, headers, data, mimeType, textEncodingName) VALUES (?, ?, ?, ?, ?, ?, ?)"); | |
if (resourceStatement.prepare() != SQLResultOk) | |
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()); | |
resourceStatement.bindInt64(2, resource->response().httpStatusCode()); | |
resourceStatement.bindText(3, resource->response().url()); | |
resourceStatement.bindText(4, headers); | |
resourceStatement.bindInt64(5, dataId); | |
resourceStatement.bindText(6, resource->response().mimeType()); | |
resourceStatement.bindText(7, resource->response().textEncodingName()); | |
if (!executeStatement(resourceStatement)) | |
return false; | |
unsigned resourceId = static_cast<unsigned>(m_database.lastInsertRowID()); | |
// Finally, insert the cache entry | |
SQLiteStatement entryStatement(m_database, "INSERT INTO CacheEntries (cache, type, resource) VALUES (?, ?, ?)"); | |
if (entryStatement.prepare() != SQLResultOk) | |
return false; | |
entryStatement.bindInt64(1, cacheStorageID); | |
entryStatement.bindInt64(2, resource->type()); | |
entryStatement.bindInt64(3, resourceId); | |
if (!executeStatement(entryStatement)) | |
return false; | |
resource->setStorageID(resourceId); | |
return true; | |
} | |
bool ApplicationCacheStorage::storeUpdatedType(ApplicationCacheResource* resource, ApplicationCache* cache) | |
{ | |
ASSERT_UNUSED(cache, cache->storageID()); | |
ASSERT(resource->storageID()); | |
// First, insert the data | |
SQLiteStatement entryStatement(m_database, "UPDATE CacheEntries SET type=? WHERE resource=?"); | |
if (entryStatement.prepare() != SQLResultOk) | |
return false; | |
entryStatement.bindInt64(1, resource->type()); | |
entryStatement.bindInt64(2, resource->storageID()); | |
return executeStatement(entryStatement); | |
} | |
bool ApplicationCacheStorage::store(ApplicationCacheResource* resource, ApplicationCache* cache) | |
{ | |
ASSERT(cache->storageID()); | |
openDatabase(true); | |
m_isMaximumSizeReached = false; | |
m_database.setMaximumSize(m_maximumSize); | |
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. | |
SQLiteStatement sizeUpdateStatement(m_database, "UPDATE Caches SET size=size+? WHERE id=?"); | |
if (sizeUpdateStatement.prepare() != SQLResultOk) | |
return false; | |
sizeUpdateStatement.bindInt64(1, resource->estimatedSizeInStorage()); | |
sizeUpdateStatement.bindInt64(2, cache->storageID()); | |
if (!executeStatement(sizeUpdateStatement)) | |
return false; | |
storeResourceTransaction.commit(); | |
return true; | |
} | |
bool ApplicationCacheStorage::storeNewestCache(ApplicationCacheGroup* group) | |
{ | |
openDatabase(true); | |
m_isMaximumSizeReached = false; | |
m_database.setMaximumSize(m_maximumSize); | |
SQLiteTransaction storeCacheTransaction(m_database); | |
storeCacheTransaction.begin(); | |
GroupStorageIDJournal groupStorageIDJournal; | |
if (!group->storageID()) { | |
// Store the group | |
if (!store(group, &groupStorageIDJournal)) { | |
checkForMaxSizeReached(); | |
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(); | |
return false; | |
} | |
// Update the newest cache in the group. | |
SQLiteStatement statement(m_database, "UPDATE CacheGroups SET newestCache=? WHERE id=?"); | |
if (statement.prepare() != SQLResultOk) | |
return false; | |
statement.bindInt64(1, group->newestCache()->storageID()); | |
statement.bindInt64(2, group->storageID()); | |
if (!executeStatement(statement)) | |
return false; | |
groupStorageIDJournal.commit(); | |
resourceStorageIDJournal.commit(); | |
storeCacheTransaction.commit(); | |
return true; | |
} | |
static inline void parseHeader(const UChar* header, size_t headerLength, ResourceResponse& response) | |
{ | |
int pos = find(header, headerLength, ':'); | |
ASSERT(pos != -1); | |
AtomicString headerName = AtomicString(header, pos); | |
String headerValue = String(header + pos + 1, headerLength - pos - 1); | |
response.setHTTPHeaderField(headerName, headerValue); | |
} | |
static inline void parseHeaders(const String& headers, ResourceResponse& response) | |
{ | |
int startPos = 0; | |
int endPos; | |
while ((endPos = headers.find('\n', startPos)) != -1) { | |
ASSERT(startPos != endPos); | |
parseHeader(headers.characters() + startPos, endPos - startPos, response); | |
startPos = endPos + 1; | |
} | |
if (startPos != static_cast<int>(headers.length())) | |
parseHeader(headers.characters(), headers.length(), response); | |
} | |
PassRefPtr<ApplicationCache> ApplicationCacheStorage::loadCache(unsigned storageID) | |
{ | |
SQLiteStatement cacheStatement(m_database, | |
"SELECT url, type, mimeType, textEncodingName, headers, CacheResourceData.data FROM CacheEntries INNER JOIN CacheResources ON CacheEntries.resource=CacheResources.id " | |
"INNER JOIN CacheResourceData ON CacheResourceData.id=CacheResources.data WHERE CacheEntries.cache=?"); | |
if (cacheStatement.prepare() != SQLResultOk) { | |
LOG_ERROR("Could not prepare cache statement, error \"%s\"", m_database.lastErrorMsg()); | |
return 0; | |
} | |
cacheStatement.bindInt64(1, storageID); | |
RefPtr<ApplicationCache> cache = ApplicationCache::create(); | |
int result; | |
while ((result = cacheStatement.step()) == SQLResultRow) { | |
KURL url(ParsedURLString, cacheStatement.getColumnText(0)); | |
unsigned type = static_cast<unsigned>(cacheStatement.getColumnInt64(1)); | |
Vector<char> blob; | |
cacheStatement.getColumnBlobAsVector(5, blob); | |
RefPtr<SharedBuffer> data = SharedBuffer::adoptVector(blob); | |
String mimeType = cacheStatement.getColumnText(2); | |
String textEncodingName = cacheStatement.getColumnText(3); | |
ResourceResponse response(url, mimeType, data->size(), textEncodingName, ""); | |
String headers = cacheStatement.getColumnText(4); | |
parseHeaders(headers, response); | |
RefPtr<ApplicationCacheResource> resource = ApplicationCacheResource::create(url, response, type, data.release()); | |
if (type & ApplicationCacheResource::Manifest) | |
cache->setManifestResource(resource.release()); | |
else | |
cache->addResource(resource.release()); | |
} | |
if (result != SQLResultDone) | |
LOG_ERROR("Could not load cache resources, error \"%s\"", m_database.lastErrorMsg()); | |
// Load the online whitelist | |
SQLiteStatement whitelistStatement(m_database, "SELECT url FROM CacheWhitelistURLs WHERE cache=?"); | |
if (whitelistStatement.prepare() != SQLResultOk) | |
return 0; | |
whitelistStatement.bindInt64(1, storageID); | |
Vector<KURL> whitelist; | |
while ((result = whitelistStatement.step()) == SQLResultRow) | |
whitelist.append(KURL(ParsedURLString, whitelistStatement.getColumnText(0))); | |
if (result != SQLResultDone) | |
LOG_ERROR("Could not load cache online whitelist, error \"%s\"", m_database.lastErrorMsg()); | |
cache->setOnlineWhitelist(whitelist); | |
// Load online whitelist wildcard flag. | |
SQLiteStatement whitelistWildcardStatement(m_database, "SELECT wildcard FROM CacheAllowsAllNetworkRequests WHERE cache=?"); | |
if (whitelistWildcardStatement.prepare() != SQLResultOk) | |
return 0; | |
whitelistWildcardStatement.bindInt64(1, storageID); | |
result = whitelistWildcardStatement.step(); | |
if (result != SQLResultRow) | |
LOG_ERROR("Could not load cache online whitelist wildcard flag, error \"%s\"", m_database.lastErrorMsg()); | |
cache->setAllowsAllNetworkRequests(whitelistWildcardStatement.getColumnInt64(0)); | |
if (whitelistWildcardStatement.step() != SQLResultDone) | |
LOG_ERROR("Too many rows for online whitelist wildcard flag"); | |
// Load fallback URLs. | |
SQLiteStatement fallbackStatement(m_database, "SELECT namespace, fallbackURL FROM FallbackURLs WHERE cache=?"); | |
if (fallbackStatement.prepare() != SQLResultOk) | |
return 0; | |
fallbackStatement.bindInt64(1, storageID); | |
FallbackURLVector fallbackURLs; | |
while ((result = fallbackStatement.step()) == SQLResultRow) | |
fallbackURLs.append(make_pair(KURL(ParsedURLString, fallbackStatement.getColumnText(0)), KURL(ParsedURLString, fallbackStatement.getColumnText(1)))); | |
if (result != SQLResultDone) | |
LOG_ERROR("Could not load fallback URLs, error \"%s\"", m_database.lastErrorMsg()); | |
cache->setFallbackURLs(fallbackURLs); | |
cache->setStorageID(storageID); | |
return cache.release(); | |
} | |
void ApplicationCacheStorage::remove(ApplicationCache* cache) | |
{ | |
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. | |
SQLiteStatement statement(m_database, "DELETE FROM Caches WHERE id=?"); | |
if (statement.prepare() != SQLResultOk) | |
return; | |
statement.bindInt64(1, cache->storageID()); | |
executeStatement(statement); | |
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. | |
SQLiteStatement groupStatement(m_database, "DELETE FROM CacheGroups WHERE id=?"); | |
if (groupStatement.prepare() != SQLResultOk) | |
return; | |
groupStatement.bindInt64(1, cache->group()->storageID()); | |
executeStatement(groupStatement); | |
cache->group()->clearStorageID(); | |
} | |
} | |
void ApplicationCacheStorage::empty() | |
{ | |
openDatabase(false); | |
if (!m_database.isOpen()) | |
return; | |
// Clear cache groups, caches and cache resources. | |
executeSQLCommand("DELETE FROM CacheGroups"); | |
executeSQLCommand("DELETE FROM Caches"); | |
// 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. | |
CacheGroupMap::const_iterator end = m_cachesInMemory.end(); | |
for (CacheGroupMap::const_iterator it = m_cachesInMemory.begin(); it != end; ++it) | |
it->second->clearStorageID(); | |
} | |
bool ApplicationCacheStorage::storeCopyOfCache(const String& cacheDirectory, ApplicationCacheHost* cacheHost) | |
{ | |
ApplicationCache* cache = cacheHost->applicationCache(); | |
if (!cache) | |
return true; | |
// Create a new cache. | |
RefPtr<ApplicationCache> cacheCopy = ApplicationCache::create(); | |
cacheCopy->setOnlineWhitelist(cache->onlineWhitelist()); | |
cacheCopy->setFallbackURLs(cache->fallbackURLs()); | |
// Traverse the cache and add copies of all resources. | |
ApplicationCache::ResourceMap::const_iterator end = cache->end(); | |
for (ApplicationCache::ResourceMap::const_iterator it = cache->begin(); it != end; ++it) { | |
ApplicationCacheResource* resource = it->second.get(); | |
RefPtr<ApplicationCacheResource> resourceCopy = ApplicationCacheResource::create(resource->url(), resource->response(), resource->type(), resource->data()); | |
cacheCopy->addResource(resourceCopy.release()); | |
} | |
// Now create a new cache group. | |
OwnPtr<ApplicationCacheGroup> groupCopy(new ApplicationCacheGroup(cache->group()->manifestURL(), true)); | |
groupCopy->setNewestCache(cacheCopy); | |
ApplicationCacheStorage copyStorage; | |
copyStorage.setCacheDirectory(cacheDirectory); | |
// Empty the cache in case something was there before. | |
copyStorage.empty(); | |
return copyStorage.storeNewestCache(groupCopy.get()); | |
} | |
bool ApplicationCacheStorage::manifestURLs(Vector<KURL>* urls) | |
{ | |
ASSERT(urls); | |
openDatabase(false); | |
if (!m_database.isOpen()) | |
return false; | |
SQLiteStatement selectURLs(m_database, "SELECT manifestURL FROM CacheGroups"); | |
if (selectURLs.prepare() != SQLResultOk) | |
return false; | |
while (selectURLs.step() == SQLResultRow) | |
urls->append(KURL(ParsedURLString, selectURLs.getColumnText(0))); | |
return true; | |
} | |
bool ApplicationCacheStorage::cacheGroupSize(const String& manifestURL, int64_t* size) | |
{ | |
ASSERT(size); | |
openDatabase(false); | |
if (!m_database.isOpen()) | |
return false; | |
SQLiteStatement statement(m_database, "SELECT sum(Caches.size) FROM Caches INNER JOIN CacheGroups ON Caches.cacheGroup=CacheGroups.id WHERE CacheGroups.manifestURL=?"); | |
if (statement.prepare() != SQLResultOk) | |
return false; | |
statement.bindText(1, manifestURL); | |
int result = statement.step(); | |
if (result == SQLResultDone) | |
return false; | |
if (result != SQLResultRow) { | |
LOG_ERROR("Could not get the size of the cache group, error \"%s\"", m_database.lastErrorMsg()); | |
return false; | |
} | |
*size = statement.getColumnInt64(0); | |
return true; | |
} | |
bool ApplicationCacheStorage::deleteCacheGroup(const String& manifestURL) | |
{ | |
SQLiteTransaction deleteTransaction(m_database); | |
// Check to see if the group is in memory. | |
ApplicationCacheGroup* group = m_cachesInMemory.get(manifestURL); | |
if (group) | |
cacheGroupMadeObsolete(group); | |
else { | |
// The cache group is not in memory, so remove it from the disk. | |
openDatabase(false); | |
if (!m_database.isOpen()) | |
return false; | |
SQLiteStatement idStatement(m_database, "SELECT id FROM CacheGroups WHERE manifestURL=?"); | |
if (idStatement.prepare() != SQLResultOk) | |
return false; | |
idStatement.bindText(1, manifestURL); | |
int result = idStatement.step(); | |
if (result == SQLResultDone) | |
return false; | |
if (result != SQLResultRow) { | |
LOG_ERROR("Could not load cache group id, error \"%s\"", m_database.lastErrorMsg()); | |
return false; | |
} | |
int64_t groupId = idStatement.getColumnInt64(0); | |
SQLiteStatement cacheStatement(m_database, "DELETE FROM Caches WHERE cacheGroup=?"); | |
if (cacheStatement.prepare() != SQLResultOk) | |
return false; | |
SQLiteStatement groupStatement(m_database, "DELETE FROM CacheGroups WHERE id=?"); | |
if (groupStatement.prepare() != SQLResultOk) | |
return false; | |
cacheStatement.bindInt64(1, groupId); | |
executeStatement(cacheStatement); | |
groupStatement.bindInt64(1, groupId); | |
executeStatement(groupStatement); | |
} | |
deleteTransaction.commit(); | |
return true; | |
} | |
void ApplicationCacheStorage::vacuumDatabaseFile() | |
{ | |
openDatabase(false); | |
if (!m_database.isOpen()) | |
return; | |
m_database.runVacuumCommand(); | |
} | |
void ApplicationCacheStorage::checkForMaxSizeReached() | |
{ | |
if (m_database.lastError() == SQLResultFull) | |
m_isMaximumSizeReached = true; | |
} | |
ApplicationCacheStorage::ApplicationCacheStorage() | |
: m_maximumSize(INT_MAX) | |
, m_isMaximumSizeReached(false) | |
{ | |
} | |
ApplicationCacheStorage& cacheStorage() | |
{ | |
DEFINE_STATIC_LOCAL(ApplicationCacheStorage, storage, ()); | |
return storage; | |
} | |
} // namespace WebCore | |
#endif // ENABLE(OFFLINE_WEB_APPLICATIONS) |