1/*
2 * Copyright (C) 2008, 2009, 2010, 2011 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#include "ApplicationCache.h"
30#include "ApplicationCacheGroup.h"
31#include "ApplicationCacheHost.h"
32#include "ApplicationCacheResource.h"
33#include "FileSystem.h"
34#include "KURL.h"
35#include "NotImplemented.h"
36#include "SQLiteStatement.h"
37#include "SQLiteTransaction.h"
38#include "SecurityOrigin.h"
39#include "UUID.h"
40#include <wtf/text/CString.h>
41#include <wtf/StdLibExtras.h>
42#include <wtf/StringExtras.h>
43#include <wtf/text/StringBuilder.h>
44
45using namespace std;
46
47namespace WebCore {
48
49static const char flatFileSubdirectory[] = "ApplicationCache";
50
51template <class T>
52class StorageIDJournal {
53public:
54    ~StorageIDJournal()
55    {
56        size_t size = m_records.size();
57        for (size_t i = 0; i < size; ++i)
58            m_records[i].restore();
59    }
60
61    void add(T* resource, unsigned storageID)
62    {
63        m_records.append(Record(resource, storageID));
64    }
65
66    void commit()
67    {
68        m_records.clear();
69    }
70
71private:
72    class Record {
73    public:
74        Record() : m_resource(0), m_storageID(0) { }
75        Record(T* resource, unsigned storageID) : m_resource(resource), m_storageID(storageID) { }
76
77        void restore()
78        {
79            m_resource->setStorageID(m_storageID);
80        }
81
82    private:
83        T* m_resource;
84        unsigned m_storageID;
85    };
86
87    Vector<Record> m_records;
88};
89
90static unsigned urlHostHash(const KURL& url)
91{
92    unsigned hostStart = url.hostStart();
93    unsigned hostEnd = url.hostEnd();
94
95    const String& urlString = url.string();
96
97    if (urlString.is8Bit())
98        return AlreadyHashed::avoidDeletedValue(StringHasher::computeHashAndMaskTop8Bits(urlString.characters8() + hostStart, hostEnd - hostStart));
99
100    return AlreadyHashed::avoidDeletedValue(StringHasher::computeHashAndMaskTop8Bits(urlString.characters16() + hostStart, hostEnd - hostStart));
101}
102
103ApplicationCacheGroup* ApplicationCacheStorage::loadCacheGroup(const KURL& manifestURL)
104{
105    openDatabase(false);
106    if (!m_database.isOpen())
107        return 0;
108
109    SQLiteStatement statement(m_database, "SELECT id, manifestURL, newestCache FROM CacheGroups WHERE newestCache IS NOT NULL AND manifestURL=?");
110    if (statement.prepare() != SQLResultOk)
111        return 0;
112
113    statement.bindText(1, manifestURL);
114
115    int result = statement.step();
116    if (result == SQLResultDone)
117        return 0;
118
119    if (result != SQLResultRow) {
120        LOG_ERROR("Could not load cache group, error \"%s\"", m_database.lastErrorMsg());
121        return 0;
122    }
123
124    unsigned newestCacheStorageID = static_cast<unsigned>(statement.getColumnInt64(2));
125
126    RefPtr<ApplicationCache> cache = loadCache(newestCacheStorageID);
127    if (!cache)
128        return 0;
129
130    ApplicationCacheGroup* group = new ApplicationCacheGroup(manifestURL);
131
132    group->setStorageID(static_cast<unsigned>(statement.getColumnInt64(0)));
133    group->setNewestCache(cache.release());
134
135    return group;
136}
137
138ApplicationCacheGroup* ApplicationCacheStorage::findOrCreateCacheGroup(const KURL& manifestURL)
139{
140    ASSERT(!manifestURL.hasFragmentIdentifier());
141
142    CacheGroupMap::AddResult result = m_cachesInMemory.add(manifestURL, 0);
143
144    if (!result.isNewEntry) {
145        ASSERT(result.iterator->value);
146        return result.iterator->value;
147    }
148
149    // Look up the group in the database
150    ApplicationCacheGroup* group = loadCacheGroup(manifestURL);
151
152    // If the group was not found we need to create it
153    if (!group) {
154        group = new ApplicationCacheGroup(manifestURL);
155        m_cacheHostSet.add(urlHostHash(manifestURL));
156    }
157
158    result.iterator->value = group;
159
160    return group;
161}
162
163ApplicationCacheGroup* ApplicationCacheStorage::findInMemoryCacheGroup(const KURL& manifestURL) const
164{
165    return m_cachesInMemory.get(manifestURL);
166}
167
168void ApplicationCacheStorage::loadManifestHostHashes()
169{
170    static bool hasLoadedHashes = false;
171
172    if (hasLoadedHashes)
173        return;
174
175    // We set this flag to true before the database has been opened
176    // to avoid trying to open the database over and over if it doesn't exist.
177    hasLoadedHashes = true;
178
179    openDatabase(false);
180    if (!m_database.isOpen())
181        return;
182
183    // Fetch the host hashes.
184    SQLiteStatement statement(m_database, "SELECT manifestHostHash FROM CacheGroups");
185    if (statement.prepare() != SQLResultOk)
186        return;
187
188    while (statement.step() == SQLResultRow)
189        m_cacheHostSet.add(static_cast<unsigned>(statement.getColumnInt64(0)));
190}
191
192ApplicationCacheGroup* ApplicationCacheStorage::cacheGroupForURL(const KURL& url)
193{
194    ASSERT(!url.hasFragmentIdentifier());
195
196    loadManifestHostHashes();
197
198    // Hash the host name and see if there's a manifest with the same host.
199    if (!m_cacheHostSet.contains(urlHostHash(url)))
200        return 0;
201
202    // Check if a cache already exists in memory.
203    CacheGroupMap::const_iterator end = m_cachesInMemory.end();
204    for (CacheGroupMap::const_iterator it = m_cachesInMemory.begin(); it != end; ++it) {
205        ApplicationCacheGroup* group = it->value;
206
207        ASSERT(!group->isObsolete());
208
209        if (!protocolHostAndPortAreEqual(url, group->manifestURL()))
210            continue;
211
212        if (ApplicationCache* cache = group->newestCache()) {
213            ApplicationCacheResource* resource = cache->resourceForURL(url);
214            if (!resource)
215                continue;
216            if (resource->type() & ApplicationCacheResource::Foreign)
217                continue;
218            return group;
219        }
220    }
221
222    if (!m_database.isOpen())
223        return 0;
224
225    // Check the database. Look for all cache groups with a newest cache.
226    SQLiteStatement statement(m_database, "SELECT id, manifestURL, newestCache FROM CacheGroups WHERE newestCache IS NOT NULL");
227    if (statement.prepare() != SQLResultOk)
228        return 0;
229
230    int result;
231    while ((result = statement.step()) == SQLResultRow) {
232        KURL manifestURL = KURL(ParsedURLString, statement.getColumnText(1));
233
234        if (m_cachesInMemory.contains(manifestURL))
235            continue;
236
237        if (!protocolHostAndPortAreEqual(url, manifestURL))
238            continue;
239
240        // We found a cache group that matches. Now check if the newest cache has a resource with
241        // a matching URL.
242        unsigned newestCacheID = static_cast<unsigned>(statement.getColumnInt64(2));
243        RefPtr<ApplicationCache> cache = loadCache(newestCacheID);
244        if (!cache)
245            continue;
246
247        ApplicationCacheResource* resource = cache->resourceForURL(url);
248        if (!resource)
249            continue;
250        if (resource->type() & ApplicationCacheResource::Foreign)
251            continue;
252
253        ApplicationCacheGroup* group = new ApplicationCacheGroup(manifestURL);
254
255        group->setStorageID(static_cast<unsigned>(statement.getColumnInt64(0)));
256        group->setNewestCache(cache.release());
257
258        m_cachesInMemory.set(group->manifestURL(), group);
259
260        return group;
261    }
262
263    if (result != SQLResultDone)
264        LOG_ERROR("Could not load cache group, error \"%s\"", m_database.lastErrorMsg());
265
266    return 0;
267}
268
269ApplicationCacheGroup* ApplicationCacheStorage::fallbackCacheGroupForURL(const KURL& url)
270{
271    ASSERT(!url.hasFragmentIdentifier());
272
273    // Check if an appropriate cache already exists in memory.
274    CacheGroupMap::const_iterator end = m_cachesInMemory.end();
275    for (CacheGroupMap::const_iterator it = m_cachesInMemory.begin(); it != end; ++it) {
276        ApplicationCacheGroup* group = it->value;
277
278        ASSERT(!group->isObsolete());
279
280        if (ApplicationCache* cache = group->newestCache()) {
281            KURL fallbackURL;
282            if (cache->isURLInOnlineWhitelist(url))
283                continue;
284            if (!cache->urlMatchesFallbackNamespace(url, &fallbackURL))
285                continue;
286            if (cache->resourceForURL(fallbackURL)->type() & ApplicationCacheResource::Foreign)
287                continue;
288            return group;
289        }
290    }
291
292    if (!m_database.isOpen())
293        return 0;
294
295    // Check the database. Look for all cache groups with a newest cache.
296    SQLiteStatement statement(m_database, "SELECT id, manifestURL, newestCache FROM CacheGroups WHERE newestCache IS NOT NULL");
297    if (statement.prepare() != SQLResultOk)
298        return 0;
299
300    int result;
301    while ((result = statement.step()) == SQLResultRow) {
302        KURL manifestURL = KURL(ParsedURLString, statement.getColumnText(1));
303
304        if (m_cachesInMemory.contains(manifestURL))
305            continue;
306
307        // Fallback namespaces always have the same origin as manifest URL, so we can avoid loading caches that cannot match.
308        if (!protocolHostAndPortAreEqual(url, manifestURL))
309            continue;
310
311        // We found a cache group that matches. Now check if the newest cache has a resource with
312        // a matching fallback namespace.
313        unsigned newestCacheID = static_cast<unsigned>(statement.getColumnInt64(2));
314        RefPtr<ApplicationCache> cache = loadCache(newestCacheID);
315
316        KURL fallbackURL;
317        if (cache->isURLInOnlineWhitelist(url))
318            continue;
319        if (!cache->urlMatchesFallbackNamespace(url, &fallbackURL))
320            continue;
321        if (cache->resourceForURL(fallbackURL)->type() & ApplicationCacheResource::Foreign)
322            continue;
323
324        ApplicationCacheGroup* group = new ApplicationCacheGroup(manifestURL);
325
326        group->setStorageID(static_cast<unsigned>(statement.getColumnInt64(0)));
327        group->setNewestCache(cache.release());
328
329        m_cachesInMemory.set(group->manifestURL(), group);
330
331        return group;
332    }
333
334    if (result != SQLResultDone)
335        LOG_ERROR("Could not load cache group, error \"%s\"", m_database.lastErrorMsg());
336
337    return 0;
338}
339
340void ApplicationCacheStorage::cacheGroupDestroyed(ApplicationCacheGroup* group)
341{
342    if (group->isObsolete()) {
343        ASSERT(!group->storageID());
344        ASSERT(m_cachesInMemory.get(group->manifestURL()) != group);
345        return;
346    }
347
348    ASSERT(m_cachesInMemory.get(group->manifestURL()) == group);
349
350    m_cachesInMemory.remove(group->manifestURL());
351
352    // If the cache group is half-created, we don't want it in the saved set (as it is not stored in database).
353    if (!group->storageID())
354        m_cacheHostSet.remove(urlHostHash(group->manifestURL()));
355}
356
357void ApplicationCacheStorage::cacheGroupMadeObsolete(ApplicationCacheGroup* group)
358{
359    ASSERT(m_cachesInMemory.get(group->manifestURL()) == group);
360    ASSERT(m_cacheHostSet.contains(urlHostHash(group->manifestURL())));
361
362    if (ApplicationCache* newestCache = group->newestCache())
363        remove(newestCache);
364
365    m_cachesInMemory.remove(group->manifestURL());
366    m_cacheHostSet.remove(urlHostHash(group->manifestURL()));
367}
368
369void ApplicationCacheStorage::setCacheDirectory(const String& cacheDirectory)
370{
371    ASSERT(m_cacheDirectory.isNull());
372    ASSERT(!cacheDirectory.isNull());
373
374    m_cacheDirectory = cacheDirectory;
375}
376
377const String& ApplicationCacheStorage::cacheDirectory() const
378{
379    return m_cacheDirectory;
380}
381
382void ApplicationCacheStorage::setMaximumSize(int64_t size)
383{
384    m_maximumSize = size;
385}
386
387int64_t ApplicationCacheStorage::maximumSize() const
388{
389    return m_maximumSize;
390}
391
392bool ApplicationCacheStorage::isMaximumSizeReached() const
393{
394    return m_isMaximumSizeReached;
395}
396
397int64_t ApplicationCacheStorage::spaceNeeded(int64_t cacheToSave)
398{
399    int64_t spaceNeeded = 0;
400    long long fileSize = 0;
401    if (!getFileSize(m_cacheFile, fileSize))
402        return 0;
403
404    int64_t currentSize = fileSize + flatFileAreaSize();
405
406    // Determine the amount of free space we have available.
407    int64_t totalAvailableSize = 0;
408    if (m_maximumSize < currentSize) {
409        // The max size is smaller than the actual size of the app cache file.
410        // This can happen if the client previously imposed a larger max size
411        // value and the app cache file has already grown beyond the current
412        // max size value.
413        // The amount of free space is just the amount of free space inside
414        // the database file. Note that this is always 0 if SQLite is compiled
415        // with AUTO_VACUUM = 1.
416        totalAvailableSize = m_database.freeSpaceSize();
417    } else {
418        // The max size is the same or larger than the current size.
419        // The amount of free space available is the amount of free space
420        // inside the database file plus the amount we can grow until we hit
421        // the max size.
422        totalAvailableSize = (m_maximumSize - currentSize) + m_database.freeSpaceSize();
423    }
424
425    // The space needed to be freed in order to accommodate the failed cache is
426    // the size of the failed cache minus any already available free space.
427    spaceNeeded = cacheToSave - totalAvailableSize;
428    // The space needed value must be positive (or else the total already
429    // available free space would be larger than the size of the failed cache and
430    // saving of the cache should have never failed).
431    ASSERT(spaceNeeded);
432    return spaceNeeded;
433}
434
435void ApplicationCacheStorage::setDefaultOriginQuota(int64_t quota)
436{
437    m_defaultOriginQuota = quota;
438}
439
440bool ApplicationCacheStorage::calculateQuotaForOrigin(const SecurityOrigin* origin, int64_t& quota)
441{
442    // If an Origin record doesn't exist, then the COUNT will be 0 and quota will be 0.
443    // Using the count to determine if a record existed or not is a safe way to determine
444    // if a quota of 0 is real, from the record, or from null.
445    SQLiteStatement statement(m_database, "SELECT COUNT(quota), quota FROM Origins WHERE origin=?");
446    if (statement.prepare() != SQLResultOk)
447        return false;
448
449    statement.bindText(1, origin->databaseIdentifier());
450    int result = statement.step();
451
452    // Return the quota, or if it was null the default.
453    if (result == SQLResultRow) {
454        bool wasNoRecord = statement.getColumnInt64(0) == 0;
455        quota = wasNoRecord ? m_defaultOriginQuota : statement.getColumnInt64(1);
456        return true;
457    }
458
459    LOG_ERROR("Could not get the quota of an origin, error \"%s\"", m_database.lastErrorMsg());
460    return false;
461}
462
463bool ApplicationCacheStorage::calculateUsageForOrigin(const SecurityOrigin* origin, int64_t& usage)
464{
465    // If an Origins record doesn't exist, then the SUM will be null,
466    // which will become 0, as expected, when converting to a number.
467    SQLiteStatement statement(m_database, "SELECT SUM(Caches.size)"
468                                          "  FROM CacheGroups"
469                                          " INNER JOIN Origins ON CacheGroups.origin = Origins.origin"
470                                          " INNER JOIN Caches ON CacheGroups.id = Caches.cacheGroup"
471                                          " WHERE Origins.origin=?");
472    if (statement.prepare() != SQLResultOk)
473        return false;
474
475    statement.bindText(1, origin->databaseIdentifier());
476    int result = statement.step();
477
478    if (result == SQLResultRow) {
479        usage = statement.getColumnInt64(0);
480        return true;
481    }
482
483    LOG_ERROR("Could not get the quota of an origin, error \"%s\"", m_database.lastErrorMsg());
484    return false;
485}
486
487bool ApplicationCacheStorage::calculateRemainingSizeForOriginExcludingCache(const SecurityOrigin* origin, ApplicationCache* cache, int64_t& remainingSize)
488{
489    openDatabase(false);
490    if (!m_database.isOpen())
491        return false;
492
493    // Remaining size = total origin quota - size of all caches with origin excluding the provided cache.
494    // Keep track of the number of caches so we can tell if the result was a calculation or not.
495    const char* query;
496    int64_t excludingCacheIdentifier = cache ? cache->storageID() : 0;
497    if (excludingCacheIdentifier != 0) {
498        query = "SELECT COUNT(Caches.size), Origins.quota - SUM(Caches.size)"
499                "  FROM CacheGroups"
500                " INNER JOIN Origins ON CacheGroups.origin = Origins.origin"
501                " INNER JOIN Caches ON CacheGroups.id = Caches.cacheGroup"
502                " WHERE Origins.origin=?"
503                "   AND Caches.id!=?";
504    } else {
505        query = "SELECT COUNT(Caches.size), Origins.quota - SUM(Caches.size)"
506                "  FROM CacheGroups"
507                " INNER JOIN Origins ON CacheGroups.origin = Origins.origin"
508                " INNER JOIN Caches ON CacheGroups.id = Caches.cacheGroup"
509                " WHERE Origins.origin=?";
510    }
511
512    SQLiteStatement statement(m_database, query);
513    if (statement.prepare() != SQLResultOk)
514        return false;
515
516    statement.bindText(1, origin->databaseIdentifier());
517    if (excludingCacheIdentifier != 0)
518        statement.bindInt64(2, excludingCacheIdentifier);
519    int result = statement.step();
520
521    // If the count was 0 that then we have to query the origin table directly
522    // for its quota. Otherwise we can use the calculated value.
523    if (result == SQLResultRow) {
524        int64_t numberOfCaches = statement.getColumnInt64(0);
525        if (numberOfCaches == 0)
526            calculateQuotaForOrigin(origin, remainingSize);
527        else
528            remainingSize = statement.getColumnInt64(1);
529        return true;
530    }
531
532    LOG_ERROR("Could not get the remaining size of an origin's quota, error \"%s\"", m_database.lastErrorMsg());
533    return false;
534}
535
536bool ApplicationCacheStorage::storeUpdatedQuotaForOrigin(const SecurityOrigin* origin, int64_t quota)
537{
538    openDatabase(true);
539    if (!m_database.isOpen())
540        return false;
541
542    if (!ensureOriginRecord(origin))
543        return false;
544
545    SQLiteStatement updateStatement(m_database, "UPDATE Origins SET quota=? WHERE origin=?");
546    if (updateStatement.prepare() != SQLResultOk)
547        return false;
548
549    updateStatement.bindInt64(1, quota);
550    updateStatement.bindText(2, origin->databaseIdentifier());
551
552    return executeStatement(updateStatement);
553}
554
555bool ApplicationCacheStorage::executeSQLCommand(const String& sql)
556{
557    ASSERT(m_database.isOpen());
558
559    bool result = m_database.executeCommand(sql);
560    if (!result)
561        LOG_ERROR("Application Cache Storage: failed to execute statement \"%s\" error \"%s\"",
562                  sql.utf8().data(), m_database.lastErrorMsg());
563
564    return result;
565}
566
567// Update the schemaVersion when the schema of any the Application Cache
568// SQLite tables changes. This allows the database to be rebuilt when
569// a new, incompatible change has been introduced to the database schema.
570static const int schemaVersion = 7;
571
572void ApplicationCacheStorage::verifySchemaVersion()
573{
574    int version = SQLiteStatement(m_database, "PRAGMA user_version").getColumnInt(0);
575    if (version == schemaVersion)
576        return;
577
578    deleteTables();
579
580    // Update user version.
581    SQLiteTransaction setDatabaseVersion(m_database);
582    setDatabaseVersion.begin();
583
584    char userVersionSQL[32];
585    int unusedNumBytes = snprintf(userVersionSQL, sizeof(userVersionSQL), "PRAGMA user_version=%d", schemaVersion);
586    ASSERT_UNUSED(unusedNumBytes, static_cast<int>(sizeof(userVersionSQL)) >= unusedNumBytes);
587
588    SQLiteStatement statement(m_database, userVersionSQL);
589    if (statement.prepare() != SQLResultOk)
590        return;
591
592    executeStatement(statement);
593    setDatabaseVersion.commit();
594}
595
596void ApplicationCacheStorage::openDatabase(bool createIfDoesNotExist)
597{
598    if (m_database.isOpen())
599        return;
600
601    // The cache directory should never be null, but if it for some weird reason is we bail out.
602    if (m_cacheDirectory.isNull())
603        return;
604
605    m_cacheFile = pathByAppendingComponent(m_cacheDirectory, "ApplicationCache.db");
606    if (!createIfDoesNotExist && !fileExists(m_cacheFile))
607        return;
608
609    makeAllDirectories(m_cacheDirectory);
610    m_database.open(m_cacheFile);
611
612    if (!m_database.isOpen())
613        return;
614
615    verifySchemaVersion();
616
617    // Create tables
618    executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheGroups (id INTEGER PRIMARY KEY AUTOINCREMENT, "
619                      "manifestHostHash INTEGER NOT NULL ON CONFLICT FAIL, manifestURL TEXT UNIQUE ON CONFLICT FAIL, newestCache INTEGER, origin TEXT)");
620    executeSQLCommand("CREATE TABLE IF NOT EXISTS Caches (id INTEGER PRIMARY KEY AUTOINCREMENT, cacheGroup INTEGER, size INTEGER)");
621    executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheWhitelistURLs (url TEXT NOT NULL ON CONFLICT FAIL, cache INTEGER NOT NULL ON CONFLICT FAIL)");
622    executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheAllowsAllNetworkRequests (wildcard INTEGER NOT NULL ON CONFLICT FAIL, cache INTEGER NOT NULL ON CONFLICT FAIL)");
623    executeSQLCommand("CREATE TABLE IF NOT EXISTS FallbackURLs (namespace TEXT NOT NULL ON CONFLICT FAIL, fallbackURL TEXT NOT NULL ON CONFLICT FAIL, "
624                      "cache INTEGER NOT NULL ON CONFLICT FAIL)");
625    executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheEntries (cache INTEGER NOT NULL ON CONFLICT FAIL, type INTEGER, resource INTEGER NOT NULL)");
626    executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheResources (id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL ON CONFLICT FAIL, "
627                      "statusCode INTEGER NOT NULL, responseURL TEXT NOT NULL, mimeType TEXT, textEncodingName TEXT, headers TEXT, data INTEGER NOT NULL ON CONFLICT FAIL)");
628    executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheResourceData (id INTEGER PRIMARY KEY AUTOINCREMENT, data BLOB, path TEXT)");
629    executeSQLCommand("CREATE TABLE IF NOT EXISTS DeletedCacheResources (id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT)");
630    executeSQLCommand("CREATE TABLE IF NOT EXISTS Origins (origin TEXT UNIQUE ON CONFLICT IGNORE, quota INTEGER NOT NULL ON CONFLICT FAIL)");
631
632    // When a cache is deleted, all its entries and its whitelist should be deleted.
633    executeSQLCommand("CREATE TRIGGER IF NOT EXISTS CacheDeleted AFTER DELETE ON Caches"
634                      " FOR EACH ROW BEGIN"
635                      "  DELETE FROM CacheEntries WHERE cache = OLD.id;"
636                      "  DELETE FROM CacheWhitelistURLs WHERE cache = OLD.id;"
637                      "  DELETE FROM CacheAllowsAllNetworkRequests WHERE cache = OLD.id;"
638                      "  DELETE FROM FallbackURLs WHERE cache = OLD.id;"
639                      " END");
640
641    // When a cache entry is deleted, its resource should also be deleted.
642    executeSQLCommand("CREATE TRIGGER IF NOT EXISTS CacheEntryDeleted AFTER DELETE ON CacheEntries"
643                      " FOR EACH ROW BEGIN"
644                      "  DELETE FROM CacheResources WHERE id = OLD.resource;"
645                      " END");
646
647    // When a cache resource is deleted, its data blob should also be deleted.
648    executeSQLCommand("CREATE TRIGGER IF NOT EXISTS CacheResourceDeleted AFTER DELETE ON CacheResources"
649                      " FOR EACH ROW BEGIN"
650                      "  DELETE FROM CacheResourceData WHERE id = OLD.data;"
651                      " END");
652
653    // When a cache resource is deleted, if it contains a non-empty path, that path should
654    // be added to the DeletedCacheResources table so the flat file at that path can
655    // be deleted at a later time.
656    executeSQLCommand("CREATE TRIGGER IF NOT EXISTS CacheResourceDataDeleted AFTER DELETE ON CacheResourceData"
657                      " FOR EACH ROW"
658                      " WHEN OLD.path NOT NULL BEGIN"
659                      "  INSERT INTO DeletedCacheResources (path) values (OLD.path);"
660                      " END");
661}
662
663bool ApplicationCacheStorage::executeStatement(SQLiteStatement& statement)
664{
665    bool result = statement.executeCommand();
666    if (!result)
667        LOG_ERROR("Application Cache Storage: failed to execute statement \"%s\" error \"%s\"",
668                  statement.query().utf8().data(), m_database.lastErrorMsg());
669
670    return result;
671}
672
673bool ApplicationCacheStorage::store(ApplicationCacheGroup* group, GroupStorageIDJournal* journal)
674{
675    ASSERT(group->storageID() == 0);
676    ASSERT(journal);
677
678    // For some reason, an app cache may be partially written to disk. In particular, there may be
679    // a cache group with an identical manifest URL and associated cache entries. We want to remove
680    // this cache group and its associated cache entries so that we can create it again (below) as
681    // a way to repair it.
682    deleteCacheGroupRecord(group->manifestURL());
683
684    SQLiteStatement statement(m_database, "INSERT INTO CacheGroups (manifestHostHash, manifestURL, origin) VALUES (?, ?, ?)");
685    if (statement.prepare() != SQLResultOk)
686        return false;
687
688    statement.bindInt64(1, urlHostHash(group->manifestURL()));
689    statement.bindText(2, group->manifestURL());
690    statement.bindText(3, group->origin()->databaseIdentifier());
691
692    if (!executeStatement(statement))
693        return false;
694
695    unsigned groupStorageID = static_cast<unsigned>(m_database.lastInsertRowID());
696
697    if (!ensureOriginRecord(group->origin()))
698        return false;
699
700    group->setStorageID(groupStorageID);
701    journal->add(group, 0);
702    return true;
703}
704
705bool ApplicationCacheStorage::store(ApplicationCache* cache, ResourceStorageIDJournal* storageIDJournal)
706{
707    ASSERT(cache->storageID() == 0);
708    ASSERT(cache->group()->storageID() != 0);
709    ASSERT(storageIDJournal);
710
711    SQLiteStatement statement(m_database, "INSERT INTO Caches (cacheGroup, size) VALUES (?, ?)");
712    if (statement.prepare() != SQLResultOk)
713        return false;
714
715    statement.bindInt64(1, cache->group()->storageID());
716    statement.bindInt64(2, cache->estimatedSizeInStorage());
717
718    if (!executeStatement(statement))
719        return false;
720
721    unsigned cacheStorageID = static_cast<unsigned>(m_database.lastInsertRowID());
722
723    // Store all resources
724    {
725        ApplicationCache::ResourceMap::const_iterator end = cache->end();
726        for (ApplicationCache::ResourceMap::const_iterator it = cache->begin(); it != end; ++it) {
727            unsigned oldStorageID = it->value->storageID();
728            if (!store(it->value.get(), cacheStorageID))
729                return false;
730
731            // Storing the resource succeeded. Log its old storageID in case
732            // it needs to be restored later.
733            storageIDJournal->add(it->value.get(), oldStorageID);
734        }
735    }
736
737    // Store the online whitelist
738    const Vector<KURL>& onlineWhitelist = cache->onlineWhitelist();
739    {
740        size_t whitelistSize = onlineWhitelist.size();
741        for (size_t i = 0; i < whitelistSize; ++i) {
742            SQLiteStatement statement(m_database, "INSERT INTO CacheWhitelistURLs (url, cache) VALUES (?, ?)");
743            statement.prepare();
744
745            statement.bindText(1, onlineWhitelist[i]);
746            statement.bindInt64(2, cacheStorageID);
747
748            if (!executeStatement(statement))
749                return false;
750        }
751    }
752
753    // Store online whitelist wildcard flag.
754    {
755        SQLiteStatement statement(m_database, "INSERT INTO CacheAllowsAllNetworkRequests (wildcard, cache) VALUES (?, ?)");
756        statement.prepare();
757
758        statement.bindInt64(1, cache->allowsAllNetworkRequests());
759        statement.bindInt64(2, cacheStorageID);
760
761        if (!executeStatement(statement))
762            return false;
763    }
764
765    // Store fallback URLs.
766    const FallbackURLVector& fallbackURLs = cache->fallbackURLs();
767    {
768        size_t fallbackCount = fallbackURLs.size();
769        for (size_t i = 0; i < fallbackCount; ++i) {
770            SQLiteStatement statement(m_database, "INSERT INTO FallbackURLs (namespace, fallbackURL, cache) VALUES (?, ?, ?)");
771            statement.prepare();
772
773            statement.bindText(1, fallbackURLs[i].first);
774            statement.bindText(2, fallbackURLs[i].second);
775            statement.bindInt64(3, cacheStorageID);
776
777            if (!executeStatement(statement))
778                return false;
779        }
780    }
781
782    cache->setStorageID(cacheStorageID);
783    return true;
784}
785
786bool ApplicationCacheStorage::store(ApplicationCacheResource* resource, unsigned cacheStorageID)
787{
788    ASSERT(cacheStorageID);
789    ASSERT(!resource->storageID());
790
791    openDatabase(true);
792
793    // openDatabase(true) could still fail, for example when cacheStorage is full or no longer available.
794    if (!m_database.isOpen())
795        return false;
796
797    // First, insert the data
798    SQLiteStatement dataStatement(m_database, "INSERT INTO CacheResourceData (data, path) VALUES (?, ?)");
799    if (dataStatement.prepare() != SQLResultOk)
800        return false;
801
802
803    String fullPath;
804    if (!resource->path().isEmpty())
805        dataStatement.bindText(2, pathGetFileName(resource->path()));
806    else if (shouldStoreResourceAsFlatFile(resource)) {
807        // First, check to see if creating the flat file would violate the maximum total quota. We don't need
808        // to check the per-origin quota here, as it was already checked in storeNewestCache().
809        if (m_database.totalSize() + flatFileAreaSize() + resource->data()->size() > m_maximumSize) {
810            m_isMaximumSizeReached = true;
811            return false;
812        }
813
814        String flatFileDirectory = pathByAppendingComponent(m_cacheDirectory, flatFileSubdirectory);
815        makeAllDirectories(flatFileDirectory);
816
817        String extension;
818
819        String fileName = resource->response().suggestedFilename();
820        size_t dotIndex = fileName.reverseFind('.');
821        if (dotIndex != notFound && dotIndex < (fileName.length() - 1))
822            extension = fileName.substring(dotIndex);
823
824        String path;
825        if (!writeDataToUniqueFileInDirectory(resource->data(), flatFileDirectory, path, extension))
826            return false;
827
828        fullPath = pathByAppendingComponent(flatFileDirectory, path);
829        resource->setPath(fullPath);
830        dataStatement.bindText(2, path);
831    } else {
832        if (resource->data()->size())
833            dataStatement.bindBlob(1, resource->data()->data(), resource->data()->size());
834    }
835
836    if (!dataStatement.executeCommand()) {
837        // Clean up the file which we may have written to:
838        if (!fullPath.isEmpty())
839            deleteFile(fullPath);
840
841        return false;
842    }
843
844    unsigned dataId = static_cast<unsigned>(m_database.lastInsertRowID());
845
846    // Then, insert the resource
847
848    // Serialize the headers
849    StringBuilder stringBuilder;
850
851    HTTPHeaderMap::const_iterator end = resource->response().httpHeaderFields().end();
852    for (HTTPHeaderMap::const_iterator it = resource->response().httpHeaderFields().begin(); it!= end; ++it) {
853        stringBuilder.append(it->key);
854        stringBuilder.append(':');
855        stringBuilder.append(it->value);
856        stringBuilder.append('\n');
857    }
858
859    String headers = stringBuilder.toString();
860
861    SQLiteStatement resourceStatement(m_database, "INSERT INTO CacheResources (url, statusCode, responseURL, headers, data, mimeType, textEncodingName) VALUES (?, ?, ?, ?, ?, ?, ?)");
862    if (resourceStatement.prepare() != SQLResultOk)
863        return false;
864
865    // The same ApplicationCacheResource are used in ApplicationCacheResource::size()
866    // to calculate the approximate size of an ApplicationCacheResource object. If
867    // you change the code below, please also change ApplicationCacheResource::size().
868    resourceStatement.bindText(1, resource->url());
869    resourceStatement.bindInt64(2, resource->response().httpStatusCode());
870    resourceStatement.bindText(3, resource->response().url());
871    resourceStatement.bindText(4, headers);
872    resourceStatement.bindInt64(5, dataId);
873    resourceStatement.bindText(6, resource->response().mimeType());
874    resourceStatement.bindText(7, resource->response().textEncodingName());
875
876    if (!executeStatement(resourceStatement))
877        return false;
878
879    unsigned resourceId = static_cast<unsigned>(m_database.lastInsertRowID());
880
881    // Finally, insert the cache entry
882    SQLiteStatement entryStatement(m_database, "INSERT INTO CacheEntries (cache, type, resource) VALUES (?, ?, ?)");
883    if (entryStatement.prepare() != SQLResultOk)
884        return false;
885
886    entryStatement.bindInt64(1, cacheStorageID);
887    entryStatement.bindInt64(2, resource->type());
888    entryStatement.bindInt64(3, resourceId);
889
890    if (!executeStatement(entryStatement))
891        return false;
892
893    // Did we successfully write the resource data to a file? If so,
894    // release the resource's data and free up a potentially large amount
895    // of memory:
896    if (!fullPath.isEmpty())
897        resource->data()->clear();
898
899    resource->setStorageID(resourceId);
900    return true;
901}
902
903bool ApplicationCacheStorage::storeUpdatedType(ApplicationCacheResource* resource, ApplicationCache* cache)
904{
905    ASSERT_UNUSED(cache, cache->storageID());
906    ASSERT(resource->storageID());
907
908    // First, insert the data
909    SQLiteStatement entryStatement(m_database, "UPDATE CacheEntries SET type=? WHERE resource=?");
910    if (entryStatement.prepare() != SQLResultOk)
911        return false;
912
913    entryStatement.bindInt64(1, resource->type());
914    entryStatement.bindInt64(2, resource->storageID());
915
916    return executeStatement(entryStatement);
917}
918
919bool ApplicationCacheStorage::store(ApplicationCacheResource* resource, ApplicationCache* cache)
920{
921    ASSERT(cache->storageID());
922
923    openDatabase(true);
924
925    if (!m_database.isOpen())
926        return false;
927
928    m_isMaximumSizeReached = false;
929    m_database.setMaximumSize(m_maximumSize - flatFileAreaSize());
930
931    SQLiteTransaction storeResourceTransaction(m_database);
932    storeResourceTransaction.begin();
933
934    if (!store(resource, cache->storageID())) {
935        checkForMaxSizeReached();
936        return false;
937    }
938
939    // A resource was added to the cache. Update the total data size for the cache.
940    SQLiteStatement sizeUpdateStatement(m_database, "UPDATE Caches SET size=size+? WHERE id=?");
941    if (sizeUpdateStatement.prepare() != SQLResultOk)
942        return false;
943
944    sizeUpdateStatement.bindInt64(1, resource->estimatedSizeInStorage());
945    sizeUpdateStatement.bindInt64(2, cache->storageID());
946
947    if (!executeStatement(sizeUpdateStatement))
948        return false;
949
950    storeResourceTransaction.commit();
951    return true;
952}
953
954bool ApplicationCacheStorage::ensureOriginRecord(const SecurityOrigin* origin)
955{
956    SQLiteStatement insertOriginStatement(m_database, "INSERT INTO Origins (origin, quota) VALUES (?, ?)");
957    if (insertOriginStatement.prepare() != SQLResultOk)
958        return false;
959
960    insertOriginStatement.bindText(1, origin->databaseIdentifier());
961    insertOriginStatement.bindInt64(2, m_defaultOriginQuota);
962    if (!executeStatement(insertOriginStatement))
963        return false;
964
965    return true;
966}
967
968bool ApplicationCacheStorage::checkOriginQuota(ApplicationCacheGroup* group, ApplicationCache* oldCache, ApplicationCache* newCache, int64_t& totalSpaceNeeded)
969{
970    // Check if the oldCache with the newCache would reach the per-origin quota.
971    int64_t remainingSpaceInOrigin;
972    const SecurityOrigin* origin = group->origin();
973    if (calculateRemainingSizeForOriginExcludingCache(origin, oldCache, remainingSpaceInOrigin)) {
974        if (remainingSpaceInOrigin < newCache->estimatedSizeInStorage()) {
975            int64_t quota;
976            if (calculateQuotaForOrigin(origin, quota)) {
977                totalSpaceNeeded = quota - remainingSpaceInOrigin + newCache->estimatedSizeInStorage();
978                return false;
979            }
980
981            ASSERT_NOT_REACHED();
982            totalSpaceNeeded = 0;
983            return false;
984        }
985    }
986
987    return true;
988}
989
990bool ApplicationCacheStorage::storeNewestCache(ApplicationCacheGroup* group, ApplicationCache* oldCache, FailureReason& failureReason)
991{
992    openDatabase(true);
993
994    if (!m_database.isOpen())
995        return false;
996
997    m_isMaximumSizeReached = false;
998    m_database.setMaximumSize(m_maximumSize - flatFileAreaSize());
999
1000    SQLiteTransaction storeCacheTransaction(m_database);
1001
1002    storeCacheTransaction.begin();
1003
1004    // Check if this would reach the per-origin quota.
1005    int64_t totalSpaceNeededIgnored;
1006    if (!checkOriginQuota(group, oldCache, group->newestCache(), totalSpaceNeededIgnored)) {
1007        failureReason = OriginQuotaReached;
1008        return false;
1009    }
1010
1011    GroupStorageIDJournal groupStorageIDJournal;
1012    if (!group->storageID()) {
1013        // Store the group
1014        if (!store(group, &groupStorageIDJournal)) {
1015            checkForMaxSizeReached();
1016            failureReason = isMaximumSizeReached() ? TotalQuotaReached : DiskOrOperationFailure;
1017            return false;
1018        }
1019    }
1020
1021    ASSERT(group->newestCache());
1022    ASSERT(!group->isObsolete());
1023    ASSERT(!group->newestCache()->storageID());
1024
1025    // Log the storageID changes to the in-memory resource objects. The journal
1026    // object will roll them back automatically in case a database operation
1027    // fails and this method returns early.
1028    ResourceStorageIDJournal resourceStorageIDJournal;
1029
1030    // Store the newest cache
1031    if (!store(group->newestCache(), &resourceStorageIDJournal)) {
1032        checkForMaxSizeReached();
1033        failureReason = isMaximumSizeReached() ? TotalQuotaReached : DiskOrOperationFailure;
1034        return false;
1035    }
1036
1037    // Update the newest cache in the group.
1038
1039    SQLiteStatement statement(m_database, "UPDATE CacheGroups SET newestCache=? WHERE id=?");
1040    if (statement.prepare() != SQLResultOk) {
1041        failureReason = DiskOrOperationFailure;
1042        return false;
1043    }
1044
1045    statement.bindInt64(1, group->newestCache()->storageID());
1046    statement.bindInt64(2, group->storageID());
1047
1048    if (!executeStatement(statement)) {
1049        failureReason = DiskOrOperationFailure;
1050        return false;
1051    }
1052
1053    groupStorageIDJournal.commit();
1054    resourceStorageIDJournal.commit();
1055    storeCacheTransaction.commit();
1056    return true;
1057}
1058
1059bool ApplicationCacheStorage::storeNewestCache(ApplicationCacheGroup* group)
1060{
1061    // Ignore the reason for failing, just attempt the store.
1062    FailureReason ignoredFailureReason;
1063    return storeNewestCache(group, 0, ignoredFailureReason);
1064}
1065
1066template <typename CharacterType>
1067static inline void parseHeader(const CharacterType* header, size_t headerLength, ResourceResponse& response)
1068{
1069    size_t pos = find(header, headerLength, ':');
1070    ASSERT(pos != notFound);
1071
1072    AtomicString headerName = AtomicString(header, pos);
1073    String headerValue = String(header + pos + 1, headerLength - pos - 1);
1074
1075    response.setHTTPHeaderField(headerName, headerValue);
1076}
1077
1078static inline void parseHeaders(const String& headers, ResourceResponse& response)
1079{
1080    unsigned startPos = 0;
1081    size_t endPos;
1082    while ((endPos = headers.find('\n', startPos)) != notFound) {
1083        ASSERT(startPos != endPos);
1084
1085        if (headers.is8Bit())
1086            parseHeader(headers.characters8() + startPos, endPos - startPos, response);
1087        else
1088            parseHeader(headers.characters16() + startPos, endPos - startPos, response);
1089
1090        startPos = endPos + 1;
1091    }
1092
1093    if (startPos != headers.length()) {
1094        if (headers.is8Bit())
1095            parseHeader(headers.characters8(), headers.length(), response);
1096        else
1097            parseHeader(headers.characters16(), headers.length(), response);
1098    }
1099}
1100
1101PassRefPtr<ApplicationCache> ApplicationCacheStorage::loadCache(unsigned storageID)
1102{
1103    SQLiteStatement cacheStatement(m_database,
1104                                   "SELECT url, statusCode, type, mimeType, textEncodingName, headers, CacheResourceData.data, CacheResourceData.path FROM CacheEntries INNER JOIN CacheResources ON CacheEntries.resource=CacheResources.id "
1105                                   "INNER JOIN CacheResourceData ON CacheResourceData.id=CacheResources.data WHERE CacheEntries.cache=?");
1106    if (cacheStatement.prepare() != SQLResultOk) {
1107        LOG_ERROR("Could not prepare cache statement, error \"%s\"", m_database.lastErrorMsg());
1108        return 0;
1109    }
1110
1111    cacheStatement.bindInt64(1, storageID);
1112
1113    RefPtr<ApplicationCache> cache = ApplicationCache::create();
1114
1115    String flatFileDirectory = pathByAppendingComponent(m_cacheDirectory, flatFileSubdirectory);
1116
1117    int result;
1118    while ((result = cacheStatement.step()) == SQLResultRow) {
1119        KURL url(ParsedURLString, cacheStatement.getColumnText(0));
1120
1121        int httpStatusCode = cacheStatement.getColumnInt(1);
1122
1123        unsigned type = static_cast<unsigned>(cacheStatement.getColumnInt64(2));
1124
1125        Vector<char> blob;
1126        cacheStatement.getColumnBlobAsVector(6, blob);
1127
1128        RefPtr<SharedBuffer> data = SharedBuffer::adoptVector(blob);
1129
1130        String path = cacheStatement.getColumnText(7);
1131        long long size = 0;
1132        if (path.isEmpty())
1133            size = data->size();
1134        else {
1135            path = pathByAppendingComponent(flatFileDirectory, path);
1136            getFileSize(path, size);
1137        }
1138
1139        String mimeType = cacheStatement.getColumnText(3);
1140        String textEncodingName = cacheStatement.getColumnText(4);
1141
1142        ResourceResponse response(url, mimeType, size, textEncodingName, "");
1143        response.setHTTPStatusCode(httpStatusCode);
1144
1145        String headers = cacheStatement.getColumnText(5);
1146        parseHeaders(headers, response);
1147
1148        RefPtr<ApplicationCacheResource> resource = ApplicationCacheResource::create(url, response, type, data.release(), path);
1149
1150        if (type & ApplicationCacheResource::Manifest)
1151            cache->setManifestResource(resource.release());
1152        else
1153            cache->addResource(resource.release());
1154    }
1155
1156    if (result != SQLResultDone)
1157        LOG_ERROR("Could not load cache resources, error \"%s\"", m_database.lastErrorMsg());
1158
1159    if (!cache->manifestResource()) {
1160        LOG_ERROR("Could not load application cache because there was no manifest resource");
1161        return nullptr;
1162    }
1163
1164    // Load the online whitelist
1165    SQLiteStatement whitelistStatement(m_database, "SELECT url FROM CacheWhitelistURLs WHERE cache=?");
1166    if (whitelistStatement.prepare() != SQLResultOk)
1167        return 0;
1168    whitelistStatement.bindInt64(1, storageID);
1169
1170    Vector<KURL> whitelist;
1171    while ((result = whitelistStatement.step()) == SQLResultRow)
1172        whitelist.append(KURL(ParsedURLString, whitelistStatement.getColumnText(0)));
1173
1174    if (result != SQLResultDone)
1175        LOG_ERROR("Could not load cache online whitelist, error \"%s\"", m_database.lastErrorMsg());
1176
1177    cache->setOnlineWhitelist(whitelist);
1178
1179    // Load online whitelist wildcard flag.
1180    SQLiteStatement whitelistWildcardStatement(m_database, "SELECT wildcard FROM CacheAllowsAllNetworkRequests WHERE cache=?");
1181    if (whitelistWildcardStatement.prepare() != SQLResultOk)
1182        return 0;
1183    whitelistWildcardStatement.bindInt64(1, storageID);
1184
1185    result = whitelistWildcardStatement.step();
1186    if (result != SQLResultRow)
1187        LOG_ERROR("Could not load cache online whitelist wildcard flag, error \"%s\"", m_database.lastErrorMsg());
1188
1189    cache->setAllowsAllNetworkRequests(whitelistWildcardStatement.getColumnInt64(0));
1190
1191    if (whitelistWildcardStatement.step() != SQLResultDone)
1192        LOG_ERROR("Too many rows for online whitelist wildcard flag");
1193
1194    // Load fallback URLs.
1195    SQLiteStatement fallbackStatement(m_database, "SELECT namespace, fallbackURL FROM FallbackURLs WHERE cache=?");
1196    if (fallbackStatement.prepare() != SQLResultOk)
1197        return 0;
1198    fallbackStatement.bindInt64(1, storageID);
1199
1200    FallbackURLVector fallbackURLs;
1201    while ((result = fallbackStatement.step()) == SQLResultRow)
1202        fallbackURLs.append(make_pair(KURL(ParsedURLString, fallbackStatement.getColumnText(0)), KURL(ParsedURLString, fallbackStatement.getColumnText(1))));
1203
1204    if (result != SQLResultDone)
1205        LOG_ERROR("Could not load fallback URLs, error \"%s\"", m_database.lastErrorMsg());
1206
1207    cache->setFallbackURLs(fallbackURLs);
1208
1209    cache->setStorageID(storageID);
1210
1211    return cache.release();
1212}
1213
1214void ApplicationCacheStorage::remove(ApplicationCache* cache)
1215{
1216    if (!cache->storageID())
1217        return;
1218
1219    openDatabase(false);
1220    if (!m_database.isOpen())
1221        return;
1222
1223    ASSERT(cache->group());
1224    ASSERT(cache->group()->storageID());
1225
1226    // All associated data will be deleted by database triggers.
1227    SQLiteStatement statement(m_database, "DELETE FROM Caches WHERE id=?");
1228    if (statement.prepare() != SQLResultOk)
1229        return;
1230
1231    statement.bindInt64(1, cache->storageID());
1232    executeStatement(statement);
1233
1234    cache->clearStorageID();
1235
1236    if (cache->group()->newestCache() == cache) {
1237        // Currently, there are no triggers on the cache group, which is why the cache had to be removed separately above.
1238        SQLiteStatement groupStatement(m_database, "DELETE FROM CacheGroups WHERE id=?");
1239        if (groupStatement.prepare() != SQLResultOk)
1240            return;
1241
1242        groupStatement.bindInt64(1, cache->group()->storageID());
1243        executeStatement(groupStatement);
1244
1245        cache->group()->clearStorageID();
1246    }
1247
1248    checkForDeletedResources();
1249}
1250
1251void ApplicationCacheStorage::empty()
1252{
1253    openDatabase(false);
1254
1255    if (!m_database.isOpen())
1256        return;
1257
1258    // Clear cache groups, caches, cache resources, and origins.
1259    executeSQLCommand("DELETE FROM CacheGroups");
1260    executeSQLCommand("DELETE FROM Caches");
1261    executeSQLCommand("DELETE FROM Origins");
1262
1263    // Clear the storage IDs for the caches in memory.
1264    // The caches will still work, but cached resources will not be saved to disk
1265    // until a cache update process has been initiated.
1266    CacheGroupMap::const_iterator end = m_cachesInMemory.end();
1267    for (CacheGroupMap::const_iterator it = m_cachesInMemory.begin(); it != end; ++it)
1268        it->value->clearStorageID();
1269
1270    checkForDeletedResources();
1271}
1272
1273void ApplicationCacheStorage::deleteTables()
1274{
1275    empty();
1276    m_database.clearAllTables();
1277}
1278
1279bool ApplicationCacheStorage::shouldStoreResourceAsFlatFile(ApplicationCacheResource* resource)
1280{
1281    return resource->response().mimeType().startsWith("audio/", false)
1282        || resource->response().mimeType().startsWith("video/", false);
1283}
1284
1285bool ApplicationCacheStorage::writeDataToUniqueFileInDirectory(SharedBuffer* data, const String& directory, String& path, const String& fileExtension)
1286{
1287    String fullPath;
1288
1289    do {
1290        path = encodeForFileName(createCanonicalUUIDString()) + fileExtension;
1291        // Guard against the above function being called on a platform which does not implement
1292        // createCanonicalUUIDString().
1293        ASSERT(!path.isEmpty());
1294        if (path.isEmpty())
1295            return false;
1296
1297        fullPath = pathByAppendingComponent(directory, path);
1298    } while (directoryName(fullPath) != directory || fileExists(fullPath));
1299
1300    PlatformFileHandle handle = openFile(fullPath, OpenForWrite);
1301    if (!handle)
1302        return false;
1303
1304    int64_t writtenBytes = writeToFile(handle, data->data(), data->size());
1305    closeFile(handle);
1306
1307    if (writtenBytes != static_cast<int64_t>(data->size())) {
1308        deleteFile(fullPath);
1309        return false;
1310    }
1311
1312    return true;
1313}
1314
1315bool ApplicationCacheStorage::storeCopyOfCache(const String& cacheDirectory, ApplicationCacheHost* cacheHost)
1316{
1317    ApplicationCache* cache = cacheHost->applicationCache();
1318    if (!cache)
1319        return true;
1320
1321    // Create a new cache.
1322    RefPtr<ApplicationCache> cacheCopy = ApplicationCache::create();
1323
1324    cacheCopy->setOnlineWhitelist(cache->onlineWhitelist());
1325    cacheCopy->setFallbackURLs(cache->fallbackURLs());
1326
1327    // Traverse the cache and add copies of all resources.
1328    ApplicationCache::ResourceMap::const_iterator end = cache->end();
1329    for (ApplicationCache::ResourceMap::const_iterator it = cache->begin(); it != end; ++it) {
1330        ApplicationCacheResource* resource = it->value.get();
1331
1332        RefPtr<ApplicationCacheResource> resourceCopy = ApplicationCacheResource::create(resource->url(), resource->response(), resource->type(), resource->data(), resource->path());
1333
1334        cacheCopy->addResource(resourceCopy.release());
1335    }
1336
1337    // Now create a new cache group.
1338    OwnPtr<ApplicationCacheGroup> groupCopy(adoptPtr(new ApplicationCacheGroup(cache->group()->manifestURL(), true)));
1339
1340    groupCopy->setNewestCache(cacheCopy);
1341
1342    ApplicationCacheStorage copyStorage;
1343    copyStorage.setCacheDirectory(cacheDirectory);
1344
1345    // Empty the cache in case something was there before.
1346    copyStorage.empty();
1347
1348    return copyStorage.storeNewestCache(groupCopy.get());
1349}
1350
1351bool ApplicationCacheStorage::manifestURLs(Vector<KURL>* urls)
1352{
1353    ASSERT(urls);
1354    openDatabase(false);
1355    if (!m_database.isOpen())
1356        return false;
1357
1358    SQLiteStatement selectURLs(m_database, "SELECT manifestURL FROM CacheGroups");
1359
1360    if (selectURLs.prepare() != SQLResultOk)
1361        return false;
1362
1363    while (selectURLs.step() == SQLResultRow)
1364        urls->append(KURL(ParsedURLString, selectURLs.getColumnText(0)));
1365
1366    return true;
1367}
1368
1369bool ApplicationCacheStorage::cacheGroupSize(const String& manifestURL, int64_t* size)
1370{
1371    ASSERT(size);
1372    openDatabase(false);
1373    if (!m_database.isOpen())
1374        return false;
1375
1376    SQLiteStatement statement(m_database, "SELECT sum(Caches.size) FROM Caches INNER JOIN CacheGroups ON Caches.cacheGroup=CacheGroups.id WHERE CacheGroups.manifestURL=?");
1377    if (statement.prepare() != SQLResultOk)
1378        return false;
1379
1380    statement.bindText(1, manifestURL);
1381
1382    int result = statement.step();
1383    if (result == SQLResultDone)
1384        return false;
1385
1386    if (result != SQLResultRow) {
1387        LOG_ERROR("Could not get the size of the cache group, error \"%s\"", m_database.lastErrorMsg());
1388        return false;
1389    }
1390
1391    *size = statement.getColumnInt64(0);
1392    return true;
1393}
1394
1395bool ApplicationCacheStorage::deleteCacheGroupRecord(const String& manifestURL)
1396{
1397    SQLiteStatement idStatement(m_database, "SELECT id FROM CacheGroups WHERE manifestURL=?");
1398    if (idStatement.prepare() != SQLResultOk)
1399        return false;
1400
1401    idStatement.bindText(1, manifestURL);
1402
1403    int result = idStatement.step();
1404    if (result != SQLResultRow)
1405        return false;
1406
1407    int64_t groupId = idStatement.getColumnInt64(0);
1408
1409    SQLiteStatement cacheStatement(m_database, "DELETE FROM Caches WHERE cacheGroup=?");
1410    if (cacheStatement.prepare() != SQLResultOk)
1411        return false;
1412
1413    SQLiteStatement groupStatement(m_database, "DELETE FROM CacheGroups WHERE id=?");
1414    if (groupStatement.prepare() != SQLResultOk)
1415        return false;
1416
1417    cacheStatement.bindInt64(1, groupId);
1418    executeStatement(cacheStatement);
1419    groupStatement.bindInt64(1, groupId);
1420    executeStatement(groupStatement);
1421    return true;
1422}
1423
1424bool ApplicationCacheStorage::deleteCacheGroup(const String& manifestURL)
1425{
1426    SQLiteTransaction deleteTransaction(m_database);
1427    // Check to see if the group is in memory.
1428    ApplicationCacheGroup* group = m_cachesInMemory.get(manifestURL);
1429    if (group)
1430        cacheGroupMadeObsolete(group);
1431    else {
1432        // The cache group is not in memory, so remove it from the disk.
1433        openDatabase(false);
1434        if (!m_database.isOpen())
1435            return false;
1436        if (!deleteCacheGroupRecord(manifestURL)) {
1437            LOG_ERROR("Could not delete cache group record, error \"%s\"", m_database.lastErrorMsg());
1438            return false;
1439        }
1440    }
1441
1442    deleteTransaction.commit();
1443
1444    checkForDeletedResources();
1445
1446    return true;
1447}
1448
1449void ApplicationCacheStorage::vacuumDatabaseFile()
1450{
1451    openDatabase(false);
1452    if (!m_database.isOpen())
1453        return;
1454
1455    m_database.runVacuumCommand();
1456}
1457
1458void ApplicationCacheStorage::checkForMaxSizeReached()
1459{
1460    if (m_database.lastError() == SQLResultFull)
1461        m_isMaximumSizeReached = true;
1462}
1463
1464void ApplicationCacheStorage::checkForDeletedResources()
1465{
1466    openDatabase(false);
1467    if (!m_database.isOpen())
1468        return;
1469
1470    // Select only the paths in DeletedCacheResources that do not also appear in CacheResourceData:
1471    SQLiteStatement selectPaths(m_database, "SELECT DeletedCacheResources.path "
1472        "FROM DeletedCacheResources "
1473        "LEFT JOIN CacheResourceData "
1474        "ON DeletedCacheResources.path = CacheResourceData.path "
1475        "WHERE (SELECT DeletedCacheResources.path == CacheResourceData.path) IS NULL");
1476
1477    if (selectPaths.prepare() != SQLResultOk)
1478        return;
1479
1480    if (selectPaths.step() != SQLResultRow)
1481        return;
1482
1483    do {
1484        String path = selectPaths.getColumnText(0);
1485        if (path.isEmpty())
1486            continue;
1487
1488        String flatFileDirectory = pathByAppendingComponent(m_cacheDirectory, flatFileSubdirectory);
1489        String fullPath = pathByAppendingComponent(flatFileDirectory, path);
1490
1491        // Don't exit the flatFileDirectory! This should only happen if the "path" entry contains a directory
1492        // component, but protect against it regardless.
1493        if (directoryName(fullPath) != flatFileDirectory)
1494            continue;
1495
1496        deleteFile(fullPath);
1497    } while (selectPaths.step() == SQLResultRow);
1498
1499    executeSQLCommand("DELETE FROM DeletedCacheResources");
1500}
1501
1502long long ApplicationCacheStorage::flatFileAreaSize()
1503{
1504    openDatabase(false);
1505    if (!m_database.isOpen())
1506        return 0;
1507
1508    SQLiteStatement selectPaths(m_database, "SELECT path FROM CacheResourceData WHERE path NOT NULL");
1509
1510    if (selectPaths.prepare() != SQLResultOk) {
1511        LOG_ERROR("Could not load flat file cache resource data, error \"%s\"", m_database.lastErrorMsg());
1512        return 0;
1513    }
1514
1515    long long totalSize = 0;
1516    String flatFileDirectory = pathByAppendingComponent(m_cacheDirectory, flatFileSubdirectory);
1517    while (selectPaths.step() == SQLResultRow) {
1518        String path = selectPaths.getColumnText(0);
1519        String fullPath = pathByAppendingComponent(flatFileDirectory, path);
1520        long long pathSize = 0;
1521        if (!getFileSize(fullPath, pathSize))
1522            continue;
1523        totalSize += pathSize;
1524    }
1525
1526    return totalSize;
1527}
1528
1529void ApplicationCacheStorage::getOriginsWithCache(HashSet<RefPtr<SecurityOrigin> >& origins)
1530{
1531    Vector<KURL> urls;
1532    if (!manifestURLs(&urls)) {
1533        LOG_ERROR("Failed to retrieve ApplicationCache manifest URLs");
1534        return;
1535    }
1536
1537    // Multiple manifest URLs might share the same SecurityOrigin, so we might be creating extra, wasted origins here.
1538    // The current schema doesn't allow for a more efficient way of building this list.
1539    size_t count = urls.size();
1540    for (size_t i = 0; i < count; ++i) {
1541        RefPtr<SecurityOrigin> origin = SecurityOrigin::create(urls[i]);
1542        origins.add(origin);
1543    }
1544}
1545
1546void ApplicationCacheStorage::deleteAllEntries()
1547{
1548    empty();
1549    vacuumDatabaseFile();
1550}
1551
1552ApplicationCacheStorage::ApplicationCacheStorage()
1553    : m_maximumSize(ApplicationCacheStorage::noQuota())
1554    , m_isMaximumSizeReached(false)
1555    , m_defaultOriginQuota(ApplicationCacheStorage::noQuota())
1556{
1557}
1558
1559ApplicationCacheStorage& cacheStorage()
1560{
1561    DEFINE_STATIC_LOCAL(ApplicationCacheStorage, storage, ());
1562
1563    return storage;
1564}
1565
1566} // namespace WebCore
1567