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