/* | |
* Copyright (C) 2007, 2008 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. | |
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of | |
* its contributors may be used to endorse or promote products derived | |
* from this software without specific prior written permission. | |
* | |
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "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 OR ITS 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 "DatabaseTracker.h" | |
#if ENABLE(DATABASE) | |
#include "Chrome.h" | |
#include "ChromeClient.h" | |
#include "Database.h" | |
#include "DatabaseThread.h" | |
#include "DatabaseTrackerClient.h" | |
#include "Logging.h" | |
#include "OriginQuotaManager.h" | |
#include "Page.h" | |
#include "ScriptExecutionContext.h" | |
#include "SecurityOrigin.h" | |
#include "SecurityOriginHash.h" | |
#include "SQLiteFileSystem.h" | |
#include "SQLiteStatement.h" | |
#include <wtf/MainThread.h> | |
#include <wtf/StdLibExtras.h> | |
using namespace std; | |
namespace WebCore { | |
OriginQuotaManager& DatabaseTracker::originQuotaManagerNoLock() | |
{ | |
ASSERT(m_quotaManager); | |
return *m_quotaManager; | |
} | |
OriginQuotaManager& DatabaseTracker::originQuotaManager() | |
{ | |
MutexLocker lockDatabase(m_databaseGuard); | |
populateOrigins(); | |
return originQuotaManagerNoLock(); | |
} | |
DatabaseTracker& DatabaseTracker::tracker() | |
{ | |
DEFINE_STATIC_LOCAL(DatabaseTracker, tracker, ()); | |
return tracker; | |
} | |
DatabaseTracker::DatabaseTracker() | |
: m_client(0) | |
{ | |
SQLiteFileSystem::registerSQLiteVFS(); | |
} | |
void DatabaseTracker::setDatabaseDirectoryPath(const String& path) | |
{ | |
MutexLocker lockDatabase(m_databaseGuard); | |
ASSERT(!m_database.isOpen()); | |
m_databaseDirectoryPath = path.threadsafeCopy(); | |
} | |
String DatabaseTracker::databaseDirectoryPath() const | |
{ | |
return m_databaseDirectoryPath.threadsafeCopy(); | |
} | |
String DatabaseTracker::trackerDatabasePath() const | |
{ | |
return SQLiteFileSystem::appendDatabaseFileNameToPath(m_databaseDirectoryPath, "Databases.db"); | |
} | |
void DatabaseTracker::openTrackerDatabase(bool createIfDoesNotExist) | |
{ | |
ASSERT(!m_databaseGuard.tryLock()); | |
if (m_database.isOpen()) | |
return; | |
String databasePath = trackerDatabasePath(); | |
if (!SQLiteFileSystem::ensureDatabaseFileExists(databasePath, createIfDoesNotExist)) | |
return; | |
if (!m_database.open(databasePath)) { | |
// FIXME: What do do here? | |
LOG_ERROR("Failed to open databasePath %s.", databasePath.ascii().data()); | |
return; | |
} | |
m_database.disableThreadingChecks(); | |
if (!m_database.tableExists("Origins")) { | |
if (!m_database.executeCommand("CREATE TABLE Origins (origin TEXT UNIQUE ON CONFLICT REPLACE, quota INTEGER NOT NULL ON CONFLICT FAIL);")) { | |
// FIXME: and here | |
LOG_ERROR("Failed to create Origins table"); | |
} | |
} | |
if (!m_database.tableExists("Databases")) { | |
if (!m_database.executeCommand("CREATE TABLE Databases (guid INTEGER PRIMARY KEY AUTOINCREMENT, origin TEXT, name TEXT, displayName TEXT, estimatedSize INTEGER, path TEXT);")) { | |
// FIXME: and here | |
LOG_ERROR("Failed to create Databases table"); | |
} | |
} | |
} | |
bool DatabaseTracker::canEstablishDatabase(ScriptExecutionContext* context, const String& name, const String& displayName, unsigned long estimatedSize) | |
{ | |
SecurityOrigin* origin = context->securityOrigin(); | |
ProposedDatabase details; | |
unsigned long long requirement; | |
unsigned long long tempUsage; | |
{ | |
Locker<OriginQuotaManager> locker(originQuotaManager()); | |
MutexLocker lockDatabase(m_databaseGuard); | |
if (!canCreateDatabase(origin, name)) | |
return false; | |
recordCreatingDatabase(origin, name); | |
populateOrigins(); | |
// Since we're imminently opening a database within this context's origin, make sure this origin is being tracked by the QuotaTracker | |
// by fetching its current usage now. | |
unsigned long long usage = usageForOriginNoLock(origin); | |
// If a database already exists, ignore the passed-in estimated size and say it's OK. | |
if (hasEntryForDatabase(origin, name)) | |
return true; | |
// If the database will fit, allow its creation. | |
requirement = usage + max(1UL, estimatedSize); | |
tempUsage = usage; | |
if (requirement < usage) { | |
doneCreatingDatabase(origin, name); | |
return false; // If the estimated size is so big it causes an overflow, don't allow creation. | |
} | |
if (requirement <= quotaForOriginNoLock(origin)) | |
return true; | |
// Give the chrome client a chance to increase the quota. | |
// Temporarily make the details of the proposed database available, so the client can get at them. | |
// FIXME: We should really just pass the details into this call, rather than using m_proposedDatabases. | |
details = ProposedDatabase(origin->threadsafeCopy(), DatabaseDetails(name.threadsafeCopy(), displayName.threadsafeCopy(), estimatedSize, 0)); | |
m_proposedDatabases.add(&details); | |
} | |
// Drop all locks before calling out; we don't know what they'll do. | |
context->databaseExceededQuota(name); | |
{ | |
MutexLocker lockDatabase(m_databaseGuard); | |
m_proposedDatabases.remove(&details); | |
} | |
// If the database will fit now, allow its creation. | |
if (requirement <= quotaForOrigin(origin)) | |
return true; | |
doneCreatingDatabase(origin, name); | |
return false; | |
} | |
bool DatabaseTracker::hasEntryForOriginNoLock(SecurityOrigin* origin) | |
{ | |
ASSERT(!m_databaseGuard.tryLock()); | |
ASSERT(m_quotaMap); | |
return m_quotaMap->contains(origin); | |
} | |
bool DatabaseTracker::hasEntryForOrigin(SecurityOrigin* origin) | |
{ | |
MutexLocker lockDatabase(m_databaseGuard); | |
populateOrigins(); | |
return hasEntryForOriginNoLock(origin); | |
} | |
bool DatabaseTracker::hasEntryForDatabase(SecurityOrigin* origin, const String& databaseIdentifier) | |
{ | |
ASSERT(!m_databaseGuard.tryLock()); | |
openTrackerDatabase(false); | |
if (!m_database.isOpen()) | |
return false; | |
SQLiteStatement statement(m_database, "SELECT guid FROM Databases WHERE origin=? AND name=?;"); | |
if (statement.prepare() != SQLResultOk) | |
return false; | |
statement.bindText(1, origin->databaseIdentifier()); | |
statement.bindText(2, databaseIdentifier); | |
return statement.step() == SQLResultRow; | |
} | |
unsigned long long DatabaseTracker::getMaxSizeForDatabase(const Database* database) | |
{ | |
ASSERT(currentThread() == database->scriptExecutionContext()->databaseThread()->getThreadID()); | |
// The maximum size for a database is the full quota for its origin, minus the current usage within the origin, | |
// plus the current usage of the given database | |
Locker<OriginQuotaManager> locker(originQuotaManager()); | |
SecurityOrigin* origin = database->securityOrigin(); | |
return quotaForOrigin(origin) - originQuotaManager().diskUsage(origin) + SQLiteFileSystem::getDatabaseFileSize(database->fileName()); | |
} | |
String DatabaseTracker::originPath(SecurityOrigin* origin) const | |
{ | |
return SQLiteFileSystem::appendDatabaseFileNameToPath(m_databaseDirectoryPath.threadsafeCopy(), origin->databaseIdentifier()); | |
} | |
String DatabaseTracker::fullPathForDatabaseNoLock(SecurityOrigin* origin, const String& name, bool createIfNotExists) | |
{ | |
ASSERT(!m_databaseGuard.tryLock()); | |
ASSERT(!originQuotaManagerNoLock().tryLock()); | |
for (HashSet<ProposedDatabase*>::iterator iter = m_proposedDatabases.begin(); iter != m_proposedDatabases.end(); ++iter) | |
if ((*iter)->first == origin && (*iter)->second.name() == name) | |
return String(); | |
String originIdentifier = origin->databaseIdentifier(); | |
String originPath = this->originPath(origin); | |
// Make sure the path for this SecurityOrigin exists | |
if (createIfNotExists && !SQLiteFileSystem::ensureDatabaseDirectoryExists(originPath)) | |
return String(); | |
// See if we have a path for this database yet | |
if (!m_database.isOpen()) | |
return String(); | |
SQLiteStatement statement(m_database, "SELECT path FROM Databases WHERE origin=? AND name=?;"); | |
if (statement.prepare() != SQLResultOk) | |
return String(); | |
statement.bindText(1, originIdentifier); | |
statement.bindText(2, name); | |
int result = statement.step(); | |
if (result == SQLResultRow) | |
return SQLiteFileSystem::appendDatabaseFileNameToPath(originPath, statement.getColumnText(0)); | |
if (!createIfNotExists) | |
return String(); | |
if (result != SQLResultDone) { | |
LOG_ERROR("Failed to retrieve filename from Database Tracker for origin %s, name %s", originIdentifier.ascii().data(), name.ascii().data()); | |
return String(); | |
} | |
statement.finalize(); | |
String fileName = SQLiteFileSystem::getFileNameForNewDatabase(originPath, name, originIdentifier, &m_database); | |
if (!addDatabase(origin, name, fileName)) | |
return String(); | |
// If this origin's quota is being tracked (open handle to a database in this origin), add this new database | |
// to the quota manager now | |
String fullFilePath = SQLiteFileSystem::appendDatabaseFileNameToPath(originPath, fileName); | |
if (originQuotaManagerNoLock().tracksOrigin(origin)) | |
originQuotaManagerNoLock().addDatabase(origin, name, fullFilePath); | |
return fullFilePath; | |
} | |
String DatabaseTracker::fullPathForDatabase(SecurityOrigin* origin, const String& name, bool createIfNotExists) | |
{ | |
Locker<OriginQuotaManager> locker(originQuotaManager()); | |
MutexLocker lockDatabase(m_databaseGuard); | |
populateOrigins(); | |
return fullPathForDatabaseNoLock(origin, name, createIfNotExists).threadsafeCopy(); | |
} | |
void DatabaseTracker::populateOrigins() | |
{ | |
ASSERT(!m_databaseGuard.tryLock()); | |
if (m_quotaMap) | |
return; | |
m_quotaMap.set(new QuotaMap); | |
if (!m_quotaManager) | |
m_quotaManager.set(new OriginQuotaManager); | |
openTrackerDatabase(false); | |
if (!m_database.isOpen()) | |
return; | |
SQLiteStatement statement(m_database, "SELECT origin, quota FROM Origins"); | |
if (statement.prepare() != SQLResultOk) { | |
LOG_ERROR("Failed to prepare statement."); | |
return; | |
} | |
int result; | |
while ((result = statement.step()) == SQLResultRow) { | |
RefPtr<SecurityOrigin> origin = SecurityOrigin::createFromDatabaseIdentifier(statement.getColumnText(0)); | |
m_quotaMap->set(origin.get()->threadsafeCopy(), statement.getColumnInt64(1)); | |
} | |
if (result != SQLResultDone) | |
LOG_ERROR("Failed to read in all origins from the database."); | |
} | |
void DatabaseTracker::origins(Vector<RefPtr<SecurityOrigin> >& result) | |
{ | |
MutexLocker lockDatabase(m_databaseGuard); | |
populateOrigins(); | |
ASSERT(m_quotaMap); | |
copyKeysToVector(*m_quotaMap, result); | |
} | |
bool DatabaseTracker::databaseNamesForOriginNoLock(SecurityOrigin* origin, Vector<String>& resultVector) | |
{ | |
ASSERT(!m_databaseGuard.tryLock()); | |
openTrackerDatabase(false); | |
if (!m_database.isOpen()) | |
return false; | |
SQLiteStatement statement(m_database, "SELECT name FROM Databases where origin=?;"); | |
if (statement.prepare() != SQLResultOk) | |
return false; | |
statement.bindText(1, origin->databaseIdentifier()); | |
int result; | |
while ((result = statement.step()) == SQLResultRow) | |
resultVector.append(statement.getColumnText(0)); | |
if (result != SQLResultDone) { | |
LOG_ERROR("Failed to retrieve all database names for origin %s", origin->databaseIdentifier().ascii().data()); | |
return false; | |
} | |
return true; | |
} | |
bool DatabaseTracker::databaseNamesForOrigin(SecurityOrigin* origin, Vector<String>& resultVector) | |
{ | |
MutexLocker lockDatabase(m_databaseGuard); | |
Vector<String> temp; | |
if (!databaseNamesForOriginNoLock(origin, temp)) | |
return false; | |
for (Vector<String>::iterator iter = temp.begin(); iter != temp.end(); ++iter) | |
resultVector.append(iter->threadsafeCopy()); | |
return true; | |
} | |
DatabaseDetails DatabaseTracker::detailsForNameAndOrigin(const String& name, SecurityOrigin* origin) | |
{ | |
String originIdentifier = origin->databaseIdentifier(); | |
String displayName; | |
int64_t expectedUsage; | |
{ | |
MutexLocker lockDatabase(m_databaseGuard); | |
for (HashSet<ProposedDatabase*>::iterator iter = m_proposedDatabases.begin(); iter != m_proposedDatabases.end(); ++iter) | |
if ((*iter)->first == origin && (*iter)->second.name() == name) { | |
ASSERT((*iter)->second.thread() == currentThread()); | |
return (*iter)->second; | |
} | |
openTrackerDatabase(false); | |
if (!m_database.isOpen()) | |
return DatabaseDetails(); | |
SQLiteStatement statement(m_database, "SELECT displayName, estimatedSize FROM Databases WHERE origin=? AND name=?"); | |
if (statement.prepare() != SQLResultOk) | |
return DatabaseDetails(); | |
statement.bindText(1, originIdentifier); | |
statement.bindText(2, name); | |
int result = statement.step(); | |
if (result == SQLResultDone) | |
return DatabaseDetails(); | |
if (result != SQLResultRow) { | |
LOG_ERROR("Error retrieving details for database %s in origin %s from tracker database", name.ascii().data(), originIdentifier.ascii().data()); | |
return DatabaseDetails(); | |
} | |
displayName = statement.getColumnText(0); | |
expectedUsage = statement.getColumnInt64(1); | |
} | |
return DatabaseDetails(name, displayName, expectedUsage, usageForDatabase(name, origin)); | |
} | |
void DatabaseTracker::setDatabaseDetails(SecurityOrigin* origin, const String& name, const String& displayName, unsigned long estimatedSize) | |
{ | |
String originIdentifier = origin->databaseIdentifier(); | |
int64_t guid = 0; | |
MutexLocker lockDatabase(m_databaseGuard); | |
openTrackerDatabase(true); | |
if (!m_database.isOpen()) | |
return; | |
SQLiteStatement statement(m_database, "SELECT guid FROM Databases WHERE origin=? AND name=?"); | |
if (statement.prepare() != SQLResultOk) | |
return; | |
statement.bindText(1, originIdentifier); | |
statement.bindText(2, name); | |
int result = statement.step(); | |
if (result == SQLResultRow) | |
guid = statement.getColumnInt64(0); | |
statement.finalize(); | |
if (guid == 0) { | |
if (result != SQLResultDone) | |
LOG_ERROR("Error to determing existence of database %s in origin %s in tracker database", name.ascii().data(), originIdentifier.ascii().data()); | |
else { | |
// This case should never occur - we should never be setting database details for a database that doesn't already exist in the tracker | |
// But since the tracker file is an external resource not under complete control of our code, it's somewhat invalid to make this an ASSERT case | |
// So we'll print an error instead | |
LOG_ERROR("Could not retrieve guid for database %s in origin %s from the tracker database - it is invalid to set database details on a database that doesn't already exist in the tracker", | |
name.ascii().data(), originIdentifier.ascii().data()); | |
} | |
return; | |
} | |
SQLiteStatement updateStatement(m_database, "UPDATE Databases SET displayName=?, estimatedSize=? WHERE guid=?"); | |
if (updateStatement.prepare() != SQLResultOk) | |
return; | |
updateStatement.bindText(1, displayName); | |
updateStatement.bindInt64(2, estimatedSize); | |
updateStatement.bindInt64(3, guid); | |
if (updateStatement.step() != SQLResultDone) { | |
LOG_ERROR("Failed to update details for database %s in origin %s", name.ascii().data(), originIdentifier.ascii().data()); | |
return; | |
} | |
if (m_client) | |
m_client->dispatchDidModifyDatabase(origin, name); | |
} | |
unsigned long long DatabaseTracker::usageForDatabase(const String& name, SecurityOrigin* origin) | |
{ | |
String path = fullPathForDatabase(origin, name, false); | |
if (path.isEmpty()) | |
return 0; | |
return SQLiteFileSystem::getDatabaseFileSize(path); | |
} | |
void DatabaseTracker::addOpenDatabase(Database* database) | |
{ | |
if (!database) | |
return; | |
MutexLocker openDatabaseMapLock(m_openDatabaseMapGuard); | |
if (!m_openDatabaseMap) | |
m_openDatabaseMap.set(new DatabaseOriginMap); | |
String name(database->stringIdentifier()); | |
DatabaseNameMap* nameMap = m_openDatabaseMap->get(database->securityOrigin()); | |
if (!nameMap) { | |
nameMap = new DatabaseNameMap; | |
m_openDatabaseMap->set(database->securityOrigin()->threadsafeCopy(), nameMap); | |
} | |
DatabaseSet* databaseSet = nameMap->get(name); | |
if (!databaseSet) { | |
databaseSet = new DatabaseSet; | |
nameMap->set(name.threadsafeCopy(), databaseSet); | |
} | |
databaseSet->add(database); | |
LOG(StorageAPI, "Added open Database %s (%p)\n", database->stringIdentifier().ascii().data(), database); | |
MutexLocker lockDatabase(m_databaseGuard); | |
doneCreatingDatabase(database->securityOrigin(), database->stringIdentifier()); | |
} | |
void DatabaseTracker::removeOpenDatabase(Database* database) | |
{ | |
if (!database) | |
return; | |
Locker<OriginQuotaManager> quotaManagerLocker(originQuotaManager()); | |
MutexLocker lockDatabase(m_databaseGuard); | |
MutexLocker openDatabaseMapLock(m_openDatabaseMapGuard); | |
if (!m_openDatabaseMap) { | |
ASSERT_NOT_REACHED(); | |
return; | |
} | |
String name(database->stringIdentifier()); | |
DatabaseNameMap* nameMap = m_openDatabaseMap->get(database->securityOrigin()); | |
if (!nameMap) { | |
ASSERT_NOT_REACHED(); | |
return; | |
} | |
DatabaseSet* databaseSet = nameMap->get(name); | |
if (!databaseSet) { | |
ASSERT_NOT_REACHED(); | |
return; | |
} | |
databaseSet->remove(database); | |
LOG(StorageAPI, "Removed open Database %s (%p)\n", database->stringIdentifier().ascii().data(), database); | |
if (!databaseSet->isEmpty()) | |
return; | |
nameMap->remove(name); | |
delete databaseSet; | |
if (!nameMap->isEmpty()) | |
return; | |
m_openDatabaseMap->remove(database->securityOrigin()); | |
delete nameMap; | |
originQuotaManagerNoLock().removeOrigin(database->securityOrigin()); | |
} | |
void DatabaseTracker::getOpenDatabases(SecurityOrigin* origin, const String& name, HashSet<RefPtr<Database> >* databases) | |
{ | |
MutexLocker openDatabaseMapLock(m_openDatabaseMapGuard); | |
if (!m_openDatabaseMap) | |
return; | |
DatabaseNameMap* nameMap = m_openDatabaseMap->get(origin); | |
if (!nameMap) | |
return; | |
DatabaseSet* databaseSet = nameMap->get(name); | |
if (!databaseSet) | |
return; | |
for (DatabaseSet::iterator it = databaseSet->begin(); it != databaseSet->end(); ++it) | |
databases->add(*it); | |
} | |
unsigned long long DatabaseTracker::usageForOriginNoLock(SecurityOrigin* origin) | |
{ | |
ASSERT(!originQuotaManagerNoLock().tryLock()); | |
// Use the OriginQuotaManager mechanism to calculate the usage | |
if (originQuotaManagerNoLock().tracksOrigin(origin)) | |
return originQuotaManagerNoLock().diskUsage(origin); | |
// If the OriginQuotaManager doesn't track this origin already, prime it to do so | |
originQuotaManagerNoLock().trackOrigin(origin); | |
Vector<String> names; | |
databaseNamesForOriginNoLock(origin, names); | |
for (unsigned i = 0; i < names.size(); ++i) | |
originQuotaManagerNoLock().addDatabase(origin, names[i], fullPathForDatabaseNoLock(origin, names[i], false)); | |
if (!originQuotaManagerNoLock().tracksOrigin(origin)) | |
return 0; | |
return originQuotaManagerNoLock().diskUsage(origin); | |
} | |
unsigned long long DatabaseTracker::usageForOrigin(SecurityOrigin* origin) | |
{ | |
Locker<OriginQuotaManager> locker(originQuotaManager()); | |
return usageForOriginNoLock(origin); | |
} | |
unsigned long long DatabaseTracker::quotaForOriginNoLock(SecurityOrigin* origin) | |
{ | |
ASSERT(!m_databaseGuard.tryLock()); | |
ASSERT(m_quotaMap); | |
return m_quotaMap->get(origin); | |
} | |
unsigned long long DatabaseTracker::quotaForOrigin(SecurityOrigin* origin) | |
{ | |
MutexLocker lockDatabase(m_databaseGuard); | |
populateOrigins(); | |
return quotaForOriginNoLock(origin); | |
} | |
void DatabaseTracker::setQuota(SecurityOrigin* origin, unsigned long long quota) | |
{ | |
MutexLocker lockDatabase(m_databaseGuard); | |
populateOrigins(); | |
if (quotaForOriginNoLock(origin) == quota) | |
return; | |
openTrackerDatabase(true); | |
if (!m_database.isOpen()) | |
return; | |
if (!m_quotaMap->contains(origin)) { | |
SQLiteStatement statement(m_database, "INSERT INTO Origins VALUES (?, ?)"); | |
if (statement.prepare() != SQLResultOk) { | |
LOG_ERROR("Unable to establish origin %s in the tracker", origin->databaseIdentifier().ascii().data()); | |
} else { | |
statement.bindText(1, origin->databaseIdentifier()); | |
statement.bindInt64(2, quota); | |
if (statement.step() != SQLResultDone) | |
LOG_ERROR("Unable to establish origin %s in the tracker", origin->databaseIdentifier().ascii().data()); | |
} | |
} else { | |
SQLiteStatement statement(m_database, "UPDATE Origins SET quota=? WHERE origin=?"); | |
bool error = statement.prepare() != SQLResultOk; | |
if (!error) { | |
statement.bindInt64(1, quota); | |
statement.bindText(2, origin->databaseIdentifier()); | |
error = !statement.executeCommand(); | |
} | |
if (error) | |
LOG_ERROR("Failed to set quota %llu in tracker database for origin %s", quota, origin->databaseIdentifier().ascii().data()); | |
} | |
// FIXME: Is it really OK to update the quota in memory if we failed to update it on disk? | |
m_quotaMap->set(origin->threadsafeCopy(), quota); | |
if (m_client) | |
m_client->dispatchDidModifyOrigin(origin); | |
} | |
bool DatabaseTracker::addDatabase(SecurityOrigin* origin, const String& name, const String& path) | |
{ | |
ASSERT(!m_databaseGuard.tryLock()); | |
ASSERT(m_quotaMap); | |
openTrackerDatabase(true); | |
if (!m_database.isOpen()) | |
return false; | |
// New database should never be added until the origin has been established | |
ASSERT(hasEntryForOriginNoLock(origin)); | |
SQLiteStatement statement(m_database, "INSERT INTO Databases (origin, name, path) VALUES (?, ?, ?);"); | |
if (statement.prepare() != SQLResultOk) | |
return false; | |
statement.bindText(1, origin->databaseIdentifier()); | |
statement.bindText(2, name); | |
statement.bindText(3, path); | |
if (!statement.executeCommand()) { | |
LOG_ERROR("Failed to add database %s to origin %s: %s\n", name.ascii().data(), origin->databaseIdentifier().ascii().data(), m_database.lastErrorMsg()); | |
return false; | |
} | |
if (m_client) | |
m_client->dispatchDidModifyOrigin(origin); | |
return true; | |
} | |
void DatabaseTracker::deleteAllDatabases() | |
{ | |
Vector<RefPtr<SecurityOrigin> > originsCopy; | |
origins(originsCopy); | |
for (unsigned i = 0; i < originsCopy.size(); ++i) | |
deleteOrigin(originsCopy[i].get()); | |
} | |
// It is the caller's responsibility to make sure that nobody is trying to create, delete, open, or close databases in this origin while the deletion is | |
// taking place. | |
void DatabaseTracker::deleteOrigin(SecurityOrigin* origin) | |
{ | |
Vector<String> databaseNames; | |
{ | |
MutexLocker lockDatabase(m_databaseGuard); | |
openTrackerDatabase(false); | |
if (!m_database.isOpen()) | |
return; | |
if (!databaseNamesForOriginNoLock(origin, databaseNames)) { | |
LOG_ERROR("Unable to retrieve list of database names for origin %s", origin->databaseIdentifier().ascii().data()); | |
return; | |
} | |
if (!canDeleteOrigin(origin)) { | |
LOG_ERROR("Tried to delete an origin (%s) while either creating database in it or already deleting it", origin->databaseIdentifier().ascii().data()); | |
ASSERT(false); | |
return; | |
} | |
recordDeletingOrigin(origin); | |
} | |
// We drop the lock here because holding locks during a call to deleteDatabaseFile will deadlock. | |
for (unsigned i = 0; i < databaseNames.size(); ++i) { | |
if (!deleteDatabaseFile(origin, databaseNames[i])) { | |
// Even if the file can't be deleted, we want to try and delete the rest, don't return early here. | |
LOG_ERROR("Unable to delete file for database %s in origin %s", databaseNames[i].ascii().data(), origin->databaseIdentifier().ascii().data()); | |
} | |
} | |
{ | |
// To satisfy the lock hierarchy, we have to lock the originQuotaManager before m_databaseGuard if there's any chance we'll to lock both. | |
Locker<OriginQuotaManager> quotaManagerLocker(originQuotaManager()); | |
MutexLocker lockDatabase(m_databaseGuard); | |
SQLiteStatement statement(m_database, "DELETE FROM Databases WHERE origin=?"); | |
doneDeletingOrigin(origin); | |
if (statement.prepare() != SQLResultOk) { | |
LOG_ERROR("Unable to prepare deletion of databases from origin %s from tracker", origin->databaseIdentifier().ascii().data()); | |
return; | |
} | |
statement.bindText(1, origin->databaseIdentifier()); | |
if (!statement.executeCommand()) { | |
LOG_ERROR("Unable to execute deletion of databases from origin %s from tracker", origin->databaseIdentifier().ascii().data()); | |
return; | |
} | |
SQLiteStatement originStatement(m_database, "DELETE FROM Origins WHERE origin=?"); | |
if (originStatement.prepare() != SQLResultOk) { | |
LOG_ERROR("Unable to prepare deletion of origin %s from tracker", origin->databaseIdentifier().ascii().data()); | |
return; | |
} | |
originStatement.bindText(1, origin->databaseIdentifier()); | |
if (!originStatement.executeCommand()) { | |
LOG_ERROR("Unable to execute deletion of databases from origin %s from tracker", origin->databaseIdentifier().ascii().data()); | |
return; | |
} | |
SQLiteFileSystem::deleteEmptyDatabaseDirectory(originPath(origin)); | |
RefPtr<SecurityOrigin> originPossiblyLastReference = origin; | |
m_quotaMap->remove(origin); | |
originQuotaManagerNoLock().removeOrigin(origin); | |
// If we removed the last origin, do some additional deletion. | |
if (m_quotaMap->isEmpty()) { | |
if (m_database.isOpen()) | |
m_database.close(); | |
SQLiteFileSystem::deleteDatabaseFile(trackerDatabasePath()); | |
SQLiteFileSystem::deleteEmptyDatabaseDirectory(m_databaseDirectoryPath); | |
} | |
if (m_client) { | |
m_client->dispatchDidModifyOrigin(origin); | |
for (unsigned i = 0; i < databaseNames.size(); ++i) | |
m_client->dispatchDidModifyDatabase(origin, databaseNames[i]); | |
} | |
} | |
} | |
bool DatabaseTracker::canCreateDatabase(SecurityOrigin *origin, const String& name) | |
{ | |
ASSERT(!m_databaseGuard.tryLock()); | |
// Can't create a database while someone else is deleting it; there's a risk of leaving untracked database debris on the disk. | |
return !deletingDatabase(origin, name) && !deletingOrigin(origin); | |
} | |
void DatabaseTracker::recordCreatingDatabase(SecurityOrigin *origin, const String& name) | |
{ | |
ASSERT(!m_databaseGuard.tryLock()); | |
NameCountMap* nameMap = m_beingCreated.get(origin); | |
if (!nameMap) { | |
nameMap = new NameCountMap(); | |
m_beingCreated.set(origin->threadsafeCopy(), nameMap); | |
} | |
long count = nameMap->get(name); | |
nameMap->set(name.threadsafeCopy(), count + 1); | |
} | |
void DatabaseTracker::doneCreatingDatabase(SecurityOrigin *origin, const String& name) | |
{ | |
ASSERT(!m_databaseGuard.tryLock()); | |
NameCountMap* nameMap = m_beingCreated.get(origin); | |
if (nameMap) { | |
long count = nameMap->get(name); | |
ASSERT(count > 0); | |
if (count <= 1) { | |
nameMap->remove(name); | |
if (nameMap->isEmpty()) { | |
m_beingCreated.remove(origin); | |
delete nameMap; | |
} | |
} else | |
nameMap->set(name, count - 1); | |
} else | |
ASSERT(false); | |
} | |
bool DatabaseTracker::creatingDatabase(SecurityOrigin *origin, const String& name) | |
{ | |
ASSERT(!m_databaseGuard.tryLock()); | |
NameCountMap* nameMap = m_beingCreated.get(origin); | |
return nameMap && nameMap->get(name); | |
} | |
bool DatabaseTracker::canDeleteDatabase(SecurityOrigin *origin, const String& name) | |
{ | |
ASSERT(!m_databaseGuard.tryLock()); | |
return !creatingDatabase(origin, name) && !deletingDatabase(origin, name); | |
} | |
void DatabaseTracker::recordDeletingDatabase(SecurityOrigin *origin, const String& name) | |
{ | |
ASSERT(!m_databaseGuard.tryLock()); | |
ASSERT(canDeleteDatabase(origin, name)); | |
NameSet* nameSet = m_beingDeleted.get(origin); | |
if (!nameSet) { | |
nameSet = new NameSet(); | |
m_beingDeleted.set(origin->threadsafeCopy(), nameSet); | |
} | |
ASSERT(!nameSet->contains(name)); | |
nameSet->add(name.threadsafeCopy()); | |
} | |
void DatabaseTracker::doneDeletingDatabase(SecurityOrigin *origin, const String& name) | |
{ | |
ASSERT(!m_databaseGuard.tryLock()); | |
NameSet* nameSet = m_beingDeleted.get(origin); | |
if (nameSet) { | |
ASSERT(nameSet->contains(name)); | |
nameSet->remove(name); | |
if (nameSet->isEmpty()) { | |
m_beingDeleted.remove(origin); | |
delete nameSet; | |
} | |
} else { | |
ASSERT(false); | |
} | |
} | |
bool DatabaseTracker::deletingDatabase(SecurityOrigin *origin, const String& name) | |
{ | |
ASSERT(!m_databaseGuard.tryLock()); | |
NameSet* nameSet = m_beingDeleted.get(origin); | |
return nameSet && nameSet->contains(name); | |
} | |
bool DatabaseTracker::canDeleteOrigin(SecurityOrigin *origin) | |
{ | |
ASSERT(!m_databaseGuard.tryLock()); | |
return !(deletingOrigin(origin) || m_beingCreated.get(origin)); | |
} | |
bool DatabaseTracker::deletingOrigin(SecurityOrigin *origin) | |
{ | |
ASSERT(!m_databaseGuard.tryLock()); | |
return m_originsBeingDeleted.contains(origin); | |
} | |
void DatabaseTracker::recordDeletingOrigin(SecurityOrigin *origin) | |
{ | |
ASSERT(!m_databaseGuard.tryLock()); | |
ASSERT(!deletingOrigin(origin)); | |
m_originsBeingDeleted.add(origin->threadsafeCopy()); | |
} | |
void DatabaseTracker::doneDeletingOrigin(SecurityOrigin *origin) | |
{ | |
ASSERT(!m_databaseGuard.tryLock()); | |
ASSERT(deletingOrigin(origin)); | |
m_originsBeingDeleted.remove(origin); | |
} | |
void DatabaseTracker::deleteDatabase(SecurityOrigin* origin, const String& name) | |
{ | |
{ | |
MutexLocker lockDatabase(m_databaseGuard); | |
openTrackerDatabase(false); | |
if (!m_database.isOpen()) | |
return; | |
if (!canDeleteDatabase(origin, name)) { | |
ASSERT(FALSE); | |
return; | |
} | |
recordDeletingDatabase(origin, name); | |
} | |
// We drop the lock here because holding locks during a call to deleteDatabaseFile will deadlock. | |
if (!deleteDatabaseFile(origin, name)) { | |
LOG_ERROR("Unable to delete file for database %s in origin %s", name.ascii().data(), origin->databaseIdentifier().ascii().data()); | |
MutexLocker lockDatabase(m_databaseGuard); | |
doneDeletingDatabase(origin, name); | |
return; | |
} | |
// To satisfy the lock hierarchy, we have to lock the originQuotaManager before m_databaseGuard if there's any chance we'll to lock both. | |
Locker<OriginQuotaManager> quotaManagerLocker(originQuotaManager()); | |
MutexLocker lockDatabase(m_databaseGuard); | |
SQLiteStatement statement(m_database, "DELETE FROM Databases WHERE origin=? AND name=?"); | |
if (statement.prepare() != SQLResultOk) { | |
LOG_ERROR("Unable to prepare deletion of database %s from origin %s from tracker", name.ascii().data(), origin->databaseIdentifier().ascii().data()); | |
doneDeletingDatabase(origin, name); | |
return; | |
} | |
statement.bindText(1, origin->databaseIdentifier()); | |
statement.bindText(2, name); | |
if (!statement.executeCommand()) { | |
LOG_ERROR("Unable to execute deletion of database %s from origin %s from tracker", name.ascii().data(), origin->databaseIdentifier().ascii().data()); | |
doneDeletingDatabase(origin, name); | |
return; | |
} | |
originQuotaManagerNoLock().removeDatabase(origin, name); | |
if (m_client) { | |
m_client->dispatchDidModifyOrigin(origin); | |
m_client->dispatchDidModifyDatabase(origin, name); | |
} | |
doneDeletingDatabase(origin, name); | |
} | |
// deleteDatabaseFile has to release locks between looking up the list of databases to close and closing them. While this is in progress, the caller | |
// is responsible for making sure no new databases are opened in the file to be deleted. | |
bool DatabaseTracker::deleteDatabaseFile(SecurityOrigin* origin, const String& name) | |
{ | |
String fullPath = fullPathForDatabase(origin, name, false); | |
if (fullPath.isEmpty()) | |
return true; | |
#ifndef NDEBUG | |
{ | |
MutexLocker lockDatabase(m_databaseGuard); | |
ASSERT(deletingDatabase(origin, name) || deletingOrigin(origin)); | |
} | |
#endif | |
Vector<RefPtr<Database> > deletedDatabases; | |
// Make sure not to hold the any locks when calling | |
// Database::markAsDeletedAndClose(), since that can cause a deadlock | |
// during the synchronous DatabaseThread call it triggers. | |
{ | |
MutexLocker openDatabaseMapLock(m_openDatabaseMapGuard); | |
if (m_openDatabaseMap) { | |
// There are some open databases, lets check if they are for this origin. | |
DatabaseNameMap* nameMap = m_openDatabaseMap->get(origin); | |
if (nameMap && nameMap->size()) { | |
// There are some open databases for this origin, let's check | |
// if they are this database by name. | |
DatabaseSet* databaseSet = nameMap->get(name); | |
if (databaseSet && databaseSet->size()) { | |
// We have some database open with this name. Mark them as deleted. | |
DatabaseSet::const_iterator end = databaseSet->end(); | |
for (DatabaseSet::const_iterator it = databaseSet->begin(); it != end; ++it) | |
deletedDatabases.append(*it); | |
} | |
} | |
} | |
} | |
for (unsigned i = 0; i < deletedDatabases.size(); ++i) | |
deletedDatabases[i]->markAsDeletedAndClose(); | |
return SQLiteFileSystem::deleteDatabaseFile(fullPath); | |
} | |
void DatabaseTracker::setClient(DatabaseTrackerClient* client) | |
{ | |
m_client = client; | |
} | |
static Mutex& notificationMutex() | |
{ | |
DEFINE_STATIC_LOCAL(Mutex, mutex, ()); | |
return mutex; | |
} | |
typedef Vector<pair<RefPtr<SecurityOrigin>, String> > NotificationQueue; | |
static NotificationQueue& notificationQueue() | |
{ | |
DEFINE_STATIC_LOCAL(NotificationQueue, queue, ()); | |
return queue; | |
} | |
void DatabaseTracker::scheduleNotifyDatabaseChanged(SecurityOrigin* origin, const String& name) | |
{ | |
MutexLocker locker(notificationMutex()); | |
notificationQueue().append(pair<RefPtr<SecurityOrigin>, String>(origin->threadsafeCopy(), name.crossThreadString())); | |
scheduleForNotification(); | |
} | |
static bool notificationScheduled = false; | |
void DatabaseTracker::scheduleForNotification() | |
{ | |
ASSERT(!notificationMutex().tryLock()); | |
if (!notificationScheduled) { | |
callOnMainThread(DatabaseTracker::notifyDatabasesChanged, 0); | |
notificationScheduled = true; | |
} | |
} | |
void DatabaseTracker::notifyDatabasesChanged(void*) | |
{ | |
// Note that if DatabaseTracker ever becomes non-singleton, we'll have to amend this notification | |
// mechanism to include which tracker the notification goes out on as well. | |
DatabaseTracker& theTracker(tracker()); | |
NotificationQueue notifications; | |
{ | |
MutexLocker locker(notificationMutex()); | |
notifications.swap(notificationQueue()); | |
notificationScheduled = false; | |
} | |
if (!theTracker.m_client) | |
return; | |
for (unsigned i = 0; i < notifications.size(); ++i) | |
theTracker.m_client->dispatchDidModifyDatabase(notifications[i].first.get(), notifications[i].second); | |
} | |
} // namespace WebCore | |
#endif |