blob: ff267674514143cd73c48a5fe55fe6f94a1d1be4 [file] [log] [blame]
/*
* 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 <wtf/HashMap.h>
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<ApplicationCacheResource> 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<ApplicationCacheResource> 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<DocumentLoader*>::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<ApplicationCache> 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<String>::const_iterator end = manifest.explicitURLs.end();
for (HashSet<String>::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<DocumentLoader*>::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<RefPtr<DocumentLoader> > documentLoaders;
if (isUpgradeAttempt) {
ASSERT(m_cacheCandidates.isEmpty());
copyToVector(m_associatedDocumentLoaders, documentLoaders);
} else {
while (!m_cacheCandidates.isEmpty()) {
HashSet<DocumentLoader*>::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<EntryMap::iterator, bool> 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<RefPtr<DocumentLoader> > loaders;
copyToVector(m_associatedDocumentLoaders, loaders);
callListeners(listenerFunction, loaders);
}
void ApplicationCacheGroup::callListeners(ListenerFunction listenerFunction, const Vector<RefPtr<DocumentLoader> >& 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<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)