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