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 <WebCore/SQLiteStatement.h> |
31 | #include <WebCore/SQLiteTransaction.h> |
32 | #include <WebCore/SecurityOrigin.h> |
33 | #include <WebCore/StorageMap.h> |
34 | #include <WebCore/SuddenTermination.h> |
35 | #include <wtf/FileSystem.h> |
36 | #include <wtf/RefPtr.h> |
37 | #include <wtf/WorkQueue.h> |
38 | #include <wtf/text/StringHash.h> |
39 | #include <wtf/text/WTFString.h> |
40 | |
41 | static const auto databaseUpdateInterval = 1_s; |
42 | |
43 | static const int maximumItemsToUpdate = 100; |
44 | |
45 | namespace WebKit { |
46 | using namespace WebCore; |
47 | |
48 | Ref<LocalStorageDatabase> LocalStorageDatabase::create(Ref<WorkQueue>&& queue, Ref<LocalStorageDatabaseTracker>&& tracker, const SecurityOriginData& securityOrigin) |
49 | { |
50 | return adoptRef(*new LocalStorageDatabase(WTFMove(queue), WTFMove(tracker), securityOrigin)); |
51 | } |
52 | |
53 | LocalStorageDatabase::LocalStorageDatabase(Ref<WorkQueue>&& queue, Ref<LocalStorageDatabaseTracker>&& tracker, const SecurityOriginData& securityOrigin) |
54 | : m_queue(WTFMove(queue)) |
55 | , m_tracker(WTFMove(tracker)) |
56 | , m_securityOrigin(securityOrigin) |
57 | , m_databasePath(m_tracker->databasePath(m_securityOrigin)) |
58 | , m_failedToOpenDatabase(false) |
59 | , m_didImportItems(false) |
60 | , m_isClosed(false) |
61 | , m_didScheduleDatabaseUpdate(false) |
62 | , m_shouldClearItems(false) |
63 | { |
64 | } |
65 | |
66 | LocalStorageDatabase::~LocalStorageDatabase() |
67 | { |
68 | ASSERT(m_isClosed); |
69 | } |
70 | |
71 | void 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); |
83 | } |
84 | |
85 | bool LocalStorageDatabase::tryToOpenDatabase(DatabaseOpeningStrategy openingStrategy) |
86 | { |
87 | if (!FileSystem::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 | |
119 | bool 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 | |
157 | void 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() != SQLITE_OK) { |
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 == SQLITE_ROW) { |
183 | String key = query.getColumnText(0); |
184 | String value = query.getColumnBlobAsString(1); |
185 | if (!key.isNull() && !value.isNull()) |
186 | items.set(key, value); |
187 | result = query.step(); |
188 | } |
189 | |
190 | if (result != SQLITE_DONE) { |
191 | LOG_ERROR("Error reading items from ItemTable for local storage" ); |
192 | return; |
193 | } |
194 | |
195 | storageMap.importItems(items); |
196 | } |
197 | |
198 | void LocalStorageDatabase::setItem(const String& key, const String& value) |
199 | { |
200 | itemDidChange(key, value); |
201 | } |
202 | |
203 | void LocalStorageDatabase::removeItem(const String& key) |
204 | { |
205 | itemDidChange(key, String()); |
206 | } |
207 | |
208 | void LocalStorageDatabase::clear() |
209 | { |
210 | m_changedItems.clear(); |
211 | m_shouldClearItems = true; |
212 | |
213 | scheduleDatabaseUpdate(); |
214 | } |
215 | |
216 | void LocalStorageDatabase::close() |
217 | { |
218 | ASSERT(!m_isClosed); |
219 | m_isClosed = true; |
220 | |
221 | if (m_didScheduleDatabaseUpdate) { |
222 | updateDatabaseWithChangedItems(m_changedItems); |
223 | m_changedItems.clear(); |
224 | } |
225 | |
226 | bool isEmpty = databaseIsEmpty(); |
227 | |
228 | if (m_database.isOpen()) |
229 | m_database.close(); |
230 | |
231 | if (isEmpty) |
232 | m_tracker->deleteDatabaseWithOrigin(m_securityOrigin); |
233 | } |
234 | |
235 | void LocalStorageDatabase::itemDidChange(const String& key, const String& value) |
236 | { |
237 | m_changedItems.set(key, value); |
238 | scheduleDatabaseUpdate(); |
239 | } |
240 | |
241 | void LocalStorageDatabase::scheduleDatabaseUpdate() |
242 | { |
243 | if (m_didScheduleDatabaseUpdate) |
244 | return; |
245 | |
246 | if (!m_disableSuddenTerminationWhileWritingToLocalStorage) |
247 | m_disableSuddenTerminationWhileWritingToLocalStorage = std::make_unique<SuddenTerminationDisabler>(); |
248 | |
249 | m_didScheduleDatabaseUpdate = true; |
250 | |
251 | m_queue->dispatchAfter(databaseUpdateInterval, [protectedThis = makeRef(*this)] { |
252 | protectedThis->updateDatabase(); |
253 | }); |
254 | } |
255 | |
256 | void LocalStorageDatabase::updateDatabase() |
257 | { |
258 | if (m_isClosed) |
259 | return; |
260 | |
261 | ASSERT(m_didScheduleDatabaseUpdate); |
262 | m_didScheduleDatabaseUpdate = false; |
263 | |
264 | HashMap<String, String> changedItems; |
265 | if (m_changedItems.size() <= maximumItemsToUpdate) { |
266 | // There are few enough changed items that we can just always write all of them. |
267 | m_changedItems.swap(changedItems); |
268 | updateDatabaseWithChangedItems(changedItems); |
269 | m_disableSuddenTerminationWhileWritingToLocalStorage = nullptr; |
270 | } else { |
271 | for (int i = 0; i < maximumItemsToUpdate; ++i) { |
272 | auto it = m_changedItems.begin(); |
273 | changedItems.add(it->key, it->value); |
274 | |
275 | m_changedItems.remove(it); |
276 | } |
277 | |
278 | ASSERT(changedItems.size() <= maximumItemsToUpdate); |
279 | |
280 | // Reschedule the update for the remaining items. |
281 | scheduleDatabaseUpdate(); |
282 | updateDatabaseWithChangedItems(changedItems); |
283 | } |
284 | } |
285 | |
286 | void LocalStorageDatabase::updateDatabaseWithChangedItems(const HashMap<String, String>& changedItems) |
287 | { |
288 | if (!m_database.isOpen()) |
289 | openDatabase(CreateIfNonExistent); |
290 | if (!m_database.isOpen()) |
291 | return; |
292 | |
293 | if (m_shouldClearItems) { |
294 | m_shouldClearItems = false; |
295 | |
296 | SQLiteStatement clearStatement(m_database, "DELETE FROM ItemTable" ); |
297 | if (clearStatement.prepare() != SQLITE_OK) { |
298 | LOG_ERROR("Failed to prepare clear statement - cannot write to local storage database" ); |
299 | return; |
300 | } |
301 | |
302 | int result = clearStatement.step(); |
303 | if (result != SQLITE_DONE) { |
304 | LOG_ERROR("Failed to clear all items in the local storage database - %i" , result); |
305 | return; |
306 | } |
307 | } |
308 | |
309 | SQLiteStatement insertStatement(m_database, "INSERT INTO ItemTable VALUES (?, ?)" ); |
310 | if (insertStatement.prepare() != SQLITE_OK) { |
311 | LOG_ERROR("Failed to prepare insert statement - cannot write to local storage database" ); |
312 | return; |
313 | } |
314 | |
315 | SQLiteStatement deleteStatement(m_database, "DELETE FROM ItemTable WHERE key=?" ); |
316 | if (deleteStatement.prepare() != SQLITE_OK) { |
317 | LOG_ERROR("Failed to prepare delete statement - cannot write to local storage database" ); |
318 | return; |
319 | } |
320 | |
321 | SQLiteTransaction transaction(m_database); |
322 | transaction.begin(); |
323 | |
324 | for (auto it = changedItems.begin(), end = changedItems.end(); it != end; ++it) { |
325 | // A null value means that the key/value pair should be deleted. |
326 | SQLiteStatement& statement = it->value.isNull() ? deleteStatement : insertStatement; |
327 | |
328 | statement.bindText(1, it->key); |
329 | |
330 | // If we're inserting a key/value pair, bind the value as well. |
331 | if (!it->value.isNull()) |
332 | statement.bindBlob(2, it->value); |
333 | |
334 | int result = statement.step(); |
335 | if (result != SQLITE_DONE) { |
336 | LOG_ERROR("Failed to update item in the local storage database - %i" , result); |
337 | break; |
338 | } |
339 | |
340 | statement.reset(); |
341 | } |
342 | |
343 | transaction.commit(); |
344 | } |
345 | |
346 | bool LocalStorageDatabase::databaseIsEmpty() |
347 | { |
348 | if (!m_database.isOpen()) |
349 | return false; |
350 | |
351 | SQLiteStatement query(m_database, "SELECT COUNT(*) FROM ItemTable" ); |
352 | if (query.prepare() != SQLITE_OK) { |
353 | LOG_ERROR("Unable to count number of rows in ItemTable for local storage" ); |
354 | return false; |
355 | } |
356 | |
357 | int result = query.step(); |
358 | if (result != SQLITE_ROW) { |
359 | LOG_ERROR("No results when counting number of rows in ItemTable for local storage" ); |
360 | return false; |
361 | } |
362 | |
363 | return !query.getColumnInt(0); |
364 | } |
365 | |
366 | } // namespace WebKit |
367 | |