// Copyright (c) 2012 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 "chrome/browser/download/download_request_limiter.h" #include "base/bind.h" #include "base/stl_util.h" #include "chrome/browser/download/download_request_infobar_delegate.h" #include "chrome/browser/infobars/infobar_service.h" #include "chrome/browser/tab_contents/tab_util.h" #include "chrome/browser/ui/blocked_content/blocked_content_tab_helper.h" #include "chrome/browser/ui/blocked_content/blocked_content_tab_helper_delegate.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/navigation_controller.h" #include "content/public/browser/navigation_entry.h" #include "content/public/browser/notification_source.h" #include "content/public/browser/notification_types.h" #include "content/public/browser/render_process_host.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents_delegate.h" using content::BrowserThread; using content::NavigationController; using content::NavigationEntry; using content::WebContents; // TabDownloadState ------------------------------------------------------------ DownloadRequestLimiter::TabDownloadState::TabDownloadState( DownloadRequestLimiter* host, WebContents* contents, WebContents* originating_web_contents) : content::WebContentsObserver(contents), host_(host), status_(DownloadRequestLimiter::ALLOW_ONE_DOWNLOAD), download_count_(0), factory_(ALLOW_THIS_IN_INITIALIZER_LIST(this)) { content::Source notification_source( &contents->GetController()); content::Source web_contents_source(contents); registrar_.Add(this, content::NOTIFICATION_NAV_ENTRY_PENDING, notification_source); registrar_.Add(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED, web_contents_source); NavigationEntry* active_entry = originating_web_contents ? originating_web_contents->GetController().GetActiveEntry() : contents->GetController().GetActiveEntry(); if (active_entry) initial_page_host_ = active_entry->GetURL().host(); } DownloadRequestLimiter::TabDownloadState::~TabDownloadState() { // We should only be destroyed after the callbacks have been notified. DCHECK(callbacks_.empty()); // And we should have invalidated the back pointer. DCHECK(!factory_.HasWeakPtrs()); } void DownloadRequestLimiter::TabDownloadState::DidGetUserGesture() { if (is_showing_prompt()) { // Don't change the state if the user clicks on the page somewhere. return; } InfoBarService* infobar_service = InfoBarService::FromWebContents(web_contents()); // See PromptUserForDownload(): if there's no InfoBarService, then // DOWNLOADS_NOT_ALLOWED is functionally equivalent to PROMPT_BEFORE_DOWNLOAD. if ((infobar_service && status_ != DownloadRequestLimiter::ALLOW_ALL_DOWNLOADS && status_ != DownloadRequestLimiter::DOWNLOADS_NOT_ALLOWED) || (!infobar_service && status_ != DownloadRequestLimiter::ALLOW_ALL_DOWNLOADS)) { // Revert to default status. host_->Remove(this); // WARNING: We've been deleted. } } void DownloadRequestLimiter::TabDownloadState::AboutToNavigateRenderView( content::RenderViewHost* render_view_host) { switch (status_) { case ALLOW_ONE_DOWNLOAD: case PROMPT_BEFORE_DOWNLOAD: // When the user reloads the page without responding to the infobar, they // are expecting DownloadRequestLimiter to behave as if they had just // initially navigated to this page. See http://crbug.com/171372 NotifyCallbacks(false); host_->Remove(this); // WARNING: We've been deleted. break; case DOWNLOADS_NOT_ALLOWED: case ALLOW_ALL_DOWNLOADS: // Don't drop this information. The user has explicitly said that they // do/don't want downloads from this host. If they accidentally Accepted // or Canceled, tough luck, they don't get another chance. They can copy // the URL into a new tab, which will make a new DownloadRequestLimiter. // See also the initial_page_host_ logic in Observe() for // NOTIFICATION_NAV_ENTRY_PENDING. break; default: NOTREACHED(); } } void DownloadRequestLimiter::TabDownloadState::PromptUserForDownload( WebContents* web_contents, const DownloadRequestLimiter::Callback& callback) { callbacks_.push_back(callback); if (is_showing_prompt()) return; // Already showing prompt. DownloadRequestInfoBarDelegate::Create( InfoBarService::FromWebContents(web_contents), factory_.GetWeakPtr()); } void DownloadRequestLimiter::TabDownloadState::Cancel() { NotifyCallbacks(false); } void DownloadRequestLimiter::TabDownloadState::Accept() { NotifyCallbacks(true); } DownloadRequestLimiter::TabDownloadState::TabDownloadState() : host_(NULL), status_(DownloadRequestLimiter::ALLOW_ONE_DOWNLOAD), download_count_(0), factory_(ALLOW_THIS_IN_INITIALIZER_LIST(this)) { } void DownloadRequestLimiter::TabDownloadState::Observe( int type, const content::NotificationSource& source, const content::NotificationDetails& details) { content::NavigationController* controller = &web_contents()->GetController(); if (type == content::NOTIFICATION_NAV_ENTRY_PENDING) { DCHECK_EQ(controller, content::Source(source).ptr()); // NOTE: Resetting state on a pending navigate isn't ideal. In particular it // is possible that queued up downloads for the page before the pending // navigation will be delivered to us after we process this request. If this // happens we may let a download through that we shouldn't have. But this is // rather rare, and it is difficult to get 100% right, so we don't deal with // it. NavigationEntry* entry = controller->GetPendingEntry(); if (!entry) return; // Redirects don't count. if (content::PageTransitionIsRedirect(entry->GetTransitionType())) return; if (status_ == DownloadRequestLimiter::ALLOW_ALL_DOWNLOADS || status_ == DownloadRequestLimiter::DOWNLOADS_NOT_ALLOWED) { // User has either allowed all downloads or canceled all downloads. Only // reset the download state if the user is navigating to a different host // (or host is empty). if (!initial_page_host_.empty() && !entry->GetURL().host().empty() && entry->GetURL().host() == initial_page_host_) return; } } else { DCHECK_EQ(content::NOTIFICATION_WEB_CONTENTS_DESTROYED, type); DCHECK_EQ(controller, &content::Source(source)->GetController()); // Tab closed, no need to handle closing the dialog as it's owned by the // WebContents. } NotifyCallbacks(false); host_->Remove(this); } void DownloadRequestLimiter::TabDownloadState::NotifyCallbacks(bool allow) { set_download_status(allow ? DownloadRequestLimiter::ALLOW_ALL_DOWNLOADS : DownloadRequestLimiter::DOWNLOADS_NOT_ALLOWED); std::vector callbacks; bool change_status = false; // Selectively send first few notifications only if number of downloads exceed // kMaxDownloadsAtOnce. In that case, we also retain the infobar instance and // don't close it. If allow is false, we send all the notifications to cancel // all remaining downloads and close the infobar. if (!allow || (callbacks_.size() < kMaxDownloadsAtOnce)) { // Null the generated weak pointer so we don't get notified again. factory_.InvalidateWeakPtrs(); callbacks.swap(callbacks_); } else { std::vector::iterator start, end; start = callbacks_.begin(); end = callbacks_.begin() + kMaxDownloadsAtOnce; callbacks.assign(start, end); callbacks_.erase(start, end); change_status = true; } for (size_t i = 0; i < callbacks.size(); ++i) host_->ScheduleNotification(callbacks[i], allow); if (change_status) set_download_status(DownloadRequestLimiter::PROMPT_BEFORE_DOWNLOAD); } // DownloadRequestLimiter ------------------------------------------------------ DownloadRequestLimiter::DownloadRequestLimiter() : factory_(ALLOW_THIS_IN_INITIALIZER_LIST(this)) { } DownloadRequestLimiter::~DownloadRequestLimiter() { // All the tabs should have closed before us, which sends notification and // removes from state_map_. As such, there should be no pending callbacks. DCHECK(state_map_.empty()); } DownloadRequestLimiter::DownloadStatus DownloadRequestLimiter::GetDownloadStatus(WebContents* web_contents) { TabDownloadState* state = GetDownloadState(web_contents, NULL, false); return state ? state->download_status() : ALLOW_ONE_DOWNLOAD; } void DownloadRequestLimiter::CanDownloadOnIOThread( int render_process_host_id, int render_view_id, int request_id, const std::string& request_method, const Callback& callback) { // This is invoked on the IO thread. Schedule the task to run on the UI // thread so that we can query UI state. DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); BrowserThread::PostTask( BrowserThread::UI, FROM_HERE, base::Bind(&DownloadRequestLimiter::CanDownload, this, render_process_host_id, render_view_id, request_id, request_method, callback)); } DownloadRequestLimiter::TabDownloadState* DownloadRequestLimiter::GetDownloadState( WebContents* web_contents, WebContents* originating_web_contents, bool create) { DCHECK(web_contents); StateMap::iterator i = state_map_.find(web_contents); if (i != state_map_.end()) return i->second; if (!create) return NULL; TabDownloadState* state = new TabDownloadState(this, web_contents, originating_web_contents); state_map_[web_contents] = state; return state; } void DownloadRequestLimiter::CanDownload(int render_process_host_id, int render_view_id, int request_id, const std::string& request_method, const Callback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); WebContents* originating_contents = tab_util::GetWebContentsByID(render_process_host_id, render_view_id); if (!originating_contents) { // The WebContents was closed, don't allow the download. ScheduleNotification(callback, false); return; } if (!originating_contents->GetDelegate()) { ScheduleNotification(callback, false); return; } // Note that because |originating_contents| might go away before // OnCanDownloadDecided is invoked, we look it up by |render_process_host_id| // and |render_view_id|. base::Callback can_download_callback = base::Bind( &DownloadRequestLimiter::OnCanDownloadDecided, factory_.GetWeakPtr(), render_process_host_id, render_view_id, request_id, request_method, callback); originating_contents->GetDelegate()->CanDownload( originating_contents->GetRenderViewHost(), request_id, request_method, can_download_callback); } void DownloadRequestLimiter::OnCanDownloadDecided( int render_process_host_id, int render_view_id, int request_id, const std::string& request_method, const Callback& orig_callback, bool allow) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); WebContents* originating_contents = tab_util::GetWebContentsByID(render_process_host_id, render_view_id); if (!originating_contents || !allow) { ScheduleNotification(orig_callback, false); return; } CanDownloadImpl(originating_contents, request_id, request_method, orig_callback); } void DownloadRequestLimiter::CanDownloadImpl(WebContents* originating_contents, int request_id, const std::string& request_method, const Callback& callback) { DCHECK(originating_contents); // If the tab requesting the download is a constrained popup that is not // shown, treat the request as if it came from the parent. WebContents* effective_contents = originating_contents; BlockedContentTabHelper* blocked_content_tab_helper = BlockedContentTabHelper::FromWebContents(originating_contents); if (blocked_content_tab_helper && blocked_content_tab_helper->delegate()) { effective_contents = blocked_content_tab_helper->delegate()-> GetConstrainingWebContents(originating_contents); } TabDownloadState* state = GetDownloadState( effective_contents, originating_contents, true); switch (state->download_status()) { case ALLOW_ALL_DOWNLOADS: if (state->download_count() && !(state->download_count() % DownloadRequestLimiter::kMaxDownloadsAtOnce)) state->set_download_status(PROMPT_BEFORE_DOWNLOAD); ScheduleNotification(callback, true); state->increment_download_count(); break; case ALLOW_ONE_DOWNLOAD: state->set_download_status(PROMPT_BEFORE_DOWNLOAD); ScheduleNotification(callback, true); break; case DOWNLOADS_NOT_ALLOWED: ScheduleNotification(callback, false); break; case PROMPT_BEFORE_DOWNLOAD: state->PromptUserForDownload(effective_contents, callback); state->increment_download_count(); break; default: NOTREACHED(); } } void DownloadRequestLimiter::ScheduleNotification(const Callback& callback, bool allow) { BrowserThread::PostTask( BrowserThread::IO, FROM_HERE, base::Bind(callback, allow)); } void DownloadRequestLimiter::Remove(TabDownloadState* state) { DCHECK(ContainsKey(state_map_, state->web_contents())); state_map_.erase(state->web_contents()); delete state; }