// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/appcache/appcache_request_handler.h"
#include "content/browser/appcache/appcache.h"
#include "content/browser/appcache/appcache_backend_impl.h"
#include "content/browser/appcache/appcache_policy.h"
#include "content/browser/appcache/appcache_url_request_job.h"
#include "net/url_request/url_request.h"
#include "net/url_request/url_request_job.h"
namespace content {
AppCacheRequestHandler::AppCacheRequestHandler(AppCacheHost* host,
ResourceType resource_type)
: host_(host),
resource_type_(resource_type),
is_waiting_for_cache_selection_(false),
found_group_id_(0),
found_cache_id_(0),
found_network_namespace_(false),
cache_entry_not_found_(false),
maybe_load_resource_executed_(false) {
DCHECK(host_);
host_->AddObserver(this);
}
AppCacheRequestHandler::~AppCacheRequestHandler() {
if (host_) {
storage()->CancelDelegateCallbacks(this);
host_->RemoveObserver(this);
}
}
AppCacheStorage* AppCacheRequestHandler::storage() const {
DCHECK(host_);
return host_->storage();
}
AppCacheURLRequestJob* AppCacheRequestHandler::MaybeLoadResource(
net::URLRequest* request, net::NetworkDelegate* network_delegate) {
maybe_load_resource_executed_ = true;
if (!host_ || !IsSchemeAndMethodSupportedForAppCache(request) ||
cache_entry_not_found_)
return NULL;
// This method can get called multiple times over the life
// of a request. The case we detect here is having scheduled
// delivery of a "network response" using a job setup on an
// earlier call thru this method. To send the request thru
// to the network involves restarting the request altogether,
// which will call thru to our interception layer again.
// This time thru, we return NULL so the request hits the wire.
if (job_.get()) {
DCHECK(job_->is_delivering_network_response() ||
job_->cache_entry_not_found());
if (job_->cache_entry_not_found())
cache_entry_not_found_ = true;
job_ = NULL;
storage()->CancelDelegateCallbacks(this);
return NULL;
}
// Clear out our 'found' fields since we're starting a request for a
// new resource, any values in those fields are no longer valid.
found_entry_ = AppCacheEntry();
found_fallback_entry_ = AppCacheEntry();
found_cache_id_ = kAppCacheNoCacheId;
found_manifest_url_ = GURL();
found_network_namespace_ = false;
if (is_main_resource())
MaybeLoadMainResource(request, network_delegate);
else
MaybeLoadSubResource(request, network_delegate);
// If its been setup to deliver a network response, we can just delete
// it now and return NULL instead to achieve that since it couldn't
// have been started yet.
if (job_.get() && job_->is_delivering_network_response()) {
DCHECK(!job_->has_been_started());
job_ = NULL;
}
return job_.get();
}
AppCacheURLRequestJob* AppCacheRequestHandler::MaybeLoadFallbackForRedirect(
net::URLRequest* request,
net::NetworkDelegate* network_delegate,
const GURL& location) {
if (!host_ || !IsSchemeAndMethodSupportedForAppCache(request) ||
cache_entry_not_found_)
return NULL;
if (is_main_resource())
return NULL;
// TODO(vabr) This is a temporary fix (see crbug/141114). We should get rid of
// it once a more general solution to crbug/121325 is in place.
if (!maybe_load_resource_executed_)
return NULL;
if (request->url().GetOrigin() == location.GetOrigin())
return NULL;
DCHECK(!job_.get()); // our jobs never generate redirects
if (found_fallback_entry_.has_response_id()) {
// 6.9.6, step 4: If this results in a redirect to another origin,
// get the resource of the fallback entry.
job_ = new AppCacheURLRequestJob(request, network_delegate,
storage(), host_, is_main_resource());
DeliverAppCachedResponse(
found_fallback_entry_, found_cache_id_, found_group_id_,
found_manifest_url_, true, found_namespace_entry_url_);
} else if (!found_network_namespace_) {
// 6.9.6, step 6: Fail the resource load.
job_ = new AppCacheURLRequestJob(request, network_delegate,
storage(), host_, is_main_resource());
DeliverErrorResponse();
} else {
// 6.9.6 step 3 and 5: Fetch the resource normally.
}
return job_.get();
}
AppCacheURLRequestJob* AppCacheRequestHandler::MaybeLoadFallbackForResponse(
net::URLRequest* request, net::NetworkDelegate* network_delegate) {
if (!host_ || !IsSchemeAndMethodSupportedForAppCache(request) ||
cache_entry_not_found_)
return NULL;
if (!found_fallback_entry_.has_response_id())
return NULL;
if (request->status().status() == net::URLRequestStatus::CANCELED) {
// 6.9.6, step 4: But not if the user canceled the download.
return NULL;
}
// We don't fallback for responses that we delivered.
if (job_.get()) {
DCHECK(!job_->is_delivering_network_response());
return NULL;
}
if (request->status().is_success()) {
int code_major = request->GetResponseCode() / 100;
if (code_major !=4 && code_major != 5)
return NULL;
// Servers can override the fallback behavior with a response header.
const std::string kFallbackOverrideHeader(
"x-chromium-appcache-fallback-override");
const std::string kFallbackOverrideValue(
"disallow-fallback");
std::string header_value;
request->GetResponseHeaderByName(kFallbackOverrideHeader, &header_value);
if (header_value == kFallbackOverrideValue)
return NULL;
}
// 6.9.6, step 4: If this results in a 4xx or 5xx status code
// or there were network errors, get the resource of the fallback entry.
job_ = new AppCacheURLRequestJob(request, network_delegate,
storage(), host_, is_main_resource());
DeliverAppCachedResponse(
found_fallback_entry_, found_cache_id_, found_group_id_,
found_manifest_url_, true, found_namespace_entry_url_);
return job_.get();
}
void AppCacheRequestHandler::GetExtraResponseInfo(
int64* cache_id, GURL* manifest_url) {
if (job_.get() && job_->is_delivering_appcache_response()) {
*cache_id = job_->cache_id();
*manifest_url = job_->manifest_url();
}
}
void AppCacheRequestHandler::PrepareForCrossSiteTransfer(int old_process_id) {
if (!host_)
return;
AppCacheBackendImpl* backend = host_->service()->GetBackend(old_process_id);
host_for_cross_site_transfer_ = backend->TransferHostOut(host_->host_id());
DCHECK_EQ(host_, host_for_cross_site_transfer_.get());
}
void AppCacheRequestHandler::CompleteCrossSiteTransfer(
int new_process_id, int new_host_id) {
if (!host_for_cross_site_transfer_.get())
return;
DCHECK_EQ(host_, host_for_cross_site_transfer_.get());
AppCacheBackendImpl* backend = host_->service()->GetBackend(new_process_id);
backend->TransferHostIn(new_host_id, host_for_cross_site_transfer_.Pass());
}
void AppCacheRequestHandler::OnDestructionImminent(AppCacheHost* host) {
storage()->CancelDelegateCallbacks(this);
host_ = NULL; // no need to RemoveObserver, the host is being deleted
// Since the host is being deleted, we don't have to complete any job
// that is current running. It's destined for the bit bucket anyway.
if (job_.get()) {
job_->Kill();
job_ = NULL;
}
}
void AppCacheRequestHandler::DeliverAppCachedResponse(
const AppCacheEntry& entry, int64 cache_id, int64 group_id,
const GURL& manifest_url, bool is_fallback,
const GURL& namespace_entry_url) {
DCHECK(host_ && job_.get() && job_->is_waiting());
DCHECK(entry.has_response_id());
if (IsResourceTypeFrame(resource_type_) && !namespace_entry_url.is_empty())
host_->NotifyMainResourceIsNamespaceEntry(namespace_entry_url);
job_->DeliverAppCachedResponse(manifest_url, group_id, cache_id,
entry, is_fallback);
}
void AppCacheRequestHandler::DeliverErrorResponse() {
DCHECK(job_.get() && job_->is_waiting());
job_->DeliverErrorResponse();
}
void AppCacheRequestHandler::DeliverNetworkResponse() {
DCHECK(job_.get() && job_->is_waiting());
job_->DeliverNetworkResponse();
}
// Main-resource handling ----------------------------------------------
void AppCacheRequestHandler::MaybeLoadMainResource(
net::URLRequest* request, net::NetworkDelegate* network_delegate) {
DCHECK(!job_.get());
DCHECK(host_);
const AppCacheHost* spawning_host =
(resource_type_ == RESOURCE_TYPE_SHARED_WORKER) ?
host_ : host_->GetSpawningHost();
GURL preferred_manifest_url = spawning_host ?
spawning_host->preferred_manifest_url() : GURL();
// We may have to wait for our storage query to complete, but
// this query can also complete syncrhonously.
job_ = new AppCacheURLRequestJob(request, network_delegate,
storage(), host_, is_main_resource());
storage()->FindResponseForMainRequest(
request->url(), preferred_manifest_url, this);
}
void AppCacheRequestHandler::OnMainResponseFound(
const GURL& url, const AppCacheEntry& entry,
const GURL& namespace_entry_url, const AppCacheEntry& fallback_entry,
int64 cache_id, int64 group_id, const GURL& manifest_url) {
DCHECK(job_.get());
DCHECK(host_);
DCHECK(is_main_resource());
DCHECK(!entry.IsForeign());
DCHECK(!fallback_entry.IsForeign());
DCHECK(!(entry.has_response_id() && fallback_entry.has_response_id()));
if (!job_.get())
return;
AppCachePolicy* policy = host_->service()->appcache_policy();
bool was_blocked_by_policy = !manifest_url.is_empty() && policy &&
!policy->CanLoadAppCache(manifest_url, host_->first_party_url());
if (was_blocked_by_policy) {
if (IsResourceTypeFrame(resource_type_)) {
host_->NotifyMainResourceBlocked(manifest_url);
} else {
DCHECK_EQ(resource_type_, RESOURCE_TYPE_SHARED_WORKER);
host_->frontend()->OnContentBlocked(host_->host_id(), manifest_url);
}
DeliverNetworkResponse();
return;
}
if (IsResourceTypeFrame(resource_type_) && cache_id != kAppCacheNoCacheId) {
// AppCacheHost loads and holds a reference to the main resource cache
// for two reasons, firstly to preload the cache into the working set
// in advance of subresource loads happening, secondly to prevent the
// AppCache from falling out of the working set on frame navigations.
host_->LoadMainResourceCache(cache_id);
host_->set_preferred_manifest_url(manifest_url);
}
// 6.11.1 Navigating across documents, steps 10 and 14.
found_entry_ = entry;
found_namespace_entry_url_ = namespace_entry_url;
found_fallback_entry_ = fallback_entry;
found_cache_id_ = cache_id;
found_group_id_ = group_id;
found_manifest_url_ = manifest_url;
found_network_namespace_ = false; // not applicable to main requests
if (found_entry_.has_response_id()) {
DCHECK(!found_fallback_entry_.has_response_id());
DeliverAppCachedResponse(
found_entry_, found_cache_id_, found_group_id_, found_manifest_url_,
false, found_namespace_entry_url_);
} else {
DeliverNetworkResponse();
}
}
// Sub-resource handling ----------------------------------------------
void AppCacheRequestHandler::MaybeLoadSubResource(
net::URLRequest* request, net::NetworkDelegate* network_delegate) {
DCHECK(!job_.get());
if (host_->is_selection_pending()) {
// We have to wait until cache selection is complete and the
// selected cache is loaded.
is_waiting_for_cache_selection_ = true;
job_ = new AppCacheURLRequestJob(request, network_delegate,
storage(), host_, is_main_resource());
return;
}
if (!host_->associated_cache() ||
!host_->associated_cache()->is_complete() ||
host_->associated_cache()->owning_group()->is_being_deleted()) {
return;
}
job_ = new AppCacheURLRequestJob(request, network_delegate,
storage(), host_, is_main_resource());
ContinueMaybeLoadSubResource();
}
void AppCacheRequestHandler::ContinueMaybeLoadSubResource() {
// 6.9.6 Changes to the networking model
// If the resource is not to be fetched using the HTTP GET mechanism or
// equivalent ... then fetch the resource normally.
DCHECK(job_.get());
DCHECK(host_->associated_cache() && host_->associated_cache()->is_complete());
const GURL& url = job_->request()->url();
AppCache* cache = host_->associated_cache();
storage()->FindResponseForSubRequest(
host_->associated_cache(), url,
&found_entry_, &found_fallback_entry_, &found_network_namespace_);
if (found_entry_.has_response_id()) {
// Step 2: If there's an entry, get it instead.
DCHECK(!found_network_namespace_ &&
!found_fallback_entry_.has_response_id());
found_cache_id_ = cache->cache_id();
found_group_id_ = cache->owning_group()->group_id();
found_manifest_url_ = cache->owning_group()->manifest_url();
DeliverAppCachedResponse(
found_entry_, found_cache_id_, found_group_id_, found_manifest_url_,
false, GURL());
return;
}
if (found_fallback_entry_.has_response_id()) {
// Step 4: Fetch the resource normally, if this results
// in certain conditions, then use the fallback.
DCHECK(!found_network_namespace_ &&
!found_entry_.has_response_id());
found_cache_id_ = cache->cache_id();
found_manifest_url_ = cache->owning_group()->manifest_url();
DeliverNetworkResponse();
return;
}
if (found_network_namespace_) {
// Step 3 and 5: Fetch the resource normally.
DCHECK(!found_entry_.has_response_id() &&
!found_fallback_entry_.has_response_id());
DeliverNetworkResponse();
return;
}
// Step 6: Fail the resource load.
DeliverErrorResponse();
}
void AppCacheRequestHandler::OnCacheSelectionComplete(AppCacheHost* host) {
DCHECK(host == host_);
if (is_main_resource())
return;
if (!is_waiting_for_cache_selection_)
return;
is_waiting_for_cache_selection_ = false;
if (!host_->associated_cache() ||
!host_->associated_cache()->is_complete()) {
DeliverNetworkResponse();
return;
}
ContinueMaybeLoadSubResource();
}
} // namespace content