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#include "EventNames.h"
30#include "FileSystem.h"
31#include "HTMLElement.h"
32#include "SQLiteDatabaseTracker.h"
33#include "SQLiteFileSystem.h"
34#include "SQLiteStatement.h"
35#include "SQLiteTransaction.h"
36#include "SecurityOrigin.h"
37#include "StorageAreaImpl.h"
38#include "StorageSyncManager.h"
39#include "StorageTracker.h"
40#include "SuddenTermination.h"
41#include <wtf/Functional.h>
42#include <wtf/MainThread.h>
43#include <wtf/text/CString.h>
44
45namespace WebCore {
46
47// If the StorageArea undergoes rapid changes, don't sync each change to disk.
48// Instead, queue up a batch of items to sync and actually do the sync at the following interval.
49static const double StorageSyncInterval = 1.0;
50
51// A sane limit on how many items we'll schedule to sync all at once.  This makes it
52// much harder to starve the rest of LocalStorage and the OS's IO subsystem in general.
53static const int MaxiumItemsToSync = 100;
54
55inline StorageAreaSync::StorageAreaSync(PassRefPtr<StorageSyncManager> storageSyncManager, PassRefPtr<StorageAreaImpl> storageArea, const String& databaseIdentifier)
56    : m_syncTimer(this, &StorageAreaSync::syncTimerFired)
57    , m_itemsCleared(false)
58    , m_finalSyncScheduled(false)
59    , m_storageArea(storageArea)
60    , m_syncManager(storageSyncManager)
61    , m_databaseIdentifier(databaseIdentifier.isolatedCopy())
62    , m_clearItemsWhileSyncing(false)
63    , m_syncScheduled(false)
64    , m_syncInProgress(false)
65    , m_databaseOpenFailed(false)
66    , m_syncCloseDatabase(false)
67    , m_importComplete(false)
68{
69    ASSERT(isMainThread());
70    ASSERT(m_storageArea);
71    ASSERT(m_syncManager);
72
73    // FIXME: If it can't import, then the default WebKit behavior should be that of private browsing,
74    // not silently ignoring it. https://bugs.webkit.org/show_bug.cgi?id=25894
75    m_syncManager->dispatch(bind(&StorageAreaSync::performImport, this));
76}
77
78PassRefPtr<StorageAreaSync> StorageAreaSync::create(PassRefPtr<StorageSyncManager> storageSyncManager, PassRefPtr<StorageAreaImpl> storageArea, const String& databaseIdentifier)
79{
80    RefPtr<StorageAreaSync> area = adoptRef(new StorageAreaSync(storageSyncManager, storageArea, databaseIdentifier));
81
82    return area.release();
83}
84
85StorageAreaSync::~StorageAreaSync()
86{
87    ASSERT(isMainThread());
88    ASSERT(!m_syncTimer.isActive());
89    ASSERT(m_finalSyncScheduled);
90}
91
92void StorageAreaSync::scheduleFinalSync()
93{
94    ASSERT(isMainThread());
95    // FIXME: We do this to avoid races, but it'd be better to make things safe without blocking.
96    blockUntilImportComplete();
97    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.
98
99    if (m_syncTimer.isActive())
100        m_syncTimer.stop();
101    else {
102        // The following is balanced by the call to enableSuddenTermination in the
103        // syncTimerFired function.
104        disableSuddenTermination();
105    }
106    // FIXME: This is synchronous.  We should do it on the background process, but
107    // we should do it safely.
108    m_finalSyncScheduled = true;
109    syncTimerFired(&m_syncTimer);
110
111    m_syncManager->dispatch(bind(&StorageAreaSync::deleteEmptyDatabase, this));
112}
113
114void StorageAreaSync::scheduleItemForSync(const String& key, const String& value)
115{
116    ASSERT(isMainThread());
117    ASSERT(!m_finalSyncScheduled);
118
119    m_changedItems.set(key, value);
120    if (!m_syncTimer.isActive()) {
121        m_syncTimer.startOneShot(StorageSyncInterval);
122
123        // The following is balanced by the call to enableSuddenTermination in the
124        // syncTimerFired function.
125        disableSuddenTermination();
126    }
127}
128
129void StorageAreaSync::scheduleClear()
130{
131    ASSERT(isMainThread());
132    ASSERT(!m_finalSyncScheduled);
133
134    m_changedItems.clear();
135    m_itemsCleared = true;
136    if (!m_syncTimer.isActive()) {
137        m_syncTimer.startOneShot(StorageSyncInterval);
138
139        // The following is balanced by the call to enableSuddenTermination in the
140        // syncTimerFired function.
141        disableSuddenTermination();
142    }
143}
144
145void StorageAreaSync::scheduleCloseDatabase()
146{
147    ASSERT(isMainThread());
148    ASSERT(!m_finalSyncScheduled);
149
150    if (!m_database.isOpen())
151        return;
152
153    m_syncCloseDatabase = true;
154
155    if (!m_syncTimer.isActive()) {
156        m_syncTimer.startOneShot(StorageSyncInterval);
157
158        // The following is balanced by the call to enableSuddenTermination in the
159        // syncTimerFired function.
160        disableSuddenTermination();
161    }
162}
163
164void StorageAreaSync::syncTimerFired(Timer<StorageAreaSync>*)
165{
166    ASSERT(isMainThread());
167
168    bool partialSync = false;
169    {
170        MutexLocker locker(m_syncLock);
171
172        // Do not schedule another sync if we're still trying to complete the
173        // previous one.  But, if we're shutting down, schedule it anyway.
174        if (m_syncInProgress && !m_finalSyncScheduled) {
175            ASSERT(!m_syncTimer.isActive());
176            m_syncTimer.startOneShot(StorageSyncInterval);
177            return;
178        }
179
180        if (m_itemsCleared) {
181            m_itemsPendingSync.clear();
182            m_clearItemsWhileSyncing = true;
183            m_itemsCleared = false;
184        }
185
186        HashMap<String, String>::iterator changed_it = m_changedItems.begin();
187        HashMap<String, String>::iterator changed_end = m_changedItems.end();
188        for (int count = 0; changed_it != changed_end; ++count, ++changed_it) {
189            if (count >= MaxiumItemsToSync && !m_finalSyncScheduled) {
190                partialSync = true;
191                break;
192            }
193            m_itemsPendingSync.set(changed_it->key.isolatedCopy(), changed_it->value.isolatedCopy());
194        }
195
196        if (partialSync) {
197            // We can't do the fast path of simply clearing all items, so we'll need to manually
198            // remove them one by one.  Done under lock since m_itemsPendingSync is modified by
199            // the background thread.
200            HashMap<String, String>::iterator pending_it = m_itemsPendingSync.begin();
201            HashMap<String, String>::iterator pending_end = m_itemsPendingSync.end();
202            for (; pending_it != pending_end; ++pending_it)
203                m_changedItems.remove(pending_it->key);
204        }
205
206        if (!m_syncScheduled) {
207            m_syncScheduled = true;
208
209            // The following is balanced by the call to enableSuddenTermination in the
210            // performSync function.
211            disableSuddenTermination();
212
213            m_syncManager->dispatch(bind(&StorageAreaSync::performSync, this));
214        }
215    }
216
217    if (partialSync) {
218        // If we didn't finish syncing, then we need to finish the job later.
219        ASSERT(!m_syncTimer.isActive());
220        m_syncTimer.startOneShot(StorageSyncInterval);
221    } else {
222        // The following is balanced by the calls to disableSuddenTermination in the
223        // scheduleItemForSync, scheduleClear, and scheduleFinalSync functions.
224        enableSuddenTermination();
225
226        m_changedItems.clear();
227    }
228}
229
230void StorageAreaSync::openDatabase(OpenDatabaseParamType openingStrategy)
231{
232    ASSERT(!isMainThread());
233    ASSERT(!m_database.isOpen());
234    ASSERT(!m_databaseOpenFailed);
235
236    SQLiteTransactionInProgressAutoCounter transactionCounter;
237
238    String databaseFilename = m_syncManager->fullDatabaseFilename(m_databaseIdentifier);
239
240    if (!fileExists(databaseFilename) && openingStrategy == SkipIfNonExistent)
241        return;
242
243    if (databaseFilename.isEmpty()) {
244        LOG_ERROR("Filename for local storage database is empty - cannot open for persistent storage");
245        markImported();
246        m_databaseOpenFailed = true;
247        return;
248    }
249
250    // A StorageTracker thread may have been scheduled to delete the db we're
251    // reopening, so cancel possible deletion.
252    StorageTracker::tracker().cancelDeletingOrigin(m_databaseIdentifier);
253
254    if (!m_database.open(databaseFilename)) {
255        LOG_ERROR("Failed to open database file %s for local storage", databaseFilename.utf8().data());
256        markImported();
257        m_databaseOpenFailed = true;
258        return;
259    }
260
261    migrateItemTableIfNeeded();
262
263    if (!m_database.executeCommand("CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB NOT NULL ON CONFLICT FAIL)")) {
264        LOG_ERROR("Failed to create table ItemTable for local storage");
265        markImported();
266        m_databaseOpenFailed = true;
267        return;
268    }
269
270    StorageTracker::tracker().setOriginDetails(m_databaseIdentifier, databaseFilename);
271}
272
273void StorageAreaSync::migrateItemTableIfNeeded()
274{
275    if (!m_database.tableExists("ItemTable"))
276        return;
277
278    {
279        SQLiteStatement query(m_database, "SELECT value FROM ItemTable LIMIT 1");
280        // this query isn't ever executed.
281        if (query.isColumnDeclaredAsBlob(0))
282            return;
283    }
284
285    // alter table for backward compliance, change the value type from TEXT to BLOB.
286    static const char* commands[] = {
287        "DROP TABLE IF EXISTS ItemTable2",
288        "CREATE TABLE ItemTable2 (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB NOT NULL ON CONFLICT FAIL)",
289        "INSERT INTO ItemTable2 SELECT * from ItemTable",
290        "DROP TABLE ItemTable",
291        "ALTER TABLE ItemTable2 RENAME TO ItemTable",
292        0,
293    };
294
295    SQLiteTransaction transaction(m_database, false);
296    transaction.begin();
297    for (size_t i = 0; commands[i]; ++i) {
298        if (!m_database.executeCommand(commands[i])) {
299            LOG_ERROR("Failed to migrate table ItemTable for local storage when executing: %s", commands[i]);
300            transaction.rollback();
301
302            // finally it will try to keep a backup of ItemTable for the future restoration.
303            // NOTICE: this will essentially DELETE the current database, but that's better
304            // than continually hitting this case and never being able to use the local storage.
305            // if this is ever hit, it's definitely a bug.
306            ASSERT_NOT_REACHED();
307            if (!m_database.executeCommand("ALTER TABLE ItemTable RENAME TO Backup_ItemTable"))
308                LOG_ERROR("Failed to save ItemTable after migration job failed.");
309
310            return;
311        }
312    }
313    transaction.commit();
314}
315
316void StorageAreaSync::performImport()
317{
318    ASSERT(!isMainThread());
319    ASSERT(!m_database.isOpen());
320
321    openDatabase(SkipIfNonExistent);
322    if (!m_database.isOpen()) {
323        markImported();
324        return;
325    }
326
327    SQLiteStatement query(m_database, "SELECT key, value FROM ItemTable");
328    if (query.prepare() != SQLResultOk) {
329        LOG_ERROR("Unable to select items from ItemTable for local storage");
330        markImported();
331        return;
332    }
333
334    HashMap<String, String> itemMap;
335
336    int result = query.step();
337    while (result == SQLResultRow) {
338        itemMap.set(query.getColumnText(0), query.getColumnBlobAsString(1));
339        result = query.step();
340    }
341
342    if (result != SQLResultDone) {
343        LOG_ERROR("Error reading items from ItemTable for local storage");
344        markImported();
345        return;
346    }
347
348    m_storageArea->importItems(itemMap);
349
350    markImported();
351}
352
353void StorageAreaSync::markImported()
354{
355    MutexLocker locker(m_importLock);
356    m_importComplete = true;
357    m_importCondition.signal();
358}
359
360// FIXME: In the future, we should allow use of StorageAreas while it's importing (when safe to do so).
361// Blocking everything until the import is complete is by far the simplest and safest thing to do, but
362// there is certainly room for safe optimization: Key/length will never be able to make use of such an
363// optimization (since the order of iteration can change as items are being added). Get can return any
364// item currently in the map. Get/remove can work whether or not it's in the map, but we'll need a list
365// of items the import should not overwrite. Clear can also work, but it'll need to kill the import
366// job first.
367void StorageAreaSync::blockUntilImportComplete()
368{
369    ASSERT(isMainThread());
370
371    // Fast path.  We set m_storageArea to 0 only after m_importComplete being true.
372    if (!m_storageArea)
373        return;
374
375    MutexLocker locker(m_importLock);
376    while (!m_importComplete)
377        m_importCondition.wait(m_importLock);
378    m_storageArea = 0;
379}
380
381void StorageAreaSync::sync(bool clearItems, const HashMap<String, String>& items)
382{
383    ASSERT(!isMainThread());
384
385    if (items.isEmpty() && !clearItems && !m_syncCloseDatabase)
386        return;
387    if (m_databaseOpenFailed)
388        return;
389
390    if (!m_database.isOpen() && m_syncCloseDatabase) {
391        m_syncCloseDatabase = false;
392        return;
393    }
394
395    if (!m_database.isOpen())
396        openDatabase(CreateIfNonExistent);
397    if (!m_database.isOpen())
398        return;
399
400    // Closing this db because it is about to be deleted by StorageTracker.
401    // The delete will be cancelled if StorageAreaSync needs to reopen the db
402    // to write new items created after the request to delete the db.
403    if (m_syncCloseDatabase) {
404        m_syncCloseDatabase = false;
405        m_database.close();
406        return;
407    }
408
409    SQLiteTransactionInProgressAutoCounter transactionCounter;
410
411    // If the clear flag is set, then we clear all items out before we write any new ones in.
412    if (clearItems) {
413        SQLiteStatement clear(m_database, "DELETE FROM ItemTable");
414        if (clear.prepare() != SQLResultOk) {
415            LOG_ERROR("Failed to prepare clear statement - cannot write to local storage database");
416            return;
417        }
418
419        int result = clear.step();
420        if (result != SQLResultDone) {
421            LOG_ERROR("Failed to clear all items in the local storage database - %i", result);
422            return;
423        }
424    }
425
426    SQLiteStatement insert(m_database, "INSERT INTO ItemTable VALUES (?, ?)");
427    if (insert.prepare() != SQLResultOk) {
428        LOG_ERROR("Failed to prepare insert statement - cannot write to local storage database");
429        return;
430    }
431
432    SQLiteStatement remove(m_database, "DELETE FROM ItemTable WHERE key=?");
433    if (remove.prepare() != SQLResultOk) {
434        LOG_ERROR("Failed to prepare delete statement - cannot write to local storage database");
435        return;
436    }
437
438    HashMap<String, String>::const_iterator end = items.end();
439
440    SQLiteTransaction transaction(m_database);
441    transaction.begin();
442    for (HashMap<String, String>::const_iterator it = items.begin(); it != end; ++it) {
443        // Based on the null-ness of the second argument, decide whether this is an insert or a delete.
444        SQLiteStatement& query = it->value.isNull() ? remove : insert;
445
446        query.bindText(1, it->key);
447
448        // If the second argument is non-null, we're doing an insert, so bind it as the value.
449        if (!it->value.isNull())
450            query.bindBlob(2, it->value);
451
452        int result = query.step();
453        if (result != SQLResultDone) {
454            LOG_ERROR("Failed to update item in the local storage database - %i", result);
455            break;
456        }
457
458        query.reset();
459    }
460    transaction.commit();
461}
462
463void StorageAreaSync::performSync()
464{
465    ASSERT(!isMainThread());
466
467    bool clearItems;
468    HashMap<String, String> items;
469    {
470        MutexLocker locker(m_syncLock);
471
472        ASSERT(m_syncScheduled);
473
474        clearItems = m_clearItemsWhileSyncing;
475        m_itemsPendingSync.swap(items);
476
477        m_clearItemsWhileSyncing = false;
478        m_syncScheduled = false;
479        m_syncInProgress = true;
480    }
481
482    sync(clearItems, items);
483
484    {
485        MutexLocker locker(m_syncLock);
486        m_syncInProgress = false;
487    }
488
489    // The following is balanced by the call to disableSuddenTermination in the
490    // syncTimerFired function.
491    enableSuddenTermination();
492}
493
494void StorageAreaSync::deleteEmptyDatabase()
495{
496    ASSERT(!isMainThread());
497    if (!m_database.isOpen())
498        return;
499
500    SQLiteStatement query(m_database, "SELECT COUNT(*) FROM ItemTable");
501    if (query.prepare() != SQLResultOk) {
502        LOG_ERROR("Unable to count number of rows in ItemTable for local storage");
503        return;
504    }
505
506    int result = query.step();
507    if (result != SQLResultRow) {
508        LOG_ERROR("No results when counting number of rows in ItemTable for local storage");
509        return;
510    }
511
512    int count = query.getColumnInt(0);
513    if (!count) {
514        query.finalize();
515        m_database.close();
516        if (StorageTracker::tracker().isActive())
517            StorageTracker::tracker().deleteOriginWithIdentifier(m_databaseIdentifier);
518        else {
519            String databaseFilename = m_syncManager->fullDatabaseFilename(m_databaseIdentifier);
520            if (!SQLiteFileSystem::deleteDatabaseFile(databaseFilename))
521                LOG_ERROR("Failed to delete database file %s\n", databaseFilename.utf8().data());
522        }
523    }
524}
525
526void StorageAreaSync::scheduleSync()
527{
528    syncTimerFired(&m_syncTimer);
529}
530
531} // namespace WebCore
532