2 * Copyright (C) 2007 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14 * its contributors may be used to endorse or promote products derived
15 * from this software without specific prior written permission.
17 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 #include "DatabaseTracker.h"
32 #include "DatabaseTrackerClient.h"
34 #include "FileSystem.h"
36 #include "SecurityOriginData.h"
37 #include "SQLiteStatement.h"
41 // HTML5 SQL Storage spec suggests 5MB as the default quota per origin
42 static const unsigned DefaultOriginQuota = 5242880;
44 struct SecurityOriginDataHash {
45 static unsigned hash(const SecurityOriginData& data)
47 unsigned hashCodes[3] = {
48 data.protocol().impl() ? data.protocol().impl()->hash() : 0,
49 data.host().impl() ? data.host().impl()->hash() : 0,
52 return StringImpl::computeHash(reinterpret_cast<UChar*>(hashCodes), 3 * sizeof(unsigned) / sizeof(UChar));
55 static bool equal(const SecurityOriginData& a, const SecurityOriginData& b)
60 static const bool safeToCompareToEmptyOrDeleted = true;
63 struct SecurityOriginDataTraits : WTF::GenericHashTraits<SecurityOriginData> {
64 static const SecurityOriginData& deletedValue()
66 // Okay deleted value because file: protocols should always have port 0
67 static SecurityOriginData key("file", "", 1);
70 static const SecurityOriginData& emptyValue()
72 // Okay empty value because file: protocols should always have port 0
73 static SecurityOriginData key("file", "", 2);
78 DatabaseTracker& DatabaseTracker::tracker()
80 static DatabaseTracker tracker;
85 DatabaseTracker::DatabaseTracker()
86 : m_defaultQuota(DefaultOriginQuota)
91 void DatabaseTracker::setDatabasePath(const String& path)
93 m_databasePath = path;
94 openTrackerDatabase();
97 const String& DatabaseTracker::databasePath()
99 return m_databasePath;
102 void DatabaseTracker::openTrackerDatabase()
104 ASSERT(!m_database.isOpen());
106 makeAllDirectories(m_databasePath);
107 String databasePath = pathByAppendingComponent(m_databasePath, "Databases.db");
109 if (!m_database.open(databasePath)) {
110 // FIXME: What do do here?
113 if (!m_database.tableExists("Origins")) {
114 if (!m_database.executeCommand("CREATE TABLE Origins (origin TEXT UNIQUE ON CONFLICT REPLACE, quota INTEGER NOT NULL ON CONFLICT FAIL);")) {
119 if (!m_database.tableExists("Databases")) {
120 if (!m_database.executeCommand("CREATE TABLE Databases (guid INTEGER PRIMARY KEY AUTOINCREMENT, origin TEXT, name TEXT, displayName TEXT, estimatedSize INTEGER, path TEXT);")) {
126 bool DatabaseTracker::canEstablishDatabase(Document* document, const String& name, const String& displayName, unsigned long estimatedSize)
128 SecurityOriginData originData = document->securityOrigin().securityOriginData();
130 // If this origin has no databases yet, establish an entry in the tracker database with the default quota
131 if (!hasEntryForOrigin(originData))
132 establishEntryForOrigin(originData);
134 // If a database already exists, you can always establish a handle to it
135 if (hasEntryForDatabase(originData, name))
138 // If the new database will fit as-is, allow its creation
139 unsigned long long usage = usageForOrigin(originData);
140 if (usage + estimatedSize < quotaForOrigin(originData))
143 // Otherwise, ask the UI Delegate for a new quota
145 if (!(page = document->page()))
148 // If no displayName was specified, pass the standard (required) name instead
149 unsigned long long newQuota = page->chrome()->requestQuotaIncreaseForNewDatabase(document->frame(), originData, displayName.length() > 0 ? displayName : name, estimatedSize);
150 setQuota(originData, newQuota);
152 return usage + estimatedSize <= newQuota;
155 bool DatabaseTracker::hasEntryForOrigin(const SecurityOriginData& origin)
158 return m_originQuotaMap->contains(origin);
161 bool DatabaseTracker::hasEntryForDatabase(const SecurityOriginData& origin, const String& databaseIdentifier)
163 SQLiteStatement statement(m_database, "SELECT guid FROM Databases WHERE origin=? AND name=?;");
165 if (statement.prepare() != SQLResultOk)
168 statement.bindText(1, origin.stringIdentifier());
169 statement.bindText(2, databaseIdentifier);
171 return statement.step() == SQLResultRow;
174 void DatabaseTracker::establishEntryForOrigin(const SecurityOriginData& origin)
176 ASSERT(!hasEntryForOrigin(origin));
178 SQLiteStatement statement(m_database, "INSERT INTO Origins VALUES (?, ?)");
179 if (statement.prepare() != SQLResultOk) {
180 LOG_ERROR("Unable to establish origin %s in the tracker", origin.stringIdentifier().ascii().data());
184 statement.bindText(1, origin.stringIdentifier());
185 statement.bindInt64(2, m_defaultQuota);
187 if (statement.step() != SQLResultDone) {
188 LOG_ERROR("Unable to establish origin %s in the tracker", origin.stringIdentifier().ascii().data());
193 m_originQuotaMap->set(origin, m_defaultQuota);
196 m_client->dispatchDidModifyOrigin(origin);
199 String DatabaseTracker::fullPathForDatabase(const SecurityOriginData& origin, const String& name, bool createIfNotExists)
201 String originIdentifier = origin.stringIdentifier();
202 String originPath = pathByAppendingComponent(m_databasePath, originIdentifier);
204 // Make sure the path for this SecurityOrigin exists
205 if (createIfNotExists && !makeAllDirectories(originPath))
208 // See if we have a path for this database yet
209 SQLiteStatement statement(m_database, "SELECT path FROM Databases WHERE origin=? AND name=?;");
211 if (statement.prepare() != SQLResultOk)
214 statement.bindText(1, originIdentifier);
215 statement.bindText(2, name);
217 int result = statement.step();
219 if (result == SQLResultRow)
220 return pathByAppendingComponent(originPath, statement.getColumnText16(0));
221 if (!createIfNotExists)
224 if (result != SQLResultDone) {
225 LOG_ERROR("Failed to retrieve filename from Database Tracker for origin %s, name %s", origin.stringIdentifier().ascii().data(), name.ascii().data());
228 statement.finalize();
230 SQLiteStatement sequenceStatement(m_database, "SELECT seq FROM sqlite_sequence WHERE name='Databases';");
232 // FIXME: More informative error handling here, even though these steps should never fail
233 if (sequenceStatement.prepare() != SQLResultOk)
235 result = sequenceStatement.step();
237 // This has a range of 2^63 and starts at 0 for every time a user resets Safari -
238 // I can't imagine it'd over overflow
240 if (result == SQLResultRow) {
241 seq = sequenceStatement.getColumnInt64(0);
242 } else if (result != SQLResultDone)
244 sequenceStatement.finalize();
249 filename = pathByAppendingComponent(originPath, String::format("%016llx.db", seq));
250 } while (fileExists(filename));
252 if (!addDatabase(origin, name, String::format("%016llx.db", seq)))
258 void DatabaseTracker::populateOrigins()
260 if (m_originQuotaMap)
263 m_originQuotaMap.set(new HashMap<SecurityOriginData, unsigned long long, SecurityOriginDataHash, SecurityOriginDataTraits>);
265 if (!m_database.isOpen())
268 SQLiteStatement statement(m_database, "SELECT origin, quota FROM Origins");
270 if (statement.prepare() != SQLResultOk)
274 while ((result = statement.step()) == SQLResultRow)
275 m_originQuotaMap->set(statement.getColumnText16(0), statement.getColumnInt64(1));
277 if (result != SQLResultDone)
278 LOG_ERROR("Failed to read in all origins from the database");
283 void DatabaseTracker::origins(Vector<SecurityOriginData>& result)
285 if (!m_originQuotaMap)
288 copyKeysToVector(*(m_originQuotaMap.get()), result);
291 bool DatabaseTracker::databaseNamesForOrigin(const SecurityOriginData& origin, Vector<String>& resultVector)
293 if (!m_database.isOpen())
296 SQLiteStatement statement(m_database, "SELECT name FROM Databases where origin=?;");
298 if (statement.prepare() != SQLResultOk)
301 statement.bindText(1, origin.stringIdentifier());
304 while ((result = statement.step()) == SQLResultRow)
305 resultVector.append(statement.getColumnText16(0));
307 if (result != SQLResultDone) {
308 LOG_ERROR("Failed to retrieve all database names for origin %s", origin.stringIdentifier().ascii().data());
315 DatabaseDetails DatabaseTracker::detailsForNameAndOrigin(const String& name, const SecurityOriginData& origin)
317 String originIdentifier = origin.stringIdentifier();
319 SQLiteStatement statement(m_database, "SELECT displayName, estimatedSize FROM Databases WHERE origin=? AND name=?");
320 if (statement.prepare() != SQLResultOk)
321 return DatabaseDetails();
323 statement.bindText(1, originIdentifier);
324 statement.bindText(2, name);
326 int result = statement.step();
327 if (result == SQLResultDone)
328 return DatabaseDetails();
330 if (result != SQLResultRow) {
331 LOG_ERROR("Error retrieving details for database %s in origin %s from tracker database", name.ascii().data(), originIdentifier.ascii().data());
332 return DatabaseDetails();
335 return DatabaseDetails(name, statement.getColumnText(0), statement.getColumnInt64(1), usageForDatabase(name, origin));
338 void DatabaseTracker::setDatabaseDetails(const SecurityOriginData& origin, const String& name, const String& displayName, unsigned long estimatedSize)
340 String originIdentifier = origin.stringIdentifier();
343 SQLiteStatement statement(m_database, "SELECT guid FROM Databases WHERE origin=? AND name=?");
344 if (statement.prepare() != SQLResultOk)
347 statement.bindText(1, originIdentifier);
348 statement.bindText(2, name);
350 int result = statement.step();
351 if (result == SQLResultRow)
352 guid = statement.getColumnInt64(0);
353 statement.finalize();
356 if (result != SQLResultDone)
357 LOG_ERROR("Error to determing existence of database %s in origin %s in tracker database", name.ascii().data(), originIdentifier.ascii().data());
359 // This case should never occur - we should never be setting database details for a database that doesn't already exist in the tracker
360 // 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
361 // So we'll print an error instead
362 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",
363 name.ascii().data(), originIdentifier.ascii().data());
368 SQLiteStatement updateStatement(m_database, "UPDATE Databases SET displayName=?, estimatedSize=? WHERE guid=?");
369 if (updateStatement.prepare() != SQLResultOk)
372 updateStatement.bindText(1, displayName);
373 updateStatement.bindInt64(2, estimatedSize);
374 updateStatement.bindInt64(3, guid);
376 if (updateStatement.step() != SQLResultDone) {
377 LOG_ERROR("Failed to update details for database %s in origin %s", name.ascii().data(), originIdentifier.ascii().data());
382 m_client->dispatchDidModifyDatabase(origin, name);
385 unsigned long long DatabaseTracker::usageForDatabase(const String& name, const SecurityOriginData& origin)
387 String path = fullPathForDatabase(origin, name, false);
392 return fileSize(path, size) ? size : 0;
395 unsigned long long DatabaseTracker::usageForOrigin(const SecurityOriginData& origin)
397 Vector<String> names;
398 databaseNamesForOrigin(origin, names);
400 unsigned long long result = 0;
401 for (unsigned i = 0; i < names.size(); ++i)
402 result += usageForDatabase(names[i], origin);
407 unsigned long long DatabaseTracker::quotaForOrigin(const SecurityOriginData& origin)
410 return m_originQuotaMap->get(origin);
413 void DatabaseTracker::setQuota(const SecurityOriginData& origin, unsigned long long quota)
416 if (!m_originQuotaMap->contains(origin))
417 establishEntryForOrigin(origin);
419 m_originQuotaMap->set(origin, quota);
421 SQLiteStatement statement(m_database, "UPDATE Origins SET quota=? WHERE origin=?");
423 bool error = statement.prepare() != SQLResultOk;
425 statement.bindInt64(1, quota);
426 statement.bindText(2, origin.stringIdentifier());
428 error = !statement.executeCommand();
432 LOG_ERROR("Failed to set quota %llu in tracker database for origin %s", quota, origin.stringIdentifier().ascii().data());
435 m_client->dispatchDidModifyOrigin(origin);
438 bool DatabaseTracker::addDatabase(const SecurityOriginData& origin, const String& name, const String& path)
440 if (!m_database.isOpen())
443 // New database should never be added until the origin has been established
444 ASSERT(hasEntryForOrigin(origin));
446 SQLiteStatement statement(m_database, "INSERT INTO Databases (origin, name, path) VALUES (?, ?, ?);");
448 if (statement.prepare() != SQLResultOk)
451 statement.bindText(1, origin.stringIdentifier());
452 statement.bindText(2, name);
453 statement.bindText(3, path);
455 if (!statement.executeCommand()) {
456 LOG_ERROR("Failed to add database %s to origin %s: %s\n", name.ascii().data(), origin.stringIdentifier().ascii().data(), statement.lastErrorMsg());
461 m_client->dispatchDidModifyOrigin(origin);
466 void DatabaseTracker::deleteAllDatabases()
470 HashMap<SecurityOriginData, unsigned long long, SecurityOriginDataHash, SecurityOriginDataTraits>::const_iterator iter = m_originQuotaMap->begin();
471 HashMap<SecurityOriginData, unsigned long long, SecurityOriginDataHash, SecurityOriginDataTraits>::const_iterator end = m_originQuotaMap->end();
473 for (; iter != end; ++iter)
474 deleteDatabasesWithOrigin(iter->first);
477 void DatabaseTracker::deleteDatabasesWithOrigin(const SecurityOriginData& origin)
479 Vector<String> databaseNames;
480 if (!databaseNamesForOrigin(origin, databaseNames)) {
481 LOG_ERROR("Unable to retrieve list of database names for origin %s", origin.stringIdentifier().ascii().data());
485 for (unsigned i = 0; i < databaseNames.size(); ++i) {
486 if (!deleteDatabaseFile(origin, databaseNames[i])) {
487 LOG_ERROR("Unable to delete file for database %s in origin %s", databaseNames[i].ascii().data(), origin.stringIdentifier().ascii().data());
492 SQLiteStatement statement(m_database, "DELETE FROM Databases WHERE origin=?");
493 if (statement.prepare() != SQLResultOk) {
494 LOG_ERROR("Unable to prepare deletion of databases from origin %s from tracker", origin.stringIdentifier().ascii().data());
498 statement.bindText(1, origin.stringIdentifier());
500 if (!statement.executeCommand()) {
501 LOG_ERROR("Unable to execute deletion of databases from origin %s from tracker", origin.stringIdentifier().ascii().data());
506 m_client->dispatchDidModifyOrigin(origin);
507 for (unsigned i = 0; i < databaseNames.size(); ++i)
508 m_client->dispatchDidModifyDatabase(origin, databaseNames[i]);
512 void DatabaseTracker::deleteDatabase(const SecurityOriginData& origin, const String& name)
514 if (!deleteDatabaseFile(origin, name)) {
515 LOG_ERROR("Unable to delete file for database %s in origin %s", name.ascii().data(), origin.stringIdentifier().ascii().data());
519 SQLiteStatement statement(m_database, "DELETE FROM Databases WHERE origin=? AND name=?");
520 if (statement.prepare() != SQLResultOk) {
521 LOG_ERROR("Unable to prepare deletion of database %s from origin %s from tracker", name.ascii().data(), origin.stringIdentifier().ascii().data());
525 statement.bindText(1, origin.stringIdentifier());
526 statement.bindText(2, name);
528 if (!statement.executeCommand()) {
529 LOG_ERROR("Unable to execute deletion of database %s from origin %s from tracker", name.ascii().data(), origin.stringIdentifier().ascii().data());
534 m_client->dispatchDidModifyOrigin(origin);
535 m_client->dispatchDidModifyDatabase(origin, name);
539 bool DatabaseTracker::deleteDatabaseFile(const SecurityOriginData& origin, const String& name)
541 String fullPath = fullPathForDatabase(origin, name, false);
542 if (fullPath.isEmpty())
545 return deleteFile(fullPath);
548 void DatabaseTracker::setClient(DatabaseTrackerClient* client)
553 void DatabaseTracker::setDefaultOriginQuota(unsigned long long quota)
555 m_defaultQuota = quota;
558 unsigned long long DatabaseTracker::defaultOriginQuota() const
560 return m_defaultQuota;
563 static Mutex& notificationMutex()
569 static Vector<pair<SecurityOriginData, String> >& notificationQueue()
571 static Vector<pair<SecurityOriginData, String> > queue;
575 void DatabaseTracker::scheduleNotifyDatabaseChanged(const SecurityOriginData& origin, const String& name)
577 MutexLocker locker(notificationMutex());
579 notificationQueue().append(pair<SecurityOriginData, String>(origin.copy(), name.copy()));
580 scheduleForNotification();
583 static bool notificationScheduled = false;
585 void DatabaseTracker::scheduleForNotification()
587 ASSERT(!notificationMutex().tryLock());
589 if (!notificationScheduled) {
590 callOnMainThread(DatabaseTracker::notifyDatabasesChanged);
591 notificationScheduled = true;
595 void DatabaseTracker::notifyDatabasesChanged()
597 // Note that if DatabaseTracker ever becomes non-singleton, we'll have to ammend this notification
598 // mechanism to inclue which tracker the notification goes out on, as well
599 DatabaseTracker& theTracker(tracker());
601 Vector<pair<SecurityOriginData, String> > notifications;
603 MutexLocker locker(notificationMutex());
605 notifications.swap(notificationQueue());
607 notificationScheduled = false;
610 if (!theTracker.m_client)
613 for (unsigned i = 0; i < notifications.size(); ++i)
614 theTracker.m_client->dispatchDidModifyDatabase(notifications[i].first, notifications[i].second);
618 } // namespace WebCore