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