/* * Copyright (C) 2008-2016 Apple Inc. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "config.h" #include "ApplicationCacheHost.h" #include "ApplicationCache.h" #include "ApplicationCacheGroup.h" #include "ApplicationCacheResource.h" #include "ContentSecurityPolicy.h" #include "DocumentLoader.h" #include "DOMApplicationCache.h" #include "EventNames.h" #include "FileSystem.h" #include "Frame.h" #include "FrameLoader.h" #include "FrameLoaderClient.h" #include "InspectorInstrumentation.h" #include "MainFrame.h" #include "Page.h" #include "ProgressEvent.h" #include "ResourceHandle.h" #include "ResourceRequest.h" #include "Settings.h" #include "SubresourceLoader.h" namespace WebCore { ApplicationCacheHost::ApplicationCacheHost(DocumentLoader& documentLoader) : m_documentLoader(documentLoader) { } ApplicationCacheHost::~ApplicationCacheHost() { ASSERT(!m_applicationCache || !m_candidateApplicationCacheGroup || m_applicationCache->group() == m_candidateApplicationCacheGroup); if (m_applicationCache) m_applicationCache->group()->disassociateDocumentLoader(m_documentLoader); else if (m_candidateApplicationCacheGroup) m_candidateApplicationCacheGroup->disassociateDocumentLoader(m_documentLoader); } void ApplicationCacheHost::selectCacheWithoutManifest() { ASSERT(m_documentLoader.frame()); ApplicationCacheGroup::selectCacheWithoutManifestURL(*m_documentLoader.frame()); } void ApplicationCacheHost::selectCacheWithManifest(const URL& manifestURL) { ASSERT(m_documentLoader.frame()); ApplicationCacheGroup::selectCache(*m_documentLoader.frame(), manifestURL); } void ApplicationCacheHost::maybeLoadMainResource(ResourceRequest& request, SubstituteData& substituteData) { // Check if this request should be loaded from the application cache if (!substituteData.isValid() && isApplicationCacheEnabled() && !isApplicationCacheBlockedForRequest(request)) { ASSERT(!m_mainResourceApplicationCache); m_mainResourceApplicationCache = ApplicationCacheGroup::cacheForMainRequest(request, &m_documentLoader); if (m_mainResourceApplicationCache) { // Get the resource from the application cache. By definition, cacheForMainRequest() returns a cache that contains the resource. ApplicationCacheResource* resource = m_mainResourceApplicationCache->resourceForRequest(request); // ApplicationCache resources have fragment identifiers stripped off of their URLs, // but we'll need to restore that for the SubstituteData. ResourceResponse responseToUse = resource->response(); if (request.url().hasFragmentIdentifier()) { URL url = responseToUse.url(); url.setFragmentIdentifier(request.url().fragmentIdentifier()); responseToUse.setURL(url); } substituteData = SubstituteData(&resource->data(), URL(), responseToUse, SubstituteData::SessionHistoryVisibility::Visible); } } } void ApplicationCacheHost::maybeLoadMainResourceForRedirect(ResourceRequest& request, SubstituteData& substituteData) { ASSERT(status() == UNCACHED); maybeLoadMainResource(request, substituteData); } bool ApplicationCacheHost::maybeLoadFallbackForMainResponse(const ResourceRequest& request, const ResourceResponse& r) { if (r.httpStatusCode() / 100 == 4 || r.httpStatusCode() / 100 == 5) { ASSERT(!m_mainResourceApplicationCache); if (isApplicationCacheEnabled() && !isApplicationCacheBlockedForRequest(request)) { m_mainResourceApplicationCache = ApplicationCacheGroup::fallbackCacheForMainRequest(request, &m_documentLoader); if (scheduleLoadFallbackResourceFromApplicationCache(m_documentLoader.mainResourceLoader(), m_mainResourceApplicationCache.get())) return true; } } return false; } bool ApplicationCacheHost::maybeLoadFallbackForMainError(const ResourceRequest& request, const ResourceError& error) { if (!error.isCancellation()) { ASSERT(!m_mainResourceApplicationCache); if (isApplicationCacheEnabled() && !isApplicationCacheBlockedForRequest(request)) { m_mainResourceApplicationCache = ApplicationCacheGroup::fallbackCacheForMainRequest(request, &m_documentLoader); if (scheduleLoadFallbackResourceFromApplicationCache(m_documentLoader.mainResourceLoader(), m_mainResourceApplicationCache.get())) return true; } } return false; } void ApplicationCacheHost::mainResourceDataReceived(const char*, int, long long, bool) { } void ApplicationCacheHost::failedLoadingMainResource() { auto* group = m_candidateApplicationCacheGroup; if (!group && m_applicationCache) { if (mainResourceApplicationCache()) { // Even when the main resource is being loaded from an application cache, loading can fail if aborted. return; } group = m_applicationCache->group(); } if (group) group->failedLoadingMainResource(m_documentLoader); } void ApplicationCacheHost::finishedLoadingMainResource() { auto* group = candidateApplicationCacheGroup(); if (!group && applicationCache() && !mainResourceApplicationCache()) group = applicationCache()->group(); if (group) group->finishedLoadingMainResource(m_documentLoader); } bool ApplicationCacheHost::maybeLoadResource(ResourceLoader& loader, const ResourceRequest& request, const URL& originalURL) { if (!isApplicationCacheEnabled() && !isApplicationCacheBlockedForRequest(request)) return false; if (request.url() != originalURL) return false; ApplicationCacheResource* resource; if (!shouldLoadResourceFromApplicationCache(request, resource)) return false; m_documentLoader.scheduleSubstituteResourceLoad(loader, *resource); return true; } bool ApplicationCacheHost::maybeLoadFallbackForRedirect(ResourceLoader* resourceLoader, ResourceRequest& request, const ResourceResponse& redirectResponse) { if (!redirectResponse.isNull() && !protocolHostAndPortAreEqual(request.url(), redirectResponse.url())) { if (scheduleLoadFallbackResourceFromApplicationCache(resourceLoader)) return true; } return false; } bool ApplicationCacheHost::maybeLoadFallbackForResponse(ResourceLoader* resourceLoader, const ResourceResponse& response) { if (response.httpStatusCode() / 100 == 4 || response.httpStatusCode() / 100 == 5) { if (scheduleLoadFallbackResourceFromApplicationCache(resourceLoader)) return true; } return false; } bool ApplicationCacheHost::maybeLoadFallbackForError(ResourceLoader* resourceLoader, const ResourceError& error) { if (!error.isCancellation()) { if (resourceLoader == m_documentLoader.mainResourceLoader()) return maybeLoadFallbackForMainError(resourceLoader->request(), error); if (scheduleLoadFallbackResourceFromApplicationCache(resourceLoader)) return true; } return false; } URL ApplicationCacheHost::createFileURL(const String& path) { // FIXME: Can we just use fileURLWithFileSystemPath instead? // fileURLWithFileSystemPath function is not suitable because URL::setPath uses encodeWithURLEscapeSequences, which it notes // does not correctly escape '#' and '?'. This function works for our purposes because // app cache media files are always created with encodeForFileName(createCanonicalUUIDString()). #if USE(CF) && PLATFORM(WIN) URL url(adoptCF(CFURLCreateWithFileSystemPath(0, path.createCFString().get(), kCFURLWindowsPathStyle, false)).get()); #else URL url; url.setProtocol(ASCIILiteral("file")); url.setPath(path); #endif return url; } static inline RefPtr bufferFromResource(ApplicationCacheResource& resource) { // FIXME: Clients probably do not need a copy of the SharedBuffer. // Remove the call to copy() once we ensure SharedBuffer will not be modified. if (resource.path().isEmpty()) return resource.data().copy(); return SharedBuffer::createWithContentsOfFile(resource.path()); } bool ApplicationCacheHost::maybeLoadSynchronously(ResourceRequest& request, ResourceError& error, ResourceResponse& response, RefPtr& data) { ApplicationCacheResource* resource; if (!shouldLoadResourceFromApplicationCache(request, resource)) return false; auto responseData = resource ? bufferFromResource(*resource) : nullptr; if (!responseData) { error = m_documentLoader.frameLoader()->client().cannotShowURLError(request); return true; } response = resource->response(); data = WTFMove(responseData); return true; } void ApplicationCacheHost::maybeLoadFallbackSynchronously(const ResourceRequest& request, ResourceError& error, ResourceResponse& response, RefPtr& data) { // If normal loading results in a redirect to a resource with another origin (indicative of a captive portal), or a 4xx or 5xx status code or equivalent, // or if there were network errors (but not if the user canceled the download), then instead get, from the cache, the resource of the fallback entry // corresponding to the matched namespace. if ((!error.isNull() && !error.isCancellation()) || response.httpStatusCode() / 100 == 4 || response.httpStatusCode() / 100 == 5 || !protocolHostAndPortAreEqual(request.url(), response.url())) { ApplicationCacheResource* resource; if (getApplicationCacheFallbackResource(request, resource)) { response = resource->response(); // FIXME: Clients proably do not need a copy of the SharedBuffer. // Remove the call to copy() once we ensure SharedBuffer will not be modified. data = resource->data().copy(); } } } bool ApplicationCacheHost::canCacheInPageCache() { return !applicationCache() && !candidateApplicationCacheGroup(); } void ApplicationCacheHost::setDOMApplicationCache(DOMApplicationCache* domApplicationCache) { ASSERT(!m_domApplicationCache || !domApplicationCache); m_domApplicationCache = domApplicationCache; } void ApplicationCacheHost::notifyDOMApplicationCache(const AtomicString& eventType, int total, int done) { if (eventType != eventNames().progressEvent) InspectorInstrumentation::updateApplicationCacheStatus(m_documentLoader.frame()); if (m_defersEvents) { // Event dispatching is deferred until document.onload has fired. m_deferredEvents.append({ eventType, total, done }); return; } dispatchDOMEvent(eventType, total, done); } void ApplicationCacheHost::stopLoadingInFrame(Frame& frame) { ASSERT(!m_applicationCache || !m_candidateApplicationCacheGroup || m_applicationCache->group() == m_candidateApplicationCacheGroup); if (m_candidateApplicationCacheGroup) m_candidateApplicationCacheGroup->stopLoadingInFrame(frame); else if (m_applicationCache) m_applicationCache->group()->stopLoadingInFrame(frame); } void ApplicationCacheHost::stopDeferringEvents() { Ref protect(m_documentLoader); // Note, do not cache the size in a local variable. // This code needs to properly handle the case where more events are added to // m_deferredEvents while iterating it. This is why we don't use a modern for loop. for (size_t i = 0; i < m_deferredEvents.size(); ++i) { auto& event = m_deferredEvents[i]; dispatchDOMEvent(event.eventType, event.progressTotal, event.progressDone); } m_deferredEvents.clear(); m_defersEvents = false; } Vector ApplicationCacheHost::resourceList() { Vector result; auto* cache = applicationCache(); if (!cache || !cache->isComplete()) return result; result.reserveInitialCapacity(cache->resources().size()); for (auto& urlAndResource : cache->resources()) { ASSERT(urlAndResource.value); auto& resource = *urlAndResource.value; unsigned type = resource.type(); bool isMaster = type & ApplicationCacheResource::Master; bool isManifest = type & ApplicationCacheResource::Manifest; bool isExplicit = type & ApplicationCacheResource::Explicit; bool isForeign = type & ApplicationCacheResource::Foreign; bool isFallback = type & ApplicationCacheResource::Fallback; result.uncheckedAppend({ resource.url(), isMaster, isManifest, isFallback, isForeign, isExplicit, resource.estimatedSizeInStorage() }); } return result; } ApplicationCacheHost::CacheInfo ApplicationCacheHost::applicationCacheInfo() { auto* cache = applicationCache(); if (!cache || !cache->isComplete()) return { { }, 0, 0, 0 }; // FIXME: Add "Creation Time" and "Update Time" to Application Caches. return { cache->manifestResource()->url(), 0, 0, cache->estimatedSizeInStorage() }; } static Ref createApplicationCacheEvent(const AtomicString& eventType, int total, int done) { if (eventType == eventNames().progressEvent) return ProgressEvent::create(eventType, true, done, total); return Event::create(eventType, false, false); } void ApplicationCacheHost::dispatchDOMEvent(const AtomicString& eventType, int total, int done) { if (!m_domApplicationCache) return; m_domApplicationCache->dispatchEvent(createApplicationCacheEvent(eventType, total, done)); } void ApplicationCacheHost::setCandidateApplicationCacheGroup(ApplicationCacheGroup* group) { ASSERT(!m_applicationCache); m_candidateApplicationCacheGroup = group; } void ApplicationCacheHost::setApplicationCache(RefPtr&& applicationCache) { if (m_candidateApplicationCacheGroup) { ASSERT(!m_applicationCache); m_candidateApplicationCacheGroup = nullptr; } m_applicationCache = WTFMove(applicationCache); } bool ApplicationCacheHost::shouldLoadResourceFromApplicationCache(const ResourceRequest& originalRequest, ApplicationCacheResource*& resource) { auto* cache = applicationCache(); if (!cache || !cache->isComplete()) return false; ResourceRequest request(originalRequest); if (auto* loaderFrame = m_documentLoader.frame()) { if (auto* document = loaderFrame->document()) document->contentSecurityPolicy()->upgradeInsecureRequestIfNeeded(request, ContentSecurityPolicy::InsecureRequestType::Load); } // If the resource is not to be fetched using the HTTP GET mechanism or equivalent, or if its URL has a different // component than the application cache's manifest, then fetch the resource normally. if (!ApplicationCache::requestIsHTTPOrHTTPSGet(request) || !equalIgnoringASCIICase(request.url().protocol(), cache->manifestResource()->url().protocol())) return false; // If the resource's URL is an master entry, the manifest, an explicit entry, or a fallback entry // in the application cache, then get the resource from the cache (instead of fetching it). resource = cache->resourceForURL(request.url()); // Resources that match fallback namespaces or online whitelist entries are fetched from the network, // unless they are also cached. if (!resource && (cache->allowsAllNetworkRequests() || cache->urlMatchesFallbackNamespace(request.url()) || cache->isURLInOnlineWhitelist(request.url()))) return false; // Resources that are not present in the manifest will always fail to load (at least, after the // cache has been primed the first time), making the testing of offline applications simpler. return true; } bool ApplicationCacheHost::getApplicationCacheFallbackResource(const ResourceRequest& request, ApplicationCacheResource*& resource, ApplicationCache* cache) { if (!cache) { cache = applicationCache(); if (!cache) return false; } if (!cache->isComplete()) return false; // If the resource is not a HTTP/HTTPS GET, then abort if (!ApplicationCache::requestIsHTTPOrHTTPSGet(request)) return false; URL fallbackURL; if (cache->isURLInOnlineWhitelist(request.url())) return false; if (!cache->urlMatchesFallbackNamespace(request.url(), &fallbackURL)) return false; resource = cache->resourceForURL(fallbackURL); ASSERT(resource); return true; } bool ApplicationCacheHost::scheduleLoadFallbackResourceFromApplicationCache(ResourceLoader* loader, ApplicationCache* cache) { if (!isApplicationCacheEnabled() && !isApplicationCacheBlockedForRequest(loader->request())) return false; ApplicationCacheResource* resource; if (!getApplicationCacheFallbackResource(loader->request(), resource, cache)) return false; loader->willSwitchToSubstituteResource(); m_documentLoader.scheduleSubstituteResourceLoad(*loader, *resource); return true; } ApplicationCacheHost::Status ApplicationCacheHost::status() const { auto* cache = applicationCache(); if (!cache) return UNCACHED; switch (cache->group()->updateStatus()) { case ApplicationCacheGroup::Checking: return CHECKING; case ApplicationCacheGroup::Downloading: return DOWNLOADING; case ApplicationCacheGroup::Idle: if (cache->group()->isObsolete()) return OBSOLETE; if (cache != cache->group()->newestCache()) return UPDATEREADY; return IDLE; } ASSERT_NOT_REACHED(); return UNCACHED; } bool ApplicationCacheHost::update() { auto* cache = applicationCache(); if (!cache) return false; auto* frame = m_documentLoader.frame(); if (!frame) return false; cache->group()->update(*frame, ApplicationCacheUpdateWithoutBrowsingContext); return true; } bool ApplicationCacheHost::swapCache() { auto* cache = applicationCache(); if (!cache) return false; // If the group of application caches to which cache belongs has the lifecycle status obsolete, unassociate document from cache. if (cache->group()->isObsolete()) { cache->group()->disassociateDocumentLoader(m_documentLoader); return true; } // If there is no newer cache, raise an INVALID_STATE_ERR exception. auto* newestCache = cache->group()->newestCache(); if (cache == newestCache) return false; ASSERT(cache->group() == newestCache->group()); setApplicationCache(newestCache); InspectorInstrumentation::updateApplicationCacheStatus(m_documentLoader.frame()); return true; } void ApplicationCacheHost::abort() { auto* frame = m_documentLoader.frame(); if (!frame) return; if (auto* cacheGroup = candidateApplicationCacheGroup()) cacheGroup->abort(*frame); else if (auto* cache = applicationCache()) cache->group()->abort(*frame); } bool ApplicationCacheHost::isApplicationCacheEnabled() { return m_documentLoader.frame() && m_documentLoader.frame()->settings().offlineWebApplicationCacheEnabled() && !m_documentLoader.frame()->page()->usesEphemeralSession(); } bool ApplicationCacheHost::isApplicationCacheBlockedForRequest(const ResourceRequest& request) { auto* frame = m_documentLoader.frame(); if (!frame) return false; if (frame->isMainFrame()) return false; return !SecurityOrigin::create(request.url())->canAccessApplicationCache(frame->document()->topOrigin()); } } // namespace WebCore