diff options
author | Steve Block <steveblock@google.com> | 2011-05-06 11:45:16 +0100 |
---|---|---|
committer | Steve Block <steveblock@google.com> | 2011-05-12 13:44:10 +0100 |
commit | cad810f21b803229eb11403f9209855525a25d57 (patch) | |
tree | 29a6fd0279be608e0fe9ffe9841f722f0f4e4269 /Source/WebCore/loader/appcache | |
parent | 121b0cf4517156d0ac5111caf9830c51b69bae8f (diff) | |
download | external_webkit-cad810f21b803229eb11403f9209855525a25d57.zip external_webkit-cad810f21b803229eb11403f9209855525a25d57.tar.gz external_webkit-cad810f21b803229eb11403f9209855525a25d57.tar.bz2 |
Merge WebKit at r75315: Initial merge by git.
Change-Id: I570314b346ce101c935ed22a626b48c2af266b84
Diffstat (limited to 'Source/WebCore/loader/appcache')
15 files changed, 4545 insertions, 0 deletions
diff --git a/Source/WebCore/loader/appcache/ApplicationCache.cpp b/Source/WebCore/loader/appcache/ApplicationCache.cpp new file mode 100644 index 0000000..2a93765 --- /dev/null +++ b/Source/WebCore/loader/appcache/ApplicationCache.cpp @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2008 Apple Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "ApplicationCache.h" + +#if ENABLE(OFFLINE_WEB_APPLICATIONS) + +#include "ApplicationCacheGroup.h" +#include "ApplicationCacheResource.h" +#include "ApplicationCacheStorage.h" +#include "ResourceRequest.h" +#include <wtf/text/CString.h> +#include <stdio.h> + +namespace WebCore { + +ApplicationCache::ApplicationCache() + : m_group(0) + , m_manifest(0) + , m_estimatedSizeInStorage(0) + , m_storageID(0) +{ +} + +ApplicationCache::~ApplicationCache() +{ + if (m_group && !m_group->isCopy()) + m_group->cacheDestroyed(this); +} + +void ApplicationCache::setGroup(ApplicationCacheGroup* group) +{ + ASSERT(!m_group || group == m_group); + m_group = group; +} + +bool ApplicationCache::isComplete() const +{ + return !m_group->cacheIsBeingUpdated(this); +} + +void ApplicationCache::setManifestResource(PassRefPtr<ApplicationCacheResource> manifest) +{ + ASSERT(manifest); + ASSERT(!m_manifest); + ASSERT(manifest->type() & ApplicationCacheResource::Manifest); + + m_manifest = manifest.get(); + + addResource(manifest); +} + +void ApplicationCache::addResource(PassRefPtr<ApplicationCacheResource> resource) +{ + ASSERT(resource); + + const String& url = resource->url(); + + ASSERT(!m_resources.contains(url)); + + if (m_storageID) { + ASSERT(!resource->storageID()); + ASSERT(resource->type() & ApplicationCacheResource::Master); + + // Add the resource to the storage. + cacheStorage().store(resource.get(), this); + } + + m_estimatedSizeInStorage += resource->estimatedSizeInStorage(); + + m_resources.set(url, resource); +} + +unsigned ApplicationCache::removeResource(const String& url) +{ + HashMap<String, RefPtr<ApplicationCacheResource> >::iterator it = m_resources.find(url); + if (it == m_resources.end()) + return 0; + + // The resource exists, get its type so we can return it. + unsigned type = it->second->type(); + + m_resources.remove(it); + + m_estimatedSizeInStorage -= it->second->estimatedSizeInStorage(); + + return type; +} + +ApplicationCacheResource* ApplicationCache::resourceForURL(const String& url) +{ + ASSERT(!KURL(ParsedURLString, url).hasFragmentIdentifier()); + return m_resources.get(url).get(); +} + +bool ApplicationCache::requestIsHTTPOrHTTPSGet(const ResourceRequest& request) +{ + if (!request.url().protocolInHTTPFamily()) + return false; + + if (!equalIgnoringCase(request.httpMethod(), "GET")) + return false; + + return true; +} + +ApplicationCacheResource* ApplicationCache::resourceForRequest(const ResourceRequest& request) +{ + // We only care about HTTP/HTTPS GET requests. + if (!requestIsHTTPOrHTTPSGet(request)) + return 0; + + KURL url(request.url()); + if (url.hasFragmentIdentifier()) + url.removeFragmentIdentifier(); + + return resourceForURL(url); +} + +void ApplicationCache::setOnlineWhitelist(const Vector<KURL>& onlineWhitelist) +{ + ASSERT(m_onlineWhitelist.isEmpty()); + m_onlineWhitelist = onlineWhitelist; +} + +bool ApplicationCache::isURLInOnlineWhitelist(const KURL& url) +{ + if (m_allowAllNetworkRequests) + return true; + + size_t whitelistSize = m_onlineWhitelist.size(); + for (size_t i = 0; i < whitelistSize; ++i) { + if (protocolHostAndPortAreEqual(url, m_onlineWhitelist[i]) && url.string().startsWith(m_onlineWhitelist[i].string())) + return true; + } + return false; +} + +void ApplicationCache::setFallbackURLs(const FallbackURLVector& fallbackURLs) +{ + ASSERT(m_fallbackURLs.isEmpty()); + m_fallbackURLs = fallbackURLs; +} + +bool ApplicationCache::urlMatchesFallbackNamespace(const KURL& url, KURL* fallbackURL) +{ + size_t fallbackCount = m_fallbackURLs.size(); + for (size_t i = 0; i < fallbackCount; ++i) { + if (protocolHostAndPortAreEqual(url, m_fallbackURLs[i].first) && url.string().startsWith(m_fallbackURLs[i].first.string())) { + if (fallbackURL) + *fallbackURL = m_fallbackURLs[i].second; + return true; + } + } + return false; +} + +void ApplicationCache::clearStorageID() +{ + m_storageID = 0; + + ResourceMap::const_iterator end = m_resources.end(); + for (ResourceMap::const_iterator it = m_resources.begin(); it != end; ++it) + it->second->clearStorageID(); +} + +#ifndef NDEBUG +void ApplicationCache::dump() +{ + HashMap<String, RefPtr<ApplicationCacheResource> >::const_iterator end = m_resources.end(); + + for (HashMap<String, RefPtr<ApplicationCacheResource> >::const_iterator it = m_resources.begin(); it != end; ++it) { + printf("%s ", it->first.ascii().data()); + ApplicationCacheResource::dumpType(it->second->type()); + } +} +#endif + +} + +#endif // ENABLE(OFFLINE_WEB_APPLICATIONS) diff --git a/Source/WebCore/loader/appcache/ApplicationCache.h b/Source/WebCore/loader/appcache/ApplicationCache.h new file mode 100644 index 0000000..f073499 --- /dev/null +++ b/Source/WebCore/loader/appcache/ApplicationCache.h @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2008 Apple Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef ApplicationCache_h +#define ApplicationCache_h + +#if ENABLE(OFFLINE_WEB_APPLICATIONS) + +#include "PlatformString.h" +#include <wtf/HashMap.h> +#include <wtf/HashSet.h> +#include <wtf/PassRefPtr.h> +#include <wtf/RefCounted.h> +#include <wtf/text/StringHash.h> + +namespace WebCore { + +class ApplicationCacheGroup; +class ApplicationCacheResource; +class DocumentLoader; +class KURL; +class ResourceRequest; + +typedef Vector<std::pair<KURL, KURL> > FallbackURLVector; + +class ApplicationCache : public RefCounted<ApplicationCache> { +public: + static PassRefPtr<ApplicationCache> create() { return adoptRef(new ApplicationCache); } + ~ApplicationCache(); + + void addResource(PassRefPtr<ApplicationCacheResource> resource); + unsigned removeResource(const String& url); + + void setManifestResource(PassRefPtr<ApplicationCacheResource> manifest); + ApplicationCacheResource* manifestResource() const { return m_manifest; } + + void setGroup(ApplicationCacheGroup*); + ApplicationCacheGroup* group() const { return m_group; } + + bool isComplete() const; + + ApplicationCacheResource* resourceForRequest(const ResourceRequest&); + ApplicationCacheResource* resourceForURL(const String& url); + + void setAllowsAllNetworkRequests(bool value) { m_allowAllNetworkRequests = value; } + bool allowsAllNetworkRequests() const { return m_allowAllNetworkRequests; } + void setOnlineWhitelist(const Vector<KURL>& onlineWhitelist); + const Vector<KURL>& onlineWhitelist() const { return m_onlineWhitelist; } + bool isURLInOnlineWhitelist(const KURL&); // There is an entry in online whitelist that has the same origin as the resource's URL and that is a prefix match for the resource's URL. + + void setFallbackURLs(const FallbackURLVector&); + const FallbackURLVector& fallbackURLs() const { return m_fallbackURLs; } + bool urlMatchesFallbackNamespace(const KURL&, KURL* fallbackURL = 0); + +#ifndef NDEBUG + void dump(); +#endif + + typedef HashMap<String, RefPtr<ApplicationCacheResource> > ResourceMap; + ResourceMap::const_iterator begin() const { return m_resources.begin(); } + ResourceMap::const_iterator end() const { return m_resources.end(); } + + void setStorageID(unsigned storageID) { m_storageID = storageID; } + unsigned storageID() const { return m_storageID; } + void clearStorageID(); + + static bool requestIsHTTPOrHTTPSGet(const ResourceRequest&); + + int64_t estimatedSizeInStorage() const { return m_estimatedSizeInStorage; } + +private: + ApplicationCache(); + + ApplicationCacheGroup* m_group; + ResourceMap m_resources; + ApplicationCacheResource* m_manifest; + + bool m_allowAllNetworkRequests; + Vector<KURL> m_onlineWhitelist; + FallbackURLVector m_fallbackURLs; + + // The total size of the resources belonging to this Application Cache instance. + // This is an estimation of the size this Application Cache occupies in the + // database file. + int64_t m_estimatedSizeInStorage; + + unsigned m_storageID; +}; + +} // namespace WebCore + +#endif // ENABLE(OFFLINE_WEB_APPLICATIONS) + +#endif // ApplicationCache_h diff --git a/Source/WebCore/loader/appcache/ApplicationCacheGroup.cpp b/Source/WebCore/loader/appcache/ApplicationCacheGroup.cpp new file mode 100644 index 0000000..6454b90 --- /dev/null +++ b/Source/WebCore/loader/appcache/ApplicationCacheGroup.cpp @@ -0,0 +1,1181 @@ +/* + * Copyright (C) 2008, 2009, 2010 Apple Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "ApplicationCacheGroup.h" + +#if ENABLE(OFFLINE_WEB_APPLICATIONS) + +#include "ApplicationCache.h" +#include "ApplicationCacheHost.h" +#include "ApplicationCacheResource.h" +#include "ApplicationCacheStorage.h" +#include "Chrome.h" +#include "ChromeClient.h" +#include "DocumentLoader.h" +#include "DOMApplicationCache.h" +#include "DOMWindow.h" +#include "Frame.h" +#include "FrameLoader.h" +#include "FrameLoaderClient.h" +#include "MainResourceLoader.h" +#include "ManifestParser.h" +#include "Page.h" +#include "SecurityOrigin.h" +#include "Settings.h" +#include <wtf/HashMap.h> + +#if ENABLE(INSPECTOR) +#include "InspectorApplicationCacheAgent.h" +#include "InspectorController.h" +#include "ProgressTracker.h" +#else +#include <wtf/UnusedParam.h> +#endif + +namespace WebCore { + +ApplicationCacheGroup::ApplicationCacheGroup(const KURL& manifestURL, bool isCopy) + : m_manifestURL(manifestURL) + , m_origin(SecurityOrigin::create(manifestURL)) + , m_updateStatus(Idle) + , m_downloadingPendingMasterResourceLoadersCount(0) + , m_progressTotal(0) + , m_progressDone(0) + , m_frame(0) + , m_storageID(0) + , m_isObsolete(false) + , m_completionType(None) + , m_isCopy(isCopy) + , m_calledReachedMaxAppCacheSize(false) + , m_loadedSize(0) + , m_availableSpaceInQuota(ApplicationCacheStorage::unknownQuota()) + , m_originQuotaReached(false) +{ +} + +ApplicationCacheGroup::~ApplicationCacheGroup() +{ + if (m_isCopy) { + ASSERT(m_newestCache); + ASSERT(m_caches.size() == 1); + ASSERT(m_caches.contains(m_newestCache.get())); + ASSERT(!m_cacheBeingUpdated); + ASSERT(m_associatedDocumentLoaders.isEmpty()); + ASSERT(m_pendingMasterResourceLoaders.isEmpty()); + ASSERT(m_newestCache->group() == this); + + return; + } + + ASSERT(!m_newestCache); + ASSERT(m_caches.isEmpty()); + + stopLoading(); + + cacheStorage().cacheGroupDestroyed(this); +} + +ApplicationCache* ApplicationCacheGroup::cacheForMainRequest(const ResourceRequest& request, DocumentLoader*) +{ + if (!ApplicationCache::requestIsHTTPOrHTTPSGet(request)) + return 0; + + KURL url(request.url()); + if (url.hasFragmentIdentifier()) + url.removeFragmentIdentifier(); + + if (ApplicationCacheGroup* group = cacheStorage().cacheGroupForURL(url)) { + ASSERT(group->newestCache()); + ASSERT(!group->isObsolete()); + + return group->newestCache(); + } + + return 0; +} + +ApplicationCache* ApplicationCacheGroup::fallbackCacheForMainRequest(const ResourceRequest& request, DocumentLoader*) +{ + if (!ApplicationCache::requestIsHTTPOrHTTPSGet(request)) + return 0; + + KURL url(request.url()); + if (url.hasFragmentIdentifier()) + url.removeFragmentIdentifier(); + + if (ApplicationCacheGroup* group = cacheStorage().fallbackCacheGroupForURL(url)) { + ASSERT(group->newestCache()); + ASSERT(!group->isObsolete()); + + return group->newestCache(); + } + + return 0; +} + +void ApplicationCacheGroup::selectCache(Frame* frame, const KURL& passedManifestURL) +{ + ASSERT(frame && frame->page()); + + if (!frame->settings()->offlineWebApplicationCacheEnabled()) + return; + + DocumentLoader* documentLoader = frame->loader()->documentLoader(); + ASSERT(!documentLoader->applicationCacheHost()->applicationCache()); + + if (passedManifestURL.isNull()) { + selectCacheWithoutManifestURL(frame); + return; + } + + KURL manifestURL(passedManifestURL); + if (manifestURL.hasFragmentIdentifier()) + manifestURL.removeFragmentIdentifier(); + + ApplicationCache* mainResourceCache = documentLoader->applicationCacheHost()->mainResourceApplicationCache(); + + if (mainResourceCache) { + if (manifestURL == mainResourceCache->group()->m_manifestURL) { + // The cache may have gotten obsoleted after we've loaded from it, but before we parsed the document and saw cache manifest. + if (mainResourceCache->group()->isObsolete()) + return; + mainResourceCache->group()->associateDocumentLoaderWithCache(documentLoader, mainResourceCache); + mainResourceCache->group()->update(frame, ApplicationCacheUpdateWithBrowsingContext); + } else { + // The main resource was loaded from cache, so the cache must have an entry for it. Mark it as foreign. + KURL resourceURL(documentLoader->responseURL()); + if (resourceURL.hasFragmentIdentifier()) + resourceURL.removeFragmentIdentifier(); + ApplicationCacheResource* resource = mainResourceCache->resourceForURL(resourceURL); + bool inStorage = resource->storageID(); + resource->addType(ApplicationCacheResource::Foreign); + if (inStorage) + cacheStorage().storeUpdatedType(resource, mainResourceCache); + + // Restart the current navigation from the top of the navigation algorithm, undoing any changes that were made + // as part of the initial load. + // The navigation will not result in the same resource being loaded, because "foreign" entries are never picked during navigation. + frame->navigationScheduler()->scheduleLocationChange(frame->document()->securityOrigin(), documentLoader->url(), frame->loader()->referrer(), true); + } + + return; + } + + // The resource was loaded from the network, check if it is a HTTP/HTTPS GET. + const ResourceRequest& request = frame->loader()->activeDocumentLoader()->request(); + + if (!ApplicationCache::requestIsHTTPOrHTTPSGet(request)) + return; + + // Check that the resource URL has the same scheme/host/port as the manifest URL. + if (!protocolHostAndPortAreEqual(manifestURL, request.url())) + return; + + // Don't change anything on disk if private browsing is enabled. + if (!frame->settings() || frame->settings()->privateBrowsingEnabled()) { + postListenerTask(ApplicationCacheHost::CHECKING_EVENT, documentLoader); + postListenerTask(ApplicationCacheHost::ERROR_EVENT, documentLoader); + return; + } + + ApplicationCacheGroup* group = cacheStorage().findOrCreateCacheGroup(manifestURL); + + documentLoader->applicationCacheHost()->setCandidateApplicationCacheGroup(group); + group->m_pendingMasterResourceLoaders.add(documentLoader); + group->m_downloadingPendingMasterResourceLoadersCount++; + + ASSERT(!group->m_cacheBeingUpdated || group->m_updateStatus != Idle); + group->update(frame, ApplicationCacheUpdateWithBrowsingContext); +} + +void ApplicationCacheGroup::selectCacheWithoutManifestURL(Frame* frame) +{ + if (!frame->settings()->offlineWebApplicationCacheEnabled()) + return; + + DocumentLoader* documentLoader = frame->loader()->documentLoader(); + ASSERT(!documentLoader->applicationCacheHost()->applicationCache()); + + ApplicationCache* mainResourceCache = documentLoader->applicationCacheHost()->mainResourceApplicationCache(); + + if (mainResourceCache) { + mainResourceCache->group()->associateDocumentLoaderWithCache(documentLoader, mainResourceCache); + mainResourceCache->group()->update(frame, ApplicationCacheUpdateWithBrowsingContext); + } +} + +void ApplicationCacheGroup::finishedLoadingMainResource(DocumentLoader* loader) +{ + ASSERT(m_pendingMasterResourceLoaders.contains(loader)); + ASSERT(m_completionType == None || m_pendingEntries.isEmpty()); + KURL url = loader->url(); + if (url.hasFragmentIdentifier()) + url.removeFragmentIdentifier(); + + switch (m_completionType) { + case None: + // The main resource finished loading before the manifest was ready. It will be handled via dispatchMainResources() later. + return; + case NoUpdate: + ASSERT(!m_cacheBeingUpdated); + associateDocumentLoaderWithCache(loader, m_newestCache.get()); + + if (ApplicationCacheResource* resource = m_newestCache->resourceForURL(url)) { + if (!(resource->type() & ApplicationCacheResource::Master)) { + resource->addType(ApplicationCacheResource::Master); + ASSERT(!resource->storageID()); + } + } else + m_newestCache->addResource(ApplicationCacheResource::create(url, loader->response(), ApplicationCacheResource::Master, loader->mainResourceData())); + + break; + case Failure: + // Cache update has been a failure, so there is no reason to keep the document associated with the incomplete cache + // (its main resource was not cached yet, so it is likely that the application changed significantly server-side). + ASSERT(!m_cacheBeingUpdated); // Already cleared out by stopLoading(). + loader->applicationCacheHost()->setApplicationCache(0); // Will unset candidate, too. + m_associatedDocumentLoaders.remove(loader); + postListenerTask(ApplicationCacheHost::ERROR_EVENT, loader); + break; + case Completed: + ASSERT(m_associatedDocumentLoaders.contains(loader)); + + if (ApplicationCacheResource* resource = m_cacheBeingUpdated->resourceForURL(url)) { + if (!(resource->type() & ApplicationCacheResource::Master)) { + resource->addType(ApplicationCacheResource::Master); + ASSERT(!resource->storageID()); + } + } else + m_cacheBeingUpdated->addResource(ApplicationCacheResource::create(url, loader->response(), ApplicationCacheResource::Master, loader->mainResourceData())); + // The "cached" event will be posted to all associated documents once update is complete. + break; + } + + m_downloadingPendingMasterResourceLoadersCount--; + checkIfLoadIsComplete(); +} + +void ApplicationCacheGroup::failedLoadingMainResource(DocumentLoader* loader) +{ + ASSERT(m_pendingMasterResourceLoaders.contains(loader)); + ASSERT(m_completionType == None || m_pendingEntries.isEmpty()); + + switch (m_completionType) { + case None: + // The main resource finished loading before the manifest was ready. It will be handled via dispatchMainResources() later. + return; + case NoUpdate: + ASSERT(!m_cacheBeingUpdated); + + // The manifest didn't change, and we have a relevant cache - but the main resource download failed mid-way, so it cannot be stored to the cache, + // and the loader does not get associated to it. If there are other main resources being downloaded for this cache group, they may still succeed. + postListenerTask(ApplicationCacheHost::ERROR_EVENT, loader); + + break; + case Failure: + // Cache update failed, too. + ASSERT(!m_cacheBeingUpdated); // Already cleared out by stopLoading(). + ASSERT(!loader->applicationCacheHost()->applicationCache() || loader->applicationCacheHost()->applicationCache() == m_cacheBeingUpdated); + + loader->applicationCacheHost()->setApplicationCache(0); // Will unset candidate, too. + m_associatedDocumentLoaders.remove(loader); + postListenerTask(ApplicationCacheHost::ERROR_EVENT, loader); + break; + case Completed: + // The cache manifest didn't list this main resource, and all cache entries were already updated successfully - but the main resource failed to load, + // so it cannot be stored to the cache. If there are other main resources being downloaded for this cache group, they may still succeed. + ASSERT(m_associatedDocumentLoaders.contains(loader)); + ASSERT(loader->applicationCacheHost()->applicationCache() == m_cacheBeingUpdated); + ASSERT(!loader->applicationCacheHost()->candidateApplicationCacheGroup()); + m_associatedDocumentLoaders.remove(loader); + loader->applicationCacheHost()->setApplicationCache(0); + + postListenerTask(ApplicationCacheHost::ERROR_EVENT, loader); + + break; + } + + m_downloadingPendingMasterResourceLoadersCount--; + checkIfLoadIsComplete(); +} + +void ApplicationCacheGroup::stopLoading() +{ + if (m_manifestHandle) { + ASSERT(!m_currentHandle); + + m_manifestHandle->setClient(0); + m_manifestHandle->cancel(); + m_manifestHandle = 0; + } + + if (m_currentHandle) { + ASSERT(!m_manifestHandle); + ASSERT(m_cacheBeingUpdated); + + m_currentHandle->setClient(0); + m_currentHandle->cancel(); + m_currentHandle = 0; + } + + m_cacheBeingUpdated = 0; + m_pendingEntries.clear(); +} + +void ApplicationCacheGroup::disassociateDocumentLoader(DocumentLoader* loader) +{ + HashSet<DocumentLoader*>::iterator it = m_associatedDocumentLoaders.find(loader); + if (it != m_associatedDocumentLoaders.end()) + m_associatedDocumentLoaders.remove(it); + + m_pendingMasterResourceLoaders.remove(loader); + + loader->applicationCacheHost()->setApplicationCache(0); // Will set candidate to 0, too. + + if (!m_associatedDocumentLoaders.isEmpty() || !m_pendingMasterResourceLoaders.isEmpty()) + return; + + if (m_caches.isEmpty()) { + // There is an initial cache attempt in progress. + ASSERT(!m_newestCache); + // Delete ourselves, causing the cache attempt to be stopped. + delete this; + return; + } + + ASSERT(m_caches.contains(m_newestCache.get())); + + // Release our reference to the newest cache. This could cause us to be deleted. + // Any ongoing updates will be stopped from destructor. + m_newestCache.release(); +} + +void ApplicationCacheGroup::cacheDestroyed(ApplicationCache* cache) +{ + if (!m_caches.contains(cache)) + return; + + m_caches.remove(cache); + + if (m_caches.isEmpty()) { + ASSERT(m_associatedDocumentLoaders.isEmpty()); + ASSERT(m_pendingMasterResourceLoaders.isEmpty()); + delete this; + } +} + +void ApplicationCacheGroup::stopLoadingInFrame(Frame* frame) +{ + if (frame != m_frame) + return; + + stopLoading(); +} + +#if ENABLE(INSPECTOR) +static void inspectorUpdateApplicationCacheStatus(Frame* frame) +{ + if (!frame) + return; + + if (Page *page = frame->page()) { + if (InspectorApplicationCacheAgent* applicationCacheAgent = page->inspectorController()->applicationCacheAgent()) { + ApplicationCacheHost::Status status = frame->loader()->documentLoader()->applicationCacheHost()->status(); + applicationCacheAgent->updateApplicationCacheStatus(status); + } + } +} +#endif + +void ApplicationCacheGroup::setNewestCache(PassRefPtr<ApplicationCache> newestCache) +{ + m_newestCache = newestCache; + + m_caches.add(m_newestCache.get()); + m_newestCache->setGroup(this); +#if ENABLE(INSPECTOR) + inspectorUpdateApplicationCacheStatus(m_frame); +#endif +} + +void ApplicationCacheGroup::makeObsolete() +{ + if (isObsolete()) + return; + + m_isObsolete = true; + cacheStorage().cacheGroupMadeObsolete(this); + ASSERT(!m_storageID); +#if ENABLE(INSPECTOR) + inspectorUpdateApplicationCacheStatus(m_frame); +#endif +} + +void ApplicationCacheGroup::update(Frame* frame, ApplicationCacheUpdateOption updateOption) +{ + if (m_updateStatus == Checking || m_updateStatus == Downloading) { + if (updateOption == ApplicationCacheUpdateWithBrowsingContext) { + postListenerTask(ApplicationCacheHost::CHECKING_EVENT, frame->loader()->documentLoader()); + if (m_updateStatus == Downloading) + postListenerTask(ApplicationCacheHost::DOWNLOADING_EVENT, frame->loader()->documentLoader()); + } + return; + } + + // Don't change anything on disk if private browsing is enabled. + if (!frame->settings() || frame->settings()->privateBrowsingEnabled()) { + ASSERT(m_pendingMasterResourceLoaders.isEmpty()); + ASSERT(m_pendingEntries.isEmpty()); + ASSERT(!m_cacheBeingUpdated); + postListenerTask(ApplicationCacheHost::CHECKING_EVENT, frame->loader()->documentLoader()); + postListenerTask(ApplicationCacheHost::NOUPDATE_EVENT, frame->loader()->documentLoader()); + return; + } + + ASSERT(!m_frame); + m_frame = frame; + + setUpdateStatus(Checking); + + postListenerTask(ApplicationCacheHost::CHECKING_EVENT, m_associatedDocumentLoaders); + if (!m_newestCache) { + ASSERT(updateOption == ApplicationCacheUpdateWithBrowsingContext); + postListenerTask(ApplicationCacheHost::CHECKING_EVENT, frame->loader()->documentLoader()); + } + + ASSERT(!m_manifestHandle); + ASSERT(!m_manifestResource); + ASSERT(m_completionType == None); + + // FIXME: Handle defer loading + m_manifestHandle = createResourceHandle(m_manifestURL, m_newestCache ? m_newestCache->manifestResource() : 0); +} + +PassRefPtr<ResourceHandle> ApplicationCacheGroup::createResourceHandle(const KURL& url, ApplicationCacheResource* newestCachedResource) +{ + ResourceRequest request(url); + m_frame->loader()->applyUserAgent(request); + request.setHTTPHeaderField("Cache-Control", "max-age=0"); + + if (newestCachedResource) { + const String& lastModified = newestCachedResource->response().httpHeaderField("Last-Modified"); + const String& eTag = newestCachedResource->response().httpHeaderField("ETag"); + if (!lastModified.isEmpty() || !eTag.isEmpty()) { + if (!lastModified.isEmpty()) + request.setHTTPHeaderField("If-Modified-Since", lastModified); + if (!eTag.isEmpty()) + request.setHTTPHeaderField("If-None-Match", eTag); + } + } + + RefPtr<ResourceHandle> handle = ResourceHandle::create(m_frame->loader()->networkingContext(), request, this, false, true); +#if ENABLE(INSPECTOR) + // Because willSendRequest only gets called during redirects, we initialize + // the identifier and the first willSendRequest here. + m_currentResourceIdentifier = m_frame->page()->progress()->createUniqueIdentifier(); + if (Page* page = m_frame->page()) { + InspectorController* inspectorController = page->inspectorController(); + inspectorController->identifierForInitialRequest(m_currentResourceIdentifier, m_frame->loader()->documentLoader(), handle->firstRequest()); + ResourceResponse redirectResponse = ResourceResponse(); + inspectorController->willSendRequest(m_currentResourceIdentifier, request, redirectResponse); + } +#endif + return handle; +} + +#if ENABLE(INSPECTOR) +void ApplicationCacheGroup::willSendRequest(ResourceHandle*, ResourceRequest& request, const ResourceResponse& redirectResponse) +{ + // This only gets called by ResourceHandleMac if there is a redirect. + if (Page* page = m_frame->page()) + page->inspectorController()->willSendRequest(m_currentResourceIdentifier, request, redirectResponse); +} +#endif + +void ApplicationCacheGroup::didReceiveResponse(ResourceHandle* handle, const ResourceResponse& response) +{ +#if ENABLE(INSPECTOR) + if (Page* page = m_frame->page()) { + if (handle == m_manifestHandle) { + if (InspectorApplicationCacheAgent* applicationCacheAgent = page->inspectorController()->applicationCacheAgent()) + applicationCacheAgent->didReceiveManifestResponse(m_currentResourceIdentifier, response); + } else + page->inspectorController()->didReceiveResponse(m_currentResourceIdentifier, m_frame->loader()->documentLoader(), response); + } +#endif + + if (handle == m_manifestHandle) { + didReceiveManifestResponse(response); + return; + } + + ASSERT(handle == m_currentHandle); + + KURL url(handle->firstRequest().url()); + if (url.hasFragmentIdentifier()) + url.removeFragmentIdentifier(); + + ASSERT(!m_currentResource); + ASSERT(m_pendingEntries.contains(url)); + + unsigned type = m_pendingEntries.get(url); + + // If this is an initial cache attempt, we should not get master resources delivered here. + if (!m_newestCache) + ASSERT(!(type & ApplicationCacheResource::Master)); + + if (m_newestCache && response.httpStatusCode() == 304) { // Not modified. + ApplicationCacheResource* newestCachedResource = m_newestCache->resourceForURL(url); + if (newestCachedResource) { + m_cacheBeingUpdated->addResource(ApplicationCacheResource::create(url, newestCachedResource->response(), type, newestCachedResource->data())); + m_pendingEntries.remove(m_currentHandle->firstRequest().url()); + m_currentHandle->cancel(); + m_currentHandle = 0; + // Load the next resource, if any. + startLoadingEntry(); + return; + } + // The server could return 304 for an unconditional request - in this case, we handle the response as a normal error. + } + + if (response.httpStatusCode() / 100 != 2 || response.url() != m_currentHandle->firstRequest().url()) { + if ((type & ApplicationCacheResource::Explicit) || (type & ApplicationCacheResource::Fallback)) { + // Note that cacheUpdateFailed() can cause the cache group to be deleted. + cacheUpdateFailed(); + } else if (response.httpStatusCode() == 404 || response.httpStatusCode() == 410) { + // Skip this resource. It is dropped from the cache. + m_currentHandle->cancel(); + m_currentHandle = 0; + m_pendingEntries.remove(url); + // Load the next resource, if any. + startLoadingEntry(); + } else { + // Copy the resource and its metadata from the newest application cache in cache group whose completeness flag is complete, and act + // as if that was the fetched resource, ignoring the resource obtained from the network. + ASSERT(m_newestCache); + ApplicationCacheResource* newestCachedResource = m_newestCache->resourceForURL(handle->firstRequest().url()); + ASSERT(newestCachedResource); + m_cacheBeingUpdated->addResource(ApplicationCacheResource::create(url, newestCachedResource->response(), type, newestCachedResource->data())); + m_pendingEntries.remove(m_currentHandle->firstRequest().url()); + m_currentHandle->cancel(); + m_currentHandle = 0; + // Load the next resource, if any. + startLoadingEntry(); + } + return; + } + + m_currentResource = ApplicationCacheResource::create(url, response, type); +} + +void ApplicationCacheGroup::didReceiveData(ResourceHandle* handle, const char* data, int length, int lengthReceived) +{ +#if ENABLE(INSPECTOR) + if (Page* page = m_frame->page()) + page->inspectorController()->didReceiveContentLength(m_currentResourceIdentifier, lengthReceived); +#else + UNUSED_PARAM(lengthReceived); +#endif + + if (handle == m_manifestHandle) { + didReceiveManifestData(data, length); + return; + } + + ASSERT(handle == m_currentHandle); + + ASSERT(m_currentResource); + m_currentResource->data()->append(data, length); + + m_loadedSize += length; +} + +void ApplicationCacheGroup::didFinishLoading(ResourceHandle* handle, double finishTime) +{ +#if ENABLE(INSPECTOR) + if (Page* page = m_frame->page()) + page->inspectorController()->didFinishLoading(m_currentResourceIdentifier, finishTime); +#endif + + if (handle == m_manifestHandle) { + didFinishLoadingManifest(); + return; + } + + // After finishing the loading of any resource, we check if it will + // fit in our last known quota limit. + if (m_availableSpaceInQuota == ApplicationCacheStorage::unknownQuota()) { + // Failed to determine what is left in the quota. Fallback to allowing anything. + if (!cacheStorage().remainingSizeForOriginExcludingCache(m_origin.get(), m_newestCache.get(), m_availableSpaceInQuota)) + m_availableSpaceInQuota = ApplicationCacheStorage::noQuota(); + } + + // Check each resource, as it loads, to see if it would fit in our + // idea of the available quota space. + if (m_availableSpaceInQuota < m_loadedSize) { + m_currentResource = 0; + cacheUpdateFailedDueToOriginQuota(); + return; + } + + ASSERT(m_currentHandle == handle); + ASSERT(m_pendingEntries.contains(handle->firstRequest().url())); + + m_pendingEntries.remove(handle->firstRequest().url()); + + ASSERT(m_cacheBeingUpdated); + + m_cacheBeingUpdated->addResource(m_currentResource.release()); + m_currentHandle = 0; + + // Load the next resource, if any. + startLoadingEntry(); +} + +void ApplicationCacheGroup::didFail(ResourceHandle* handle, const ResourceError& error) +{ +#if ENABLE(INSPECTOR) + if (Page* page = m_frame->page()) + page->inspectorController()->didFailLoading(m_currentResourceIdentifier, error); +#else + UNUSED_PARAM(error); +#endif + + if (handle == m_manifestHandle) { + cacheUpdateFailed(); + return; + } + + unsigned type = m_currentResource ? m_currentResource->type() : m_pendingEntries.get(handle->firstRequest().url()); + KURL url(handle->firstRequest().url()); + if (url.hasFragmentIdentifier()) + url.removeFragmentIdentifier(); + + ASSERT(!m_currentResource || !m_pendingEntries.contains(url)); + m_currentResource = 0; + m_pendingEntries.remove(url); + + if ((type & ApplicationCacheResource::Explicit) || (type & ApplicationCacheResource::Fallback)) { + // Note that cacheUpdateFailed() can cause the cache group to be deleted. + cacheUpdateFailed(); + } else { + // Copy the resource and its metadata from the newest application cache in cache group whose completeness flag is complete, and act + // as if that was the fetched resource, ignoring the resource obtained from the network. + ASSERT(m_newestCache); + ApplicationCacheResource* newestCachedResource = m_newestCache->resourceForURL(url); + ASSERT(newestCachedResource); + m_cacheBeingUpdated->addResource(ApplicationCacheResource::create(url, newestCachedResource->response(), type, newestCachedResource->data())); + // Load the next resource, if any. + startLoadingEntry(); + } +} + +void ApplicationCacheGroup::didReceiveManifestResponse(const ResourceResponse& response) +{ + ASSERT(!m_manifestResource); + ASSERT(m_manifestHandle); + + if (response.httpStatusCode() == 404 || response.httpStatusCode() == 410) { + manifestNotFound(); + return; + } + + if (response.httpStatusCode() == 304) + return; + + if (response.httpStatusCode() / 100 != 2 || response.url() != m_manifestHandle->firstRequest().url() || !equalIgnoringCase(response.mimeType(), "text/cache-manifest")) { + cacheUpdateFailed(); + return; + } + + m_manifestResource = ApplicationCacheResource::create(m_manifestHandle->firstRequest().url(), response, ApplicationCacheResource::Manifest); +} + +void ApplicationCacheGroup::didReceiveManifestData(const char* data, int length) +{ + if (m_manifestResource) + m_manifestResource->data()->append(data, length); +} + +void ApplicationCacheGroup::didFinishLoadingManifest() +{ + bool isUpgradeAttempt = m_newestCache; + + if (!isUpgradeAttempt && !m_manifestResource) { + // The server returned 304 Not Modified even though we didn't send a conditional request. + cacheUpdateFailed(); + return; + } + + m_manifestHandle = 0; + + // Check if the manifest was not modified. + if (isUpgradeAttempt) { + ApplicationCacheResource* newestManifest = m_newestCache->manifestResource(); + ASSERT(newestManifest); + + if (!m_manifestResource || // The resource will be null if HTTP response was 304 Not Modified. + (newestManifest->data()->size() == m_manifestResource->data()->size() && !memcmp(newestManifest->data()->data(), m_manifestResource->data()->data(), newestManifest->data()->size()))) { + + m_completionType = NoUpdate; + m_manifestResource = 0; + deliverDelayedMainResources(); + + return; + } + } + + Manifest manifest; + if (!parseManifest(m_manifestURL, m_manifestResource->data()->data(), m_manifestResource->data()->size(), manifest)) { + cacheUpdateFailed(); + return; + } + + ASSERT(!m_cacheBeingUpdated); + m_cacheBeingUpdated = ApplicationCache::create(); + m_cacheBeingUpdated->setGroup(this); + + HashSet<DocumentLoader*>::const_iterator masterEnd = m_pendingMasterResourceLoaders.end(); + for (HashSet<DocumentLoader*>::const_iterator iter = m_pendingMasterResourceLoaders.begin(); iter != masterEnd; ++iter) + associateDocumentLoaderWithCache(*iter, m_cacheBeingUpdated.get()); + + // We have the manifest, now download the resources. + setUpdateStatus(Downloading); + + postListenerTask(ApplicationCacheHost::DOWNLOADING_EVENT, m_associatedDocumentLoaders); + + ASSERT(m_pendingEntries.isEmpty()); + + if (isUpgradeAttempt) { + ApplicationCache::ResourceMap::const_iterator end = m_newestCache->end(); + for (ApplicationCache::ResourceMap::const_iterator it = m_newestCache->begin(); it != end; ++it) { + unsigned type = it->second->type(); + if (type & ApplicationCacheResource::Master) + addEntry(it->first, type); + } + } + + HashSet<String>::const_iterator end = manifest.explicitURLs.end(); + for (HashSet<String>::const_iterator it = manifest.explicitURLs.begin(); it != end; ++it) + addEntry(*it, ApplicationCacheResource::Explicit); + + size_t fallbackCount = manifest.fallbackURLs.size(); + for (size_t i = 0; i < fallbackCount; ++i) + addEntry(manifest.fallbackURLs[i].second, ApplicationCacheResource::Fallback); + + m_cacheBeingUpdated->setOnlineWhitelist(manifest.onlineWhitelistedURLs); + m_cacheBeingUpdated->setFallbackURLs(manifest.fallbackURLs); + m_cacheBeingUpdated->setAllowsAllNetworkRequests(manifest.allowAllNetworkRequests); + + m_progressTotal = m_pendingEntries.size(); + m_progressDone = 0; + + startLoadingEntry(); +} + +void ApplicationCacheGroup::didReachMaxAppCacheSize() +{ + ASSERT(m_frame); + ASSERT(m_cacheBeingUpdated); + m_frame->page()->chrome()->client()->reachedMaxAppCacheSize(cacheStorage().spaceNeeded(m_cacheBeingUpdated->estimatedSizeInStorage())); + m_calledReachedMaxAppCacheSize = true; + checkIfLoadIsComplete(); +} + +void ApplicationCacheGroup::didReachOriginQuota(PassRefPtr<Frame> frame) +{ + // Inform the client the origin quota has been reached, + // they may decide to increase the quota. + frame->page()->chrome()->client()->reachedApplicationCacheOriginQuota(m_origin.get()); +} + +void ApplicationCacheGroup::cacheUpdateFailed() +{ + stopLoading(); + m_manifestResource = 0; + + // Wait for master resource loads to finish. + m_completionType = Failure; + deliverDelayedMainResources(); +} + +void ApplicationCacheGroup::cacheUpdateFailedDueToOriginQuota() +{ + if (!m_originQuotaReached) { + m_originQuotaReached = true; + scheduleReachedOriginQuotaCallback(); + } + + cacheUpdateFailed(); +} + +void ApplicationCacheGroup::manifestNotFound() +{ + makeObsolete(); + + postListenerTask(ApplicationCacheHost::OBSOLETE_EVENT, m_associatedDocumentLoaders); + postListenerTask(ApplicationCacheHost::ERROR_EVENT, m_pendingMasterResourceLoaders); + + stopLoading(); + + ASSERT(m_pendingEntries.isEmpty()); + m_manifestResource = 0; + + while (!m_pendingMasterResourceLoaders.isEmpty()) { + HashSet<DocumentLoader*>::iterator it = m_pendingMasterResourceLoaders.begin(); + + ASSERT((*it)->applicationCacheHost()->candidateApplicationCacheGroup() == this); + ASSERT(!(*it)->applicationCacheHost()->applicationCache()); + (*it)->applicationCacheHost()->setCandidateApplicationCacheGroup(0); + m_pendingMasterResourceLoaders.remove(it); + } + + m_downloadingPendingMasterResourceLoadersCount = 0; + setUpdateStatus(Idle); + m_frame = 0; + + if (m_caches.isEmpty()) { + ASSERT(m_associatedDocumentLoaders.isEmpty()); + ASSERT(!m_cacheBeingUpdated); + delete this; + } +} + +void ApplicationCacheGroup::checkIfLoadIsComplete() +{ + if (m_manifestHandle || !m_pendingEntries.isEmpty() || m_downloadingPendingMasterResourceLoadersCount) + return; + + // We're done, all resources have finished downloading (successfully or not). + + bool isUpgradeAttempt = m_newestCache; + + switch (m_completionType) { + case None: + ASSERT_NOT_REACHED(); + return; + case NoUpdate: + ASSERT(isUpgradeAttempt); + ASSERT(!m_cacheBeingUpdated); + + // The storage could have been manually emptied by the user. + if (!m_storageID) + cacheStorage().storeNewestCache(this); + + postListenerTask(ApplicationCacheHost::NOUPDATE_EVENT, m_associatedDocumentLoaders); + break; + case Failure: + ASSERT(!m_cacheBeingUpdated); + postListenerTask(ApplicationCacheHost::ERROR_EVENT, m_associatedDocumentLoaders); + if (m_caches.isEmpty()) { + ASSERT(m_associatedDocumentLoaders.isEmpty()); + delete this; + return; + } + break; + case Completed: { + // FIXME: Fetch the resource from manifest URL again, and check whether it is identical to the one used for update (in case the application was upgraded server-side in the meanwhile). (<rdar://problem/6467625>) + + ASSERT(m_cacheBeingUpdated); + if (m_manifestResource) + m_cacheBeingUpdated->setManifestResource(m_manifestResource.release()); + else { + // We can get here as a result of retrying the Complete step, following + // a failure of the cache storage to save the newest cache due to hitting + // the maximum size. In such a case, m_manifestResource may be 0, as + // the manifest was already set on the newest cache object. + ASSERT(cacheStorage().isMaximumSizeReached() && m_calledReachedMaxAppCacheSize); + } + + ApplicationCacheStorage::FailureReason failureReason; + RefPtr<ApplicationCache> oldNewestCache = (m_newestCache == m_cacheBeingUpdated) ? 0 : m_newestCache; + setNewestCache(m_cacheBeingUpdated.release()); + if (cacheStorage().storeNewestCache(this, oldNewestCache.get(), failureReason)) { + // New cache stored, now remove the old cache. + if (oldNewestCache) + cacheStorage().remove(oldNewestCache.get()); + + // Fire the final progress event. + ASSERT(m_progressDone == m_progressTotal); + postListenerTask(ApplicationCacheHost::PROGRESS_EVENT, m_progressTotal, m_progressDone, m_associatedDocumentLoaders); + + // Fire the success event. + postListenerTask(isUpgradeAttempt ? ApplicationCacheHost::UPDATEREADY_EVENT : ApplicationCacheHost::CACHED_EVENT, m_associatedDocumentLoaders); + // It is clear that the origin quota was not reached, so clear the flag if it was set. + m_originQuotaReached = false; + } else { + if (failureReason == ApplicationCacheStorage::OriginQuotaReached) { + // We ran out of space for this origin. Roll back to previous state. + if (oldNewestCache) + setNewestCache(oldNewestCache.release()); + cacheUpdateFailedDueToOriginQuota(); + return; + } + + if (failureReason == ApplicationCacheStorage::TotalQuotaReached && !m_calledReachedMaxAppCacheSize) { + // We ran out of space. All the changes in the cache storage have + // been rolled back. We roll back to the previous state in here, + // as well, call the chrome client asynchronously and retry to + // save the new cache. + + // Save a reference to the new cache. + m_cacheBeingUpdated = m_newestCache.release(); + if (oldNewestCache) { + // Reinstate the oldNewestCache. + setNewestCache(oldNewestCache.release()); + } + scheduleReachedMaxAppCacheSizeCallback(); + return; + } + + // Run the "cache failure steps" + // Fire the error events to all pending master entries, as well any other cache hosts + // currently associated with a cache in this group. + postListenerTask(ApplicationCacheHost::ERROR_EVENT, m_associatedDocumentLoaders); + // Disassociate the pending master entries from the failed new cache. Note that + // all other loaders in the m_associatedDocumentLoaders are still associated with + // some other cache in this group. They are not associated with the failed new cache. + + // Need to copy loaders, because the cache group may be destroyed at the end of iteration. + Vector<DocumentLoader*> loaders; + copyToVector(m_pendingMasterResourceLoaders, loaders); + size_t count = loaders.size(); + for (size_t i = 0; i != count; ++i) + disassociateDocumentLoader(loaders[i]); // This can delete this group. + + // Reinstate the oldNewestCache, if there was one. + if (oldNewestCache) { + // This will discard the failed new cache. + setNewestCache(oldNewestCache.release()); + } else { + // We must have been deleted by the last call to disassociateDocumentLoader(). + return; + } + } + break; + } + } + + // Empty cache group's list of pending master entries. + m_pendingMasterResourceLoaders.clear(); + m_completionType = None; + setUpdateStatus(Idle); + m_frame = 0; + m_loadedSize = 0; + m_availableSpaceInQuota = ApplicationCacheStorage::unknownQuota(); + m_calledReachedMaxAppCacheSize = false; +} + +void ApplicationCacheGroup::startLoadingEntry() +{ + ASSERT(m_cacheBeingUpdated); + + if (m_pendingEntries.isEmpty()) { + m_completionType = Completed; + deliverDelayedMainResources(); + return; + } + + EntryMap::const_iterator it = m_pendingEntries.begin(); + + postListenerTask(ApplicationCacheHost::PROGRESS_EVENT, m_progressTotal, m_progressDone, m_associatedDocumentLoaders); + m_progressDone++; + + ASSERT(!m_currentHandle); + + m_currentHandle = createResourceHandle(KURL(ParsedURLString, it->first), m_newestCache ? m_newestCache->resourceForURL(it->first) : 0); +} + +void ApplicationCacheGroup::deliverDelayedMainResources() +{ + // Need to copy loaders, because the cache group may be destroyed at the end of iteration. + Vector<DocumentLoader*> loaders; + copyToVector(m_pendingMasterResourceLoaders, loaders); + size_t count = loaders.size(); + for (size_t i = 0; i != count; ++i) { + DocumentLoader* loader = loaders[i]; + if (loader->isLoadingMainResource()) + continue; + + const ResourceError& error = loader->mainDocumentError(); + if (error.isNull()) + finishedLoadingMainResource(loader); + else + failedLoadingMainResource(loader); + } + if (!count) + checkIfLoadIsComplete(); +} + +void ApplicationCacheGroup::addEntry(const String& url, unsigned type) +{ + ASSERT(m_cacheBeingUpdated); + ASSERT(!KURL(ParsedURLString, url).hasFragmentIdentifier()); + + // Don't add the URL if we already have an master resource in the cache + // (i.e., the main resource finished loading before the manifest). + if (ApplicationCacheResource* resource = m_cacheBeingUpdated->resourceForURL(url)) { + ASSERT(resource->type() & ApplicationCacheResource::Master); + ASSERT(!m_frame->loader()->documentLoader()->isLoadingMainResource()); + + resource->addType(type); + return; + } + + // Don't add the URL if it's the same as the manifest URL. + ASSERT(m_manifestResource); + if (m_manifestResource->url() == url) { + m_manifestResource->addType(type); + return; + } + + pair<EntryMap::iterator, bool> result = m_pendingEntries.add(url, type); + + if (!result.second) + result.first->second |= type; +} + +void ApplicationCacheGroup::associateDocumentLoaderWithCache(DocumentLoader* loader, ApplicationCache* cache) +{ + // If teardown started already, revive the group. + if (!m_newestCache && !m_cacheBeingUpdated) + m_newestCache = cache; + + ASSERT(!m_isObsolete); + + loader->applicationCacheHost()->setApplicationCache(cache); + + ASSERT(!m_associatedDocumentLoaders.contains(loader)); + m_associatedDocumentLoaders.add(loader); +} + +class ChromeClientCallbackTimer: public TimerBase { +public: + ChromeClientCallbackTimer(ApplicationCacheGroup* cacheGroup) + : m_cacheGroup(cacheGroup) + { + } + +private: + virtual void fired() + { + m_cacheGroup->didReachMaxAppCacheSize(); + delete this; + } + // Note that there is no need to use a RefPtr here. The ApplicationCacheGroup instance is guaranteed + // to be alive when the timer fires since invoking the ChromeClient callback is part of its normal + // update machinery and nothing can yet cause it to get deleted. + ApplicationCacheGroup* m_cacheGroup; +}; + +void ApplicationCacheGroup::scheduleReachedMaxAppCacheSizeCallback() +{ + ASSERT(isMainThread()); + ChromeClientCallbackTimer* timer = new ChromeClientCallbackTimer(this); + timer->startOneShot(0); + // The timer will delete itself once it fires. +} + +void ApplicationCacheGroup::scheduleReachedOriginQuotaCallback() +{ + // FIXME: it might be nice to run this asynchronously, because there is no return value to wait for. + didReachOriginQuota(m_frame); +} + +class CallCacheListenerTask : public ScriptExecutionContext::Task { +public: + static PassOwnPtr<CallCacheListenerTask> create(PassRefPtr<DocumentLoader> loader, ApplicationCacheHost::EventID eventID, int progressTotal, int progressDone) + { + return adoptPtr(new CallCacheListenerTask(loader, eventID, progressTotal, progressDone)); + } + + virtual void performTask(ScriptExecutionContext* context) + { + + ASSERT_UNUSED(context, context->isDocument()); + Frame* frame = m_documentLoader->frame(); + if (!frame) + return; + + ASSERT(frame->loader()->documentLoader() == m_documentLoader.get()); + + m_documentLoader->applicationCacheHost()->notifyDOMApplicationCache(m_eventID, m_progressTotal, m_progressDone); + } + +private: + CallCacheListenerTask(PassRefPtr<DocumentLoader> loader, ApplicationCacheHost::EventID eventID, int progressTotal, int progressDone) + : m_documentLoader(loader) + , m_eventID(eventID) + , m_progressTotal(progressTotal) + , m_progressDone(progressDone) + { + } + + RefPtr<DocumentLoader> m_documentLoader; + ApplicationCacheHost::EventID m_eventID; + int m_progressTotal; + int m_progressDone; +}; + +void ApplicationCacheGroup::postListenerTask(ApplicationCacheHost::EventID eventID, int progressTotal, int progressDone, const HashSet<DocumentLoader*>& loaderSet) +{ + HashSet<DocumentLoader*>::const_iterator loaderSetEnd = loaderSet.end(); + for (HashSet<DocumentLoader*>::const_iterator iter = loaderSet.begin(); iter != loaderSetEnd; ++iter) + postListenerTask(eventID, progressTotal, progressDone, *iter); +} + +void ApplicationCacheGroup::postListenerTask(ApplicationCacheHost::EventID eventID, int progressTotal, int progressDone, DocumentLoader* loader) +{ + Frame* frame = loader->frame(); + if (!frame) + return; + + ASSERT(frame->loader()->documentLoader() == loader); + + frame->document()->postTask(CallCacheListenerTask::create(loader, eventID, progressTotal, progressDone)); +} + +void ApplicationCacheGroup::setUpdateStatus(UpdateStatus status) +{ + m_updateStatus = status; +#if ENABLE(INSPECTOR) + inspectorUpdateApplicationCacheStatus(m_frame); +#endif +} + +void ApplicationCacheGroup::clearStorageID() +{ + m_storageID = 0; + + HashSet<ApplicationCache*>::const_iterator end = m_caches.end(); + for (HashSet<ApplicationCache*>::const_iterator it = m_caches.begin(); it != end; ++it) + (*it)->clearStorageID(); +} + + +} + +#endif // ENABLE(OFFLINE_WEB_APPLICATIONS) diff --git a/Source/WebCore/loader/appcache/ApplicationCacheGroup.h b/Source/WebCore/loader/appcache/ApplicationCacheGroup.h new file mode 100644 index 0000000..29d0749 --- /dev/null +++ b/Source/WebCore/loader/appcache/ApplicationCacheGroup.h @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2008, 2009, 2010 Apple Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef ApplicationCacheGroup_h +#define ApplicationCacheGroup_h + +#if ENABLE(OFFLINE_WEB_APPLICATIONS) + +#include "DOMApplicationCache.h" +#include "KURL.h" +#include "PlatformString.h" +#include "ResourceHandle.h" +#include "ResourceHandleClient.h" +#include "SharedBuffer.h" + +#include <wtf/Noncopyable.h> +#include <wtf/HashMap.h> +#include <wtf/HashSet.h> + +namespace WebCore { + +class ApplicationCache; +class ApplicationCacheResource; +class Document; +class DocumentLoader; +class Frame; +class SecurityOrigin; + +enum ApplicationCacheUpdateOption { + ApplicationCacheUpdateWithBrowsingContext, + ApplicationCacheUpdateWithoutBrowsingContext +}; + +class ApplicationCacheGroup : public Noncopyable, ResourceHandleClient { +public: + ApplicationCacheGroup(const KURL& manifestURL, bool isCopy = false); + ~ApplicationCacheGroup(); + + enum UpdateStatus { Idle, Checking, Downloading }; + + static ApplicationCache* cacheForMainRequest(const ResourceRequest&, DocumentLoader*); + static ApplicationCache* fallbackCacheForMainRequest(const ResourceRequest&, DocumentLoader*); + + static void selectCache(Frame*, const KURL& manifestURL); + static void selectCacheWithoutManifestURL(Frame*); + + const KURL& manifestURL() const { return m_manifestURL; } + const SecurityOrigin* origin() const { return m_origin.get(); } + UpdateStatus updateStatus() const { return m_updateStatus; } + void setUpdateStatus(UpdateStatus status); + + void setStorageID(unsigned storageID) { m_storageID = storageID; } + unsigned storageID() const { return m_storageID; } + void clearStorageID(); + + void update(Frame*, ApplicationCacheUpdateOption); // FIXME: Frame should not be needed when updating without browsing context. + void cacheDestroyed(ApplicationCache*); + + bool cacheIsBeingUpdated(const ApplicationCache* cache) const { return cache == m_cacheBeingUpdated; } + + void stopLoadingInFrame(Frame*); + + ApplicationCache* newestCache() const { return m_newestCache.get(); } + void setNewestCache(PassRefPtr<ApplicationCache>); + + void makeObsolete(); + bool isObsolete() const { return m_isObsolete; } + + void finishedLoadingMainResource(DocumentLoader*); + void failedLoadingMainResource(DocumentLoader*); + + void disassociateDocumentLoader(DocumentLoader*); + + bool isCopy() const { return m_isCopy; } + +private: + static void postListenerTask(ApplicationCacheHost::EventID id, const HashSet<DocumentLoader*>& set) { postListenerTask(id, 0, 0, set); } + static void postListenerTask(ApplicationCacheHost::EventID id, DocumentLoader* loader) { postListenerTask(id, 0, 0, loader); } + static void postListenerTask(ApplicationCacheHost::EventID, int progressTotal, int progressDone, const HashSet<DocumentLoader*>&); + static void postListenerTask(ApplicationCacheHost::EventID, int progressTotal, int progressDone, DocumentLoader*); + + void scheduleReachedMaxAppCacheSizeCallback(); + void scheduleReachedOriginQuotaCallback(); + + PassRefPtr<ResourceHandle> createResourceHandle(const KURL&, ApplicationCacheResource* newestCachedResource); + + // For normal resource loading, WebKit client is asked about each resource individually. Since application cache does not belong to any particular document, + // the existing client callback cannot be used, so assume that any client that enables application cache also wants it to use credential storage. + virtual bool shouldUseCredentialStorage(ResourceHandle*) { return true; } + +#if ENABLE(INSPECTOR) + virtual void willSendRequest(ResourceHandle*, ResourceRequest&, const ResourceResponse&); +#endif + virtual void didReceiveResponse(ResourceHandle*, const ResourceResponse&); + virtual void didReceiveData(ResourceHandle*, const char*, int length, int lengthReceived); + virtual void didFinishLoading(ResourceHandle*, double finishTime); + virtual void didFail(ResourceHandle*, const ResourceError&); + + void didReceiveManifestResponse(const ResourceResponse&); + void didReceiveManifestData(const char*, int); + void didFinishLoadingManifest(); + void didReachMaxAppCacheSize(); + void didReachOriginQuota(PassRefPtr<Frame> frame); + + void startLoadingEntry(); + void deliverDelayedMainResources(); + void checkIfLoadIsComplete(); + void cacheUpdateFailed(); + void cacheUpdateFailedDueToOriginQuota(); + void manifestNotFound(); + + void addEntry(const String&, unsigned type); + + void associateDocumentLoaderWithCache(DocumentLoader*, ApplicationCache*); + + void stopLoading(); + + KURL m_manifestURL; + RefPtr<SecurityOrigin> m_origin; + UpdateStatus m_updateStatus; + + // This is the newest complete cache in the group. + RefPtr<ApplicationCache> m_newestCache; + + // All complete caches in this cache group. + HashSet<ApplicationCache*> m_caches; + + // The cache being updated (if any). Note that cache updating does not immediately create a new + // ApplicationCache object, so this may be null even when update status is not Idle. + RefPtr<ApplicationCache> m_cacheBeingUpdated; + + // List of pending master entries, used during the update process to ensure that new master entries are cached. + HashSet<DocumentLoader*> m_pendingMasterResourceLoaders; + // How many of the above pending master entries have not yet finished downloading. + int m_downloadingPendingMasterResourceLoadersCount; + + // These are all the document loaders that are associated with a cache in this group. + HashSet<DocumentLoader*> m_associatedDocumentLoaders; + + // The URLs and types of pending cache entries. + typedef HashMap<String, unsigned> EntryMap; + EntryMap m_pendingEntries; + + // The total number of items to be processed to update the cache group and the number that have been done. + int m_progressTotal; + int m_progressDone; + + // Frame used for fetching resources when updating. + // FIXME: An update started by a particular frame should not stop if it is destroyed, but there are other frames associated with the same cache group. + Frame* m_frame; + + // An obsolete cache group is never stored, but the opposite is not true - storing may fail for multiple reasons, such as exceeding disk quota. + unsigned m_storageID; + bool m_isObsolete; + + // During update, this is used to handle asynchronously arriving results. + enum CompletionType { + None, + NoUpdate, + Failure, + Completed + }; + CompletionType m_completionType; + + // Whether this cache group is a copy that's only used for transferring the cache to another file. + bool m_isCopy; + + // This flag is set immediately after the ChromeClient::reachedMaxAppCacheSize() callback is invoked as a result of the storage layer failing to save a cache + // due to reaching the maximum size of the application cache database file. This flag is used by ApplicationCacheGroup::checkIfLoadIsComplete() to decide + // the course of action in case of this failure (i.e. call the ChromeClient callback or run the failure steps). + bool m_calledReachedMaxAppCacheSize; + + RefPtr<ResourceHandle> m_currentHandle; + RefPtr<ApplicationCacheResource> m_currentResource; + +#if ENABLE(INSPECTOR) + unsigned long m_currentResourceIdentifier; +#endif + + RefPtr<ApplicationCacheResource> m_manifestResource; + RefPtr<ResourceHandle> m_manifestHandle; + + int64_t m_loadedSize; + int64_t m_availableSpaceInQuota; + bool m_originQuotaReached; + + friend class ChromeClientCallbackTimer; + friend class OriginQuotaReachedCallbackTimer; +}; + +} // namespace WebCore + +#endif // ENABLE(OFFLINE_WEB_APPLICATIONS) + +#endif // ApplicationCacheGroup_h diff --git a/Source/WebCore/loader/appcache/ApplicationCacheHost.cpp b/Source/WebCore/loader/appcache/ApplicationCacheHost.cpp new file mode 100644 index 0000000..d5707cf --- /dev/null +++ b/Source/WebCore/loader/appcache/ApplicationCacheHost.cpp @@ -0,0 +1,466 @@ +/* + * Copyright (C) 2008, 2009 Apple Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "ApplicationCacheHost.h" + +#if ENABLE(OFFLINE_WEB_APPLICATIONS) + +#include "ApplicationCache.h" +#include "ApplicationCacheGroup.h" +#include "ApplicationCacheResource.h" +#include "DocumentLoader.h" +#include "DOMApplicationCache.h" +#include "Frame.h" +#include "FrameLoader.h" +#include "FrameLoaderClient.h" +#include "MainResourceLoader.h" +#include "ProgressEvent.h" +#include "ResourceLoader.h" +#include "ResourceRequest.h" +#include "Settings.h" + +namespace WebCore { + +ApplicationCacheHost::ApplicationCacheHost(DocumentLoader* documentLoader) + : m_domApplicationCache(0) + , m_documentLoader(documentLoader) + , m_defersEvents(true) + , m_candidateApplicationCacheGroup(0) +{ + ASSERT(m_documentLoader); +} + +ApplicationCacheHost::~ApplicationCacheHost() +{ + ASSERT(!m_applicationCache || !m_candidateApplicationCacheGroup || m_applicationCache->group() == m_candidateApplicationCacheGroup); + + if (m_applicationCache) + m_applicationCache->group()->disassociateDocumentLoader(m_documentLoader); + else if (m_candidateApplicationCacheGroup) + m_candidateApplicationCacheGroup->disassociateDocumentLoader(m_documentLoader); +} + +void ApplicationCacheHost::selectCacheWithoutManifest() +{ + ApplicationCacheGroup::selectCacheWithoutManifestURL(m_documentLoader->frame()); +} + +void ApplicationCacheHost::selectCacheWithManifest(const KURL& manifestURL) +{ + ApplicationCacheGroup::selectCache(m_documentLoader->frame(), manifestURL); +} + +void ApplicationCacheHost::maybeLoadMainResource(ResourceRequest& request, SubstituteData& substituteData) +{ + // Check if this request should be loaded from the application cache + if (!substituteData.isValid() && isApplicationCacheEnabled()) { + ASSERT(!m_mainResourceApplicationCache); + + m_mainResourceApplicationCache = ApplicationCacheGroup::cacheForMainRequest(request, m_documentLoader); + + if (m_mainResourceApplicationCache) { + // Get the resource from the application cache. By definition, cacheForMainRequest() returns a cache that contains the resource. + ApplicationCacheResource* resource = m_mainResourceApplicationCache->resourceForRequest(request); + substituteData = SubstituteData(resource->data(), + resource->response().mimeType(), + resource->response().textEncodingName(), KURL()); + } + } +} + +void ApplicationCacheHost::maybeLoadMainResourceForRedirect(ResourceRequest& request, SubstituteData& substituteData) +{ + ASSERT(status() == UNCACHED); + maybeLoadMainResource(request, substituteData); +} + +bool ApplicationCacheHost::maybeLoadFallbackForMainResponse(const ResourceRequest& request, const ResourceResponse& r) +{ + if (r.httpStatusCode() / 100 == 4 || r.httpStatusCode() / 100 == 5) { + ASSERT(!m_mainResourceApplicationCache); + if (isApplicationCacheEnabled()) { + m_mainResourceApplicationCache = ApplicationCacheGroup::fallbackCacheForMainRequest(request, documentLoader()); + + if (scheduleLoadFallbackResourceFromApplicationCache(documentLoader()->mainResourceLoader(), m_mainResourceApplicationCache.get())) + return true; + } + } + return false; +} + +bool ApplicationCacheHost::maybeLoadFallbackForMainError(const ResourceRequest& request, const ResourceError& error) +{ + if (!error.isCancellation()) { + ASSERT(!m_mainResourceApplicationCache); + if (isApplicationCacheEnabled()) { + m_mainResourceApplicationCache = ApplicationCacheGroup::fallbackCacheForMainRequest(request, m_documentLoader); + + if (scheduleLoadFallbackResourceFromApplicationCache(documentLoader()->mainResourceLoader(), m_mainResourceApplicationCache.get())) + return true; + } + } + return false; +} + +void ApplicationCacheHost::mainResourceDataReceived(const char*, int, long long, bool) +{ + // This method is here to facilitate alternate implemetations of this interface by the host browser. +} + +void ApplicationCacheHost::failedLoadingMainResource() +{ + ApplicationCacheGroup* group = m_candidateApplicationCacheGroup; + if (!group && m_applicationCache) { + if (mainResourceApplicationCache()) { + // Even when the main resource is being loaded from an application cache, loading can fail if aborted. + return; + } + group = m_applicationCache->group(); + } + + if (group) + group->failedLoadingMainResource(m_documentLoader); +} + +void ApplicationCacheHost::finishedLoadingMainResource() +{ + ApplicationCacheGroup* group = candidateApplicationCacheGroup(); + if (!group && applicationCache() && !mainResourceApplicationCache()) + group = applicationCache()->group(); + + if (group) + group->finishedLoadingMainResource(m_documentLoader); +} + +bool ApplicationCacheHost::maybeLoadResource(ResourceLoader* loader, ResourceRequest& request, const KURL& originalURL) +{ + if (!isApplicationCacheEnabled()) + return false; + + if (request.url() != originalURL) + return false; + + ApplicationCacheResource* resource; + if (!shouldLoadResourceFromApplicationCache(request, resource)) + return false; + + m_documentLoader->m_pendingSubstituteResources.set(loader, resource); + m_documentLoader->deliverSubstituteResourcesAfterDelay(); + + return true; +} + +bool ApplicationCacheHost::maybeLoadFallbackForRedirect(ResourceLoader* resourceLoader, ResourceRequest& request, const ResourceResponse& redirectResponse) +{ + if (!redirectResponse.isNull() && !protocolHostAndPortAreEqual(request.url(), redirectResponse.url())) + if (scheduleLoadFallbackResourceFromApplicationCache(resourceLoader)) + return true; + return false; +} + +bool ApplicationCacheHost::maybeLoadFallbackForResponse(ResourceLoader* resourceLoader, const ResourceResponse& response) +{ + if (response.httpStatusCode() / 100 == 4 || response.httpStatusCode() / 100 == 5) + if (scheduleLoadFallbackResourceFromApplicationCache(resourceLoader)) + return true; + return false; +} + +bool ApplicationCacheHost::maybeLoadFallbackForError(ResourceLoader* resourceLoader, const ResourceError& error) +{ + if (!error.isCancellation()) + if (scheduleLoadFallbackResourceFromApplicationCache(resourceLoader)) + return true; + return false; +} + +bool ApplicationCacheHost::maybeLoadSynchronously(ResourceRequest& request, ResourceError& error, ResourceResponse& response, Vector<char>& data) +{ + ApplicationCacheResource* resource; + if (shouldLoadResourceFromApplicationCache(request, resource)) { + if (resource) { + response = resource->response(); + data.append(resource->data()->data(), resource->data()->size()); + } else { + error = documentLoader()->frameLoader()->client()->cannotShowURLError(request); + } + return true; + } + return false; +} + +void ApplicationCacheHost::maybeLoadFallbackSynchronously(const ResourceRequest& request, ResourceError& error, ResourceResponse& response, Vector<char>& data) +{ + // If normal loading results in a redirect to a resource with another origin (indicative of a captive portal), or a 4xx or 5xx status code or equivalent, + // or if there were network errors (but not if the user canceled the download), then instead get, from the cache, the resource of the fallback entry + // corresponding to the matched namespace. + if ((!error.isNull() && !error.isCancellation()) + || response.httpStatusCode() / 100 == 4 || response.httpStatusCode() / 100 == 5 + || !protocolHostAndPortAreEqual(request.url(), response.url())) { + ApplicationCacheResource* resource; + if (getApplicationCacheFallbackResource(request, resource)) { + response = resource->response(); + data.clear(); + data.append(resource->data()->data(), resource->data()->size()); + } + } +} + +bool ApplicationCacheHost::canCacheInPageCache() const +{ + return !applicationCache() && !candidateApplicationCacheGroup(); +} + +void ApplicationCacheHost::setDOMApplicationCache(DOMApplicationCache* domApplicationCache) +{ + ASSERT(!m_domApplicationCache || !domApplicationCache); + m_domApplicationCache = domApplicationCache; +} + +void ApplicationCacheHost::notifyDOMApplicationCache(EventID id, int total, int done) +{ + if (m_defersEvents) { + // Event dispatching is deferred until document.onload has fired. + m_deferredEvents.append(DeferredEvent(id, total, done)); + return; + } + dispatchDOMEvent(id, total, done); +} + +void ApplicationCacheHost::stopLoadingInFrame(Frame* frame) +{ + ASSERT(!m_applicationCache || !m_candidateApplicationCacheGroup || m_applicationCache->group() == m_candidateApplicationCacheGroup); + + if (m_candidateApplicationCacheGroup) + m_candidateApplicationCacheGroup->stopLoadingInFrame(frame); + else if (m_applicationCache) + m_applicationCache->group()->stopLoadingInFrame(frame); +} + +void ApplicationCacheHost::stopDeferringEvents() +{ + RefPtr<DocumentLoader> protect(documentLoader()); + for (unsigned i = 0; i < m_deferredEvents.size(); ++i) { + const DeferredEvent& deferred = m_deferredEvents[i]; + dispatchDOMEvent(deferred.eventID, deferred.progressTotal, deferred.progressDone); + } + m_deferredEvents.clear(); + m_defersEvents = false; +} + +#if ENABLE(INSPECTOR) +void ApplicationCacheHost::fillResourceList(ResourceInfoList* resources) +{ + ApplicationCache* cache = applicationCache(); + if (!cache || !cache->isComplete()) + return; + + ApplicationCache::ResourceMap::const_iterator end = cache->end(); + for (ApplicationCache::ResourceMap::const_iterator it = cache->begin(); it != end; ++it) { + RefPtr<ApplicationCacheResource> resource = it->second; + unsigned type = resource->type(); + bool isMaster = type & ApplicationCacheResource::Master; + bool isManifest = type & ApplicationCacheResource::Manifest; + bool isExplicit = type & ApplicationCacheResource::Explicit; + bool isForeign = type & ApplicationCacheResource::Foreign; + bool isFallback = type & ApplicationCacheResource::Fallback; + resources->append(ResourceInfo(resource->url(), isMaster, isManifest, isFallback, isForeign, isExplicit, resource->estimatedSizeInStorage())); + } +} + +ApplicationCacheHost::CacheInfo ApplicationCacheHost::applicationCacheInfo() +{ + ApplicationCache* cache = applicationCache(); + if (!cache || !cache->isComplete()) + return CacheInfo(KURL(), 0, 0, 0); + + // FIXME: Add "Creation Time" and "Update Time" to Application Caches. + return CacheInfo(cache->manifestResource()->url(), 0, 0, cache->estimatedSizeInStorage()); +} +#endif + +void ApplicationCacheHost::dispatchDOMEvent(EventID id, int total, int done) +{ + if (m_domApplicationCache) { + const AtomicString& eventType = DOMApplicationCache::toEventType(id); + ExceptionCode ec = 0; + RefPtr<Event> event; + if (id == PROGRESS_EVENT) + event = ProgressEvent::create(eventType, true, done, total); + else + event = Event::create(eventType, false, false); + m_domApplicationCache->dispatchEvent(event, ec); + ASSERT(!ec); + } +} + +void ApplicationCacheHost::setCandidateApplicationCacheGroup(ApplicationCacheGroup* group) +{ + ASSERT(!m_applicationCache); + m_candidateApplicationCacheGroup = group; +} + +void ApplicationCacheHost::setApplicationCache(PassRefPtr<ApplicationCache> applicationCache) +{ + if (m_candidateApplicationCacheGroup) { + ASSERT(!m_applicationCache); + m_candidateApplicationCacheGroup = 0; + } + + m_applicationCache = applicationCache; +} + +bool ApplicationCacheHost::shouldLoadResourceFromApplicationCache(const ResourceRequest& request, ApplicationCacheResource*& resource) +{ + ApplicationCache* cache = applicationCache(); + if (!cache || !cache->isComplete()) + return false; + + // If the resource is not to be fetched using the HTTP GET mechanism or equivalent, or if its URL has a different + // <scheme> component than the application cache's manifest, then fetch the resource normally. + if (!ApplicationCache::requestIsHTTPOrHTTPSGet(request) || !equalIgnoringCase(request.url().protocol(), cache->manifestResource()->url().protocol())) + return false; + + // If the resource's URL is an master entry, the manifest, an explicit entry, or a fallback entry + // in the application cache, then get the resource from the cache (instead of fetching it). + resource = cache->resourceForURL(request.url()); + + // Resources that match fallback namespaces or online whitelist entries are fetched from the network, + // unless they are also cached. + if (!resource && (cache->urlMatchesFallbackNamespace(request.url()) || cache->isURLInOnlineWhitelist(request.url()))) + return false; + + // Resources that are not present in the manifest will always fail to load (at least, after the + // cache has been primed the first time), making the testing of offline applications simpler. + return true; +} + +bool ApplicationCacheHost::getApplicationCacheFallbackResource(const ResourceRequest& request, ApplicationCacheResource*& resource, ApplicationCache* cache) +{ + if (!cache) { + cache = applicationCache(); + if (!cache) + return false; + } + if (!cache->isComplete()) + return false; + + // If the resource is not a HTTP/HTTPS GET, then abort + if (!ApplicationCache::requestIsHTTPOrHTTPSGet(request)) + return false; + + KURL fallbackURL; + if (!cache->urlMatchesFallbackNamespace(request.url(), &fallbackURL)) + return false; + + resource = cache->resourceForURL(fallbackURL); + ASSERT(resource); + + return true; +} + +bool ApplicationCacheHost::scheduleLoadFallbackResourceFromApplicationCache(ResourceLoader* loader, ApplicationCache* cache) +{ + if (!isApplicationCacheEnabled()) + return false; + + ApplicationCacheResource* resource; + if (!getApplicationCacheFallbackResource(loader->request(), resource, cache)) + return false; + + m_documentLoader->m_pendingSubstituteResources.set(loader, resource); + m_documentLoader->deliverSubstituteResourcesAfterDelay(); + + loader->handle()->cancel(); + + return true; +} + +ApplicationCacheHost::Status ApplicationCacheHost::status() const +{ + ApplicationCache* cache = applicationCache(); + if (!cache) + return UNCACHED; + + switch (cache->group()->updateStatus()) { + case ApplicationCacheGroup::Checking: + return CHECKING; + case ApplicationCacheGroup::Downloading: + return DOWNLOADING; + case ApplicationCacheGroup::Idle: { + if (cache->group()->isObsolete()) + return OBSOLETE; + if (cache != cache->group()->newestCache()) + return UPDATEREADY; + return IDLE; + } + } + + ASSERT_NOT_REACHED(); + return UNCACHED; +} + +bool ApplicationCacheHost::update() +{ + ApplicationCache* cache = applicationCache(); + if (!cache) + return false; + cache->group()->update(m_documentLoader->frame(), ApplicationCacheUpdateWithoutBrowsingContext); + return true; +} + +bool ApplicationCacheHost::swapCache() +{ + ApplicationCache* cache = applicationCache(); + if (!cache) + return false; + + // If the group of application caches to which cache belongs has the lifecycle status obsolete, unassociate document from cache. + if (cache->group()->isObsolete()) { + cache->group()->disassociateDocumentLoader(m_documentLoader); + return true; + } + + // If there is no newer cache, raise an INVALID_STATE_ERR exception. + ApplicationCache* newestCache = cache->group()->newestCache(); + if (cache == newestCache) + return false; + + ASSERT(cache->group() == newestCache->group()); + setApplicationCache(newestCache); + + return true; +} + +bool ApplicationCacheHost::isApplicationCacheEnabled() +{ + return m_documentLoader->frame()->settings() + && m_documentLoader->frame()->settings()->offlineWebApplicationCacheEnabled(); +} + +} // namespace WebCore + +#endif // ENABLE(OFFLINE_WEB_APPLICATIONS) diff --git a/Source/WebCore/loader/appcache/ApplicationCacheHost.h b/Source/WebCore/loader/appcache/ApplicationCacheHost.h new file mode 100644 index 0000000..8ac5357 --- /dev/null +++ b/Source/WebCore/loader/appcache/ApplicationCacheHost.h @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2009, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef ApplicationCacheHost_h +#define ApplicationCacheHost_h + +#if ENABLE(OFFLINE_WEB_APPLICATIONS) + +#include "KURL.h" +#include <wtf/Deque.h> +#include <wtf/OwnPtr.h> +#include <wtf/PassRefPtr.h> +#include <wtf/RefPtr.h> +#include <wtf/Vector.h> + +namespace WebCore { + class DOMApplicationCache; + class DocumentLoader; + class Frame; + class ResourceLoader; + class ResourceError; + class ResourceRequest; + class ResourceResponse; + class SubstituteData; +#if PLATFORM(CHROMIUM) + class ApplicationCacheHostInternal; +#else + class ApplicationCache; + class ApplicationCacheGroup; + class ApplicationCacheResource; + class ApplicationCacheStorage; +#endif + + class ApplicationCacheHost : public Noncopyable { + public: + // The Status numeric values are specified in the HTML5 spec. + enum Status { + UNCACHED = 0, + IDLE = 1, + CHECKING = 2, + DOWNLOADING = 3, + UPDATEREADY = 4, + OBSOLETE = 5 + }; + + enum EventID { + CHECKING_EVENT = 0, + ERROR_EVENT, + NOUPDATE_EVENT, + DOWNLOADING_EVENT, + PROGRESS_EVENT, + UPDATEREADY_EVENT, + CACHED_EVENT, + OBSOLETE_EVENT // Must remain the last value, this is used to size arrays. + }; + +#if ENABLE(INSPECTOR) + struct CacheInfo { + CacheInfo(const KURL& manifest, double creationTime, double updateTime, long long size) + : m_manifest(manifest) + , m_creationTime(creationTime) + , m_updateTime(updateTime) + , m_size(size) { } + KURL m_manifest; + double m_creationTime; + double m_updateTime; + long long m_size; + }; + + struct ResourceInfo { + ResourceInfo(const KURL& resource, bool isMaster, bool isManifest, bool isFallback, bool isForeign, bool isExplicit, long long size) + : m_resource(resource) + , m_isMaster(isMaster) + , m_isManifest(isManifest) + , m_isFallback(isFallback) + , m_isForeign(isForeign) + , m_isExplicit(isExplicit) + , m_size(size) { } + KURL m_resource; + bool m_isMaster; + bool m_isManifest; + bool m_isFallback; + bool m_isForeign; + bool m_isExplicit; + long long m_size; + }; + + typedef Vector<ResourceInfo> ResourceInfoList; +#endif + + ApplicationCacheHost(DocumentLoader*); + ~ApplicationCacheHost(); + + void selectCacheWithoutManifest(); + void selectCacheWithManifest(const KURL& manifestURL); + + void maybeLoadMainResource(ResourceRequest&, SubstituteData&); + void maybeLoadMainResourceForRedirect(ResourceRequest&, SubstituteData&); + bool maybeLoadFallbackForMainResponse(const ResourceRequest&, const ResourceResponse&); + bool maybeLoadFallbackForMainError(const ResourceRequest&, const ResourceError&); + void mainResourceDataReceived(const char* data, int length, long long lengthReceived, bool allAtOnce); + void finishedLoadingMainResource(); + void failedLoadingMainResource(); + + bool maybeLoadResource(ResourceLoader*, ResourceRequest&, const KURL& originalURL); + bool maybeLoadFallbackForRedirect(ResourceLoader*, ResourceRequest&, const ResourceResponse&); + bool maybeLoadFallbackForResponse(ResourceLoader*, const ResourceResponse&); + bool maybeLoadFallbackForError(ResourceLoader*, const ResourceError&); + + bool maybeLoadSynchronously(ResourceRequest&, ResourceError&, ResourceResponse&, Vector<char>& data); + void maybeLoadFallbackSynchronously(const ResourceRequest&, ResourceError&, ResourceResponse&, Vector<char>& data); + + bool canCacheInPageCache() const; + + Status status() const; + bool update(); + bool swapCache(); + + void setDOMApplicationCache(DOMApplicationCache*); + void notifyDOMApplicationCache(EventID, int progressTotal, int progressDone); + + void stopLoadingInFrame(Frame*); + + void stopDeferringEvents(); // Also raises the events that have been queued up. + +#if ENABLE(INSPECTOR) + void fillResourceList(ResourceInfoList*); + CacheInfo applicationCacheInfo(); +#endif + + private: + bool isApplicationCacheEnabled(); + DocumentLoader* documentLoader() const { return m_documentLoader; } + + struct DeferredEvent { + EventID eventID; + int progressTotal; + int progressDone; + DeferredEvent(EventID id, int total, int done) : eventID(id), progressTotal(total), progressDone(done) { } + }; + + DOMApplicationCache* m_domApplicationCache; + DocumentLoader* m_documentLoader; + bool m_defersEvents; // Events are deferred until after document onload. + Vector<DeferredEvent> m_deferredEvents; + + void dispatchDOMEvent(EventID, int progressTotal, int progressDone); + +#if PLATFORM(CHROMIUM) + friend class ApplicationCacheHostInternal; + OwnPtr<ApplicationCacheHostInternal> m_internal; +#else + friend class ApplicationCacheGroup; + friend class ApplicationCacheStorage; + + bool scheduleLoadFallbackResourceFromApplicationCache(ResourceLoader*, ApplicationCache* = 0); + bool shouldLoadResourceFromApplicationCache(const ResourceRequest&, ApplicationCacheResource*&); + bool getApplicationCacheFallbackResource(const ResourceRequest&, ApplicationCacheResource*&, ApplicationCache* = 0); + void setCandidateApplicationCacheGroup(ApplicationCacheGroup* group); + ApplicationCacheGroup* candidateApplicationCacheGroup() const { return m_candidateApplicationCacheGroup; } + void setApplicationCache(PassRefPtr<ApplicationCache> applicationCache); + ApplicationCache* applicationCache() const { return m_applicationCache.get(); } + ApplicationCache* mainResourceApplicationCache() const { return m_mainResourceApplicationCache.get(); } + + + // The application cache that the document loader is associated with (if any). + RefPtr<ApplicationCache> m_applicationCache; + + // Before an application cache has finished loading, this will be the candidate application + // group that the document loader is associated with. + ApplicationCacheGroup* m_candidateApplicationCacheGroup; + + // This is the application cache the main resource was loaded from (if any). + RefPtr<ApplicationCache> m_mainResourceApplicationCache; +#endif + }; + +} // namespace WebCore + +#endif // ENABLE(OFFLINE_WEB_APPLICATIONS) +#endif // ApplicationCacheHost_h diff --git a/Source/WebCore/loader/appcache/ApplicationCacheResource.cpp b/Source/WebCore/loader/appcache/ApplicationCacheResource.cpp new file mode 100644 index 0000000..03c5c83 --- /dev/null +++ b/Source/WebCore/loader/appcache/ApplicationCacheResource.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2008 Apple Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "ApplicationCacheResource.h" +#include <stdio.h> + +#if ENABLE(OFFLINE_WEB_APPLICATIONS) + +namespace WebCore { + +ApplicationCacheResource::ApplicationCacheResource(const KURL& url, const ResourceResponse& response, unsigned type, PassRefPtr<SharedBuffer> data) + : SubstituteResource(url, response, data) + , m_type(type) + , m_storageID(0) + , m_estimatedSizeInStorage(0) +{ +} + +void ApplicationCacheResource::addType(unsigned type) +{ + // Caller should take care of storing the new type in database. + m_type |= type; +} + +int64_t ApplicationCacheResource::estimatedSizeInStorage() +{ + if (m_estimatedSizeInStorage) + return m_estimatedSizeInStorage; + + if (data()) + m_estimatedSizeInStorage = data()->size(); + + HTTPHeaderMap::const_iterator end = response().httpHeaderFields().end(); + for (HTTPHeaderMap::const_iterator it = response().httpHeaderFields().begin(); it != end; ++it) + m_estimatedSizeInStorage += (it->first.length() + it->second.length() + 2) * sizeof(UChar); + + m_estimatedSizeInStorage += url().string().length() * sizeof(UChar); + m_estimatedSizeInStorage += sizeof(int); // response().m_httpStatusCode + m_estimatedSizeInStorage += response().url().string().length() * sizeof(UChar); + m_estimatedSizeInStorage += sizeof(unsigned); // dataId + m_estimatedSizeInStorage += response().mimeType().length() * sizeof(UChar); + m_estimatedSizeInStorage += response().textEncodingName().length() * sizeof(UChar); + + return m_estimatedSizeInStorage; +} + +#ifndef NDEBUG +void ApplicationCacheResource::dumpType(unsigned type) +{ + if (type & Master) + printf("master "); + if (type & Manifest) + printf("manifest "); + if (type & Explicit) + printf("explicit "); + if (type & Foreign) + printf("foreign "); + if (type & Fallback) + printf("fallback "); + + printf("\n"); +} +#endif + +} // namespace WebCore + +#endif // ENABLE(OFFLINE_WEB_APPLICATIONS) diff --git a/Source/WebCore/loader/appcache/ApplicationCacheResource.h b/Source/WebCore/loader/appcache/ApplicationCacheResource.h new file mode 100644 index 0000000..2ca7846 --- /dev/null +++ b/Source/WebCore/loader/appcache/ApplicationCacheResource.h @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2008 Apple Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef ApplicationCacheResource_h +#define ApplicationCacheResource_h + +#if ENABLE(OFFLINE_WEB_APPLICATIONS) + +#include "SubstituteResource.h" + +namespace WebCore { + +class ApplicationCacheResource : public SubstituteResource { +public: + enum Type { + Master = 1 << 0, + Manifest = 1 << 1, + Explicit = 1 << 2, + Foreign = 1 << 3, + Fallback = 1 << 4 + }; + + static PassRefPtr<ApplicationCacheResource> create(const KURL& url, const ResourceResponse& response, unsigned type, PassRefPtr<SharedBuffer> buffer = SharedBuffer::create()) + { + ASSERT(!url.hasFragmentIdentifier()); + return adoptRef(new ApplicationCacheResource(url, response, type, buffer)); + } + + unsigned type() const { return m_type; } + void addType(unsigned type); + + void setStorageID(unsigned storageID) { m_storageID = storageID; } + unsigned storageID() const { return m_storageID; } + void clearStorageID() { m_storageID = 0; } + int64_t estimatedSizeInStorage(); + +#ifndef NDEBUG + static void dumpType(unsigned type); +#endif + +private: + ApplicationCacheResource(const KURL& url, const ResourceResponse& response, unsigned type, PassRefPtr<SharedBuffer> buffer); + + unsigned m_type; + unsigned m_storageID; + int64_t m_estimatedSizeInStorage; +}; + +} // namespace WebCore + +#endif // ENABLE(OFFLINE_WEB_APPLICATIONS) + +#endif // ApplicationCacheResource_h diff --git a/Source/WebCore/loader/appcache/ApplicationCacheStorage.cpp b/Source/WebCore/loader/appcache/ApplicationCacheStorage.cpp new file mode 100644 index 0000000..7b20775 --- /dev/null +++ b/Source/WebCore/loader/appcache/ApplicationCacheStorage.cpp @@ -0,0 +1,1306 @@ +/* + * Copyright (C) 2008, 2009, 2010 Apple Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "ApplicationCacheStorage.h" + +#if ENABLE(OFFLINE_WEB_APPLICATIONS) + +#include "ApplicationCache.h" +#include "ApplicationCacheGroup.h" +#include "ApplicationCacheHost.h" +#include "ApplicationCacheResource.h" +#include "FileSystem.h" +#include "KURL.h" +#include "SQLiteStatement.h" +#include "SQLiteTransaction.h" +#include "SecurityOrigin.h" +#include <wtf/text/CString.h> +#include <wtf/StdLibExtras.h> +#include <wtf/StringExtras.h> + +using namespace std; + +namespace WebCore { + +template <class T> +class StorageIDJournal { +public: + ~StorageIDJournal() + { + size_t size = m_records.size(); + for (size_t i = 0; i < size; ++i) + m_records[i].restore(); + } + + void add(T* resource, unsigned storageID) + { + m_records.append(Record(resource, storageID)); + } + + void commit() + { + m_records.clear(); + } + +private: + class Record { + public: + Record() : m_resource(0), m_storageID(0) { } + Record(T* resource, unsigned storageID) : m_resource(resource), m_storageID(storageID) { } + + void restore() + { + m_resource->setStorageID(m_storageID); + } + + private: + T* m_resource; + unsigned m_storageID; + }; + + Vector<Record> m_records; +}; + +static unsigned urlHostHash(const KURL& url) +{ + unsigned hostStart = url.hostStart(); + unsigned hostEnd = url.hostEnd(); + + return AlreadyHashed::avoidDeletedValue(StringImpl::computeHash(url.string().characters() + hostStart, hostEnd - hostStart)); +} + +ApplicationCacheGroup* ApplicationCacheStorage::loadCacheGroup(const KURL& manifestURL) +{ + openDatabase(false); + if (!m_database.isOpen()) + return 0; + + SQLiteStatement statement(m_database, "SELECT id, manifestURL, newestCache FROM CacheGroups WHERE newestCache IS NOT NULL AND manifestURL=?"); + if (statement.prepare() != SQLResultOk) + return 0; + + statement.bindText(1, manifestURL); + + int result = statement.step(); + if (result == SQLResultDone) + return 0; + + if (result != SQLResultRow) { + LOG_ERROR("Could not load cache group, error \"%s\"", m_database.lastErrorMsg()); + return 0; + } + + unsigned newestCacheStorageID = static_cast<unsigned>(statement.getColumnInt64(2)); + + RefPtr<ApplicationCache> cache = loadCache(newestCacheStorageID); + if (!cache) + return 0; + + ApplicationCacheGroup* group = new ApplicationCacheGroup(manifestURL); + + group->setStorageID(static_cast<unsigned>(statement.getColumnInt64(0))); + group->setNewestCache(cache.release()); + + return group; +} + +ApplicationCacheGroup* ApplicationCacheStorage::findOrCreateCacheGroup(const KURL& manifestURL) +{ + ASSERT(!manifestURL.hasFragmentIdentifier()); + + std::pair<CacheGroupMap::iterator, bool> result = m_cachesInMemory.add(manifestURL, 0); + + if (!result.second) { + ASSERT(result.first->second); + return result.first->second; + } + + // Look up the group in the database + ApplicationCacheGroup* group = loadCacheGroup(manifestURL); + + // If the group was not found we need to create it + if (!group) { + group = new ApplicationCacheGroup(manifestURL); + m_cacheHostSet.add(urlHostHash(manifestURL)); + } + + result.first->second = group; + + return group; +} + +void ApplicationCacheStorage::loadManifestHostHashes() +{ + static bool hasLoadedHashes = false; + + if (hasLoadedHashes) + return; + + // We set this flag to true before the database has been opened + // to avoid trying to open the database over and over if it doesn't exist. + hasLoadedHashes = true; + + openDatabase(false); + if (!m_database.isOpen()) + return; + + // Fetch the host hashes. + SQLiteStatement statement(m_database, "SELECT manifestHostHash FROM CacheGroups"); + if (statement.prepare() != SQLResultOk) + return; + + while (statement.step() == SQLResultRow) + m_cacheHostSet.add(static_cast<unsigned>(statement.getColumnInt64(0))); +} + +ApplicationCacheGroup* ApplicationCacheStorage::cacheGroupForURL(const KURL& url) +{ + ASSERT(!url.hasFragmentIdentifier()); + + loadManifestHostHashes(); + + // Hash the host name and see if there's a manifest with the same host. + if (!m_cacheHostSet.contains(urlHostHash(url))) + return 0; + + // Check if a cache already exists in memory. + CacheGroupMap::const_iterator end = m_cachesInMemory.end(); + for (CacheGroupMap::const_iterator it = m_cachesInMemory.begin(); it != end; ++it) { + ApplicationCacheGroup* group = it->second; + + ASSERT(!group->isObsolete()); + + if (!protocolHostAndPortAreEqual(url, group->manifestURL())) + continue; + + if (ApplicationCache* cache = group->newestCache()) { + ApplicationCacheResource* resource = cache->resourceForURL(url); + if (!resource) + continue; + if (resource->type() & ApplicationCacheResource::Foreign) + continue; + return group; + } + } + + if (!m_database.isOpen()) + return 0; + + // Check the database. Look for all cache groups with a newest cache. + SQLiteStatement statement(m_database, "SELECT id, manifestURL, newestCache FROM CacheGroups WHERE newestCache IS NOT NULL"); + if (statement.prepare() != SQLResultOk) + return 0; + + int result; + while ((result = statement.step()) == SQLResultRow) { + KURL manifestURL = KURL(ParsedURLString, statement.getColumnText(1)); + + if (m_cachesInMemory.contains(manifestURL)) + continue; + + if (!protocolHostAndPortAreEqual(url, manifestURL)) + continue; + + // We found a cache group that matches. Now check if the newest cache has a resource with + // a matching URL. + unsigned newestCacheID = static_cast<unsigned>(statement.getColumnInt64(2)); + RefPtr<ApplicationCache> cache = loadCache(newestCacheID); + if (!cache) + continue; + + ApplicationCacheResource* resource = cache->resourceForURL(url); + if (!resource) + continue; + if (resource->type() & ApplicationCacheResource::Foreign) + continue; + + ApplicationCacheGroup* group = new ApplicationCacheGroup(manifestURL); + + group->setStorageID(static_cast<unsigned>(statement.getColumnInt64(0))); + group->setNewestCache(cache.release()); + + m_cachesInMemory.set(group->manifestURL(), group); + + return group; + } + + if (result != SQLResultDone) + LOG_ERROR("Could not load cache group, error \"%s\"", m_database.lastErrorMsg()); + + return 0; +} + +ApplicationCacheGroup* ApplicationCacheStorage::fallbackCacheGroupForURL(const KURL& url) +{ + ASSERT(!url.hasFragmentIdentifier()); + + // Check if an appropriate cache already exists in memory. + CacheGroupMap::const_iterator end = m_cachesInMemory.end(); + for (CacheGroupMap::const_iterator it = m_cachesInMemory.begin(); it != end; ++it) { + ApplicationCacheGroup* group = it->second; + + ASSERT(!group->isObsolete()); + + if (ApplicationCache* cache = group->newestCache()) { + KURL fallbackURL; + if (!cache->urlMatchesFallbackNamespace(url, &fallbackURL)) + continue; + if (cache->resourceForURL(fallbackURL)->type() & ApplicationCacheResource::Foreign) + continue; + return group; + } + } + + if (!m_database.isOpen()) + return 0; + + // Check the database. Look for all cache groups with a newest cache. + SQLiteStatement statement(m_database, "SELECT id, manifestURL, newestCache FROM CacheGroups WHERE newestCache IS NOT NULL"); + if (statement.prepare() != SQLResultOk) + return 0; + + int result; + while ((result = statement.step()) == SQLResultRow) { + KURL manifestURL = KURL(ParsedURLString, statement.getColumnText(1)); + + if (m_cachesInMemory.contains(manifestURL)) + continue; + + // Fallback namespaces always have the same origin as manifest URL, so we can avoid loading caches that cannot match. + if (!protocolHostAndPortAreEqual(url, manifestURL)) + continue; + + // We found a cache group that matches. Now check if the newest cache has a resource with + // a matching fallback namespace. + unsigned newestCacheID = static_cast<unsigned>(statement.getColumnInt64(2)); + RefPtr<ApplicationCache> cache = loadCache(newestCacheID); + + KURL fallbackURL; + if (!cache->urlMatchesFallbackNamespace(url, &fallbackURL)) + continue; + if (cache->resourceForURL(fallbackURL)->type() & ApplicationCacheResource::Foreign) + continue; + + ApplicationCacheGroup* group = new ApplicationCacheGroup(manifestURL); + + group->setStorageID(static_cast<unsigned>(statement.getColumnInt64(0))); + group->setNewestCache(cache.release()); + + m_cachesInMemory.set(group->manifestURL(), group); + + return group; + } + + if (result != SQLResultDone) + LOG_ERROR("Could not load cache group, error \"%s\"", m_database.lastErrorMsg()); + + return 0; +} + +void ApplicationCacheStorage::cacheGroupDestroyed(ApplicationCacheGroup* group) +{ + if (group->isObsolete()) { + ASSERT(!group->storageID()); + ASSERT(m_cachesInMemory.get(group->manifestURL()) != group); + return; + } + + ASSERT(m_cachesInMemory.get(group->manifestURL()) == group); + + m_cachesInMemory.remove(group->manifestURL()); + + // If the cache group is half-created, we don't want it in the saved set (as it is not stored in database). + if (!group->storageID()) + m_cacheHostSet.remove(urlHostHash(group->manifestURL())); +} + +void ApplicationCacheStorage::cacheGroupMadeObsolete(ApplicationCacheGroup* group) +{ + ASSERT(m_cachesInMemory.get(group->manifestURL()) == group); + ASSERT(m_cacheHostSet.contains(urlHostHash(group->manifestURL()))); + + if (ApplicationCache* newestCache = group->newestCache()) + remove(newestCache); + + m_cachesInMemory.remove(group->manifestURL()); + m_cacheHostSet.remove(urlHostHash(group->manifestURL())); +} + +void ApplicationCacheStorage::setCacheDirectory(const String& cacheDirectory) +{ + ASSERT(m_cacheDirectory.isNull()); + ASSERT(!cacheDirectory.isNull()); + + m_cacheDirectory = cacheDirectory; +} + +const String& ApplicationCacheStorage::cacheDirectory() const +{ + return m_cacheDirectory; +} + +void ApplicationCacheStorage::setMaximumSize(int64_t size) +{ + m_maximumSize = size; +} + +int64_t ApplicationCacheStorage::maximumSize() const +{ + return m_maximumSize; +} + +bool ApplicationCacheStorage::isMaximumSizeReached() const +{ + return m_isMaximumSizeReached; +} + +int64_t ApplicationCacheStorage::spaceNeeded(int64_t cacheToSave) +{ + int64_t spaceNeeded = 0; + long long fileSize = 0; + if (!getFileSize(m_cacheFile, fileSize)) + return 0; + + int64_t currentSize = fileSize; + + // Determine the amount of free space we have available. + int64_t totalAvailableSize = 0; + if (m_maximumSize < currentSize) { + // The max size is smaller than the actual size of the app cache file. + // This can happen if the client previously imposed a larger max size + // value and the app cache file has already grown beyond the current + // max size value. + // The amount of free space is just the amount of free space inside + // the database file. Note that this is always 0 if SQLite is compiled + // with AUTO_VACUUM = 1. + totalAvailableSize = m_database.freeSpaceSize(); + } else { + // The max size is the same or larger than the current size. + // The amount of free space available is the amount of free space + // inside the database file plus the amount we can grow until we hit + // the max size. + totalAvailableSize = (m_maximumSize - currentSize) + m_database.freeSpaceSize(); + } + + // The space needed to be freed in order to accommodate the failed cache is + // the size of the failed cache minus any already available free space. + spaceNeeded = cacheToSave - totalAvailableSize; + // The space needed value must be positive (or else the total already + // available free space would be larger than the size of the failed cache and + // saving of the cache should have never failed). + ASSERT(spaceNeeded); + return spaceNeeded; +} + +void ApplicationCacheStorage::setDefaultOriginQuota(int64_t quota) +{ + m_defaultOriginQuota = quota; +} + +bool ApplicationCacheStorage::quotaForOrigin(const SecurityOrigin* origin, int64_t& quota) +{ + // If an Origin record doesn't exist, then the COUNT will be 0 and quota will be 0. + // Using the count to determine if a record existed or not is a safe way to determine + // if a quota of 0 is real, from the record, or from null. + SQLiteStatement statement(m_database, "SELECT COUNT(quota), quota FROM Origins WHERE origin=?"); + if (statement.prepare() != SQLResultOk) + return false; + + statement.bindText(1, origin->databaseIdentifier()); + int result = statement.step(); + + // Return the quota, or if it was null the default. + if (result == SQLResultRow) { + bool wasNoRecord = statement.getColumnInt64(0) == 0; + quota = wasNoRecord ? m_defaultOriginQuota : statement.getColumnInt64(1); + return true; + } + + LOG_ERROR("Could not get the quota of an origin, error \"%s\"", m_database.lastErrorMsg()); + return false; +} + +bool ApplicationCacheStorage::usageForOrigin(const SecurityOrigin* origin, int64_t& usage) +{ + // If an Origins record doesn't exist, then the SUM will be null, + // which will become 0, as expected, when converting to a number. + SQLiteStatement statement(m_database, "SELECT SUM(Caches.size)" + " FROM CacheGroups" + " INNER JOIN Origins ON CacheGroups.origin = Origins.origin" + " INNER JOIN Caches ON CacheGroups.id = Caches.cacheGroup" + " WHERE Origins.origin=?"); + if (statement.prepare() != SQLResultOk) + return false; + + statement.bindText(1, origin->databaseIdentifier()); + int result = statement.step(); + + if (result == SQLResultRow) { + usage = statement.getColumnInt64(0); + return true; + } + + LOG_ERROR("Could not get the quota of an origin, error \"%s\"", m_database.lastErrorMsg()); + return false; +} + +bool ApplicationCacheStorage::remainingSizeForOriginExcludingCache(const SecurityOrigin* origin, ApplicationCache* cache, int64_t& remainingSize) +{ + openDatabase(false); + if (!m_database.isOpen()) + return false; + + // Remaining size = total origin quota - size of all caches with origin excluding the provided cache. + // Keep track of the number of caches so we can tell if the result was a calculation or not. + const char* query; + int64_t excludingCacheIdentifier = cache ? cache->storageID() : 0; + if (excludingCacheIdentifier != 0) { + query = "SELECT COUNT(Caches.size), Origins.quota - SUM(Caches.size)" + " FROM CacheGroups" + " INNER JOIN Origins ON CacheGroups.origin = Origins.origin" + " INNER JOIN Caches ON CacheGroups.id = Caches.cacheGroup" + " WHERE Origins.origin=?" + " AND Caches.id!=?"; + } else { + query = "SELECT COUNT(Caches.size), Origins.quota - SUM(Caches.size)" + " FROM CacheGroups" + " INNER JOIN Origins ON CacheGroups.origin = Origins.origin" + " INNER JOIN Caches ON CacheGroups.id = Caches.cacheGroup" + " WHERE Origins.origin=?"; + } + + SQLiteStatement statement(m_database, query); + if (statement.prepare() != SQLResultOk) + return false; + + statement.bindText(1, origin->databaseIdentifier()); + if (excludingCacheIdentifier != 0) + statement.bindInt64(2, excludingCacheIdentifier); + int result = statement.step(); + + // If the count was 0 that then we have to query the origin table directly + // for its quota. Otherwise we can use the calculated value. + if (result == SQLResultRow) { + int64_t numberOfCaches = statement.getColumnInt64(0); + if (numberOfCaches == 0) + quotaForOrigin(origin, remainingSize); + else + remainingSize = statement.getColumnInt64(1); + return true; + } + + LOG_ERROR("Could not get the remaining size of an origin's quota, error \"%s\"", m_database.lastErrorMsg()); + return false; +} + +bool ApplicationCacheStorage::storeUpdatedQuotaForOrigin(const SecurityOrigin* origin, int64_t quota) +{ + openDatabase(true); + if (!m_database.isOpen()) + return false; + + if (!ensureOriginRecord(origin)) + return false; + + SQLiteStatement updateStatement(m_database, "UPDATE Origins SET quota=? WHERE origin=?"); + if (updateStatement.prepare() != SQLResultOk) + return false; + + updateStatement.bindInt64(1, quota); + updateStatement.bindText(2, origin->databaseIdentifier()); + + return executeStatement(updateStatement); +} + +bool ApplicationCacheStorage::executeSQLCommand(const String& sql) +{ + ASSERT(m_database.isOpen()); + + bool result = m_database.executeCommand(sql); + if (!result) + LOG_ERROR("Application Cache Storage: failed to execute statement \"%s\" error \"%s\"", + sql.utf8().data(), m_database.lastErrorMsg()); + + return result; +} + +// Update the schemaVersion when the schema of any the Application Cache +// SQLite tables changes. This allows the database to be rebuilt when +// a new, incompatible change has been introduced to the database schema. +static const int schemaVersion = 6; + +void ApplicationCacheStorage::verifySchemaVersion() +{ + int version = SQLiteStatement(m_database, "PRAGMA user_version").getColumnInt(0); + if (version == schemaVersion) + return; + + m_database.clearAllTables(); + + // Update user version. + SQLiteTransaction setDatabaseVersion(m_database); + setDatabaseVersion.begin(); + + char userVersionSQL[32]; + int unusedNumBytes = snprintf(userVersionSQL, sizeof(userVersionSQL), "PRAGMA user_version=%d", schemaVersion); + ASSERT_UNUSED(unusedNumBytes, static_cast<int>(sizeof(userVersionSQL)) >= unusedNumBytes); + + SQLiteStatement statement(m_database, userVersionSQL); + if (statement.prepare() != SQLResultOk) + return; + + executeStatement(statement); + setDatabaseVersion.commit(); +} + +void ApplicationCacheStorage::openDatabase(bool createIfDoesNotExist) +{ + if (m_database.isOpen()) + return; + + // The cache directory should never be null, but if it for some weird reason is we bail out. + if (m_cacheDirectory.isNull()) + return; + + m_cacheFile = pathByAppendingComponent(m_cacheDirectory, "ApplicationCache.db"); + if (!createIfDoesNotExist && !fileExists(m_cacheFile)) + return; + + makeAllDirectories(m_cacheDirectory); + m_database.open(m_cacheFile); + + if (!m_database.isOpen()) + return; + + verifySchemaVersion(); + + // Create tables + executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheGroups (id INTEGER PRIMARY KEY AUTOINCREMENT, " + "manifestHostHash INTEGER NOT NULL ON CONFLICT FAIL, manifestURL TEXT UNIQUE ON CONFLICT FAIL, newestCache INTEGER, origin TEXT)"); + executeSQLCommand("CREATE TABLE IF NOT EXISTS Caches (id INTEGER PRIMARY KEY AUTOINCREMENT, cacheGroup INTEGER, size INTEGER)"); + executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheWhitelistURLs (url TEXT NOT NULL ON CONFLICT FAIL, cache INTEGER NOT NULL ON CONFLICT FAIL)"); + executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheAllowsAllNetworkRequests (wildcard INTEGER NOT NULL ON CONFLICT FAIL, cache INTEGER NOT NULL ON CONFLICT FAIL)"); + executeSQLCommand("CREATE TABLE IF NOT EXISTS FallbackURLs (namespace TEXT NOT NULL ON CONFLICT FAIL, fallbackURL TEXT NOT NULL ON CONFLICT FAIL, " + "cache INTEGER NOT NULL ON CONFLICT FAIL)"); + executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheEntries (cache INTEGER NOT NULL ON CONFLICT FAIL, type INTEGER, resource INTEGER NOT NULL)"); + executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheResources (id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL ON CONFLICT FAIL, " + "statusCode INTEGER NOT NULL, responseURL TEXT NOT NULL, mimeType TEXT, textEncodingName TEXT, headers TEXT, data INTEGER NOT NULL ON CONFLICT FAIL)"); + executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheResourceData (id INTEGER PRIMARY KEY AUTOINCREMENT, data BLOB)"); + executeSQLCommand("CREATE TABLE IF NOT EXISTS Origins (origin TEXT UNIQUE ON CONFLICT IGNORE, quota INTEGER NOT NULL ON CONFLICT FAIL)"); + + // When a cache is deleted, all its entries and its whitelist should be deleted. + executeSQLCommand("CREATE TRIGGER IF NOT EXISTS CacheDeleted AFTER DELETE ON Caches" + " FOR EACH ROW BEGIN" + " DELETE FROM CacheEntries WHERE cache = OLD.id;" + " DELETE FROM CacheWhitelistURLs WHERE cache = OLD.id;" + " DELETE FROM CacheAllowsAllNetworkRequests WHERE cache = OLD.id;" + " DELETE FROM FallbackURLs WHERE cache = OLD.id;" + " END"); + + // When a cache entry is deleted, its resource should also be deleted. + executeSQLCommand("CREATE TRIGGER IF NOT EXISTS CacheEntryDeleted AFTER DELETE ON CacheEntries" + " FOR EACH ROW BEGIN" + " DELETE FROM CacheResources WHERE id = OLD.resource;" + " END"); + + // When a cache resource is deleted, its data blob should also be deleted. + executeSQLCommand("CREATE TRIGGER IF NOT EXISTS CacheResourceDeleted AFTER DELETE ON CacheResources" + " FOR EACH ROW BEGIN" + " DELETE FROM CacheResourceData WHERE id = OLD.data;" + " END"); +} + +bool ApplicationCacheStorage::executeStatement(SQLiteStatement& statement) +{ + bool result = statement.executeCommand(); + if (!result) + LOG_ERROR("Application Cache Storage: failed to execute statement \"%s\" error \"%s\"", + statement.query().utf8().data(), m_database.lastErrorMsg()); + + return result; +} + +bool ApplicationCacheStorage::store(ApplicationCacheGroup* group, GroupStorageIDJournal* journal) +{ + ASSERT(group->storageID() == 0); + ASSERT(journal); + + SQLiteStatement statement(m_database, "INSERT INTO CacheGroups (manifestHostHash, manifestURL, origin) VALUES (?, ?, ?)"); + if (statement.prepare() != SQLResultOk) + return false; + + statement.bindInt64(1, urlHostHash(group->manifestURL())); + statement.bindText(2, group->manifestURL()); + statement.bindText(3, group->origin()->databaseIdentifier()); + + if (!executeStatement(statement)) + return false; + + unsigned groupStorageID = static_cast<unsigned>(m_database.lastInsertRowID()); + + if (!ensureOriginRecord(group->origin())) + return false; + + group->setStorageID(groupStorageID); + journal->add(group, 0); + return true; +} + +bool ApplicationCacheStorage::store(ApplicationCache* cache, ResourceStorageIDJournal* storageIDJournal) +{ + ASSERT(cache->storageID() == 0); + ASSERT(cache->group()->storageID() != 0); + ASSERT(storageIDJournal); + + SQLiteStatement statement(m_database, "INSERT INTO Caches (cacheGroup, size) VALUES (?, ?)"); + if (statement.prepare() != SQLResultOk) + return false; + + statement.bindInt64(1, cache->group()->storageID()); + statement.bindInt64(2, cache->estimatedSizeInStorage()); + + if (!executeStatement(statement)) + return false; + + unsigned cacheStorageID = static_cast<unsigned>(m_database.lastInsertRowID()); + + // Store all resources + { + ApplicationCache::ResourceMap::const_iterator end = cache->end(); + for (ApplicationCache::ResourceMap::const_iterator it = cache->begin(); it != end; ++it) { + unsigned oldStorageID = it->second->storageID(); + if (!store(it->second.get(), cacheStorageID)) + return false; + + // Storing the resource succeeded. Log its old storageID in case + // it needs to be restored later. + storageIDJournal->add(it->second.get(), oldStorageID); + } + } + + // Store the online whitelist + const Vector<KURL>& onlineWhitelist = cache->onlineWhitelist(); + { + size_t whitelistSize = onlineWhitelist.size(); + for (size_t i = 0; i < whitelistSize; ++i) { + SQLiteStatement statement(m_database, "INSERT INTO CacheWhitelistURLs (url, cache) VALUES (?, ?)"); + statement.prepare(); + + statement.bindText(1, onlineWhitelist[i]); + statement.bindInt64(2, cacheStorageID); + + if (!executeStatement(statement)) + return false; + } + } + + // Store online whitelist wildcard flag. + { + SQLiteStatement statement(m_database, "INSERT INTO CacheAllowsAllNetworkRequests (wildcard, cache) VALUES (?, ?)"); + statement.prepare(); + + statement.bindInt64(1, cache->allowsAllNetworkRequests()); + statement.bindInt64(2, cacheStorageID); + + if (!executeStatement(statement)) + return false; + } + + // Store fallback URLs. + const FallbackURLVector& fallbackURLs = cache->fallbackURLs(); + { + size_t fallbackCount = fallbackURLs.size(); + for (size_t i = 0; i < fallbackCount; ++i) { + SQLiteStatement statement(m_database, "INSERT INTO FallbackURLs (namespace, fallbackURL, cache) VALUES (?, ?, ?)"); + statement.prepare(); + + statement.bindText(1, fallbackURLs[i].first); + statement.bindText(2, fallbackURLs[i].second); + statement.bindInt64(3, cacheStorageID); + + if (!executeStatement(statement)) + return false; + } + } + + cache->setStorageID(cacheStorageID); + return true; +} + +bool ApplicationCacheStorage::store(ApplicationCacheResource* resource, unsigned cacheStorageID) +{ + ASSERT(cacheStorageID); + ASSERT(!resource->storageID()); + + openDatabase(true); + + // openDatabase(true) could still fail, for example when cacheStorage is full or no longer available. + if (!m_database.isOpen()) + return false; + + // First, insert the data + SQLiteStatement dataStatement(m_database, "INSERT INTO CacheResourceData (data) VALUES (?)"); + if (dataStatement.prepare() != SQLResultOk) + return false; + + if (resource->data()->size()) + dataStatement.bindBlob(1, resource->data()->data(), resource->data()->size()); + + if (!dataStatement.executeCommand()) + return false; + + unsigned dataId = static_cast<unsigned>(m_database.lastInsertRowID()); + + // Then, insert the resource + + // Serialize the headers + Vector<UChar> stringBuilder; + + HTTPHeaderMap::const_iterator end = resource->response().httpHeaderFields().end(); + for (HTTPHeaderMap::const_iterator it = resource->response().httpHeaderFields().begin(); it!= end; ++it) { + stringBuilder.append(it->first.characters(), it->first.length()); + stringBuilder.append((UChar)':'); + stringBuilder.append(it->second.characters(), it->second.length()); + stringBuilder.append((UChar)'\n'); + } + + String headers = String::adopt(stringBuilder); + + SQLiteStatement resourceStatement(m_database, "INSERT INTO CacheResources (url, statusCode, responseURL, headers, data, mimeType, textEncodingName) VALUES (?, ?, ?, ?, ?, ?, ?)"); + if (resourceStatement.prepare() != SQLResultOk) + return false; + + // The same ApplicationCacheResource are used in ApplicationCacheResource::size() + // to calculate the approximate size of an ApplicationCacheResource object. If + // you change the code below, please also change ApplicationCacheResource::size(). + resourceStatement.bindText(1, resource->url()); + resourceStatement.bindInt64(2, resource->response().httpStatusCode()); + resourceStatement.bindText(3, resource->response().url()); + resourceStatement.bindText(4, headers); + resourceStatement.bindInt64(5, dataId); + resourceStatement.bindText(6, resource->response().mimeType()); + resourceStatement.bindText(7, resource->response().textEncodingName()); + + if (!executeStatement(resourceStatement)) + return false; + + unsigned resourceId = static_cast<unsigned>(m_database.lastInsertRowID()); + + // Finally, insert the cache entry + SQLiteStatement entryStatement(m_database, "INSERT INTO CacheEntries (cache, type, resource) VALUES (?, ?, ?)"); + if (entryStatement.prepare() != SQLResultOk) + return false; + + entryStatement.bindInt64(1, cacheStorageID); + entryStatement.bindInt64(2, resource->type()); + entryStatement.bindInt64(3, resourceId); + + if (!executeStatement(entryStatement)) + return false; + + resource->setStorageID(resourceId); + return true; +} + +bool ApplicationCacheStorage::storeUpdatedType(ApplicationCacheResource* resource, ApplicationCache* cache) +{ + ASSERT_UNUSED(cache, cache->storageID()); + ASSERT(resource->storageID()); + + // First, insert the data + SQLiteStatement entryStatement(m_database, "UPDATE CacheEntries SET type=? WHERE resource=?"); + if (entryStatement.prepare() != SQLResultOk) + return false; + + entryStatement.bindInt64(1, resource->type()); + entryStatement.bindInt64(2, resource->storageID()); + + return executeStatement(entryStatement); +} + +bool ApplicationCacheStorage::store(ApplicationCacheResource* resource, ApplicationCache* cache) +{ + ASSERT(cache->storageID()); + + openDatabase(true); + + if (!m_database.isOpen()) + return false; + + m_isMaximumSizeReached = false; + m_database.setMaximumSize(m_maximumSize); + + SQLiteTransaction storeResourceTransaction(m_database); + storeResourceTransaction.begin(); + + if (!store(resource, cache->storageID())) { + checkForMaxSizeReached(); + return false; + } + + // A resource was added to the cache. Update the total data size for the cache. + SQLiteStatement sizeUpdateStatement(m_database, "UPDATE Caches SET size=size+? WHERE id=?"); + if (sizeUpdateStatement.prepare() != SQLResultOk) + return false; + + sizeUpdateStatement.bindInt64(1, resource->estimatedSizeInStorage()); + sizeUpdateStatement.bindInt64(2, cache->storageID()); + + if (!executeStatement(sizeUpdateStatement)) + return false; + + storeResourceTransaction.commit(); + return true; +} + +bool ApplicationCacheStorage::ensureOriginRecord(const SecurityOrigin* origin) +{ + SQLiteStatement insertOriginStatement(m_database, "INSERT INTO Origins (origin, quota) VALUES (?, ?)"); + if (insertOriginStatement.prepare() != SQLResultOk) + return false; + + insertOriginStatement.bindText(1, origin->databaseIdentifier()); + insertOriginStatement.bindInt64(2, m_defaultOriginQuota); + if (!executeStatement(insertOriginStatement)) + return false; + + return true; +} + +bool ApplicationCacheStorage::storeNewestCache(ApplicationCacheGroup* group, ApplicationCache* oldCache, FailureReason& failureReason) +{ + openDatabase(true); + + if (!m_database.isOpen()) + return false; + + m_isMaximumSizeReached = false; + m_database.setMaximumSize(m_maximumSize); + + SQLiteTransaction storeCacheTransaction(m_database); + + storeCacheTransaction.begin(); + + // Check if this would reach the per-origin quota. + int64_t remainingSpaceInOrigin; + if (remainingSizeForOriginExcludingCache(group->origin(), oldCache, remainingSpaceInOrigin)) { + if (remainingSpaceInOrigin < group->newestCache()->estimatedSizeInStorage()) { + failureReason = OriginQuotaReached; + return false; + } + } + + GroupStorageIDJournal groupStorageIDJournal; + if (!group->storageID()) { + // Store the group + if (!store(group, &groupStorageIDJournal)) { + checkForMaxSizeReached(); + failureReason = isMaximumSizeReached() ? TotalQuotaReached : DiskOrOperationFailure; + return false; + } + } + + ASSERT(group->newestCache()); + ASSERT(!group->isObsolete()); + ASSERT(!group->newestCache()->storageID()); + + // Log the storageID changes to the in-memory resource objects. The journal + // object will roll them back automatically in case a database operation + // fails and this method returns early. + ResourceStorageIDJournal resourceStorageIDJournal; + + // Store the newest cache + if (!store(group->newestCache(), &resourceStorageIDJournal)) { + checkForMaxSizeReached(); + failureReason = isMaximumSizeReached() ? TotalQuotaReached : DiskOrOperationFailure; + return false; + } + + // Update the newest cache in the group. + + SQLiteStatement statement(m_database, "UPDATE CacheGroups SET newestCache=? WHERE id=?"); + if (statement.prepare() != SQLResultOk) { + failureReason = DiskOrOperationFailure; + return false; + } + + statement.bindInt64(1, group->newestCache()->storageID()); + statement.bindInt64(2, group->storageID()); + + if (!executeStatement(statement)) { + failureReason = DiskOrOperationFailure; + return false; + } + + groupStorageIDJournal.commit(); + resourceStorageIDJournal.commit(); + storeCacheTransaction.commit(); + return true; +} + +bool ApplicationCacheStorage::storeNewestCache(ApplicationCacheGroup* group) +{ + // Ignore the reason for failing, just attempt the store. + FailureReason ignoredFailureReason; + return storeNewestCache(group, 0, ignoredFailureReason); +} + +static inline void parseHeader(const UChar* header, size_t headerLength, ResourceResponse& response) +{ + size_t pos = find(header, headerLength, ':'); + ASSERT(pos != notFound); + + AtomicString headerName = AtomicString(header, pos); + String headerValue = String(header + pos + 1, headerLength - pos - 1); + + response.setHTTPHeaderField(headerName, headerValue); +} + +static inline void parseHeaders(const String& headers, ResourceResponse& response) +{ + unsigned startPos = 0; + size_t endPos; + while ((endPos = headers.find('\n', startPos)) != notFound) { + ASSERT(startPos != endPos); + + parseHeader(headers.characters() + startPos, endPos - startPos, response); + + startPos = endPos + 1; + } + + if (startPos != headers.length()) + parseHeader(headers.characters(), headers.length(), response); +} + +PassRefPtr<ApplicationCache> ApplicationCacheStorage::loadCache(unsigned storageID) +{ + SQLiteStatement cacheStatement(m_database, + "SELECT url, type, mimeType, textEncodingName, headers, CacheResourceData.data FROM CacheEntries INNER JOIN CacheResources ON CacheEntries.resource=CacheResources.id " + "INNER JOIN CacheResourceData ON CacheResourceData.id=CacheResources.data WHERE CacheEntries.cache=?"); + if (cacheStatement.prepare() != SQLResultOk) { + LOG_ERROR("Could not prepare cache statement, error \"%s\"", m_database.lastErrorMsg()); + return 0; + } + + cacheStatement.bindInt64(1, storageID); + + RefPtr<ApplicationCache> cache = ApplicationCache::create(); + + int result; + while ((result = cacheStatement.step()) == SQLResultRow) { + KURL url(ParsedURLString, cacheStatement.getColumnText(0)); + + unsigned type = static_cast<unsigned>(cacheStatement.getColumnInt64(1)); + + Vector<char> blob; + cacheStatement.getColumnBlobAsVector(5, blob); + + RefPtr<SharedBuffer> data = SharedBuffer::adoptVector(blob); + + String mimeType = cacheStatement.getColumnText(2); + String textEncodingName = cacheStatement.getColumnText(3); + + ResourceResponse response(url, mimeType, data->size(), textEncodingName, ""); + + String headers = cacheStatement.getColumnText(4); + parseHeaders(headers, response); + + RefPtr<ApplicationCacheResource> resource = ApplicationCacheResource::create(url, response, type, data.release()); + + if (type & ApplicationCacheResource::Manifest) + cache->setManifestResource(resource.release()); + else + cache->addResource(resource.release()); + } + + if (result != SQLResultDone) + LOG_ERROR("Could not load cache resources, error \"%s\"", m_database.lastErrorMsg()); + + // Load the online whitelist + SQLiteStatement whitelistStatement(m_database, "SELECT url FROM CacheWhitelistURLs WHERE cache=?"); + if (whitelistStatement.prepare() != SQLResultOk) + return 0; + whitelistStatement.bindInt64(1, storageID); + + Vector<KURL> whitelist; + while ((result = whitelistStatement.step()) == SQLResultRow) + whitelist.append(KURL(ParsedURLString, whitelistStatement.getColumnText(0))); + + if (result != SQLResultDone) + LOG_ERROR("Could not load cache online whitelist, error \"%s\"", m_database.lastErrorMsg()); + + cache->setOnlineWhitelist(whitelist); + + // Load online whitelist wildcard flag. + SQLiteStatement whitelistWildcardStatement(m_database, "SELECT wildcard FROM CacheAllowsAllNetworkRequests WHERE cache=?"); + if (whitelistWildcardStatement.prepare() != SQLResultOk) + return 0; + whitelistWildcardStatement.bindInt64(1, storageID); + + result = whitelistWildcardStatement.step(); + if (result != SQLResultRow) + LOG_ERROR("Could not load cache online whitelist wildcard flag, error \"%s\"", m_database.lastErrorMsg()); + + cache->setAllowsAllNetworkRequests(whitelistWildcardStatement.getColumnInt64(0)); + + if (whitelistWildcardStatement.step() != SQLResultDone) + LOG_ERROR("Too many rows for online whitelist wildcard flag"); + + // Load fallback URLs. + SQLiteStatement fallbackStatement(m_database, "SELECT namespace, fallbackURL FROM FallbackURLs WHERE cache=?"); + if (fallbackStatement.prepare() != SQLResultOk) + return 0; + fallbackStatement.bindInt64(1, storageID); + + FallbackURLVector fallbackURLs; + while ((result = fallbackStatement.step()) == SQLResultRow) + fallbackURLs.append(make_pair(KURL(ParsedURLString, fallbackStatement.getColumnText(0)), KURL(ParsedURLString, fallbackStatement.getColumnText(1)))); + + if (result != SQLResultDone) + LOG_ERROR("Could not load fallback URLs, error \"%s\"", m_database.lastErrorMsg()); + + cache->setFallbackURLs(fallbackURLs); + + cache->setStorageID(storageID); + + return cache.release(); +} + +void ApplicationCacheStorage::remove(ApplicationCache* cache) +{ + if (!cache->storageID()) + return; + + openDatabase(false); + if (!m_database.isOpen()) + return; + + ASSERT(cache->group()); + ASSERT(cache->group()->storageID()); + + // All associated data will be deleted by database triggers. + SQLiteStatement statement(m_database, "DELETE FROM Caches WHERE id=?"); + if (statement.prepare() != SQLResultOk) + return; + + statement.bindInt64(1, cache->storageID()); + executeStatement(statement); + + cache->clearStorageID(); + + if (cache->group()->newestCache() == cache) { + // Currently, there are no triggers on the cache group, which is why the cache had to be removed separately above. + SQLiteStatement groupStatement(m_database, "DELETE FROM CacheGroups WHERE id=?"); + if (groupStatement.prepare() != SQLResultOk) + return; + + groupStatement.bindInt64(1, cache->group()->storageID()); + executeStatement(groupStatement); + + cache->group()->clearStorageID(); + } +} + +void ApplicationCacheStorage::empty() +{ + openDatabase(false); + + if (!m_database.isOpen()) + return; + + // Clear cache groups, caches, cache resources, and origins. + executeSQLCommand("DELETE FROM CacheGroups"); + executeSQLCommand("DELETE FROM Caches"); + executeSQLCommand("DELETE FROM Origins"); + + // Clear the storage IDs for the caches in memory. + // The caches will still work, but cached resources will not be saved to disk + // until a cache update process has been initiated. + CacheGroupMap::const_iterator end = m_cachesInMemory.end(); + for (CacheGroupMap::const_iterator it = m_cachesInMemory.begin(); it != end; ++it) + it->second->clearStorageID(); +} + +bool ApplicationCacheStorage::storeCopyOfCache(const String& cacheDirectory, ApplicationCacheHost* cacheHost) +{ + ApplicationCache* cache = cacheHost->applicationCache(); + if (!cache) + return true; + + // Create a new cache. + RefPtr<ApplicationCache> cacheCopy = ApplicationCache::create(); + + cacheCopy->setOnlineWhitelist(cache->onlineWhitelist()); + cacheCopy->setFallbackURLs(cache->fallbackURLs()); + + // Traverse the cache and add copies of all resources. + ApplicationCache::ResourceMap::const_iterator end = cache->end(); + for (ApplicationCache::ResourceMap::const_iterator it = cache->begin(); it != end; ++it) { + ApplicationCacheResource* resource = it->second.get(); + + RefPtr<ApplicationCacheResource> resourceCopy = ApplicationCacheResource::create(resource->url(), resource->response(), resource->type(), resource->data()); + + cacheCopy->addResource(resourceCopy.release()); + } + + // Now create a new cache group. + OwnPtr<ApplicationCacheGroup> groupCopy(adoptPtr(new ApplicationCacheGroup(cache->group()->manifestURL(), true))); + + groupCopy->setNewestCache(cacheCopy); + + ApplicationCacheStorage copyStorage; + copyStorage.setCacheDirectory(cacheDirectory); + + // Empty the cache in case something was there before. + copyStorage.empty(); + + return copyStorage.storeNewestCache(groupCopy.get()); +} + +bool ApplicationCacheStorage::manifestURLs(Vector<KURL>* urls) +{ + ASSERT(urls); + openDatabase(false); + if (!m_database.isOpen()) + return false; + + SQLiteStatement selectURLs(m_database, "SELECT manifestURL FROM CacheGroups"); + + if (selectURLs.prepare() != SQLResultOk) + return false; + + while (selectURLs.step() == SQLResultRow) + urls->append(KURL(ParsedURLString, selectURLs.getColumnText(0))); + + return true; +} + +bool ApplicationCacheStorage::cacheGroupSize(const String& manifestURL, int64_t* size) +{ + ASSERT(size); + openDatabase(false); + if (!m_database.isOpen()) + return false; + + SQLiteStatement statement(m_database, "SELECT sum(Caches.size) FROM Caches INNER JOIN CacheGroups ON Caches.cacheGroup=CacheGroups.id WHERE CacheGroups.manifestURL=?"); + if (statement.prepare() != SQLResultOk) + return false; + + statement.bindText(1, manifestURL); + + int result = statement.step(); + if (result == SQLResultDone) + return false; + + if (result != SQLResultRow) { + LOG_ERROR("Could not get the size of the cache group, error \"%s\"", m_database.lastErrorMsg()); + return false; + } + + *size = statement.getColumnInt64(0); + return true; +} + +bool ApplicationCacheStorage::deleteCacheGroup(const String& manifestURL) +{ + SQLiteTransaction deleteTransaction(m_database); + // Check to see if the group is in memory. + ApplicationCacheGroup* group = m_cachesInMemory.get(manifestURL); + if (group) + cacheGroupMadeObsolete(group); + else { + // The cache group is not in memory, so remove it from the disk. + openDatabase(false); + if (!m_database.isOpen()) + return false; + + SQLiteStatement idStatement(m_database, "SELECT id FROM CacheGroups WHERE manifestURL=?"); + if (idStatement.prepare() != SQLResultOk) + return false; + + idStatement.bindText(1, manifestURL); + + int result = idStatement.step(); + if (result == SQLResultDone) + return false; + + if (result != SQLResultRow) { + LOG_ERROR("Could not load cache group id, error \"%s\"", m_database.lastErrorMsg()); + return false; + } + + int64_t groupId = idStatement.getColumnInt64(0); + + SQLiteStatement cacheStatement(m_database, "DELETE FROM Caches WHERE cacheGroup=?"); + if (cacheStatement.prepare() != SQLResultOk) + return false; + + SQLiteStatement groupStatement(m_database, "DELETE FROM CacheGroups WHERE id=?"); + if (groupStatement.prepare() != SQLResultOk) + return false; + + cacheStatement.bindInt64(1, groupId); + executeStatement(cacheStatement); + groupStatement.bindInt64(1, groupId); + executeStatement(groupStatement); + } + + deleteTransaction.commit(); + return true; +} + +void ApplicationCacheStorage::vacuumDatabaseFile() +{ + openDatabase(false); + if (!m_database.isOpen()) + return; + + m_database.runVacuumCommand(); +} + +void ApplicationCacheStorage::checkForMaxSizeReached() +{ + if (m_database.lastError() == SQLResultFull) + m_isMaximumSizeReached = true; +} + +ApplicationCacheStorage::ApplicationCacheStorage() + : m_maximumSize(ApplicationCacheStorage::noQuota()) + , m_isMaximumSizeReached(false) + , m_defaultOriginQuota(ApplicationCacheStorage::noQuota()) +{ +} + +ApplicationCacheStorage& cacheStorage() +{ + DEFINE_STATIC_LOCAL(ApplicationCacheStorage, storage, ()); + + return storage; +} + +} // namespace WebCore + +#endif // ENABLE(OFFLINE_WEB_APPLICATIONS) diff --git a/Source/WebCore/loader/appcache/ApplicationCacheStorage.h b/Source/WebCore/loader/appcache/ApplicationCacheStorage.h new file mode 100644 index 0000000..7db34e6 --- /dev/null +++ b/Source/WebCore/loader/appcache/ApplicationCacheStorage.h @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2008, 2010 Apple Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef ApplicationCacheStorage_h +#define ApplicationCacheStorage_h + +#if ENABLE(OFFLINE_WEB_APPLICATIONS) + +#include "PlatformString.h" +#include "SQLiteDatabase.h" +#include <wtf/HashCountedSet.h> +#include <wtf/text/StringHash.h> + +namespace WebCore { + +class ApplicationCache; +class ApplicationCacheGroup; +class ApplicationCacheHost; +class ApplicationCacheResource; +class KURL; +template <class T> +class StorageIDJournal; +class SecurityOrigin; + +class ApplicationCacheStorage : public Noncopyable { +public: + enum FailureReason { + OriginQuotaReached, + TotalQuotaReached, + DiskOrOperationFailure + }; + + void setCacheDirectory(const String&); + const String& cacheDirectory() const; + + void setMaximumSize(int64_t size); + int64_t maximumSize() const; + bool isMaximumSizeReached() const; + int64_t spaceNeeded(int64_t cacheToSave); + + int64_t defaultOriginQuota() const { return m_defaultOriginQuota; } + void setDefaultOriginQuota(int64_t quota); + bool usageForOrigin(const SecurityOrigin*, int64_t& usage); + bool quotaForOrigin(const SecurityOrigin*, int64_t& quota); + bool remainingSizeForOriginExcludingCache(const SecurityOrigin*, ApplicationCache*, int64_t& remainingSize); + bool storeUpdatedQuotaForOrigin(const SecurityOrigin*, int64_t quota); + + ApplicationCacheGroup* cacheGroupForURL(const KURL&); // Cache to load a main resource from. + ApplicationCacheGroup* fallbackCacheGroupForURL(const KURL&); // Cache that has a fallback entry to load a main resource from if normal loading fails. + + ApplicationCacheGroup* findOrCreateCacheGroup(const KURL& manifestURL); + void cacheGroupDestroyed(ApplicationCacheGroup*); + void cacheGroupMadeObsolete(ApplicationCacheGroup*); + + bool storeNewestCache(ApplicationCacheGroup*, ApplicationCache* oldCache, FailureReason& failureReason); + bool storeNewestCache(ApplicationCacheGroup*); // Updates the cache group, but doesn't remove old cache. + bool store(ApplicationCacheResource*, ApplicationCache*); + bool storeUpdatedType(ApplicationCacheResource*, ApplicationCache*); + + // Removes the group if the cache to be removed is the newest one (so, storeNewestCache() needs to be called beforehand when updating). + void remove(ApplicationCache*); + + void empty(); + + static bool storeCopyOfCache(const String& cacheDirectory, ApplicationCacheHost*); + + bool manifestURLs(Vector<KURL>* urls); + bool cacheGroupSize(const String& manifestURL, int64_t* size); + bool deleteCacheGroup(const String& manifestURL); + void vacuumDatabaseFile(); + + static int64_t unknownQuota() { return -1; } + static int64_t noQuota() { return std::numeric_limits<int64_t>::max(); } +private: + ApplicationCacheStorage(); + PassRefPtr<ApplicationCache> loadCache(unsigned storageID); + ApplicationCacheGroup* loadCacheGroup(const KURL& manifestURL); + + typedef StorageIDJournal<ApplicationCacheResource> ResourceStorageIDJournal; + typedef StorageIDJournal<ApplicationCacheGroup> GroupStorageIDJournal; + + bool store(ApplicationCacheGroup*, GroupStorageIDJournal*); + bool store(ApplicationCache*, ResourceStorageIDJournal*); + bool store(ApplicationCacheResource*, unsigned cacheStorageID); + + bool ensureOriginRecord(const SecurityOrigin*); + + void loadManifestHostHashes(); + + void verifySchemaVersion(); + + void openDatabase(bool createIfDoesNotExist); + + bool executeStatement(SQLiteStatement&); + bool executeSQLCommand(const String&); + + void checkForMaxSizeReached(); + + String m_cacheDirectory; + String m_cacheFile; + + int64_t m_maximumSize; + bool m_isMaximumSizeReached; + + int64_t m_defaultOriginQuota; + + SQLiteDatabase m_database; + + // In order to quickly determine if a given resource exists in an application cache, + // we keep a hash set of the hosts of the manifest URLs of all non-obsolete cache groups. + HashCountedSet<unsigned, AlreadyHashed> m_cacheHostSet; + + typedef HashMap<String, ApplicationCacheGroup*> CacheGroupMap; + CacheGroupMap m_cachesInMemory; // Excludes obsolete cache groups. + + friend ApplicationCacheStorage& cacheStorage(); +}; + +ApplicationCacheStorage& cacheStorage(); + +} // namespace WebCore + +#endif // ENABLE(OFFLINE_WEB_APPLICATIONS) + +#endif // ApplicationCacheStorage_h diff --git a/Source/WebCore/loader/appcache/DOMApplicationCache.cpp b/Source/WebCore/loader/appcache/DOMApplicationCache.cpp new file mode 100644 index 0000000..b9297b1 --- /dev/null +++ b/Source/WebCore/loader/appcache/DOMApplicationCache.cpp @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2008, 2009 Apple Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "DOMApplicationCache.h" + +#if ENABLE(OFFLINE_WEB_APPLICATIONS) + +#include "ApplicationCacheHost.h" +#include "DocumentLoader.h" +#include "Event.h" +#include "EventException.h" +#include "EventListener.h" +#include "EventNames.h" +#include "Frame.h" +#include "FrameLoader.h" + +namespace WebCore { + +DOMApplicationCache::DOMApplicationCache(Frame* frame) + : m_frame(frame) +{ + ApplicationCacheHost* cacheHost = applicationCacheHost(); + if (cacheHost) + cacheHost->setDOMApplicationCache(this); +} + +void DOMApplicationCache::disconnectFrame() +{ + ApplicationCacheHost* cacheHost = applicationCacheHost(); + if (cacheHost) + cacheHost->setDOMApplicationCache(0); + m_frame = 0; +} + +ApplicationCacheHost* DOMApplicationCache::applicationCacheHost() const +{ + if (!m_frame || !m_frame->loader()->documentLoader()) + return 0; + return m_frame->loader()->documentLoader()->applicationCacheHost(); +} + +unsigned short DOMApplicationCache::status() const +{ + ApplicationCacheHost* cacheHost = applicationCacheHost(); + if (!cacheHost) + return ApplicationCacheHost::UNCACHED; + return cacheHost->status(); +} + +void DOMApplicationCache::update(ExceptionCode& ec) +{ + ApplicationCacheHost* cacheHost = applicationCacheHost(); + if (!cacheHost || !cacheHost->update()) + ec = INVALID_STATE_ERR; +} + +void DOMApplicationCache::swapCache(ExceptionCode& ec) +{ + ApplicationCacheHost* cacheHost = applicationCacheHost(); + if (!cacheHost || !cacheHost->swapCache()) + ec = INVALID_STATE_ERR; +} + +ScriptExecutionContext* DOMApplicationCache::scriptExecutionContext() const +{ + if (m_frame) + return m_frame->document(); + return 0; +} + +const AtomicString& DOMApplicationCache::toEventType(ApplicationCacheHost::EventID id) +{ + switch (id) { + case ApplicationCacheHost::CHECKING_EVENT: + return eventNames().checkingEvent; + case ApplicationCacheHost::ERROR_EVENT: + return eventNames().errorEvent; + case ApplicationCacheHost::NOUPDATE_EVENT: + return eventNames().noupdateEvent; + case ApplicationCacheHost::DOWNLOADING_EVENT: + return eventNames().downloadingEvent; + case ApplicationCacheHost::PROGRESS_EVENT: + return eventNames().progressEvent; + case ApplicationCacheHost::UPDATEREADY_EVENT: + return eventNames().updatereadyEvent; + case ApplicationCacheHost::CACHED_EVENT: + return eventNames().cachedEvent; + case ApplicationCacheHost::OBSOLETE_EVENT: + return eventNames().obsoleteEvent; + } + ASSERT_NOT_REACHED(); + return eventNames().errorEvent; +} + +EventTargetData* DOMApplicationCache::eventTargetData() +{ + return &m_eventTargetData; +} + +EventTargetData* DOMApplicationCache::ensureEventTargetData() +{ + return &m_eventTargetData; +} + +} // namespace WebCore + +#endif // ENABLE(OFFLINE_WEB_APPLICATIONS) diff --git a/Source/WebCore/loader/appcache/DOMApplicationCache.h b/Source/WebCore/loader/appcache/DOMApplicationCache.h new file mode 100644 index 0000000..2a806fa --- /dev/null +++ b/Source/WebCore/loader/appcache/DOMApplicationCache.h @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2008, 2009 Apple Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef DOMApplicationCache_h +#define DOMApplicationCache_h + +#if ENABLE(OFFLINE_WEB_APPLICATIONS) + +#include "ApplicationCacheHost.h" +#include "EventListener.h" +#include "EventNames.h" +#include "EventTarget.h" +#include <wtf/Forward.h> +#include <wtf/HashMap.h> +#include <wtf/PassRefPtr.h> +#include <wtf/RefCounted.h> +#include <wtf/Vector.h> +#include <wtf/text/AtomicStringHash.h> + +namespace WebCore { + +class Frame; +class KURL; + +class DOMApplicationCache : public RefCounted<DOMApplicationCache>, public EventTarget { +public: + static PassRefPtr<DOMApplicationCache> create(Frame* frame) { return adoptRef(new DOMApplicationCache(frame)); } + ~DOMApplicationCache() { ASSERT(!m_frame); } + + Frame* frame() const { return m_frame; } + void disconnectFrame(); + + unsigned short status() const; + void update(ExceptionCode&); + void swapCache(ExceptionCode&); + + // EventTarget impl + + using RefCounted<DOMApplicationCache>::ref; + using RefCounted<DOMApplicationCache>::deref; + + // Explicitly named attribute event listener helpers + + DEFINE_ATTRIBUTE_EVENT_LISTENER(checking); + DEFINE_ATTRIBUTE_EVENT_LISTENER(error); + DEFINE_ATTRIBUTE_EVENT_LISTENER(noupdate); + DEFINE_ATTRIBUTE_EVENT_LISTENER(downloading); + DEFINE_ATTRIBUTE_EVENT_LISTENER(progress); + DEFINE_ATTRIBUTE_EVENT_LISTENER(updateready); + DEFINE_ATTRIBUTE_EVENT_LISTENER(cached); + DEFINE_ATTRIBUTE_EVENT_LISTENER(obsolete); + + virtual ScriptExecutionContext* scriptExecutionContext() const; + DOMApplicationCache* toDOMApplicationCache() { return this; } + + static const AtomicString& toEventType(ApplicationCacheHost::EventID id); + +private: + DOMApplicationCache(Frame*); + + virtual void refEventTarget() { ref(); } + virtual void derefEventTarget() { deref(); } + virtual EventTargetData* eventTargetData(); + virtual EventTargetData* ensureEventTargetData(); + + ApplicationCacheHost* applicationCacheHost() const; + + Frame* m_frame; + EventTargetData m_eventTargetData; +}; + +} // namespace WebCore + +#endif // ENABLE(OFFLINE_WEB_APPLICATIONS) + +#endif // DOMApplicationCache_h diff --git a/Source/WebCore/loader/appcache/DOMApplicationCache.idl b/Source/WebCore/loader/appcache/DOMApplicationCache.idl new file mode 100644 index 0000000..9113ffa --- /dev/null +++ b/Source/WebCore/loader/appcache/DOMApplicationCache.idl @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2008, 2009 Apple Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +module offline { + + interface [ + Conditional=OFFLINE_WEB_APPLICATIONS, + EventTarget, + OmitConstructor, + DontCheckEnums + ] DOMApplicationCache { + // update status + const unsigned short UNCACHED = 0; + const unsigned short IDLE = 1; + const unsigned short CHECKING = 2; + const unsigned short DOWNLOADING = 3; + const unsigned short UPDATEREADY = 4; + const unsigned short OBSOLETE = 5; + readonly attribute unsigned short status; + + void update() + raises(DOMException); + void swapCache() + raises(DOMException); + + // events + attribute EventListener onchecking; + attribute EventListener onerror; + attribute EventListener onnoupdate; + attribute EventListener ondownloading; + attribute EventListener onprogress; + attribute EventListener onupdateready; + attribute EventListener oncached; + attribute EventListener onobsolete; + + // EventTarget interface + void addEventListener(in DOMString type, + in EventListener listener, + in boolean useCapture); + void removeEventListener(in DOMString type, + in EventListener listener, + in boolean useCapture); + boolean dispatchEvent(in Event evt) + raises(EventException); + }; + +} diff --git a/Source/WebCore/loader/appcache/ManifestParser.cpp b/Source/WebCore/loader/appcache/ManifestParser.cpp new file mode 100644 index 0000000..f58a55d --- /dev/null +++ b/Source/WebCore/loader/appcache/ManifestParser.cpp @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2008 Apple Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "ManifestParser.h" + +#if ENABLE(OFFLINE_WEB_APPLICATIONS) + +#include "CharacterNames.h" +#include "KURL.h" +#include "TextResourceDecoder.h" + +using namespace std; + +namespace WebCore { + +enum Mode { Explicit, Fallback, OnlineWhitelist, Unknown }; + +bool parseManifest(const KURL& manifestURL, const char* data, int length, Manifest& manifest) +{ + ASSERT(manifest.explicitURLs.isEmpty()); + ASSERT(manifest.onlineWhitelistedURLs.isEmpty()); + ASSERT(manifest.fallbackURLs.isEmpty()); + manifest.allowAllNetworkRequests = false; + + Mode mode = Explicit; + + RefPtr<TextResourceDecoder> decoder = TextResourceDecoder::create("text/cache-manifest", "UTF-8"); + String s = decoder->decode(data, length); + s += decoder->flush(); + + // Look for the magic signature: "^\xFEFF?CACHE MANIFEST[ \t]?" (the BOM is removed by TextResourceDecoder). + // Example: "CACHE MANIFEST #comment" is a valid signature. + // Example: "CACHE MANIFEST;V2" is not. + if (!s.startsWith("CACHE MANIFEST")) + return false; + + const UChar* end = s.characters() + s.length(); + const UChar* p = s.characters() + 14; // "CACHE MANIFEST" is 14 characters. + + if (p < end && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') + return false; + + // Skip to the end of the line. + while (p < end && *p != '\r' && *p != '\n') + p++; + + while (1) { + // Skip whitespace + while (p < end && (*p == '\n' || *p == '\r' || *p == ' ' || *p == '\t')) + p++; + + if (p == end) + break; + + const UChar* lineStart = p; + + // Find the end of the line + while (p < end && *p != '\r' && *p != '\n') + p++; + + // Check if we have a comment + if (*lineStart == '#') + continue; + + // Get rid of trailing whitespace + const UChar* tmp = p - 1; + while (tmp > lineStart && (*tmp == ' ' || *tmp == '\t')) + tmp--; + + String line(lineStart, tmp - lineStart + 1); + + if (line == "CACHE:") + mode = Explicit; + else if (line == "FALLBACK:") + mode = Fallback; + else if (line == "NETWORK:") + mode = OnlineWhitelist; + else if (line.endsWith(":")) + mode = Unknown; + else if (mode == Unknown) + continue; + else if (mode == Explicit || mode == OnlineWhitelist) { + const UChar* p = line.characters(); + const UChar* lineEnd = p + line.length(); + + // Look for whitespace separating the URL from subsequent ignored tokens. + while (p < lineEnd && *p != '\t' && *p != ' ') + p++; + + if (mode == OnlineWhitelist && p - line.characters() == 1 && *line.characters() == '*') { + // Wildcard was found. + manifest.allowAllNetworkRequests = true; + continue; + } + + KURL url(manifestURL, String(line.characters(), p - line.characters())); + + if (!url.isValid()) + continue; + + if (url.hasFragmentIdentifier()) + url.removeFragmentIdentifier(); + + if (!equalIgnoringCase(url.protocol(), manifestURL.protocol())) + continue; + + if (mode == Explicit && manifestURL.protocolIs("https") && !protocolHostAndPortAreEqual(manifestURL, url)) + continue; + + if (mode == Explicit) + manifest.explicitURLs.add(url.string()); + else + manifest.onlineWhitelistedURLs.append(url); + + } else if (mode == Fallback) { + const UChar* p = line.characters(); + const UChar* lineEnd = p + line.length(); + + // Look for whitespace separating the two URLs + while (p < lineEnd && *p != '\t' && *p != ' ') + p++; + + if (p == lineEnd) { + // There was no whitespace separating the URLs. + continue; + } + + KURL namespaceURL(manifestURL, String(line.characters(), p - line.characters())); + if (!namespaceURL.isValid()) + continue; + if (namespaceURL.hasFragmentIdentifier()) + namespaceURL.removeFragmentIdentifier(); + + if (!protocolHostAndPortAreEqual(manifestURL, namespaceURL)) + continue; + + // Skip whitespace separating fallback namespace from URL. + while (p < lineEnd && (*p == '\t' || *p == ' ')) + p++; + + // Look for whitespace separating the URL from subsequent ignored tokens. + const UChar* fallbackStart = p; + while (p < lineEnd && *p != '\t' && *p != ' ') + p++; + + KURL fallbackURL(manifestURL, String(fallbackStart, p - fallbackStart)); + if (!fallbackURL.isValid()) + continue; + if (fallbackURL.hasFragmentIdentifier()) + fallbackURL.removeFragmentIdentifier(); + + if (!protocolHostAndPortAreEqual(manifestURL, fallbackURL)) + continue; + + manifest.fallbackURLs.append(make_pair(namespaceURL, fallbackURL)); + } else + ASSERT_NOT_REACHED(); + } + + return true; +} + +} + +#endif // ENABLE(OFFLINE_WEB_APPLICATIONS) diff --git a/Source/WebCore/loader/appcache/ManifestParser.h b/Source/WebCore/loader/appcache/ManifestParser.h new file mode 100644 index 0000000..f0369ee --- /dev/null +++ b/Source/WebCore/loader/appcache/ManifestParser.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2008 Apple Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef ManifestParser_h +#define ManifestParser_h + +#if ENABLE(OFFLINE_WEB_APPLICATIONS) + +#include "ApplicationCache.h" + +namespace WebCore { + + class KURL; + + struct Manifest { + Vector<KURL> onlineWhitelistedURLs; + HashSet<String> explicitURLs; + FallbackURLVector fallbackURLs; + bool allowAllNetworkRequests; // Wildcard found in NETWORK section. + }; + + bool parseManifest(const KURL& manifestURL, const char* data, int length, Manifest&); + +} + +#endif // ENABLE(OFFLINE_WEB_APPLICATIONS) + +#endif // ManifestParser_h |