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 <WebCore/SuddenTermination.h> 37#include <wtf/PassRefPtr.h> 38#include <wtf/text/StringHash.h> 39#include <wtf/text/WTFString.h> 40 41using namespace WebCore; 42 43static const auto databaseUpdateInterval = std::chrono::seconds(1); 44 45static const int maximumItemsToUpdate = 100; 46 47namespace WebKit { 48 49PassRefPtr<LocalStorageDatabase> LocalStorageDatabase::create(PassRefPtr<WorkQueue> queue, PassRefPtr<LocalStorageDatabaseTracker> tracker, PassRefPtr<SecurityOrigin> securityOrigin) 50{ 51 return adoptRef(new LocalStorageDatabase(queue, tracker, securityOrigin)); 52} 53 54LocalStorageDatabase::LocalStorageDatabase(PassRefPtr<WorkQueue> queue, PassRefPtr<LocalStorageDatabaseTracker> tracker, PassRefPtr<SecurityOrigin> securityOrigin) 55 : m_queue(queue) 56 , m_tracker(tracker) 57 , m_securityOrigin(securityOrigin) 58 , m_databasePath(m_tracker->databasePath(m_securityOrigin.get())) 59 , m_failedToOpenDatabase(false) 60 , m_didImportItems(false) 61 , m_isClosed(false) 62 , m_didScheduleDatabaseUpdate(false) 63 , m_shouldClearItems(false) 64{ 65} 66 67LocalStorageDatabase::~LocalStorageDatabase() 68{ 69 ASSERT(m_isClosed); 70} 71 72void LocalStorageDatabase::openDatabase(DatabaseOpeningStrategy openingStrategy) 73{ 74 ASSERT(!m_database.isOpen()); 75 ASSERT(!m_failedToOpenDatabase); 76 77 if (!tryToOpenDatabase(openingStrategy)) { 78 m_failedToOpenDatabase = true; 79 return; 80 } 81 82 if (m_database.isOpen()) 83 m_tracker->didOpenDatabaseWithOrigin(m_securityOrigin.get()); 84} 85 86bool LocalStorageDatabase::tryToOpenDatabase(DatabaseOpeningStrategy openingStrategy) 87{ 88 if (!fileExists(m_databasePath) && openingStrategy == SkipIfNonExistent) 89 return true; 90 91 if (m_databasePath.isEmpty()) { 92 LOG_ERROR("Filename for local storage database is empty - cannot open for persistent storage"); 93 return false; 94 } 95 96 if (!m_database.open(m_databasePath)) { 97 LOG_ERROR("Failed to open database file %s for local storage", m_databasePath.utf8().data()); 98 return false; 99 } 100 101 // Since a WorkQueue isn't bound to a specific thread, we have to disable threading checks 102 // even though we never access the database from different threads simultaneously. 103 m_database.disableThreadingChecks(); 104 105 if (!migrateItemTableIfNeeded()) { 106 // We failed to migrate the item table. In order to avoid trying to migrate the table over and over, 107 // just delete it and start from scratch. 108 if (!m_database.executeCommand("DROP TABLE ItemTable")) 109 LOG_ERROR("Failed to delete table ItemTable for local storage"); 110 } 111 112 if (!m_database.executeCommand("CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB NOT NULL ON CONFLICT FAIL)")) { 113 LOG_ERROR("Failed to create table ItemTable for local storage"); 114 return false; 115 } 116 117 return true; 118} 119 120bool LocalStorageDatabase::migrateItemTableIfNeeded() 121{ 122 if (!m_database.tableExists("ItemTable")) 123 return true; 124 125 SQLiteStatement query(m_database, "SELECT value FROM ItemTable LIMIT 1"); 126 127 // This query isn't ever executed, it's just used to check the column type. 128 if (query.isColumnDeclaredAsBlob(0)) 129 return true; 130 131 // 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. 132 static const char* commands[] = { 133 "DROP TABLE IF EXISTS ItemTable2", 134 "CREATE TABLE ItemTable2 (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB NOT NULL ON CONFLICT FAIL)", 135 "INSERT INTO ItemTable2 SELECT * from ItemTable", 136 "DROP TABLE ItemTable", 137 "ALTER TABLE ItemTable2 RENAME TO ItemTable", 138 0, 139 }; 140 141 SQLiteTransaction transaction(m_database, false); 142 transaction.begin(); 143 144 for (size_t i = 0; commands[i]; ++i) { 145 if (m_database.executeCommand(commands[i])) 146 continue; 147 148 LOG_ERROR("Failed to migrate table ItemTable for local storage when executing: %s", commands[i]); 149 transaction.rollback(); 150 151 return false; 152 } 153 154 transaction.commit(); 155 return true; 156} 157 158void LocalStorageDatabase::importItems(StorageMap& storageMap) 159{ 160 if (m_didImportItems) 161 return; 162 163 // FIXME: If it can't import, then the default WebKit behavior should be that of private browsing, 164 // not silently ignoring it. https://bugs.webkit.org/show_bug.cgi?id=25894 165 166 // We set this to true even if we don't end up importing any items due to failure because 167 // there's really no good way to recover other than not importing anything. 168 m_didImportItems = true; 169 170 openDatabase(SkipIfNonExistent); 171 if (!m_database.isOpen()) 172 return; 173 174 SQLiteStatement query(m_database, "SELECT key, value FROM ItemTable"); 175 if (query.prepare() != SQLResultOk) { 176 LOG_ERROR("Unable to select items from ItemTable for local storage"); 177 return; 178 } 179 180 HashMap<String, String> items; 181 182 int result = query.step(); 183 while (result == SQLResultRow) { 184 items.set(query.getColumnText(0), query.getColumnBlobAsString(1)); 185 result = query.step(); 186 } 187 188 if (result != SQLResultDone) { 189 LOG_ERROR("Error reading items from ItemTable for local storage"); 190 return; 191 } 192 193 storageMap.importItems(items); 194} 195 196void LocalStorageDatabase::setItem(const String& key, const String& value) 197{ 198 itemDidChange(key, value); 199} 200 201void LocalStorageDatabase::removeItem(const String& key) 202{ 203 itemDidChange(key, String()); 204} 205 206void LocalStorageDatabase::clear() 207{ 208 m_changedItems.clear(); 209 m_shouldClearItems = true; 210 211 scheduleDatabaseUpdate(); 212} 213 214void LocalStorageDatabase::close() 215{ 216 ASSERT(!m_isClosed); 217 m_isClosed = true; 218 219 if (m_didScheduleDatabaseUpdate) { 220 updateDatabaseWithChangedItems(m_changedItems); 221 m_changedItems.clear(); 222 } 223 224 bool isEmpty = databaseIsEmpty(); 225 226 if (m_database.isOpen()) 227 m_database.close(); 228 229 if (isEmpty) 230 m_tracker->deleteDatabaseWithOrigin(m_securityOrigin.get()); 231} 232 233void LocalStorageDatabase::itemDidChange(const String& key, const String& value) 234{ 235 m_changedItems.set(key, value); 236 scheduleDatabaseUpdate(); 237} 238 239void LocalStorageDatabase::scheduleDatabaseUpdate() 240{ 241 if (m_didScheduleDatabaseUpdate) 242 return; 243 244 if (!m_disableSuddenTerminationWhileWritingToLocalStorage) 245 m_disableSuddenTerminationWhileWritingToLocalStorage = std::make_unique<SuddenTerminationDisabler>(); 246 247 m_didScheduleDatabaseUpdate = true; 248 249 RefPtr<LocalStorageDatabase> localStorageDatabase(this); 250 m_queue->dispatchAfter(databaseUpdateInterval, [localStorageDatabase] { 251 localStorageDatabase->updateDatabase(); 252 }); 253} 254 255void LocalStorageDatabase::updateDatabase() 256{ 257 if (m_isClosed) 258 return; 259 260 ASSERT(m_didScheduleDatabaseUpdate); 261 m_didScheduleDatabaseUpdate = false; 262 263 HashMap<String, String> changedItems; 264 if (m_changedItems.size() <= maximumItemsToUpdate) { 265 // There are few enough changed items that we can just always write all of them. 266 m_changedItems.swap(changedItems); 267 updateDatabaseWithChangedItems(changedItems); 268 m_disableSuddenTerminationWhileWritingToLocalStorage = nullptr; 269 } else { 270 for (int i = 0; i < maximumItemsToUpdate; ++i) { 271 auto it = m_changedItems.begin(); 272 changedItems.add(it->key, it->value); 273 274 m_changedItems.remove(it); 275 } 276 277 ASSERT(changedItems.size() <= maximumItemsToUpdate); 278 279 // Reschedule the update for the remaining items. 280 scheduleDatabaseUpdate(); 281 updateDatabaseWithChangedItems(changedItems); 282 } 283} 284 285void LocalStorageDatabase::updateDatabaseWithChangedItems(const HashMap<String, String>& changedItems) 286{ 287 if (!m_database.isOpen()) 288 openDatabase(CreateIfNonExistent); 289 if (!m_database.isOpen()) 290 return; 291 292 if (m_shouldClearItems) { 293 m_shouldClearItems = false; 294 295 SQLiteStatement clearStatement(m_database, "DELETE FROM ItemTable"); 296 if (clearStatement.prepare() != SQLResultOk) { 297 LOG_ERROR("Failed to prepare clear statement - cannot write to local storage database"); 298 return; 299 } 300 301 int result = clearStatement.step(); 302 if (result != SQLResultDone) { 303 LOG_ERROR("Failed to clear all items in the local storage database - %i", result); 304 return; 305 } 306 } 307 308 SQLiteStatement insertStatement(m_database, "INSERT INTO ItemTable VALUES (?, ?)"); 309 if (insertStatement.prepare() != SQLResultOk) { 310 LOG_ERROR("Failed to prepare insert statement - cannot write to local storage database"); 311 return; 312 } 313 314 SQLiteStatement deleteStatement(m_database, "DELETE FROM ItemTable WHERE key=?"); 315 if (deleteStatement.prepare() != SQLResultOk) { 316 LOG_ERROR("Failed to prepare delete statement - cannot write to local storage database"); 317 return; 318 } 319 320 SQLiteTransaction transaction(m_database); 321 transaction.begin(); 322 323 for (auto it = changedItems.begin(), end = changedItems.end(); it != end; ++it) { 324 // A null value means that the key/value pair should be deleted. 325 SQLiteStatement& statement = it->value.isNull() ? deleteStatement : insertStatement; 326 327 statement.bindText(1, it->key); 328 329 // If we're inserting a key/value pair, bind the value as well. 330 if (!it->value.isNull()) 331 statement.bindBlob(2, it->value); 332 333 int result = statement.step(); 334 if (result != SQLResultDone) { 335 LOG_ERROR("Failed to update item in the local storage database - %i", result); 336 break; 337 } 338 339 statement.reset(); 340 } 341 342 transaction.commit(); 343} 344 345bool LocalStorageDatabase::databaseIsEmpty() 346{ 347 if (!m_database.isOpen()) 348 return false; 349 350 SQLiteStatement query(m_database, "SELECT COUNT(*) FROM ItemTable"); 351 if (query.prepare() != SQLResultOk) { 352 LOG_ERROR("Unable to count number of rows in ItemTable for local storage"); 353 return false; 354 } 355 356 int result = query.step(); 357 if (result != SQLResultRow) { 358 LOG_ERROR("No results when counting number of rows in ItemTable for local storage"); 359 return false; 360 } 361 362 return !query.getColumnInt(0); 363} 364 365} // namespace WebKit 366