ApplicationCacheGroup.cpp   [plain text]


/*
 * 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 {

const unsigned maximumCacheSize = 5 * 1024 * 1024;

ApplicationCacheGroup::ApplicationCacheGroup(const KURL& manifestURL)
    : m_manifestURL(manifestURL)
    , m_status(Idle)
    , m_savedNewestCachePointer(0)
    , m_frame(0)
    , m_storageID(0)
    , m_loadedSize(0)
{
}

ApplicationCacheGroup::~ApplicationCacheGroup()
{
    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().string());
            ASSERT(resource);
            if (!resource)
                return;
            
            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.string());
               
        // Add the main resource URL as an implicit entry.
        group->addEntry(url.string(), 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.string()));
 
    EntryMap::iterator it = m_pendingEntries.find(url.string());
    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);

        m_manifestHandle->setClient(0);
        m_manifestHandle->cancel();
        m_manifestHandle = 0;
    }
    
    if (m_currentHandle) {
        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.isEmpty() || 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;
    }
    
    if (m_caches.isEmpty()) {
        // There is an initial cache attempt in progress
        ASSERT(m_cacheBeingUpdated);
        
        // 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_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.string()));
    
    unsigned type = m_pendingEntries.get(url.string());
    
    // 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);

    m_loadedSize += length;
}

void ApplicationCacheGroup::didFinishLoading(ResourceHandle* handle)
{
    if (handle == m_manifestHandle) {
        didFinishLoadingManifest();
        return;
    }
    
    if (m_loadedSize > maximumCacheSize) {
        cacheUpdateFailed();
        return;
    }
 
    ASSERT(m_currentHandle == handle);
    ASSERT(m_pendingEntries.contains(handle->request().url().string()));
    
    m_pendingEntries.remove(handle->request().url().string());
    
    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;
    m_loadedSize = 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().string() == 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)