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