1// Copyright (c) 2011 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/download/download_request_limiter.h"
6
7#include "base/stl_util-inl.h"
8#include "chrome/browser/download/download_request_infobar_delegate.h"
9#include "chrome/browser/tab_contents/tab_util.h"
10#include "content/browser/browser_thread.h"
11#include "content/browser/tab_contents/navigation_controller.h"
12#include "content/browser/tab_contents/navigation_entry.h"
13#include "content/browser/tab_contents/tab_contents.h"
14#include "content/browser/tab_contents/tab_contents_delegate.h"
15#include "content/common/notification_source.h"
16
17// TabDownloadState ------------------------------------------------------------
18
19DownloadRequestLimiter::TabDownloadState::TabDownloadState(
20    DownloadRequestLimiter* host,
21    NavigationController* controller,
22    NavigationController* originating_controller)
23    : host_(host),
24      controller_(controller),
25      status_(DownloadRequestLimiter::ALLOW_ONE_DOWNLOAD),
26      download_count_(0),
27      infobar_(NULL) {
28  Source<NavigationController> notification_source(controller);
29  registrar_.Add(this, NotificationType::NAV_ENTRY_PENDING,
30                 notification_source);
31  registrar_.Add(this, NotificationType::TAB_CLOSED, notification_source);
32
33  NavigationEntry* active_entry = originating_controller ?
34      originating_controller->GetActiveEntry() : controller->GetActiveEntry();
35  if (active_entry)
36    initial_page_host_ = active_entry->url().host();
37}
38
39DownloadRequestLimiter::TabDownloadState::~TabDownloadState() {
40  // We should only be destroyed after the callbacks have been notified.
41  DCHECK(callbacks_.empty());
42
43  // And we should have closed the infobar.
44  DCHECK(!infobar_);
45}
46
47void DownloadRequestLimiter::TabDownloadState::OnUserGesture() {
48  if (is_showing_prompt()) {
49    // Don't change the state if the user clicks on the page some where.
50    return;
51  }
52
53  if (status_ != DownloadRequestLimiter::ALLOW_ALL_DOWNLOADS &&
54      status_ != DownloadRequestLimiter::DOWNLOADS_NOT_ALLOWED) {
55    // Revert to default status.
56    host_->Remove(this);
57    // WARNING: We've been deleted.
58    return;
59  }
60}
61
62void DownloadRequestLimiter::TabDownloadState::PromptUserForDownload(
63    TabContents* tab,
64    DownloadRequestLimiter::Callback* callback) {
65  callbacks_.push_back(callback);
66
67  if (is_showing_prompt())
68    return;  // Already showing prompt.
69
70  if (DownloadRequestLimiter::delegate_) {
71    NotifyCallbacks(DownloadRequestLimiter::delegate_->ShouldAllowDownload());
72  } else {
73    infobar_ = new DownloadRequestInfoBarDelegate(tab, this);
74    tab->AddInfoBar(infobar_);
75  }
76}
77
78void DownloadRequestLimiter::TabDownloadState::Cancel() {
79  NotifyCallbacks(false);
80}
81
82void DownloadRequestLimiter::TabDownloadState::Accept() {
83  NotifyCallbacks(true);
84}
85
86void DownloadRequestLimiter::TabDownloadState::Observe(
87    NotificationType type,
88    const NotificationSource& source,
89    const NotificationDetails& details) {
90  if ((type != NotificationType::NAV_ENTRY_PENDING &&
91       type != NotificationType::TAB_CLOSED) ||
92      Source<NavigationController>(source).ptr() != controller_) {
93    NOTREACHED();
94    return;
95  }
96
97  switch (type.value) {
98    case NotificationType::NAV_ENTRY_PENDING: {
99      // NOTE: resetting state on a pending navigate isn't ideal. In particular
100      // it is possible that queued up downloads for the page before the
101      // pending navigate will be delivered to us after we process this
102      // request. If this happens we may let a download through that we
103      // shouldn't have. But this is rather rare, and it is difficult to get
104      // 100% right, so we don't deal with it.
105      NavigationEntry* entry = controller_->pending_entry();
106      if (!entry)
107        return;
108
109      if (PageTransition::IsRedirect(entry->transition_type())) {
110        // Redirects don't count.
111        return;
112      }
113
114      if (status_ == DownloadRequestLimiter::ALLOW_ALL_DOWNLOADS ||
115          status_ == DownloadRequestLimiter::DOWNLOADS_NOT_ALLOWED) {
116        // User has either allowed all downloads or canceled all downloads. Only
117        // reset the download state if the user is navigating to a different
118        // host (or host is empty).
119        if (!initial_page_host_.empty() && !entry->url().host().empty() &&
120            entry->url().host() == initial_page_host_) {
121          return;
122        }
123      }
124      break;
125    }
126
127    case NotificationType::TAB_CLOSED:
128      // Tab closed, no need to handle closing the dialog as it's owned by the
129      // TabContents, break so that we get deleted after switch.
130      break;
131
132    default:
133      NOTREACHED();
134  }
135
136  NotifyCallbacks(false);
137  host_->Remove(this);
138}
139
140void DownloadRequestLimiter::TabDownloadState::NotifyCallbacks(bool allow) {
141  status_ = allow ?
142      DownloadRequestLimiter::ALLOW_ALL_DOWNLOADS :
143      DownloadRequestLimiter::DOWNLOADS_NOT_ALLOWED;
144  std::vector<DownloadRequestLimiter::Callback*> callbacks;
145  bool change_status = false;
146
147  // Selectively send first few notifications only if number of downloads exceed
148  // kMaxDownloadsAtOnce. In that case, we also retain the infobar instance and
149  // don't close it. If allow is false, we send all the notifications to cancel
150  // all remaining downloads and close the infobar.
151  if (!allow || (callbacks_.size() < kMaxDownloadsAtOnce)) {
152    if (infobar_) {
153      // Reset the delegate so we don't get notified again.
154      infobar_->set_host(NULL);
155      infobar_ = NULL;
156    }
157    callbacks.swap(callbacks_);
158  } else {
159    std::vector<DownloadRequestLimiter::Callback*>::iterator start, end;
160    start = callbacks_.begin();
161    end = callbacks_.begin() + kMaxDownloadsAtOnce;
162    callbacks.assign(start, end);
163    callbacks_.erase(start, end);
164    change_status = true;
165  }
166
167  for (size_t i = 0; i < callbacks.size(); ++i)
168    host_->ScheduleNotification(callbacks[i], allow);
169
170  if (change_status)
171    status_ = DownloadRequestLimiter::PROMPT_BEFORE_DOWNLOAD;
172}
173
174// DownloadRequestLimiter ------------------------------------------------------
175
176DownloadRequestLimiter::DownloadRequestLimiter() {
177}
178
179DownloadRequestLimiter::~DownloadRequestLimiter() {
180  // All the tabs should have closed before us, which sends notification and
181  // removes from state_map_. As such, there should be no pending callbacks.
182  DCHECK(state_map_.empty());
183}
184
185DownloadRequestLimiter::DownloadStatus
186    DownloadRequestLimiter::GetDownloadStatus(TabContents* tab) {
187  TabDownloadState* state = GetDownloadState(&tab->controller(), NULL, false);
188  return state ? state->download_status() : ALLOW_ONE_DOWNLOAD;
189}
190
191void DownloadRequestLimiter::CanDownloadOnIOThread(int render_process_host_id,
192                                                   int render_view_id,
193                                                   int request_id,
194                                                   Callback* callback) {
195  // This is invoked on the IO thread. Schedule the task to run on the UI
196  // thread so that we can query UI state.
197  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
198  BrowserThread::PostTask(
199      BrowserThread::UI, FROM_HERE,
200      NewRunnableMethod(this, &DownloadRequestLimiter::CanDownload,
201                        render_process_host_id, render_view_id, request_id,
202                        callback));
203}
204
205void DownloadRequestLimiter::OnUserGesture(TabContents* tab) {
206  TabDownloadState* state = GetDownloadState(&tab->controller(), NULL, false);
207  if (!state)
208    return;
209
210  state->OnUserGesture();
211}
212
213// static
214void DownloadRequestLimiter::SetTestingDelegate(TestingDelegate* delegate) {
215  delegate_ = delegate;
216}
217
218DownloadRequestLimiter::TabDownloadState* DownloadRequestLimiter::
219    GetDownloadState(NavigationController* controller,
220                     NavigationController* originating_controller,
221                     bool create) {
222  DCHECK(controller);
223  StateMap::iterator i = state_map_.find(controller);
224  if (i != state_map_.end())
225    return i->second;
226
227  if (!create)
228    return NULL;
229
230  TabDownloadState* state =
231      new TabDownloadState(this, controller, originating_controller);
232  state_map_[controller] = state;
233  return state;
234}
235
236void DownloadRequestLimiter::CanDownload(int render_process_host_id,
237                                         int render_view_id,
238                                         int request_id,
239                                         Callback* callback) {
240  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
241
242  TabContents* originating_tab =
243      tab_util::GetTabContentsByID(render_process_host_id, render_view_id);
244  if (!originating_tab) {
245    // The tab was closed, don't allow the download.
246    ScheduleNotification(callback, false);
247    return;
248  }
249  CanDownloadImpl(originating_tab, request_id, callback);
250}
251
252void DownloadRequestLimiter::CanDownloadImpl(
253    TabContents* originating_tab,
254    int request_id,
255    Callback* callback) {
256  // FYI: Chrome Frame overrides CanDownload in ExternalTabContainer in order
257  // to cancel the download operation in chrome and let the host browser
258  // take care of it.
259  if (!originating_tab->CanDownload(request_id)) {
260    ScheduleNotification(callback, false);
261    return;
262  }
263
264  // If the tab requesting the download is a constrained popup that is not
265  // shown, treat the request as if it came from the parent.
266  TabContents* effective_tab = originating_tab;
267  if (effective_tab->delegate()) {
268    effective_tab =
269        effective_tab->delegate()->GetConstrainingContents(effective_tab);
270  }
271
272  TabDownloadState* state = GetDownloadState(
273      &effective_tab->controller(), &originating_tab->controller(), true);
274  switch (state->download_status()) {
275    case ALLOW_ALL_DOWNLOADS:
276      if (state->download_count() && !(state->download_count() %
277            DownloadRequestLimiter::kMaxDownloadsAtOnce))
278        state->set_download_status(PROMPT_BEFORE_DOWNLOAD);
279      ScheduleNotification(callback, true);
280      state->increment_download_count();
281      break;
282
283    case ALLOW_ONE_DOWNLOAD:
284      state->set_download_status(PROMPT_BEFORE_DOWNLOAD);
285      ScheduleNotification(callback, true);
286      break;
287
288    case DOWNLOADS_NOT_ALLOWED:
289      ScheduleNotification(callback, false);
290      break;
291
292    case PROMPT_BEFORE_DOWNLOAD:
293      state->PromptUserForDownload(effective_tab, callback);
294      state->increment_download_count();
295      break;
296
297    default:
298      NOTREACHED();
299  }
300}
301
302void DownloadRequestLimiter::ScheduleNotification(Callback* callback,
303                                                  bool allow) {
304  BrowserThread::PostTask(
305      BrowserThread::IO, FROM_HERE,
306      NewRunnableMethod(
307          this, &DownloadRequestLimiter::NotifyCallback, callback, allow));
308}
309
310void DownloadRequestLimiter::NotifyCallback(Callback* callback, bool allow) {
311  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
312  if (allow)
313    callback->ContinueDownload();
314  else
315    callback->CancelDownload();
316}
317
318void DownloadRequestLimiter::Remove(TabDownloadState* state) {
319  DCHECK(ContainsKey(state_map_, state->controller()));
320  state_map_.erase(state->controller());
321  delete state;
322}
323
324// static
325DownloadRequestLimiter::TestingDelegate* DownloadRequestLimiter::delegate_ =
326    NULL;
327