1 /*
2 * Copyright (C) 2008, 2009, 2010 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. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26 #include "config.h"
27 #include "StorageAreaSync.h"
28
29 #if ENABLE(DOM_STORAGE)
30
31 #include "EventNames.h"
32 #include "FileSystem.h"
33 #include "HTMLElement.h"
34 #include "SQLiteFileSystem.h"
35 #include "SQLiteStatement.h"
36 #include "SecurityOrigin.h"
37 #include "StorageAreaImpl.h"
38 #include "StorageSyncManager.h"
39 #include "StorageTracker.h"
40 #include "SuddenTermination.h"
41 #include <wtf/text/CString.h>
42
43 namespace WebCore {
44
45 // If the StorageArea undergoes rapid changes, don't sync each change to disk.
46 // Instead, queue up a batch of items to sync and actually do the sync at the following interval.
47 static const double StorageSyncInterval = 1.0;
48
49 // A sane limit on how many items we'll schedule to sync all at once. This makes it
50 // much harder to starve the rest of LocalStorage and the OS's IO subsystem in general.
51 static const int MaxiumItemsToSync = 100;
52
StorageAreaSync(PassRefPtr<StorageSyncManager> storageSyncManager,PassRefPtr<StorageAreaImpl> storageArea,const String & databaseIdentifier)53 inline StorageAreaSync::StorageAreaSync(PassRefPtr<StorageSyncManager> storageSyncManager, PassRefPtr<StorageAreaImpl> storageArea, const String& databaseIdentifier)
54 : m_syncTimer(this, &StorageAreaSync::syncTimerFired)
55 , m_itemsCleared(false)
56 , m_finalSyncScheduled(false)
57 , m_storageArea(storageArea)
58 , m_syncManager(storageSyncManager)
59 , m_databaseIdentifier(databaseIdentifier.crossThreadString())
60 , m_clearItemsWhileSyncing(false)
61 , m_syncScheduled(false)
62 , m_syncInProgress(false)
63 , m_databaseOpenFailed(false)
64 , m_syncCloseDatabase(false)
65 , m_importComplete(false)
66 {
67 ASSERT(isMainThread());
68 ASSERT(m_storageArea);
69 ASSERT(m_syncManager);
70 }
71
create(PassRefPtr<StorageSyncManager> storageSyncManager,PassRefPtr<StorageAreaImpl> storageArea,const String & databaseIdentifier)72 PassRefPtr<StorageAreaSync> StorageAreaSync::create(PassRefPtr<StorageSyncManager> storageSyncManager, PassRefPtr<StorageAreaImpl> storageArea, const String& databaseIdentifier)
73 {
74 RefPtr<StorageAreaSync> area = adoptRef(new StorageAreaSync(storageSyncManager, storageArea, databaseIdentifier));
75
76 // FIXME: If it can't import, then the default WebKit behavior should be that of private browsing,
77 // not silently ignoring it. https://bugs.webkit.org/show_bug.cgi?id=25894
78 if (!area->m_syncManager->scheduleImport(area.get()))
79 area->m_importComplete = true;
80
81 return area.release();
82 }
83
~StorageAreaSync()84 StorageAreaSync::~StorageAreaSync()
85 {
86 ASSERT(isMainThread());
87 ASSERT(!m_syncTimer.isActive());
88 ASSERT(m_finalSyncScheduled);
89 }
90
scheduleFinalSync()91 void StorageAreaSync::scheduleFinalSync()
92 {
93 ASSERT(isMainThread());
94 // FIXME: We do this to avoid races, but it'd be better to make things safe without blocking.
95 blockUntilImportComplete();
96 m_storageArea = 0; // This is done in blockUntilImportComplete() but this is here as a form of documentation that we must be absolutely sure the ref count cycle is broken.
97
98 if (m_syncTimer.isActive())
99 m_syncTimer.stop();
100 else {
101 // The following is balanced by the call to enableSuddenTermination in the
102 // syncTimerFired function.
103 disableSuddenTermination();
104 }
105 // FIXME: This is synchronous. We should do it on the background process, but
106 // we should do it safely.
107 m_finalSyncScheduled = true;
108 syncTimerFired(&m_syncTimer);
109 m_syncManager->scheduleDeleteEmptyDatabase(this);
110 }
111
scheduleItemForSync(const String & key,const String & value)112 void StorageAreaSync::scheduleItemForSync(const String& key, const String& value)
113 {
114 ASSERT(isMainThread());
115 ASSERT(!m_finalSyncScheduled);
116
117 m_changedItems.set(key, value);
118 if (!m_syncTimer.isActive()) {
119 m_syncTimer.startOneShot(StorageSyncInterval);
120
121 // The following is balanced by the call to enableSuddenTermination in the
122 // syncTimerFired function.
123 disableSuddenTermination();
124 }
125 }
126
scheduleClear()127 void StorageAreaSync::scheduleClear()
128 {
129 ASSERT(isMainThread());
130 ASSERT(!m_finalSyncScheduled);
131
132 m_changedItems.clear();
133 m_itemsCleared = true;
134 if (!m_syncTimer.isActive()) {
135 m_syncTimer.startOneShot(StorageSyncInterval);
136
137 // The following is balanced by the call to enableSuddenTermination in the
138 // syncTimerFired function.
139 disableSuddenTermination();
140 }
141 }
142
scheduleCloseDatabase()143 void StorageAreaSync::scheduleCloseDatabase()
144 {
145 ASSERT(isMainThread());
146 ASSERT(!m_finalSyncScheduled);
147
148 if (!m_database.isOpen())
149 return;
150
151 m_syncCloseDatabase = true;
152
153 if (!m_syncTimer.isActive()) {
154 m_syncTimer.startOneShot(StorageSyncInterval);
155
156 // The following is balanced by the call to enableSuddenTermination in the
157 // syncTimerFired function.
158 disableSuddenTermination();
159 }
160 }
161
syncTimerFired(Timer<StorageAreaSync> *)162 void StorageAreaSync::syncTimerFired(Timer<StorageAreaSync>*)
163 {
164 ASSERT(isMainThread());
165
166 bool partialSync = false;
167 {
168 MutexLocker locker(m_syncLock);
169
170 // Do not schedule another sync if we're still trying to complete the
171 // previous one. But, if we're shutting down, schedule it anyway.
172 if (m_syncInProgress && !m_finalSyncScheduled) {
173 ASSERT(!m_syncTimer.isActive());
174 m_syncTimer.startOneShot(StorageSyncInterval);
175 return;
176 }
177
178 if (m_itemsCleared) {
179 m_itemsPendingSync.clear();
180 m_clearItemsWhileSyncing = true;
181 m_itemsCleared = false;
182 }
183
184 HashMap<String, String>::iterator changed_it = m_changedItems.begin();
185 HashMap<String, String>::iterator changed_end = m_changedItems.end();
186 for (int count = 0; changed_it != changed_end; ++count, ++changed_it) {
187 if (count >= MaxiumItemsToSync && !m_finalSyncScheduled) {
188 partialSync = true;
189 break;
190 }
191 m_itemsPendingSync.set(changed_it->first.crossThreadString(), changed_it->second.crossThreadString());
192 }
193
194 if (partialSync) {
195 // We can't do the fast path of simply clearing all items, so we'll need to manually
196 // remove them one by one. Done under lock since m_itemsPendingSync is modified by
197 // the background thread.
198 HashMap<String, String>::iterator pending_it = m_itemsPendingSync.begin();
199 HashMap<String, String>::iterator pending_end = m_itemsPendingSync.end();
200 for (; pending_it != pending_end; ++pending_it)
201 m_changedItems.remove(pending_it->first);
202 }
203
204 if (!m_syncScheduled) {
205 m_syncScheduled = true;
206
207 // The following is balanced by the call to enableSuddenTermination in the
208 // performSync function.
209 disableSuddenTermination();
210
211 m_syncManager->scheduleSync(this);
212 }
213 }
214
215 if (partialSync) {
216 // If we didn't finish syncing, then we need to finish the job later.
217 ASSERT(!m_syncTimer.isActive());
218 m_syncTimer.startOneShot(StorageSyncInterval);
219 } else {
220 // The following is balanced by the calls to disableSuddenTermination in the
221 // scheduleItemForSync, scheduleClear, and scheduleFinalSync functions.
222 enableSuddenTermination();
223
224 m_changedItems.clear();
225 }
226 }
227
openDatabase(OpenDatabaseParamType openingStrategy)228 void StorageAreaSync::openDatabase(OpenDatabaseParamType openingStrategy)
229 {
230 ASSERT(!isMainThread());
231 ASSERT(!m_database.isOpen());
232 ASSERT(!m_databaseOpenFailed);
233
234 String databaseFilename = m_syncManager->fullDatabaseFilename(m_databaseIdentifier);
235
236 if (!fileExists(databaseFilename) && openingStrategy == SkipIfNonExistent)
237 return;
238
239 if (databaseFilename.isEmpty()) {
240 LOG_ERROR("Filename for local storage database is empty - cannot open for persistent storage");
241 markImported();
242 m_databaseOpenFailed = true;
243 return;
244 }
245
246 // A StorageTracker thread may have been scheduled to delete the db we're
247 // reopening, so cancel possible deletion.
248 StorageTracker::tracker().cancelDeletingOrigin(m_databaseIdentifier);
249
250 if (!m_database.open(databaseFilename)) {
251 LOG_ERROR("Failed to open database file %s for local storage", databaseFilename.utf8().data());
252 markImported();
253 m_databaseOpenFailed = true;
254 return;
255 }
256
257 if (!m_database.executeCommand("CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value TEXT NOT NULL ON CONFLICT FAIL)")) {
258 LOG_ERROR("Failed to create table ItemTable for local storage");
259 markImported();
260 m_databaseOpenFailed = true;
261 return;
262 }
263
264 StorageTracker::tracker().setOriginDetails(m_databaseIdentifier, databaseFilename);
265 }
266
performImport()267 void StorageAreaSync::performImport()
268 {
269 ASSERT(!isMainThread());
270 ASSERT(!m_database.isOpen());
271
272 openDatabase(SkipIfNonExistent);
273 if (!m_database.isOpen()) {
274 markImported();
275 return;
276 }
277
278 SQLiteStatement query(m_database, "SELECT key, value FROM ItemTable");
279 if (query.prepare() != SQLResultOk) {
280 LOG_ERROR("Unable to select items from ItemTable for local storage");
281 markImported();
282 return;
283 }
284
285 HashMap<String, String> itemMap;
286
287 int result = query.step();
288 while (result == SQLResultRow) {
289 itemMap.set(query.getColumnText(0), query.getColumnText(1));
290 result = query.step();
291 }
292
293 if (result != SQLResultDone) {
294 LOG_ERROR("Error reading items from ItemTable for local storage");
295 markImported();
296 return;
297 }
298
299 HashMap<String, String>::iterator it = itemMap.begin();
300 HashMap<String, String>::iterator end = itemMap.end();
301
302 for (; it != end; ++it)
303 m_storageArea->importItem(it->first, it->second);
304
305 markImported();
306 }
307
markImported()308 void StorageAreaSync::markImported()
309 {
310 MutexLocker locker(m_importLock);
311 m_importComplete = true;
312 m_importCondition.signal();
313 }
314
315 // FIXME: In the future, we should allow use of StorageAreas while it's importing (when safe to do so).
316 // Blocking everything until the import is complete is by far the simplest and safest thing to do, but
317 // there is certainly room for safe optimization: Key/length will never be able to make use of such an
318 // optimization (since the order of iteration can change as items are being added). Get can return any
319 // item currently in the map. Get/remove can work whether or not it's in the map, but we'll need a list
320 // of items the import should not overwrite. Clear can also work, but it'll need to kill the import
321 // job first.
blockUntilImportComplete()322 void StorageAreaSync::blockUntilImportComplete()
323 {
324 ASSERT(isMainThread());
325
326 // Fast path. We set m_storageArea to 0 only after m_importComplete being true.
327 if (!m_storageArea)
328 return;
329
330 MutexLocker locker(m_importLock);
331 while (!m_importComplete)
332 m_importCondition.wait(m_importLock);
333 m_storageArea = 0;
334 }
335
sync(bool clearItems,const HashMap<String,String> & items)336 void StorageAreaSync::sync(bool clearItems, const HashMap<String, String>& items)
337 {
338 ASSERT(!isMainThread());
339
340 if (items.isEmpty() && !clearItems)
341 return;
342 if (m_databaseOpenFailed)
343 return;
344 if (!m_database.isOpen())
345 openDatabase(CreateIfNonExistent);
346 if (!m_database.isOpen())
347 return;
348
349 // Closing this db because it is about to be deleted by StorageTracker.
350 // The delete will be cancelled if StorageAreaSync needs to reopen the db
351 // to write new items created after the request to delete the db.
352 if (m_syncCloseDatabase) {
353 m_syncCloseDatabase = false;
354 m_database.close();
355 return;
356 }
357
358 // If the clear flag is set, then we clear all items out before we write any new ones in.
359 if (clearItems) {
360 SQLiteStatement clear(m_database, "DELETE FROM ItemTable");
361 if (clear.prepare() != SQLResultOk) {
362 LOG_ERROR("Failed to prepare clear statement - cannot write to local storage database");
363 return;
364 }
365
366 int result = clear.step();
367 if (result != SQLResultDone) {
368 LOG_ERROR("Failed to clear all items in the local storage database - %i", result);
369 return;
370 }
371 }
372
373 SQLiteStatement insert(m_database, "INSERT INTO ItemTable VALUES (?, ?)");
374 if (insert.prepare() != SQLResultOk) {
375 LOG_ERROR("Failed to prepare insert statement - cannot write to local storage database");
376 return;
377 }
378
379 SQLiteStatement remove(m_database, "DELETE FROM ItemTable WHERE key=?");
380 if (remove.prepare() != SQLResultOk) {
381 LOG_ERROR("Failed to prepare delete statement - cannot write to local storage database");
382 return;
383 }
384
385 HashMap<String, String>::const_iterator end = items.end();
386
387 for (HashMap<String, String>::const_iterator it = items.begin(); it != end; ++it) {
388 // Based on the null-ness of the second argument, decide whether this is an insert or a delete.
389 SQLiteStatement& query = it->second.isNull() ? remove : insert;
390
391 query.bindText(1, it->first);
392
393 // If the second argument is non-null, we're doing an insert, so bind it as the value.
394 if (!it->second.isNull())
395 query.bindText(2, it->second);
396
397 int result = query.step();
398 if (result != SQLResultDone) {
399 LOG_ERROR("Failed to update item in the local storage database - %i", result);
400 break;
401 }
402
403 query.reset();
404 }
405 }
406
performSync()407 void StorageAreaSync::performSync()
408 {
409 ASSERT(!isMainThread());
410
411 bool clearItems;
412 HashMap<String, String> items;
413 {
414 MutexLocker locker(m_syncLock);
415
416 ASSERT(m_syncScheduled);
417
418 clearItems = m_clearItemsWhileSyncing;
419 m_itemsPendingSync.swap(items);
420
421 m_clearItemsWhileSyncing = false;
422 m_syncScheduled = false;
423 m_syncInProgress = true;
424 }
425
426 sync(clearItems, items);
427
428 {
429 MutexLocker locker(m_syncLock);
430 m_syncInProgress = false;
431 }
432
433 // The following is balanced by the call to disableSuddenTermination in the
434 // syncTimerFired function.
435 enableSuddenTermination();
436 }
437
deleteEmptyDatabase()438 void StorageAreaSync::deleteEmptyDatabase()
439 {
440 ASSERT(!isMainThread());
441 if (!m_database.isOpen())
442 return;
443
444 SQLiteStatement query(m_database, "SELECT COUNT(*) FROM ItemTable");
445 if (query.prepare() != SQLResultOk) {
446 LOG_ERROR("Unable to count number of rows in ItemTable for local storage");
447 return;
448 }
449
450 int result = query.step();
451 if (result != SQLResultRow) {
452 LOG_ERROR("No results when counting number of rows in ItemTable for local storage");
453 return;
454 }
455
456 int count = query.getColumnInt(0);
457 if (!count) {
458 query.finalize();
459 m_database.close();
460 if (StorageTracker::tracker().isActive())
461 StorageTracker::tracker().deleteOrigin(m_databaseIdentifier);
462 else {
463 String databaseFilename = m_syncManager->fullDatabaseFilename(m_databaseIdentifier);
464 if (!SQLiteFileSystem::deleteDatabaseFile(databaseFilename))
465 LOG_ERROR("Failed to delete database file %s\n", databaseFilename.utf8().data());
466 }
467 }
468 }
469
scheduleSync()470 void StorageAreaSync::scheduleSync()
471 {
472 syncTimerFired(&m_syncTimer);
473 }
474
475 } // namespace WebCore
476
477 #endif // ENABLE(DOM_STORAGE)
478