1/* 2 * Copyright (C) 2008, 2009, 2010, 2013 Apple Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' 14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS 17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 23 * THE POSSIBILITY OF SUCH DAMAGE. 24 */ 25 26#include "config.h" 27#include "LocalStorageDatabase.h" 28 29#include "LocalStorageDatabaseTracker.h" 30#include "WorkQueue.h" 31#include <WebCore/FileSystem.h> 32#include <WebCore/SQLiteStatement.h> 33#include <WebCore/SQLiteTransaction.h> 34#include <WebCore/SecurityOrigin.h> 35#include <WebCore/StorageMap.h> 36#include <wtf/PassRefPtr.h> 37#include <wtf/text/StringHash.h> 38#include <wtf/text/WTFString.h> 39 40using namespace WebCore; 41 42static const double databaseUpdateIntervalInSeconds = 1.0; 43 44static const int maximumItemsToUpdate = 100; 45 46namespace WebKit { 47 48PassRefPtr<LocalStorageDatabase> LocalStorageDatabase::create(PassRefPtr<WorkQueue> queue, PassRefPtr<LocalStorageDatabaseTracker> tracker, PassRefPtr<SecurityOrigin> securityOrigin) 49{ 50 return adoptRef(new LocalStorageDatabase(queue, tracker, securityOrigin)); 51} 52 53LocalStorageDatabase::LocalStorageDatabase(PassRefPtr<WorkQueue> queue, PassRefPtr<LocalStorageDatabaseTracker> tracker, PassRefPtr<SecurityOrigin> securityOrigin) 54 : m_queue(queue) 55 , m_tracker(tracker) 56 , m_securityOrigin(securityOrigin) 57 , m_databasePath(m_tracker->databasePath(m_securityOrigin.get())) 58 , m_failedToOpenDatabase(false) 59 , m_didImportItems(false) 60 , m_isClosed(false) 61 , m_didScheduleDatabaseUpdate(false) 62 , m_shouldClearItems(false) 63{ 64} 65 66LocalStorageDatabase::~LocalStorageDatabase() 67{ 68 ASSERT(m_isClosed); 69} 70 71void LocalStorageDatabase::openDatabase(DatabaseOpeningStrategy openingStrategy) 72{ 73 ASSERT(!m_database.isOpen()); 74 ASSERT(!m_failedToOpenDatabase); 75 76 if (!tryToOpenDatabase(openingStrategy)) { 77 m_failedToOpenDatabase = true; 78 return; 79 } 80 81 if (m_database.isOpen()) 82 m_tracker->didOpenDatabaseWithOrigin(m_securityOrigin.get()); 83} 84 85bool LocalStorageDatabase::tryToOpenDatabase(DatabaseOpeningStrategy openingStrategy) 86{ 87 if (!fileExists(m_databasePath) && openingStrategy == SkipIfNonExistent) 88 return true; 89 90 if (m_databasePath.isEmpty()) { 91 LOG_ERROR("Filename for local storage database is empty - cannot open for persistent storage"); 92 return false; 93 } 94 95 if (!m_database.open(m_databasePath)) { 96 LOG_ERROR("Failed to open database file %s for local storage", m_databasePath.utf8().data()); 97 return false; 98 } 99 100 // Since a WorkQueue isn't bound to a specific thread, we have to disable threading checks 101 // even though we never access the database from different threads simultaneously. 102 m_database.disableThreadingChecks(); 103 104 if (!migrateItemTableIfNeeded()) { 105 // We failed to migrate the item table. In order to avoid trying to migrate the table over and over, 106 // just delete it and start from scratch. 107 if (!m_database.executeCommand("DROP TABLE ItemTable")) 108 LOG_ERROR("Failed to delete table ItemTable for local storage"); 109 } 110 111 if (!m_database.executeCommand("CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB NOT NULL ON CONFLICT FAIL)")) { 112 LOG_ERROR("Failed to create table ItemTable for local storage"); 113 return false; 114 } 115 116 return true; 117} 118 119bool LocalStorageDatabase::migrateItemTableIfNeeded() 120{ 121 if (!m_database.tableExists("ItemTable")) 122 return true; 123 124 SQLiteStatement query(m_database, "SELECT value FROM ItemTable LIMIT 1"); 125 126 // This query isn't ever executed, it's just used to check the column type. 127 if (query.isColumnDeclaredAsBlob(0)) 128 return true; 129 130 // Create a new table with the right type, copy all the data over to it and then replace the new table with the old table. 131 static const char* commands[] = { 132 "DROP TABLE IF EXISTS ItemTable2", 133 "CREATE TABLE ItemTable2 (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB NOT NULL ON CONFLICT FAIL)", 134 "INSERT INTO ItemTable2 SELECT * from ItemTable", 135 "DROP TABLE ItemTable", 136 "ALTER TABLE ItemTable2 RENAME TO ItemTable", 137 0, 138 }; 139 140 SQLiteTransaction transaction(m_database, false); 141 transaction.begin(); 142 143 for (size_t i = 0; commands[i]; ++i) { 144 if (m_database.executeCommand(commands[i])) 145 continue; 146 147 LOG_ERROR("Failed to migrate table ItemTable for local storage when executing: %s", commands[i]); 148 transaction.rollback(); 149 150 return false; 151 } 152 153 transaction.commit(); 154 return true; 155} 156 157void LocalStorageDatabase::importItems(StorageMap& storageMap) 158{ 159 if (m_didImportItems) 160 return; 161 162 // FIXME: If it can't import, then the default WebKit behavior should be that of private browsing, 163 // not silently ignoring it. https://bugs.webkit.org/show_bug.cgi?id=25894 164 165 // We set this to true even if we don't end up importing any items due to failure because 166 // there's really no good way to recover other than not importing anything. 167 m_didImportItems = true; 168 169 openDatabase(SkipIfNonExistent); 170 if (!m_database.isOpen()) 171 return; 172 173 SQLiteStatement query(m_database, "SELECT key, value FROM ItemTable"); 174 if (query.prepare() != SQLResultOk) { 175 LOG_ERROR("Unable to select items from ItemTable for local storage"); 176 return; 177 } 178 179 HashMap<String, String> items; 180 181 int result = query.step(); 182 while (result == SQLResultRow) { 183 items.set(query.getColumnText(0), query.getColumnBlobAsString(1)); 184 result = query.step(); 185 } 186 187 if (result != SQLResultDone) { 188 LOG_ERROR("Error reading items from ItemTable for local storage"); 189 return; 190 } 191 192 storageMap.importItems(items); 193} 194 195void LocalStorageDatabase::setItem(const String& key, const String& value) 196{ 197 itemDidChange(key, value); 198} 199 200void LocalStorageDatabase::removeItem(const String& key) 201{ 202 itemDidChange(key, String()); 203} 204 205void LocalStorageDatabase::clear() 206{ 207 m_changedItems.clear(); 208 m_shouldClearItems = true; 209 210 scheduleDatabaseUpdate(); 211} 212 213void LocalStorageDatabase::close() 214{ 215 ASSERT(!m_isClosed); 216 m_isClosed = true; 217 218 if (m_didScheduleDatabaseUpdate) { 219 updateDatabaseWithChangedItems(m_changedItems); 220 m_changedItems.clear(); 221 } 222 223 bool isEmpty = databaseIsEmpty(); 224 225 if (m_database.isOpen()) 226 m_database.close(); 227 228 if (isEmpty) 229 m_tracker->deleteDatabaseWithOrigin(m_securityOrigin.get()); 230} 231 232void LocalStorageDatabase::itemDidChange(const String& key, const String& value) 233{ 234 m_changedItems.set(key, value); 235 scheduleDatabaseUpdate(); 236} 237 238void LocalStorageDatabase::scheduleDatabaseUpdate() 239{ 240 if (m_didScheduleDatabaseUpdate) 241 return; 242 243 m_didScheduleDatabaseUpdate = true; 244 m_queue->dispatchAfterDelay(bind(&LocalStorageDatabase::updateDatabase, this), databaseUpdateIntervalInSeconds); 245} 246 247void LocalStorageDatabase::updateDatabase() 248{ 249 if (m_isClosed) 250 return; 251 252 ASSERT(m_didScheduleDatabaseUpdate); 253 m_didScheduleDatabaseUpdate = false; 254 255 HashMap<String, String> changedItems; 256 if (m_changedItems.size() <= maximumItemsToUpdate) { 257 // There are few enough changed items that we can just always write all of them. 258 m_changedItems.swap(changedItems); 259 } else { 260 for (int i = 0; i < maximumItemsToUpdate; ++i) { 261 auto it = m_changedItems.begin(); 262 changedItems.add(it->key, it->value); 263 264 m_changedItems.remove(it); 265 } 266 267 ASSERT(changedItems.size() <= maximumItemsToUpdate); 268 269 // Reschedule the update for the remaining items. 270 scheduleDatabaseUpdate(); 271 } 272 273 updateDatabaseWithChangedItems(changedItems); 274} 275 276void LocalStorageDatabase::updateDatabaseWithChangedItems(const HashMap<String, String>& changedItems) 277{ 278 if (!m_database.isOpen()) 279 openDatabase(CreateIfNonExistent); 280 if (!m_database.isOpen()) 281 return; 282 283 if (m_shouldClearItems) { 284 m_shouldClearItems = false; 285 286 SQLiteStatement clearStatement(m_database, "DELETE FROM ItemTable"); 287 if (clearStatement.prepare() != SQLResultOk) { 288 LOG_ERROR("Failed to prepare clear statement - cannot write to local storage database"); 289 return; 290 } 291 292 int result = clearStatement.step(); 293 if (result != SQLResultDone) { 294 LOG_ERROR("Failed to clear all items in the local storage database - %i", result); 295 return; 296 } 297 } 298 299 SQLiteStatement insertStatement(m_database, "INSERT INTO ItemTable VALUES (?, ?)"); 300 if (insertStatement.prepare() != SQLResultOk) { 301 LOG_ERROR("Failed to prepare insert statement - cannot write to local storage database"); 302 return; 303 } 304 305 SQLiteStatement deleteStatement(m_database, "DELETE FROM ItemTable WHERE key=?"); 306 if (deleteStatement.prepare() != SQLResultOk) { 307 LOG_ERROR("Failed to prepare delete statement - cannot write to local storage database"); 308 return; 309 } 310 311 SQLiteTransaction transaction(m_database); 312 transaction.begin(); 313 314 for (auto it = changedItems.begin(), end = changedItems.end(); it != end; ++it) { 315 // A null value means that the key/value pair should be deleted. 316 SQLiteStatement& statement = it->value.isNull() ? deleteStatement : insertStatement; 317 318 statement.bindText(1, it->key); 319 320 // If we're inserting a key/value pair, bind the value as well. 321 if (!it->value.isNull()) 322 statement.bindBlob(2, it->value); 323 324 int result = statement.step(); 325 if (result != SQLResultDone) { 326 LOG_ERROR("Failed to update item in the local storage database - %i", result); 327 break; 328 } 329 330 statement.reset(); 331 } 332 333 transaction.commit(); 334} 335 336bool LocalStorageDatabase::databaseIsEmpty() 337{ 338 if (!m_database.isOpen()) 339 return false; 340 341 SQLiteStatement query(m_database, "SELECT COUNT(*) FROM ItemTable"); 342 if (query.prepare() != SQLResultOk) { 343 LOG_ERROR("Unable to count number of rows in ItemTable for local storage"); 344 return false; 345 } 346 347 int result = query.step(); 348 if (result != SQLResultRow) { 349 LOG_ERROR("No results when counting number of rows in ItemTable for local storage"); 350 return false; 351 } 352 353 return !query.getColumnInt(0); 354} 355 356} // namespace WebKit 357