| // 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 "chrome/browser/download/download_request_limiter.h" |
| |
| #include "base/stl_util-inl.h" |
| #include "chrome/browser/download/download_request_infobar_delegate.h" |
| #include "chrome/browser/tab_contents/tab_util.h" |
| #include "content/browser/browser_thread.h" |
| #include "content/browser/tab_contents/navigation_controller.h" |
| #include "content/browser/tab_contents/navigation_entry.h" |
| #include "content/browser/tab_contents/tab_contents.h" |
| #include "content/browser/tab_contents/tab_contents_delegate.h" |
| #include "content/common/notification_source.h" |
| |
| // TabDownloadState ------------------------------------------------------------ |
| |
| DownloadRequestLimiter::TabDownloadState::TabDownloadState( |
| DownloadRequestLimiter* host, |
| NavigationController* controller, |
| NavigationController* originating_controller) |
| : host_(host), |
| controller_(controller), |
| status_(DownloadRequestLimiter::ALLOW_ONE_DOWNLOAD), |
| download_count_(0), |
| infobar_(NULL) { |
| Source<NavigationController> notification_source(controller); |
| registrar_.Add(this, NotificationType::NAV_ENTRY_PENDING, |
| notification_source); |
| registrar_.Add(this, NotificationType::TAB_CLOSED, notification_source); |
| |
| NavigationEntry* active_entry = originating_controller ? |
| originating_controller->GetActiveEntry() : controller->GetActiveEntry(); |
| if (active_entry) |
| initial_page_host_ = active_entry->url().host(); |
| } |
| |
| DownloadRequestLimiter::TabDownloadState::~TabDownloadState() { |
| // We should only be destroyed after the callbacks have been notified. |
| DCHECK(callbacks_.empty()); |
| |
| // And we should have closed the infobar. |
| DCHECK(!infobar_); |
| } |
| |
| void DownloadRequestLimiter::TabDownloadState::OnUserGesture() { |
| if (is_showing_prompt()) { |
| // Don't change the state if the user clicks on the page some where. |
| return; |
| } |
| |
| if (status_ != DownloadRequestLimiter::ALLOW_ALL_DOWNLOADS && |
| status_ != DownloadRequestLimiter::DOWNLOADS_NOT_ALLOWED) { |
| // Revert to default status. |
| host_->Remove(this); |
| // WARNING: We've been deleted. |
| return; |
| } |
| } |
| |
| void DownloadRequestLimiter::TabDownloadState::PromptUserForDownload( |
| TabContents* tab, |
| DownloadRequestLimiter::Callback* callback) { |
| callbacks_.push_back(callback); |
| |
| if (is_showing_prompt()) |
| return; // Already showing prompt. |
| |
| if (DownloadRequestLimiter::delegate_) { |
| NotifyCallbacks(DownloadRequestLimiter::delegate_->ShouldAllowDownload()); |
| } else { |
| infobar_ = new DownloadRequestInfoBarDelegate(tab, this); |
| tab->AddInfoBar(infobar_); |
| } |
| } |
| |
| void DownloadRequestLimiter::TabDownloadState::Cancel() { |
| NotifyCallbacks(false); |
| } |
| |
| void DownloadRequestLimiter::TabDownloadState::Accept() { |
| NotifyCallbacks(true); |
| } |
| |
| void DownloadRequestLimiter::TabDownloadState::Observe( |
| NotificationType type, |
| const NotificationSource& source, |
| const NotificationDetails& details) { |
| if ((type != NotificationType::NAV_ENTRY_PENDING && |
| type != NotificationType::TAB_CLOSED) || |
| Source<NavigationController>(source).ptr() != controller_) { |
| NOTREACHED(); |
| return; |
| } |
| |
| switch (type.value) { |
| case NotificationType::NAV_ENTRY_PENDING: { |
| // 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 navigate 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_->pending_entry(); |
| if (!entry) |
| return; |
| |
| if (PageTransition::IsRedirect(entry->transition_type())) { |
| // Redirects don't count. |
| 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->url().host().empty() && |
| entry->url().host() == initial_page_host_) { |
| return; |
| } |
| } |
| break; |
| } |
| |
| case NotificationType::TAB_CLOSED: |
| // Tab closed, no need to handle closing the dialog as it's owned by the |
| // TabContents, break so that we get deleted after switch. |
| break; |
| |
| default: |
| NOTREACHED(); |
| } |
| |
| NotifyCallbacks(false); |
| host_->Remove(this); |
| } |
| |
| void DownloadRequestLimiter::TabDownloadState::NotifyCallbacks(bool allow) { |
| status_ = allow ? |
| DownloadRequestLimiter::ALLOW_ALL_DOWNLOADS : |
| DownloadRequestLimiter::DOWNLOADS_NOT_ALLOWED; |
| std::vector<DownloadRequestLimiter::Callback*> 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)) { |
| if (infobar_) { |
| // Reset the delegate so we don't get notified again. |
| infobar_->set_host(NULL); |
| infobar_ = NULL; |
| } |
| callbacks.swap(callbacks_); |
| } else { |
| std::vector<DownloadRequestLimiter::Callback*>::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) |
| status_ = DownloadRequestLimiter::PROMPT_BEFORE_DOWNLOAD; |
| } |
| |
| // DownloadRequestLimiter ------------------------------------------------------ |
| |
| DownloadRequestLimiter::DownloadRequestLimiter() { |
| } |
| |
| 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(TabContents* tab) { |
| TabDownloadState* state = GetDownloadState(&tab->controller(), 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, |
| 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, |
| NewRunnableMethod(this, &DownloadRequestLimiter::CanDownload, |
| render_process_host_id, render_view_id, request_id, |
| callback)); |
| } |
| |
| void DownloadRequestLimiter::OnUserGesture(TabContents* tab) { |
| TabDownloadState* state = GetDownloadState(&tab->controller(), NULL, false); |
| if (!state) |
| return; |
| |
| state->OnUserGesture(); |
| } |
| |
| // static |
| void DownloadRequestLimiter::SetTestingDelegate(TestingDelegate* delegate) { |
| delegate_ = delegate; |
| } |
| |
| DownloadRequestLimiter::TabDownloadState* DownloadRequestLimiter:: |
| GetDownloadState(NavigationController* controller, |
| NavigationController* originating_controller, |
| bool create) { |
| DCHECK(controller); |
| StateMap::iterator i = state_map_.find(controller); |
| if (i != state_map_.end()) |
| return i->second; |
| |
| if (!create) |
| return NULL; |
| |
| TabDownloadState* state = |
| new TabDownloadState(this, controller, originating_controller); |
| state_map_[controller] = state; |
| return state; |
| } |
| |
| void DownloadRequestLimiter::CanDownload(int render_process_host_id, |
| int render_view_id, |
| int request_id, |
| Callback* callback) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| |
| TabContents* originating_tab = |
| tab_util::GetTabContentsByID(render_process_host_id, render_view_id); |
| if (!originating_tab) { |
| // The tab was closed, don't allow the download. |
| ScheduleNotification(callback, false); |
| return; |
| } |
| CanDownloadImpl(originating_tab, request_id, callback); |
| } |
| |
| void DownloadRequestLimiter::CanDownloadImpl( |
| TabContents* originating_tab, |
| int request_id, |
| Callback* callback) { |
| // FYI: Chrome Frame overrides CanDownload in ExternalTabContainer in order |
| // to cancel the download operation in chrome and let the host browser |
| // take care of it. |
| if (!originating_tab->CanDownload(request_id)) { |
| ScheduleNotification(callback, false); |
| return; |
| } |
| |
| // If the tab requesting the download is a constrained popup that is not |
| // shown, treat the request as if it came from the parent. |
| TabContents* effective_tab = originating_tab; |
| if (effective_tab->delegate()) { |
| effective_tab = |
| effective_tab->delegate()->GetConstrainingContents(effective_tab); |
| } |
| |
| TabDownloadState* state = GetDownloadState( |
| &effective_tab->controller(), &originating_tab->controller(), 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_tab, callback); |
| state->increment_download_count(); |
| break; |
| |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| void DownloadRequestLimiter::ScheduleNotification(Callback* callback, |
| bool allow) { |
| BrowserThread::PostTask( |
| BrowserThread::IO, FROM_HERE, |
| NewRunnableMethod( |
| this, &DownloadRequestLimiter::NotifyCallback, callback, allow)); |
| } |
| |
| void DownloadRequestLimiter::NotifyCallback(Callback* callback, bool allow) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
| if (allow) |
| callback->ContinueDownload(); |
| else |
| callback->CancelDownload(); |
| } |
| |
| void DownloadRequestLimiter::Remove(TabDownloadState* state) { |
| DCHECK(ContainsKey(state_map_, state->controller())); |
| state_map_.erase(state->controller()); |
| delete state; |
| } |
| |
| // static |
| DownloadRequestLimiter::TestingDelegate* DownloadRequestLimiter::delegate_ = |
| NULL; |