/* * 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 "ApplicationCacheGroup.h" #if ENABLE(OFFLINE_WEB_APPLICATIONS) #include "ApplicationCache.h" #include "ApplicationCacheResource.h" #include "ApplicationCacheStorage.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 namespace WebCore { ApplicationCacheGroup::ApplicationCacheGroup(const KURL& manifestURL, bool isCopy) : m_manifestURL(manifestURL) , m_status(Idle) , m_savedNewestCachePointer(0) , m_frame(0) , m_storageID(0) , m_isCopy(isCopy) { } 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_cacheCandidates.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* loader) { if (!ApplicationCache::requestIsHTTPOrHTTPSGet(request)) return 0; ASSERT(loader->frame()); ASSERT(loader->frame()->page()); if (loader->frame() != loader->frame()->page()->mainFrame()) return 0; if (ApplicationCacheGroup* group = cacheStorage().cacheGroupForURL(request.url())) { ASSERT(group->newestCache()); return group->newestCache(); } return 0; } void ApplicationCacheGroup::selectCache(Frame* frame, const KURL& manifestURL) { ASSERT(frame && frame->page()); if (!frame->settings()->offlineWebApplicationCacheEnabled()) return; DocumentLoader* documentLoader = frame->loader()->documentLoader(); ASSERT(!documentLoader->applicationCache()); if (manifestURL.isNull()) { selectCacheWithoutManifestURL(frame); return; } ApplicationCache* mainResourceCache = documentLoader->mainResourceApplicationCache(); // Check if the main resource is being loaded as part of navigation of the main frame bool isMainFrame = frame->page()->mainFrame() == frame; if (!isMainFrame) { if (mainResourceCache && manifestURL != mainResourceCache->group()->manifestURL()) { ApplicationCacheResource* resource = mainResourceCache->resourceForURL(documentLoader->originalURL()); ASSERT(resource); resource->addType(ApplicationCacheResource::Foreign); } return; } if (mainResourceCache) { if (manifestURL == mainResourceCache->group()->m_manifestURL) { mainResourceCache->group()->associateDocumentLoaderWithCache(documentLoader, mainResourceCache); mainResourceCache->group()->update(frame); } else { // FIXME: If the resource being loaded was loaded from an application cache and the URI of // that application cache's manifest is not the same as the manifest URI with which the algorithm was invoked // then we should "undo" the navigation. } 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)) { selectCacheWithoutManifestURL(frame); return; } // Check that the resource URL has the same scheme/host/port as the manifest URL. if (!protocolHostAndPortAreEqual(manifestURL, request.url())) { selectCacheWithoutManifestURL(frame); return; } ApplicationCacheGroup* group = cacheStorage().findOrCreateCacheGroup(manifestURL); if (ApplicationCache* cache = group->newestCache()) { ASSERT(cache->manifestResource()); group->associateDocumentLoaderWithCache(frame->loader()->documentLoader(), cache); if (!frame->loader()->documentLoader()->isLoadingMainResource()) group->finishedLoadingMainResource(frame->loader()->documentLoader()); group->update(frame); } else { bool isUpdating = group->m_cacheBeingUpdated; if (!isUpdating) group->m_cacheBeingUpdated = ApplicationCache::create(); documentLoader->setCandidateApplicationCacheGroup(group); group->m_cacheCandidates.add(documentLoader); const KURL& url = frame->loader()->documentLoader()->originalURL(); unsigned type = 0; // If the resource has already been downloaded, remove it so that it will be replaced with the implicit resource if (isUpdating) type = group->m_cacheBeingUpdated->removeResource(url); // Add the main resource URL as an implicit entry. group->addEntry(url, type | ApplicationCacheResource::Implicit); if (!frame->loader()->documentLoader()->isLoadingMainResource()) group->finishedLoadingMainResource(frame->loader()->documentLoader()); if (!isUpdating) group->update(frame); } } void ApplicationCacheGroup::selectCacheWithoutManifestURL(Frame* frame) { if (!frame->settings()->offlineWebApplicationCacheEnabled()) return; DocumentLoader* documentLoader = frame->loader()->documentLoader(); ASSERT(!documentLoader->applicationCache()); ApplicationCache* mainResourceCache = documentLoader->mainResourceApplicationCache(); bool isMainFrame = frame->page()->mainFrame() == frame; if (isMainFrame && mainResourceCache) { mainResourceCache->group()->associateDocumentLoaderWithCache(documentLoader, mainResourceCache); mainResourceCache->group()->update(frame); } } void ApplicationCacheGroup::finishedLoadingMainResource(DocumentLoader* loader) { const KURL& url = loader->originalURL(); if (ApplicationCache* cache = loader->applicationCache()) { RefPtr resource = ApplicationCacheResource::create(url, loader->response(), ApplicationCacheResource::Implicit, loader->mainResourceData()); cache->addResource(resource.release()); if (!m_cacheBeingUpdated) return; } ASSERT(m_pendingEntries.contains(url)); EntryMap::iterator it = m_pendingEntries.find(url); ASSERT(it->second & ApplicationCacheResource::Implicit); RefPtr resource = ApplicationCacheResource::create(url, loader->response(), it->second, loader->mainResourceData()); ASSERT(m_cacheBeingUpdated); m_cacheBeingUpdated->addResource(resource.release()); m_pendingEntries.remove(it); checkIfLoadIsComplete(); } void ApplicationCacheGroup::failedLoadingMainResource(DocumentLoader* loader) { ASSERT(m_cacheCandidates.contains(loader) || m_associatedDocumentLoaders.contains(loader)); // Note that cacheUpdateFailed() can cause the cache group to be deleted. cacheUpdateFailed(); } void ApplicationCacheGroup::stopLoading() { if (m_manifestHandle) { ASSERT(!m_currentHandle); ASSERT(!m_cacheBeingUpdated); 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; } void ApplicationCacheGroup::documentLoaderDestroyed(DocumentLoader* loader) { HashSet::iterator it = m_associatedDocumentLoaders.find(loader); if (it != m_associatedDocumentLoaders.end()) { ASSERT(!m_cacheCandidates.contains(loader)); m_associatedDocumentLoaders.remove(it); } else { ASSERT(m_cacheCandidates.contains(loader)); m_cacheCandidates.remove(loader); } if (!m_associatedDocumentLoaders.isEmpty() || !m_cacheCandidates.isEmpty()) return; // We should only have the newest cache remaining, or there is an initial cache attempt in progress. ASSERT(m_caches.size() == 1 || m_cacheBeingUpdated); // If a cache update is in progress, stop it. if (m_caches.size() == 1) { ASSERT(m_caches.contains(m_newestCache.get())); // Release our reference to the newest cache. m_savedNewestCachePointer = m_newestCache.get(); // This could cause us to be deleted. m_newestCache = 0; return; } // There is an initial cache attempt in progress ASSERT(m_cacheBeingUpdated); ASSERT(m_caches.size() == 0); // Delete ourselves, causing the cache attempt to be stopped. delete this; } void ApplicationCacheGroup::cacheDestroyed(ApplicationCache* cache) { ASSERT(m_caches.contains(cache)); m_caches.remove(cache); if (cache != m_savedNewestCachePointer) cacheStorage().remove(cache); if (m_caches.isEmpty()) delete this; } void ApplicationCacheGroup::setNewestCache(PassRefPtr newestCache) { ASSERT(!m_newestCache); ASSERT(!m_caches.contains(newestCache.get())); ASSERT(!newestCache->group()); m_newestCache = newestCache; m_caches.add(m_newestCache.get()); m_newestCache->setGroup(this); } void ApplicationCacheGroup::update(Frame* frame) { if (m_status == Checking || m_status == Downloading) return; ASSERT(!m_frame); m_frame = frame; m_status = Checking; callListenersOnAssociatedDocuments(&DOMApplicationCache::callCheckingListener); ASSERT(!m_manifestHandle); ASSERT(!m_manifestResource); // FIXME: Handle defer loading ResourceRequest request(m_manifestURL); m_frame->loader()->applyUserAgent(request); m_manifestHandle = ResourceHandle::create(request, this, m_frame, false, true, false); } void ApplicationCacheGroup::didReceiveResponse(ResourceHandle* handle, const ResourceResponse& response) { if (handle == m_manifestHandle) { didReceiveManifestResponse(response); return; } ASSERT(handle == m_currentHandle); int statusCode = response.httpStatusCode() / 100; if (statusCode == 4 || statusCode == 5) { // Note that cacheUpdateFailed() can cause the cache group to be deleted. cacheUpdateFailed(); return; } const KURL& url = handle->request().url(); 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 implicit resources delivered here. if (!m_newestCache) ASSERT(!(type & ApplicationCacheResource::Implicit)); m_currentResource = ApplicationCacheResource::create(url, response, type); } void ApplicationCacheGroup::didReceiveData(ResourceHandle* handle, const char* data, int length, int lengthReceived) { 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 (handle == m_manifestHandle) { didFinishLoadingManifest(); return; } ASSERT(m_currentHandle == handle); ASSERT(m_pendingEntries.contains(handle->request().url())); m_pendingEntries.remove(handle->request().url()); ASSERT(m_cacheBeingUpdated); m_cacheBeingUpdated->addResource(m_currentResource.release()); m_currentHandle = 0; // Load the next file. if (!m_pendingEntries.isEmpty()) { startLoadingEntry(); return; } checkIfLoadIsComplete(); } void ApplicationCacheGroup::didFail(ResourceHandle* handle, const ResourceError&) { if (handle == m_manifestHandle) { didFailToLoadManifest(); return; } // Note that cacheUpdateFailed() can cause the cache group to be deleted. cacheUpdateFailed(); } void ApplicationCacheGroup::didReceiveManifestResponse(const ResourceResponse& response) { int statusCode = response.httpStatusCode() / 100; if (statusCode == 4 || statusCode == 5 || !equalIgnoringCase(response.mimeType(), "text/cache-manifest")) { didFailToLoadManifest(); return; } ASSERT(!m_manifestResource); ASSERT(m_manifestHandle); m_manifestResource = ApplicationCacheResource::create(m_manifestHandle->request().url(), response, ApplicationCacheResource::Manifest); } void ApplicationCacheGroup::didReceiveManifestData(const char* data, int length) { ASSERT(m_manifestResource); m_manifestResource->data()->append(data, length); } void ApplicationCacheGroup::didFinishLoadingManifest() { if (!m_manifestResource) { didFailToLoadManifest(); return; } bool isUpgradeAttempt = m_newestCache; m_manifestHandle = 0; // Check if the manifest is byte-for-byte identical. if (isUpgradeAttempt) { ApplicationCacheResource* newestManifest = m_newestCache->manifestResource(); ASSERT(newestManifest); if (newestManifest->data()->size() == m_manifestResource->data()->size() && !memcmp(newestManifest->data()->data(), m_manifestResource->data()->data(), newestManifest->data()->size())) { callListenersOnAssociatedDocuments(&DOMApplicationCache::callNoUpdateListener); m_status = Idle; m_frame = 0; m_manifestResource = 0; return; } } Manifest manifest; if (!parseManifest(m_manifestURL, m_manifestResource->data()->data(), m_manifestResource->data()->size(), manifest)) { didFailToLoadManifest(); return; } // FIXME: Add the opportunistic caching namespaces and their fallbacks. // We have the manifest, now download the resources. m_status = Downloading; callListenersOnAssociatedDocuments(&DOMApplicationCache::callDownloadingListener); #ifndef NDEBUG // We should only have implicit entries. { EntryMap::const_iterator end = m_pendingEntries.end(); for (EntryMap::const_iterator it = m_pendingEntries.begin(); it != end; ++it) ASSERT(it->second & ApplicationCacheResource::Implicit); } #endif if (isUpgradeAttempt) { ASSERT(!m_cacheBeingUpdated); m_cacheBeingUpdated = ApplicationCache::create(); 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::Opportunistic | ApplicationCacheResource::Implicit | ApplicationCacheResource::Dynamic)) 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); m_cacheBeingUpdated->setOnlineWhitelist(manifest.onlineWhitelistedURLs); startLoadingEntry(); } void ApplicationCacheGroup::cacheUpdateFailed() { stopLoading(); callListenersOnAssociatedDocuments(&DOMApplicationCache::callErrorListener); m_pendingEntries.clear(); m_manifestResource = 0; while (!m_cacheCandidates.isEmpty()) { HashSet::iterator it = m_cacheCandidates.begin(); ASSERT((*it)->candidateApplicationCacheGroup() == this); (*it)->setCandidateApplicationCacheGroup(0); m_cacheCandidates.remove(it); } m_status = Idle; m_frame = 0; // If there are no associated caches, delete ourselves if (m_associatedDocumentLoaders.isEmpty()) delete this; } void ApplicationCacheGroup::didFailToLoadManifest() { // Note that cacheUpdateFailed() can cause the cache group to be deleted. cacheUpdateFailed(); } void ApplicationCacheGroup::checkIfLoadIsComplete() { ASSERT(m_cacheBeingUpdated); if (m_manifestHandle) return; if (!m_pendingEntries.isEmpty()) return; // We're done bool isUpgradeAttempt = m_newestCache; m_cacheBeingUpdated->setManifestResource(m_manifestResource.release()); m_status = Idle; m_frame = 0; Vector > documentLoaders; if (isUpgradeAttempt) { ASSERT(m_cacheCandidates.isEmpty()); copyToVector(m_associatedDocumentLoaders, documentLoaders); } else { while (!m_cacheCandidates.isEmpty()) { HashSet::iterator it = m_cacheCandidates.begin(); DocumentLoader* loader = *it; ASSERT(!loader->applicationCache()); ASSERT(loader->candidateApplicationCacheGroup() == this); associateDocumentLoaderWithCache(loader, m_cacheBeingUpdated.get()); documentLoaders.append(loader); m_cacheCandidates.remove(it); } } setNewestCache(m_cacheBeingUpdated.release()); // Store the cache cacheStorage().storeNewestCache(this); callListeners(isUpgradeAttempt ? &DOMApplicationCache::callUpdateReadyListener : &DOMApplicationCache::callCachedListener, documentLoaders); } void ApplicationCacheGroup::startLoadingEntry() { ASSERT(m_cacheBeingUpdated); if (m_pendingEntries.isEmpty()) { checkIfLoadIsComplete(); return; } EntryMap::const_iterator it = m_pendingEntries.begin(); // If this is an initial cache attempt, we do not want to fetch any implicit entries, // since those are fed to us by the normal loader machinery. if (!m_newestCache) { // Get the first URL in the entry table that is not implicit EntryMap::const_iterator end = m_pendingEntries.end(); while (it->second & ApplicationCacheResource::Implicit) { ++it; if (it == end) return; } } callListenersOnAssociatedDocuments(&DOMApplicationCache::callProgressListener); // FIXME: If this is an upgrade attempt, the newest cache should be used as an HTTP cache. ASSERT(!m_currentHandle); ResourceRequest request(it->first); m_frame->loader()->applyUserAgent(request); m_currentHandle = ResourceHandle::create(request, this, m_frame, false, true, false); } void ApplicationCacheGroup::addEntry(const String& url, unsigned type) { ASSERT(m_cacheBeingUpdated); // Don't add the URL if we already have an implicit resource in the cache if (ApplicationCacheResource* resource = m_cacheBeingUpdated->resourceForURL(url)) { ASSERT(resource->type() & ApplicationCacheResource::Implicit); resource->addType(type); return; } // Don't add the URL if it's the same as the manifest URL. if (m_manifestResource && 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) { loader->setApplicationCache(cache); ASSERT(!m_associatedDocumentLoaders.contains(loader)); m_associatedDocumentLoaders.add(loader); } void ApplicationCacheGroup::callListenersOnAssociatedDocuments(ListenerFunction listenerFunction) { Vector > loaders; copyToVector(m_associatedDocumentLoaders, loaders); callListeners(listenerFunction, loaders); } void ApplicationCacheGroup::callListeners(ListenerFunction listenerFunction, const Vector >& loaders) { for (unsigned i = 0; i < loaders.size(); i++) { Frame* frame = loaders[i]->frame(); if (!frame) continue; ASSERT(frame->loader()->documentLoader() == loaders[i]); DOMWindow* window = frame->domWindow(); if (DOMApplicationCache* domCache = window->optionalApplicationCache()) (domCache->*listenerFunction)(); } } 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)