1/*
2 * Copyright (C) 2008, 2009 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 "ApplicationCacheStorage.h"
28
29#if ENABLE(OFFLINE_WEB_APPLICATIONS)
30
31#include "ApplicationCache.h"
32#include "ApplicationCacheHost.h"
33#include "ApplicationCacheGroup.h"
34#include "ApplicationCacheResource.h"
35#include "CString.h"
36#include "FileSystem.h"
37#include "KURL.h"
38#include "SQLiteStatement.h"
39#include "SQLiteTransaction.h"
40#include <wtf/StdLibExtras.h>
41#include <wtf/StringExtras.h>
42
43using namespace std;
44
45namespace WebCore {
46
47template <class T>
48class StorageIDJournal {
49public:
50    ~StorageIDJournal()
51    {
52        size_t size = m_records.size();
53        for (size_t i = 0; i < size; ++i)
54            m_records[i].restore();
55    }
56
57    void add(T* resource, unsigned storageID)
58    {
59        m_records.append(Record(resource, storageID));
60    }
61
62    void commit()
63    {
64        m_records.clear();
65    }
66
67private:
68    class Record {
69    public:
70        Record() : m_resource(0), m_storageID(0) { }
71        Record(T* resource, unsigned storageID) : m_resource(resource), m_storageID(storageID) { }
72
73        void restore()
74        {
75            m_resource->setStorageID(m_storageID);
76        }
77
78    private:
79        T* m_resource;
80        unsigned m_storageID;
81    };
82
83    Vector<Record> m_records;
84};
85
86static unsigned urlHostHash(const KURL& url)
87{
88    unsigned hostStart = url.hostStart();
89    unsigned hostEnd = url.hostEnd();
90
91    return AlreadyHashed::avoidDeletedValue(StringImpl::computeHash(url.string().characters() + hostStart, hostEnd - hostStart));
92}
93
94ApplicationCacheGroup* ApplicationCacheStorage::loadCacheGroup(const KURL& manifestURL)
95{
96    openDatabase(false);
97    if (!m_database.isOpen())
98        return 0;
99
100    SQLiteStatement statement(m_database, "SELECT id, manifestURL, newestCache FROM CacheGroups WHERE newestCache IS NOT NULL AND manifestURL=?");
101    if (statement.prepare() != SQLResultOk)
102        return 0;
103
104    statement.bindText(1, manifestURL);
105
106    int result = statement.step();
107    if (result == SQLResultDone)
108        return 0;
109
110    if (result != SQLResultRow) {
111        LOG_ERROR("Could not load cache group, error \"%s\"", m_database.lastErrorMsg());
112        return 0;
113    }
114
115    unsigned newestCacheStorageID = static_cast<unsigned>(statement.getColumnInt64(2));
116
117    RefPtr<ApplicationCache> cache = loadCache(newestCacheStorageID);
118    if (!cache)
119        return 0;
120
121    ApplicationCacheGroup* group = new ApplicationCacheGroup(manifestURL);
122
123    group->setStorageID(static_cast<unsigned>(statement.getColumnInt64(0)));
124    group->setNewestCache(cache.release());
125
126    return group;
127}
128
129ApplicationCacheGroup* ApplicationCacheStorage::findOrCreateCacheGroup(const KURL& manifestURL)
130{
131    ASSERT(!manifestURL.hasFragmentIdentifier());
132
133    std::pair<CacheGroupMap::iterator, bool> result = m_cachesInMemory.add(manifestURL, 0);
134
135    if (!result.second) {
136        ASSERT(result.first->second);
137        return result.first->second;
138    }
139
140    // Look up the group in the database
141    ApplicationCacheGroup* group = loadCacheGroup(manifestURL);
142
143    // If the group was not found we need to create it
144    if (!group) {
145        group = new ApplicationCacheGroup(manifestURL);
146        m_cacheHostSet.add(urlHostHash(manifestURL));
147    }
148
149    result.first->second = group;
150
151    return group;
152}
153
154void ApplicationCacheStorage::loadManifestHostHashes()
155{
156    static bool hasLoadedHashes = false;
157
158    if (hasLoadedHashes)
159        return;
160
161    // We set this flag to true before the database has been opened
162    // to avoid trying to open the database over and over if it doesn't exist.
163    hasLoadedHashes = true;
164
165    openDatabase(false);
166    if (!m_database.isOpen())
167        return;
168
169    // Fetch the host hashes.
170    SQLiteStatement statement(m_database, "SELECT manifestHostHash FROM CacheGroups");
171    if (statement.prepare() != SQLResultOk)
172        return;
173
174    int result;
175    while ((result = statement.step()) == SQLResultRow)
176        m_cacheHostSet.add(static_cast<unsigned>(statement.getColumnInt64(0)));
177}
178
179ApplicationCacheGroup* ApplicationCacheStorage::cacheGroupForURL(const KURL& url)
180{
181    ASSERT(!url.hasFragmentIdentifier());
182
183    loadManifestHostHashes();
184
185    // Hash the host name and see if there's a manifest with the same host.
186    if (!m_cacheHostSet.contains(urlHostHash(url)))
187        return 0;
188
189    // Check if a cache already exists in memory.
190    CacheGroupMap::const_iterator end = m_cachesInMemory.end();
191    for (CacheGroupMap::const_iterator it = m_cachesInMemory.begin(); it != end; ++it) {
192        ApplicationCacheGroup* group = it->second;
193
194        ASSERT(!group->isObsolete());
195
196        if (!protocolHostAndPortAreEqual(url, group->manifestURL()))
197            continue;
198
199        if (ApplicationCache* cache = group->newestCache()) {
200            ApplicationCacheResource* resource = cache->resourceForURL(url);
201            if (!resource)
202                continue;
203            if (resource->type() & ApplicationCacheResource::Foreign)
204                continue;
205            return group;
206        }
207    }
208
209    if (!m_database.isOpen())
210        return 0;
211
212    // Check the database. Look for all cache groups with a newest cache.
213    SQLiteStatement statement(m_database, "SELECT id, manifestURL, newestCache FROM CacheGroups WHERE newestCache IS NOT NULL");
214    if (statement.prepare() != SQLResultOk)
215        return 0;
216
217    int result;
218    while ((result = statement.step()) == SQLResultRow) {
219        KURL manifestURL = KURL(ParsedURLString, statement.getColumnText(1));
220
221        if (m_cachesInMemory.contains(manifestURL))
222            continue;
223
224        if (!protocolHostAndPortAreEqual(url, manifestURL))
225            continue;
226
227        // We found a cache group that matches. Now check if the newest cache has a resource with
228        // a matching URL.
229        unsigned newestCacheID = static_cast<unsigned>(statement.getColumnInt64(2));
230        RefPtr<ApplicationCache> cache = loadCache(newestCacheID);
231        if (!cache)
232            continue;
233
234        ApplicationCacheResource* resource = cache->resourceForURL(url);
235        if (!resource)
236            continue;
237        if (resource->type() & ApplicationCacheResource::Foreign)
238            continue;
239
240        ApplicationCacheGroup* group = new ApplicationCacheGroup(manifestURL);
241
242        group->setStorageID(static_cast<unsigned>(statement.getColumnInt64(0)));
243        group->setNewestCache(cache.release());
244
245        m_cachesInMemory.set(group->manifestURL(), group);
246
247        return group;
248    }
249
250    if (result != SQLResultDone)
251        LOG_ERROR("Could not load cache group, error \"%s\"", m_database.lastErrorMsg());
252
253    return 0;
254}
255
256ApplicationCacheGroup* ApplicationCacheStorage::fallbackCacheGroupForURL(const KURL& url)
257{
258    ASSERT(!url.hasFragmentIdentifier());
259
260    // Check if an appropriate cache already exists in memory.
261    CacheGroupMap::const_iterator end = m_cachesInMemory.end();
262    for (CacheGroupMap::const_iterator it = m_cachesInMemory.begin(); it != end; ++it) {
263        ApplicationCacheGroup* group = it->second;
264
265        ASSERT(!group->isObsolete());
266
267        if (ApplicationCache* cache = group->newestCache()) {
268            KURL fallbackURL;
269            if (!cache->urlMatchesFallbackNamespace(url, &fallbackURL))
270                continue;
271            if (cache->resourceForURL(fallbackURL)->type() & ApplicationCacheResource::Foreign)
272                continue;
273            return group;
274        }
275    }
276
277    if (!m_database.isOpen())
278        return 0;
279
280    // Check the database. Look for all cache groups with a newest cache.
281    SQLiteStatement statement(m_database, "SELECT id, manifestURL, newestCache FROM CacheGroups WHERE newestCache IS NOT NULL");
282    if (statement.prepare() != SQLResultOk)
283        return 0;
284
285    int result;
286    while ((result = statement.step()) == SQLResultRow) {
287        KURL manifestURL = KURL(ParsedURLString, statement.getColumnText(1));
288
289        if (m_cachesInMemory.contains(manifestURL))
290            continue;
291
292        // Fallback namespaces always have the same origin as manifest URL, so we can avoid loading caches that cannot match.
293        if (!protocolHostAndPortAreEqual(url, manifestURL))
294            continue;
295
296        // We found a cache group that matches. Now check if the newest cache has a resource with
297        // a matching fallback namespace.
298        unsigned newestCacheID = static_cast<unsigned>(statement.getColumnInt64(2));
299        RefPtr<ApplicationCache> cache = loadCache(newestCacheID);
300
301        KURL fallbackURL;
302        if (!cache->urlMatchesFallbackNamespace(url, &fallbackURL))
303            continue;
304        if (cache->resourceForURL(fallbackURL)->type() & ApplicationCacheResource::Foreign)
305            continue;
306
307        ApplicationCacheGroup* group = new ApplicationCacheGroup(manifestURL);
308
309        group->setStorageID(static_cast<unsigned>(statement.getColumnInt64(0)));
310        group->setNewestCache(cache.release());
311
312        m_cachesInMemory.set(group->manifestURL(), group);
313
314        return group;
315    }
316
317    if (result != SQLResultDone)
318        LOG_ERROR("Could not load cache group, error \"%s\"", m_database.lastErrorMsg());
319
320    return 0;
321}
322
323void ApplicationCacheStorage::cacheGroupDestroyed(ApplicationCacheGroup* group)
324{
325    if (group->isObsolete()) {
326        ASSERT(!group->storageID());
327        ASSERT(m_cachesInMemory.get(group->manifestURL()) != group);
328        return;
329    }
330
331    ASSERT(m_cachesInMemory.get(group->manifestURL()) == group);
332
333    m_cachesInMemory.remove(group->manifestURL());
334
335    // If the cache group is half-created, we don't want it in the saved set (as it is not stored in database).
336    if (!group->storageID())
337        m_cacheHostSet.remove(urlHostHash(group->manifestURL()));
338}
339
340void ApplicationCacheStorage::cacheGroupMadeObsolete(ApplicationCacheGroup* group)
341{
342    ASSERT(m_cachesInMemory.get(group->manifestURL()) == group);
343    ASSERT(m_cacheHostSet.contains(urlHostHash(group->manifestURL())));
344
345    if (ApplicationCache* newestCache = group->newestCache())
346        remove(newestCache);
347
348    m_cachesInMemory.remove(group->manifestURL());
349    m_cacheHostSet.remove(urlHostHash(group->manifestURL()));
350}
351
352void ApplicationCacheStorage::setCacheDirectory(const String& cacheDirectory)
353{
354    ASSERT(m_cacheDirectory.isNull());
355    ASSERT(!cacheDirectory.isNull());
356
357    m_cacheDirectory = cacheDirectory;
358}
359
360const String& ApplicationCacheStorage::cacheDirectory() const
361{
362    return m_cacheDirectory;
363}
364
365void ApplicationCacheStorage::setMaximumSize(int64_t size)
366{
367    m_maximumSize = size;
368}
369
370int64_t ApplicationCacheStorage::maximumSize() const
371{
372    return m_maximumSize;
373}
374
375bool ApplicationCacheStorage::isMaximumSizeReached() const
376{
377    return m_isMaximumSizeReached;
378}
379
380int64_t ApplicationCacheStorage::spaceNeeded(int64_t cacheToSave)
381{
382    int64_t spaceNeeded = 0;
383    long long fileSize = 0;
384    if (!getFileSize(m_cacheFile, fileSize))
385        return 0;
386
387    int64_t currentSize = fileSize;
388
389    // Determine the amount of free space we have available.
390    int64_t totalAvailableSize = 0;
391    if (m_maximumSize < currentSize) {
392        // The max size is smaller than the actual size of the app cache file.
393        // This can happen if the client previously imposed a larger max size
394        // value and the app cache file has already grown beyond the current
395        // max size value.
396        // The amount of free space is just the amount of free space inside
397        // the database file. Note that this is always 0 if SQLite is compiled
398        // with AUTO_VACUUM = 1.
399        totalAvailableSize = m_database.freeSpaceSize();
400    } else {
401        // The max size is the same or larger than the current size.
402        // The amount of free space available is the amount of free space
403        // inside the database file plus the amount we can grow until we hit
404        // the max size.
405        totalAvailableSize = (m_maximumSize - currentSize) + m_database.freeSpaceSize();
406    }
407
408    // The space needed to be freed in order to accommodate the failed cache is
409    // the size of the failed cache minus any already available free space.
410    spaceNeeded = cacheToSave - totalAvailableSize;
411    // The space needed value must be positive (or else the total already
412    // available free space would be larger than the size of the failed cache and
413    // saving of the cache should have never failed).
414    ASSERT(spaceNeeded);
415    return spaceNeeded;
416}
417
418bool ApplicationCacheStorage::executeSQLCommand(const String& sql)
419{
420    ASSERT(m_database.isOpen());
421
422    bool result = m_database.executeCommand(sql);
423    if (!result)
424        LOG_ERROR("Application Cache Storage: failed to execute statement \"%s\" error \"%s\"",
425                  sql.utf8().data(), m_database.lastErrorMsg());
426
427    return result;
428}
429
430static const int schemaVersion = 5;
431
432void ApplicationCacheStorage::verifySchemaVersion()
433{
434    int version = SQLiteStatement(m_database, "PRAGMA user_version").getColumnInt(0);
435    if (version == schemaVersion)
436        return;
437
438    m_database.clearAllTables();
439
440    // Update user version.
441    SQLiteTransaction setDatabaseVersion(m_database);
442    setDatabaseVersion.begin();
443
444    char userVersionSQL[32];
445    int unusedNumBytes = snprintf(userVersionSQL, sizeof(userVersionSQL), "PRAGMA user_version=%d", schemaVersion);
446    ASSERT_UNUSED(unusedNumBytes, static_cast<int>(sizeof(userVersionSQL)) >= unusedNumBytes);
447
448    SQLiteStatement statement(m_database, userVersionSQL);
449    if (statement.prepare() != SQLResultOk)
450        return;
451
452    executeStatement(statement);
453    setDatabaseVersion.commit();
454}
455
456void ApplicationCacheStorage::openDatabase(bool createIfDoesNotExist)
457{
458    if (m_database.isOpen())
459        return;
460
461    // The cache directory should never be null, but if it for some weird reason is we bail out.
462    if (m_cacheDirectory.isNull())
463        return;
464
465    m_cacheFile = pathByAppendingComponent(m_cacheDirectory, "ApplicationCache.db");
466    if (!createIfDoesNotExist && !fileExists(m_cacheFile))
467        return;
468
469    makeAllDirectories(m_cacheDirectory);
470    m_database.open(m_cacheFile);
471
472    if (!m_database.isOpen())
473        return;
474
475    verifySchemaVersion();
476
477    // Create tables
478    executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheGroups (id INTEGER PRIMARY KEY AUTOINCREMENT, "
479                      "manifestHostHash INTEGER NOT NULL ON CONFLICT FAIL, manifestURL TEXT UNIQUE ON CONFLICT FAIL, newestCache INTEGER)");
480    executeSQLCommand("CREATE TABLE IF NOT EXISTS Caches (id INTEGER PRIMARY KEY AUTOINCREMENT, cacheGroup INTEGER, size INTEGER)");
481    executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheWhitelistURLs (url TEXT NOT NULL ON CONFLICT FAIL, cache INTEGER NOT NULL ON CONFLICT FAIL)");
482    executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheAllowsAllNetworkRequests (wildcard INTEGER NOT NULL ON CONFLICT FAIL, cache INTEGER NOT NULL ON CONFLICT FAIL)");
483    executeSQLCommand("CREATE TABLE IF NOT EXISTS FallbackURLs (namespace TEXT NOT NULL ON CONFLICT FAIL, fallbackURL TEXT NOT NULL ON CONFLICT FAIL, "
484                      "cache INTEGER NOT NULL ON CONFLICT FAIL)");
485    executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheEntries (cache INTEGER NOT NULL ON CONFLICT FAIL, type INTEGER, resource INTEGER NOT NULL)");
486    executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheResources (id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL ON CONFLICT FAIL, "
487                      "statusCode INTEGER NOT NULL, responseURL TEXT NOT NULL, mimeType TEXT, textEncodingName TEXT, headers TEXT, data INTEGER NOT NULL ON CONFLICT FAIL)");
488    executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheResourceData (id INTEGER PRIMARY KEY AUTOINCREMENT, data BLOB)");
489
490    // When a cache is deleted, all its entries and its whitelist should be deleted.
491    executeSQLCommand("CREATE TRIGGER IF NOT EXISTS CacheDeleted AFTER DELETE ON Caches"
492                      " FOR EACH ROW BEGIN"
493                      "  DELETE FROM CacheEntries WHERE cache = OLD.id;"
494                      "  DELETE FROM CacheWhitelistURLs WHERE cache = OLD.id;"
495                      "  DELETE FROM CacheAllowsAllNetworkRequests WHERE cache = OLD.id;"
496                      "  DELETE FROM FallbackURLs WHERE cache = OLD.id;"
497                      " END");
498
499    // When a cache entry is deleted, its resource should also be deleted.
500    executeSQLCommand("CREATE TRIGGER IF NOT EXISTS CacheEntryDeleted AFTER DELETE ON CacheEntries"
501                      " FOR EACH ROW BEGIN"
502                      "  DELETE FROM CacheResources WHERE id = OLD.resource;"
503                      " END");
504
505    // When a cache resource is deleted, its data blob should also be deleted.
506    executeSQLCommand("CREATE TRIGGER IF NOT EXISTS CacheResourceDeleted AFTER DELETE ON CacheResources"
507                      " FOR EACH ROW BEGIN"
508                      "  DELETE FROM CacheResourceData WHERE id = OLD.data;"
509                      " END");
510}
511
512bool ApplicationCacheStorage::executeStatement(SQLiteStatement& statement)
513{
514    bool result = statement.executeCommand();
515    if (!result)
516        LOG_ERROR("Application Cache Storage: failed to execute statement \"%s\" error \"%s\"",
517                  statement.query().utf8().data(), m_database.lastErrorMsg());
518
519    return result;
520}
521
522bool ApplicationCacheStorage::store(ApplicationCacheGroup* group, GroupStorageIDJournal* journal)
523{
524    ASSERT(group->storageID() == 0);
525    ASSERT(journal);
526
527    SQLiteStatement statement(m_database, "INSERT INTO CacheGroups (manifestHostHash, manifestURL) VALUES (?, ?)");
528    if (statement.prepare() != SQLResultOk)
529        return false;
530
531    statement.bindInt64(1, urlHostHash(group->manifestURL()));
532    statement.bindText(2, group->manifestURL());
533
534    if (!executeStatement(statement))
535        return false;
536
537    group->setStorageID(static_cast<unsigned>(m_database.lastInsertRowID()));
538    journal->add(group, 0);
539    return true;
540}
541
542bool ApplicationCacheStorage::store(ApplicationCache* cache, ResourceStorageIDJournal* storageIDJournal)
543{
544    ASSERT(cache->storageID() == 0);
545    ASSERT(cache->group()->storageID() != 0);
546    ASSERT(storageIDJournal);
547
548    SQLiteStatement statement(m_database, "INSERT INTO Caches (cacheGroup, size) VALUES (?, ?)");
549    if (statement.prepare() != SQLResultOk)
550        return false;
551
552    statement.bindInt64(1, cache->group()->storageID());
553    statement.bindInt64(2, cache->estimatedSizeInStorage());
554
555    if (!executeStatement(statement))
556        return false;
557
558    unsigned cacheStorageID = static_cast<unsigned>(m_database.lastInsertRowID());
559
560    // Store all resources
561    {
562        ApplicationCache::ResourceMap::const_iterator end = cache->end();
563        for (ApplicationCache::ResourceMap::const_iterator it = cache->begin(); it != end; ++it) {
564            unsigned oldStorageID = it->second->storageID();
565            if (!store(it->second.get(), cacheStorageID))
566                return false;
567
568            // Storing the resource succeeded. Log its old storageID in case
569            // it needs to be restored later.
570            storageIDJournal->add(it->second.get(), oldStorageID);
571        }
572    }
573
574    // Store the online whitelist
575    const Vector<KURL>& onlineWhitelist = cache->onlineWhitelist();
576    {
577        size_t whitelistSize = onlineWhitelist.size();
578        for (size_t i = 0; i < whitelistSize; ++i) {
579            SQLiteStatement statement(m_database, "INSERT INTO CacheWhitelistURLs (url, cache) VALUES (?, ?)");
580            statement.prepare();
581
582            statement.bindText(1, onlineWhitelist[i]);
583            statement.bindInt64(2, cacheStorageID);
584
585            if (!executeStatement(statement))
586                return false;
587        }
588    }
589
590    // Store online whitelist wildcard flag.
591    {
592        SQLiteStatement statement(m_database, "INSERT INTO CacheAllowsAllNetworkRequests (wildcard, cache) VALUES (?, ?)");
593        statement.prepare();
594
595        statement.bindInt64(1, cache->allowsAllNetworkRequests());
596        statement.bindInt64(2, cacheStorageID);
597
598        if (!executeStatement(statement))
599            return false;
600    }
601
602    // Store fallback URLs.
603    const FallbackURLVector& fallbackURLs = cache->fallbackURLs();
604    {
605        size_t fallbackCount = fallbackURLs.size();
606        for (size_t i = 0; i < fallbackCount; ++i) {
607            SQLiteStatement statement(m_database, "INSERT INTO FallbackURLs (namespace, fallbackURL, cache) VALUES (?, ?, ?)");
608            statement.prepare();
609
610            statement.bindText(1, fallbackURLs[i].first);
611            statement.bindText(2, fallbackURLs[i].second);
612            statement.bindInt64(3, cacheStorageID);
613
614            if (!executeStatement(statement))
615                return false;
616        }
617    }
618
619    cache->setStorageID(cacheStorageID);
620    return true;
621}
622
623bool ApplicationCacheStorage::store(ApplicationCacheResource* resource, unsigned cacheStorageID)
624{
625    ASSERT(cacheStorageID);
626    ASSERT(!resource->storageID());
627
628    openDatabase(true);
629
630    // First, insert the data
631    SQLiteStatement dataStatement(m_database, "INSERT INTO CacheResourceData (data) VALUES (?)");
632    if (dataStatement.prepare() != SQLResultOk)
633        return false;
634
635    if (resource->data()->size())
636        dataStatement.bindBlob(1, resource->data()->data(), resource->data()->size());
637
638    if (!dataStatement.executeCommand())
639        return false;
640
641    unsigned dataId = static_cast<unsigned>(m_database.lastInsertRowID());
642
643    // Then, insert the resource
644
645    // Serialize the headers
646    Vector<UChar> stringBuilder;
647
648    HTTPHeaderMap::const_iterator end = resource->response().httpHeaderFields().end();
649    for (HTTPHeaderMap::const_iterator it = resource->response().httpHeaderFields().begin(); it!= end; ++it) {
650        stringBuilder.append(it->first.characters(), it->first.length());
651        stringBuilder.append((UChar)':');
652        stringBuilder.append(it->second.characters(), it->second.length());
653        stringBuilder.append((UChar)'\n');
654    }
655
656    String headers = String::adopt(stringBuilder);
657
658    SQLiteStatement resourceStatement(m_database, "INSERT INTO CacheResources (url, statusCode, responseURL, headers, data, mimeType, textEncodingName) VALUES (?, ?, ?, ?, ?, ?, ?)");
659    if (resourceStatement.prepare() != SQLResultOk)
660        return false;
661
662    // The same ApplicationCacheResource are used in ApplicationCacheResource::size()
663    // to calculate the approximate size of an ApplicationCacheResource object. If
664    // you change the code below, please also change ApplicationCacheResource::size().
665    resourceStatement.bindText(1, resource->url());
666    resourceStatement.bindInt64(2, resource->response().httpStatusCode());
667    resourceStatement.bindText(3, resource->response().url());
668    resourceStatement.bindText(4, headers);
669    resourceStatement.bindInt64(5, dataId);
670    resourceStatement.bindText(6, resource->response().mimeType());
671    resourceStatement.bindText(7, resource->response().textEncodingName());
672
673    if (!executeStatement(resourceStatement))
674        return false;
675
676    unsigned resourceId = static_cast<unsigned>(m_database.lastInsertRowID());
677
678    // Finally, insert the cache entry
679    SQLiteStatement entryStatement(m_database, "INSERT INTO CacheEntries (cache, type, resource) VALUES (?, ?, ?)");
680    if (entryStatement.prepare() != SQLResultOk)
681        return false;
682
683    entryStatement.bindInt64(1, cacheStorageID);
684    entryStatement.bindInt64(2, resource->type());
685    entryStatement.bindInt64(3, resourceId);
686
687    if (!executeStatement(entryStatement))
688        return false;
689
690    resource->setStorageID(resourceId);
691    return true;
692}
693
694bool ApplicationCacheStorage::storeUpdatedType(ApplicationCacheResource* resource, ApplicationCache* cache)
695{
696    ASSERT_UNUSED(cache, cache->storageID());
697    ASSERT(resource->storageID());
698
699    // First, insert the data
700    SQLiteStatement entryStatement(m_database, "UPDATE CacheEntries SET type=? WHERE resource=?");
701    if (entryStatement.prepare() != SQLResultOk)
702        return false;
703
704    entryStatement.bindInt64(1, resource->type());
705    entryStatement.bindInt64(2, resource->storageID());
706
707    return executeStatement(entryStatement);
708}
709
710bool ApplicationCacheStorage::store(ApplicationCacheResource* resource, ApplicationCache* cache)
711{
712    ASSERT(cache->storageID());
713
714    openDatabase(true);
715
716    m_isMaximumSizeReached = false;
717    m_database.setMaximumSize(m_maximumSize);
718
719    SQLiteTransaction storeResourceTransaction(m_database);
720    storeResourceTransaction.begin();
721
722    if (!store(resource, cache->storageID())) {
723        checkForMaxSizeReached();
724        return false;
725    }
726
727    // A resource was added to the cache. Update the total data size for the cache.
728    SQLiteStatement sizeUpdateStatement(m_database, "UPDATE Caches SET size=size+? WHERE id=?");
729    if (sizeUpdateStatement.prepare() != SQLResultOk)
730        return false;
731
732    sizeUpdateStatement.bindInt64(1, resource->estimatedSizeInStorage());
733    sizeUpdateStatement.bindInt64(2, cache->storageID());
734
735    if (!executeStatement(sizeUpdateStatement))
736        return false;
737
738    storeResourceTransaction.commit();
739    return true;
740}
741
742bool ApplicationCacheStorage::storeNewestCache(ApplicationCacheGroup* group)
743{
744    openDatabase(true);
745
746    m_isMaximumSizeReached = false;
747    m_database.setMaximumSize(m_maximumSize);
748
749    SQLiteTransaction storeCacheTransaction(m_database);
750
751    storeCacheTransaction.begin();
752
753    GroupStorageIDJournal groupStorageIDJournal;
754    if (!group->storageID()) {
755        // Store the group
756        if (!store(group, &groupStorageIDJournal)) {
757            checkForMaxSizeReached();
758            return false;
759        }
760    }
761
762    ASSERT(group->newestCache());
763    ASSERT(!group->isObsolete());
764    ASSERT(!group->newestCache()->storageID());
765
766    // Log the storageID changes to the in-memory resource objects. The journal
767    // object will roll them back automatically in case a database operation
768    // fails and this method returns early.
769    ResourceStorageIDJournal resourceStorageIDJournal;
770
771    // Store the newest cache
772    if (!store(group->newestCache(), &resourceStorageIDJournal)) {
773        checkForMaxSizeReached();
774        return false;
775    }
776
777    // Update the newest cache in the group.
778
779    SQLiteStatement statement(m_database, "UPDATE CacheGroups SET newestCache=? WHERE id=?");
780    if (statement.prepare() != SQLResultOk)
781        return false;
782
783    statement.bindInt64(1, group->newestCache()->storageID());
784    statement.bindInt64(2, group->storageID());
785
786    if (!executeStatement(statement))
787        return false;
788
789    groupStorageIDJournal.commit();
790    resourceStorageIDJournal.commit();
791    storeCacheTransaction.commit();
792    return true;
793}
794
795static inline void parseHeader(const UChar* header, size_t headerLength, ResourceResponse& response)
796{
797    int pos = find(header, headerLength, ':');
798    ASSERT(pos != -1);
799
800    AtomicString headerName = AtomicString(header, pos);
801    String headerValue = String(header + pos + 1, headerLength - pos - 1);
802
803    response.setHTTPHeaderField(headerName, headerValue);
804}
805
806static inline void parseHeaders(const String& headers, ResourceResponse& response)
807{
808    int startPos = 0;
809    int endPos;
810    while ((endPos = headers.find('\n', startPos)) != -1) {
811        ASSERT(startPos != endPos);
812
813        parseHeader(headers.characters() + startPos, endPos - startPos, response);
814
815        startPos = endPos + 1;
816    }
817
818    if (startPos != static_cast<int>(headers.length()))
819        parseHeader(headers.characters(), headers.length(), response);
820}
821
822PassRefPtr<ApplicationCache> ApplicationCacheStorage::loadCache(unsigned storageID)
823{
824    SQLiteStatement cacheStatement(m_database,
825                                   "SELECT url, type, mimeType, textEncodingName, headers, CacheResourceData.data FROM CacheEntries INNER JOIN CacheResources ON CacheEntries.resource=CacheResources.id "
826                                   "INNER JOIN CacheResourceData ON CacheResourceData.id=CacheResources.data WHERE CacheEntries.cache=?");
827    if (cacheStatement.prepare() != SQLResultOk) {
828        LOG_ERROR("Could not prepare cache statement, error \"%s\"", m_database.lastErrorMsg());
829        return 0;
830    }
831
832    cacheStatement.bindInt64(1, storageID);
833
834    RefPtr<ApplicationCache> cache = ApplicationCache::create();
835
836    int result;
837    while ((result = cacheStatement.step()) == SQLResultRow) {
838        KURL url(ParsedURLString, cacheStatement.getColumnText(0));
839
840        unsigned type = static_cast<unsigned>(cacheStatement.getColumnInt64(1));
841
842        Vector<char> blob;
843        cacheStatement.getColumnBlobAsVector(5, blob);
844
845        RefPtr<SharedBuffer> data = SharedBuffer::adoptVector(blob);
846
847        String mimeType = cacheStatement.getColumnText(2);
848        String textEncodingName = cacheStatement.getColumnText(3);
849
850        ResourceResponse response(url, mimeType, data->size(), textEncodingName, "");
851
852        String headers = cacheStatement.getColumnText(4);
853        parseHeaders(headers, response);
854
855        RefPtr<ApplicationCacheResource> resource = ApplicationCacheResource::create(url, response, type, data.release());
856
857        if (type & ApplicationCacheResource::Manifest)
858            cache->setManifestResource(resource.release());
859        else
860            cache->addResource(resource.release());
861    }
862
863    if (result != SQLResultDone)
864        LOG_ERROR("Could not load cache resources, error \"%s\"", m_database.lastErrorMsg());
865
866    // Load the online whitelist
867    SQLiteStatement whitelistStatement(m_database, "SELECT url FROM CacheWhitelistURLs WHERE cache=?");
868    if (whitelistStatement.prepare() != SQLResultOk)
869        return 0;
870    whitelistStatement.bindInt64(1, storageID);
871
872    Vector<KURL> whitelist;
873    while ((result = whitelistStatement.step()) == SQLResultRow)
874        whitelist.append(KURL(ParsedURLString, whitelistStatement.getColumnText(0)));
875
876    if (result != SQLResultDone)
877        LOG_ERROR("Could not load cache online whitelist, error \"%s\"", m_database.lastErrorMsg());
878
879    cache->setOnlineWhitelist(whitelist);
880
881    // Load online whitelist wildcard flag.
882    SQLiteStatement whitelistWildcardStatement(m_database, "SELECT wildcard FROM CacheAllowsAllNetworkRequests WHERE cache=?");
883    if (whitelistWildcardStatement.prepare() != SQLResultOk)
884        return 0;
885    whitelistWildcardStatement.bindInt64(1, storageID);
886
887    result = whitelistWildcardStatement.step();
888    if (result != SQLResultRow)
889        LOG_ERROR("Could not load cache online whitelist wildcard flag, error \"%s\"", m_database.lastErrorMsg());
890
891    cache->setAllowsAllNetworkRequests(whitelistWildcardStatement.getColumnInt64(0));
892
893    if (whitelistWildcardStatement.step() != SQLResultDone)
894        LOG_ERROR("Too many rows for online whitelist wildcard flag");
895
896    // Load fallback URLs.
897    SQLiteStatement fallbackStatement(m_database, "SELECT namespace, fallbackURL FROM FallbackURLs WHERE cache=?");
898    if (fallbackStatement.prepare() != SQLResultOk)
899        return 0;
900    fallbackStatement.bindInt64(1, storageID);
901
902    FallbackURLVector fallbackURLs;
903    while ((result = fallbackStatement.step()) == SQLResultRow)
904        fallbackURLs.append(make_pair(KURL(ParsedURLString, fallbackStatement.getColumnText(0)), KURL(ParsedURLString, fallbackStatement.getColumnText(1))));
905
906    if (result != SQLResultDone)
907        LOG_ERROR("Could not load fallback URLs, error \"%s\"", m_database.lastErrorMsg());
908
909    cache->setFallbackURLs(fallbackURLs);
910
911    cache->setStorageID(storageID);
912
913    return cache.release();
914}
915
916void ApplicationCacheStorage::remove(ApplicationCache* cache)
917{
918    if (!cache->storageID())
919        return;
920
921    openDatabase(false);
922    if (!m_database.isOpen())
923        return;
924
925    ASSERT(cache->group());
926    ASSERT(cache->group()->storageID());
927
928    // All associated data will be deleted by database triggers.
929    SQLiteStatement statement(m_database, "DELETE FROM Caches WHERE id=?");
930    if (statement.prepare() != SQLResultOk)
931        return;
932
933    statement.bindInt64(1, cache->storageID());
934    executeStatement(statement);
935
936    cache->clearStorageID();
937
938    if (cache->group()->newestCache() == cache) {
939        // Currently, there are no triggers on the cache group, which is why the cache had to be removed separately above.
940        SQLiteStatement groupStatement(m_database, "DELETE FROM CacheGroups WHERE id=?");
941        if (groupStatement.prepare() != SQLResultOk)
942            return;
943
944        groupStatement.bindInt64(1, cache->group()->storageID());
945        executeStatement(groupStatement);
946
947        cache->group()->clearStorageID();
948    }
949}
950
951void ApplicationCacheStorage::empty()
952{
953    openDatabase(false);
954
955    if (!m_database.isOpen())
956        return;
957
958    // Clear cache groups, caches and cache resources.
959    executeSQLCommand("DELETE FROM CacheGroups");
960    executeSQLCommand("DELETE FROM Caches");
961
962    // Clear the storage IDs for the caches in memory.
963    // The caches will still work, but cached resources will not be saved to disk
964    // until a cache update process has been initiated.
965    CacheGroupMap::const_iterator end = m_cachesInMemory.end();
966    for (CacheGroupMap::const_iterator it = m_cachesInMemory.begin(); it != end; ++it)
967        it->second->clearStorageID();
968}
969
970bool ApplicationCacheStorage::storeCopyOfCache(const String& cacheDirectory, ApplicationCacheHost* cacheHost)
971{
972    ApplicationCache* cache = cacheHost->applicationCache();
973    if (!cache)
974        return true;
975
976    // Create a new cache.
977    RefPtr<ApplicationCache> cacheCopy = ApplicationCache::create();
978
979    cacheCopy->setOnlineWhitelist(cache->onlineWhitelist());
980    cacheCopy->setFallbackURLs(cache->fallbackURLs());
981
982    // Traverse the cache and add copies of all resources.
983    ApplicationCache::ResourceMap::const_iterator end = cache->end();
984    for (ApplicationCache::ResourceMap::const_iterator it = cache->begin(); it != end; ++it) {
985        ApplicationCacheResource* resource = it->second.get();
986
987        RefPtr<ApplicationCacheResource> resourceCopy = ApplicationCacheResource::create(resource->url(), resource->response(), resource->type(), resource->data());
988
989        cacheCopy->addResource(resourceCopy.release());
990    }
991
992    // Now create a new cache group.
993    OwnPtr<ApplicationCacheGroup> groupCopy(new ApplicationCacheGroup(cache->group()->manifestURL(), true));
994
995    groupCopy->setNewestCache(cacheCopy);
996
997    ApplicationCacheStorage copyStorage;
998    copyStorage.setCacheDirectory(cacheDirectory);
999
1000    // Empty the cache in case something was there before.
1001    copyStorage.empty();
1002
1003    return copyStorage.storeNewestCache(groupCopy.get());
1004}
1005
1006bool ApplicationCacheStorage::manifestURLs(Vector<KURL>* urls)
1007{
1008    ASSERT(urls);
1009    openDatabase(false);
1010    if (!m_database.isOpen())
1011        return false;
1012
1013    SQLiteStatement selectURLs(m_database, "SELECT manifestURL FROM CacheGroups");
1014
1015    if (selectURLs.prepare() != SQLResultOk)
1016        return false;
1017
1018    while (selectURLs.step() == SQLResultRow)
1019        urls->append(KURL(ParsedURLString, selectURLs.getColumnText(0)));
1020
1021    return true;
1022}
1023
1024bool ApplicationCacheStorage::cacheGroupSize(const String& manifestURL, int64_t* size)
1025{
1026    ASSERT(size);
1027    openDatabase(false);
1028    if (!m_database.isOpen())
1029        return false;
1030
1031    SQLiteStatement statement(m_database, "SELECT sum(Caches.size) FROM Caches INNER JOIN CacheGroups ON Caches.cacheGroup=CacheGroups.id WHERE CacheGroups.manifestURL=?");
1032    if (statement.prepare() != SQLResultOk)
1033        return false;
1034
1035    statement.bindText(1, manifestURL);
1036
1037    int result = statement.step();
1038    if (result == SQLResultDone)
1039        return false;
1040
1041    if (result != SQLResultRow) {
1042        LOG_ERROR("Could not get the size of the cache group, error \"%s\"", m_database.lastErrorMsg());
1043        return false;
1044    }
1045
1046    *size = statement.getColumnInt64(0);
1047    return true;
1048}
1049
1050bool ApplicationCacheStorage::deleteCacheGroup(const String& manifestURL)
1051{
1052    SQLiteTransaction deleteTransaction(m_database);
1053    // Check to see if the group is in memory.
1054    ApplicationCacheGroup* group = m_cachesInMemory.get(manifestURL);
1055    if (group)
1056        cacheGroupMadeObsolete(group);
1057    else {
1058        // The cache group is not in memory, so remove it from the disk.
1059        openDatabase(false);
1060        if (!m_database.isOpen())
1061            return false;
1062
1063        SQLiteStatement idStatement(m_database, "SELECT id FROM CacheGroups WHERE manifestURL=?");
1064        if (idStatement.prepare() != SQLResultOk)
1065            return false;
1066
1067        idStatement.bindText(1, manifestURL);
1068
1069        int result = idStatement.step();
1070        if (result == SQLResultDone)
1071            return false;
1072
1073        if (result != SQLResultRow) {
1074            LOG_ERROR("Could not load cache group id, error \"%s\"", m_database.lastErrorMsg());
1075            return false;
1076        }
1077
1078        int64_t groupId = idStatement.getColumnInt64(0);
1079
1080        SQLiteStatement cacheStatement(m_database, "DELETE FROM Caches WHERE cacheGroup=?");
1081        if (cacheStatement.prepare() != SQLResultOk)
1082            return false;
1083
1084        SQLiteStatement groupStatement(m_database, "DELETE FROM CacheGroups WHERE id=?");
1085        if (groupStatement.prepare() != SQLResultOk)
1086            return false;
1087
1088        cacheStatement.bindInt64(1, groupId);
1089        executeStatement(cacheStatement);
1090        groupStatement.bindInt64(1, groupId);
1091        executeStatement(groupStatement);
1092    }
1093
1094    deleteTransaction.commit();
1095    return true;
1096}
1097
1098void ApplicationCacheStorage::vacuumDatabaseFile()
1099{
1100    openDatabase(false);
1101    if (!m_database.isOpen())
1102        return;
1103
1104    m_database.runVacuumCommand();
1105}
1106
1107void ApplicationCacheStorage::checkForMaxSizeReached()
1108{
1109    if (m_database.lastError() == SQLResultFull)
1110        m_isMaximumSizeReached = true;
1111}
1112
1113ApplicationCacheStorage::ApplicationCacheStorage()
1114    : m_maximumSize(INT_MAX)
1115    , m_isMaximumSizeReached(false)
1116{
1117}
1118
1119ApplicationCacheStorage& cacheStorage()
1120{
1121    DEFINE_STATIC_LOCAL(ApplicationCacheStorage, storage, ());
1122
1123    return storage;
1124}
1125
1126} // namespace WebCore
1127
1128#endif // ENABLE(OFFLINE_WEB_APPLICATIONS)
1129