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 unsigned long long newQuota = page->chrome()->requestQuotaIncreaseForNewDatabase(document->frame(), originData, displayName, estimatedSize);
149 setQuota(originData, newQuota);
151 return usage + estimatedSize <= newQuota;
154 bool DatabaseTracker::hasEntryForOrigin(const SecurityOriginData& origin)
157 return m_originQuotaMap->contains(origin);
160 bool DatabaseTracker::hasEntryForDatabase(const SecurityOriginData& origin, const String& databaseIdentifier)
162 SQLiteStatement statement(m_database, "SELECT guid FROM Databases WHERE origin=? AND name=?;");
164 if (statement.prepare() != SQLResultOk)
167 statement.bindText(1, origin.stringIdentifier());
168 statement.bindText(2, databaseIdentifier);
170 return statement.step() == SQLResultRow;
173 void DatabaseTracker::establishEntryForOrigin(const SecurityOriginData& origin)
175 ASSERT(!hasEntryForOrigin(origin));
177 SQLiteStatement statement(m_database, "INSERT INTO Origins VALUES (?, ?)");
178 if (statement.prepare() != SQLResultOk) {
179 LOG_ERROR("Unable to establish origin %s in the tracker", origin.stringIdentifier().ascii().data());
183 statement.bindText(1, origin.stringIdentifier());
184 statement.bindInt64(2, m_defaultQuota);
186 if (statement.step() != SQLResultDone) {
187 LOG_ERROR("Unable to establish origin %s in the tracker", origin.stringIdentifier().ascii().data());
192 m_originQuotaMap->set(origin, m_defaultQuota);
195 m_client->dispatchDidModifyOrigin(origin);
198 String DatabaseTracker::fullPathForDatabase(const SecurityOriginData& origin, const String& name, bool createIfNotExists)
200 String originIdentifier = origin.stringIdentifier();
201 String originPath = pathByAppendingComponent(m_databasePath, originIdentifier);
203 // Make sure the path for this SecurityOrigin exists
204 if (createIfNotExists && !makeAllDirectories(originPath))
207 // See if we have a path for this database yet
208 SQLiteStatement statement(m_database, "SELECT path FROM Databases WHERE origin=? AND name=?;");
210 if (statement.prepare() != SQLResultOk)
213 statement.bindText(1, originIdentifier);
214 statement.bindText(2, name);
216 int result = statement.step();
218 if (result == SQLResultRow)
219 return pathByAppendingComponent(originPath, statement.getColumnText16(0));
220 if (!createIfNotExists)
223 if (result != SQLResultDone) {
224 LOG_ERROR("Failed to retrieve filename from Database Tracker for origin %s, name %s", origin.stringIdentifier().ascii().data(), name.ascii().data());
227 statement.finalize();
229 SQLiteStatement sequenceStatement(m_database, "SELECT seq FROM sqlite_sequence WHERE name='Databases';");
231 // FIXME: More informative error handling here, even though these steps should never fail
232 if (sequenceStatement.prepare() != SQLResultOk)
234 result = sequenceStatement.step();
236 // This has a range of 2^63 and starts at 0 for every time a user resets Safari -
237 // I can't imagine it'd over overflow
239 if (result == SQLResultRow) {
240 seq = sequenceStatement.getColumnInt64(0);
241 } else if (result != SQLResultDone)
243 sequenceStatement.finalize();
248 filename = pathByAppendingComponent(originPath, String::format("%016llx.db", seq));
249 } while (fileExists(filename));
251 if (!addDatabase(origin, name, String::format("%016llx.db", seq)))
257 void DatabaseTracker::populateOrigins()
259 if (m_originQuotaMap)
262 m_originQuotaMap.set(new HashMap<SecurityOriginData, unsigned long long, SecurityOriginDataHash, SecurityOriginDataTraits>);
264 if (!m_database.isOpen())
267 SQLiteStatement statement(m_database, "SELECT origin, quota FROM Origins");
269 if (statement.prepare() != SQLResultOk)
273 while ((result = statement.step()) == SQLResultRow)
274 m_originQuotaMap->set(statement.getColumnText16(0), statement.getColumnInt64(1));
276 if (result != SQLResultDone)
277 LOG_ERROR("Failed to read in all origins from the database");
282 void DatabaseTracker::origins(Vector<SecurityOriginData>& result)
284 if (!m_originQuotaMap)
287 copyKeysToVector(*(m_originQuotaMap.get()), result);
290 bool DatabaseTracker::databaseNamesForOrigin(const SecurityOriginData& origin, Vector<String>& resultVector)
292 if (!m_database.isOpen())
295 SQLiteStatement statement(m_database, "SELECT name FROM Databases where origin=?;");
297 if (statement.prepare() != SQLResultOk)
300 statement.bindText(1, origin.stringIdentifier());
303 while ((result = statement.step()) == SQLResultRow)
304 resultVector.append(statement.getColumnText16(0));
306 if (result != SQLResultDone) {
307 LOG_ERROR("Failed to retrieve all database names for origin %s", origin.stringIdentifier().ascii().data());
314 DatabaseDetails DatabaseTracker::detailsForNameAndOrigin(const String& name, const SecurityOriginData& origin)
316 String originIdentifier = origin.stringIdentifier();
318 SQLiteStatement statement(m_database, "SELECT displayName, estimatedSize FROM Databases WHERE origin=? AND name=?");
319 if (statement.prepare() != SQLResultOk)
320 return DatabaseDetails();
322 statement.bindText(1, originIdentifier);
323 statement.bindText(2, name);
325 int result = statement.step();
326 if (result == SQLResultDone)
327 return DatabaseDetails();
329 if (result != SQLResultRow) {
330 LOG_ERROR("Error retrieving details for database %s in origin %s from tracker database", name.ascii().data(), originIdentifier.ascii().data());
331 return DatabaseDetails();
334 return DatabaseDetails(name, statement.getColumnText(0), statement.getColumnInt64(1), usageForDatabase(name, origin));
337 void DatabaseTracker::setDatabaseDetails(const SecurityOriginData& origin, const String& name, const String& displayName, unsigned long estimatedSize)
339 String originIdentifier = origin.stringIdentifier();
342 SQLiteStatement statement(m_database, "SELECT guid FROM Databases WHERE origin=? AND name=?");
343 if (statement.prepare() != SQLResultOk)
346 statement.bindText(1, originIdentifier);
347 statement.bindText(2, name);
349 int result = statement.step();
350 if (result == SQLResultRow)
351 guid = statement.getColumnInt64(0);
352 statement.finalize();
355 if (result != SQLResultDone)
356 LOG_ERROR("Error to determing existence of database %s in origin %s in tracker database", name.ascii().data(), originIdentifier.ascii().data());
358 // This case should never occur - we should never be setting database details for a database that doesn't already exist in the tracker
359 // 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
360 // So we'll print an error instead
361 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",
362 name.ascii().data(), originIdentifier.ascii().data());
367 SQLiteStatement updateStatement(m_database, "UPDATE Databases SET displayName=?, estimatedSize=? WHERE guid=?");
368 if (updateStatement.prepare() != SQLResultOk)
371 updateStatement.bindText(1, displayName);
372 updateStatement.bindInt64(2, estimatedSize);
373 updateStatement.bindInt64(3, guid);
375 if (updateStatement.step() != SQLResultDone) {
376 LOG_ERROR("Failed to update details for database %s in origin %s", name.ascii().data(), originIdentifier.ascii().data());
381 m_client->dispatchDidModifyDatabase(origin, name);
384 unsigned long long DatabaseTracker::usageForDatabase(const String& name, const SecurityOriginData& origin)
386 String path = fullPathForDatabase(origin, name, false);
391 return fileSize(path, size) ? size : 0;
394 unsigned long long DatabaseTracker::usageForOrigin(const SecurityOriginData& origin)
396 Vector<String> names;
397 databaseNamesForOrigin(origin, names);
399 unsigned long long result = 0;
400 for (unsigned i = 0; i < names.size(); ++i)
401 result += usageForDatabase(names[i], origin);
406 unsigned long long DatabaseTracker::quotaForOrigin(const SecurityOriginData& origin)
409 return m_originQuotaMap->get(origin);
412 void DatabaseTracker::setQuota(const SecurityOriginData& origin, unsigned long long quota)
415 if (!m_originQuotaMap->contains(origin))
416 establishEntryForOrigin(origin);
418 m_originQuotaMap->set(origin, quota);
420 SQLiteStatement statement(m_database, "UPDATE Origins SET quota=? WHERE origin=?");
422 bool error = statement.prepare() != SQLResultOk;
424 statement.bindInt64(1, quota);
425 statement.bindText(2, origin.stringIdentifier());
427 error = !statement.executeCommand();
431 LOG_ERROR("Failed to set quota %llu in tracker database for origin %s", quota, origin.stringIdentifier().ascii().data());
434 m_client->dispatchDidModifyOrigin(origin);
437 bool DatabaseTracker::addDatabase(const SecurityOriginData& origin, const String& name, const String& path)
439 if (!m_database.isOpen())
442 // New database should never be added until the origin has been established
443 ASSERT(hasEntryForOrigin(origin));
445 SQLiteStatement statement(m_database, "INSERT INTO Databases (origin, name, path) VALUES (?, ?, ?);");
447 if (statement.prepare() != SQLResultOk)
450 statement.bindText(1, origin.stringIdentifier());
451 statement.bindText(2, name);
452 statement.bindText(3, path);
454 if (!statement.executeCommand()) {
455 LOG_ERROR("Failed to add database %s to origin %s: %s\n", name.ascii().data(), origin.stringIdentifier().ascii().data(), statement.lastErrorMsg());
460 m_client->dispatchDidModifyOrigin(origin);
465 void DatabaseTracker::deleteAllDatabases()
469 HashMap<SecurityOriginData, unsigned long long, SecurityOriginDataHash, SecurityOriginDataTraits>::const_iterator iter = m_originQuotaMap->begin();
470 HashMap<SecurityOriginData, unsigned long long, SecurityOriginDataHash, SecurityOriginDataTraits>::const_iterator end = m_originQuotaMap->end();
472 for (; iter != end; ++iter)
473 deleteDatabasesWithOrigin(iter->first);
476 void DatabaseTracker::deleteDatabasesWithOrigin(const SecurityOriginData& origin)
478 Vector<String> databaseNames;
479 if (!databaseNamesForOrigin(origin, databaseNames)) {
480 LOG_ERROR("Unable to retrieve list of database names for origin %s", origin.stringIdentifier().ascii().data());
484 for (unsigned i = 0; i < databaseNames.size(); ++i) {
485 if (!deleteDatabaseFile(origin, databaseNames[i])) {
486 LOG_ERROR("Unable to delete file for database %s in origin %s", databaseNames[i].ascii().data(), origin.stringIdentifier().ascii().data());
491 SQLiteStatement statement(m_database, "DELETE FROM Databases WHERE origin=?");
492 if (statement.prepare() != SQLResultOk) {
493 LOG_ERROR("Unable to prepare deletion of databases from origin %s from tracker", origin.stringIdentifier().ascii().data());
497 statement.bindText(1, origin.stringIdentifier());
499 if (!statement.executeCommand()) {
500 LOG_ERROR("Unable to execute deletion of databases from origin %s from tracker", origin.stringIdentifier().ascii().data());
505 m_client->dispatchDidModifyOrigin(origin);
506 for (unsigned i = 0; i < databaseNames.size(); ++i)
507 m_client->dispatchDidModifyDatabase(origin, databaseNames[i]);
511 void DatabaseTracker::deleteDatabase(const SecurityOriginData& origin, const String& name)
513 if (!deleteDatabaseFile(origin, name)) {
514 LOG_ERROR("Unable to delete file for database %s in origin %s", name.ascii().data(), origin.stringIdentifier().ascii().data());
518 SQLiteStatement statement(m_database, "DELETE FROM Databases WHERE origin=? AND name=?");
519 if (statement.prepare() != SQLResultOk) {
520 LOG_ERROR("Unable to prepare deletion of database %s from origin %s from tracker", name.ascii().data(), origin.stringIdentifier().ascii().data());
524 statement.bindText(1, origin.stringIdentifier());
525 statement.bindText(2, name);
527 if (!statement.executeCommand()) {
528 LOG_ERROR("Unable to execute deletion of database %s from origin %s from tracker", name.ascii().data(), origin.stringIdentifier().ascii().data());
533 m_client->dispatchDidModifyOrigin(origin);
534 m_client->dispatchDidModifyDatabase(origin, name);
538 bool DatabaseTracker::deleteDatabaseFile(const SecurityOriginData& origin, const String& name)
540 String fullPath = fullPathForDatabase(origin, name, false);
541 if (fullPath.isEmpty())
544 return deleteFile(fullPath);
547 void DatabaseTracker::setClient(DatabaseTrackerClient* client)
552 void DatabaseTracker::setDefaultOriginQuota(unsigned long long quota)
554 m_defaultQuota = quota;
557 unsigned long long DatabaseTracker::defaultOriginQuota() const
559 return m_defaultQuota;
562 static Mutex& notificationMutex()
568 static Vector<pair<SecurityOriginData, String> >& notificationQueue()
570 static Vector<pair<SecurityOriginData, String> > queue;
574 void DatabaseTracker::scheduleNotifyDatabaseChanged(const SecurityOriginData& origin, const String& name)
576 MutexLocker locker(notificationMutex());
578 notificationQueue().append(pair<SecurityOriginData, String>(origin.copy(), name.copy()));
579 scheduleForNotification();
582 static bool notificationScheduled = false;
584 void DatabaseTracker::scheduleForNotification()
586 ASSERT(!notificationMutex().tryLock());
588 if (!notificationScheduled) {
589 callOnMainThread(DatabaseTracker::notifyDatabasesChanged);
590 notificationScheduled = true;
594 void DatabaseTracker::notifyDatabasesChanged()
596 // Note that if DatabaseTracker ever becomes non-singleton, we'll have to ammend this notification
597 // mechanism to inclue which tracker the notification goes out on, as well
598 DatabaseTracker& theTracker(tracker());
600 Vector<pair<SecurityOriginData, String> > notifications;
602 MutexLocker locker(notificationMutex());
604 notifications.swap(notificationQueue());
606 notificationScheduled = false;
609 if (!theTracker.m_client)
612 for (unsigned i = 0; i < notifications.size(); ++i)
613 theTracker.m_client->dispatchDidModifyDatabase(notifications[i].first, notifications[i].second);
617 } // namespace WebCore