diff -r 000000000000 -r 4f2f89ce4247 WebCore/loader/appcache/ApplicationCacheGroup.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebCore/loader/appcache/ApplicationCacheGroup.cpp Fri Sep 17 09:02:29 2010 +0300 @@ -0,0 +1,1111 @@ +/* + * 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 "MainResourceLoader.h" +#include "ManifestParser.h" +#include "Page.h" +#include "Settings.h" +#include + +#if ENABLE(INSPECTOR) +#include "InspectorApplicationCacheAgent.h" +#include "InspectorController.h" +#include "ProgressTracker.h" +#else +#include +#endif + +namespace WebCore { + +ApplicationCacheGroup::ApplicationCacheGroup(const KURL& manifestURL, bool isCopy) + : m_manifestURL(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) +{ +} + +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) { + 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 documentURL(documentLoader->url()); + if (documentURL.hasFragmentIdentifier()) + documentURL.removeFragmentIdentifier(); + ApplicationCacheResource* resource = mainResourceCache->resourceForURL(documentURL); + 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->redirectScheduler()->scheduleLocationChange(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::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; + } +} + +#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 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 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 handle = ResourceHandle::create(request, this, m_frame, 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, 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); +} + +void ApplicationCacheGroup::didFinishLoading(ResourceHandle* handle) +{ +#if ENABLE(INSPECTOR) + if (Page* page = m_frame->page()) + page->inspectorController()->didFinishLoading(m_currentResourceIdentifier); +#endif + + if (handle == m_manifestHandle) { + didFinishLoadingManifest(); + 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::const_iterator masterEnd = m_pendingMasterResourceLoaders.end(); + for (HashSet::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::const_iterator end = manifest.explicitURLs.end(); + for (HashSet::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::cacheUpdateFailed() +{ + stopLoading(); + m_manifestResource = 0; + + // Wait for master resource loads to finish. + m_completionType = Failure; + deliverDelayedMainResources(); +} + +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::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). () + + 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); + } + + RefPtr oldNewestCache = (m_newestCache == m_cacheBeingUpdated) ? 0 : m_newestCache; + + setNewestCache(m_cacheBeingUpdated.release()); + if (cacheStorage().storeNewestCache(this)) { + // 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); + } else { + if (cacheStorage().isMaximumSizeReached() && !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; + } else { + // 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 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_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 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 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. +} + +class CallCacheListenerTask : public ScriptExecutionContext::Task { +public: + static PassOwnPtr create(PassRefPtr 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 loader, ApplicationCacheHost::EventID eventID, int progressTotal, int progressDone) + : m_documentLoader(loader) + , m_eventID(eventID) + , m_progressTotal(progressTotal) + , m_progressDone(progressDone) + { + } + + RefPtr m_documentLoader; + ApplicationCacheHost::EventID m_eventID; + int m_progressTotal; + int m_progressDone; +}; + +void ApplicationCacheGroup::postListenerTask(ApplicationCacheHost::EventID eventID, int progressTotal, int progressDone, const HashSet& loaderSet) +{ + HashSet::const_iterator loaderSetEnd = loaderSet.end(); + for (HashSet::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::const_iterator end = m_caches.end(); + for (HashSet::const_iterator it = m_caches.begin(); it != end; ++it) + (*it)->clearStorageID(); +} + + +} + +#endif // ENABLE(OFFLINE_WEB_APPLICATIONS)