普通文本  |  447行  |  15.74 KB

// 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/content_settings/host_content_settings_map.h"
#include "chrome/browser/content_settings/tab_specific_content_settings.h"
#include "chrome/browser/download/download_request_infobar_delegate.h"
#include "chrome/browser/infobars/infobar_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/tab_contents/tab_util.h"
#include "content/public/browser/browser_context.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/resource_dispatcher_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "url/gurl.h"

using content::BrowserThread;
using content::NavigationController;
using content::NavigationEntry;

// TabDownloadState ------------------------------------------------------------

DownloadRequestLimiter::TabDownloadState::TabDownloadState(
    DownloadRequestLimiter* host,
    content::WebContents* contents,
    content::WebContents* originating_web_contents)
    : content::WebContentsObserver(contents),
      web_contents_(contents),
      host_(host),
      status_(DownloadRequestLimiter::ALLOW_ONE_DOWNLOAD),
      download_count_(0),
      factory_(this) {
  content::Source<NavigationController> notification_source(
      &contents->GetController());
  registrar_.Add(this, content::NOTIFICATION_NAV_ENTRY_PENDING,
                 notification_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::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, web_contents());
      // 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::DidGetUserGesture() {
  if (is_showing_prompt()) {
    // Don't change the state if the user clicks on the page somewhere.
    return;
  }

  // See PromptUserForDownload(): if there's no InfoBarService, then
  // DOWNLOADS_NOT_ALLOWED is functionally equivalent to PROMPT_BEFORE_DOWNLOAD.
  if ((status_ != DownloadRequestLimiter::ALLOW_ALL_DOWNLOADS) &&
      (!InfoBarService::FromWebContents(web_contents()) ||
       (status_ != DownloadRequestLimiter::DOWNLOADS_NOT_ALLOWED))) {
    // Revert to default status.
    host_->Remove(this, web_contents());
    // WARNING: We've been deleted.
  }
}

void DownloadRequestLimiter::TabDownloadState::WebContentsDestroyed(
    content::WebContents* web_contents) {
  // Tab closed, no need to handle closing the dialog as it's owned by the
  // WebContents.

  NotifyCallbacks(false);
  // Note that web_contents() is NULL at this point.
  host_->Remove(this, web_contents);
  // WARNING: We've been deleted.
}

void DownloadRequestLimiter::TabDownloadState::PromptUserForDownload(
    const DownloadRequestLimiter::Callback& callback) {
  callbacks_.push_back(callback);
  DCHECK(web_contents_);
  if (is_showing_prompt())
    return;
  DownloadRequestInfoBarDelegate::Create(
      InfoBarService::FromWebContents(web_contents_), factory_.GetWeakPtr());
}

void DownloadRequestLimiter::TabDownloadState::SetContentSetting(
    ContentSetting setting) {
  if (!web_contents_)
    return;
  HostContentSettingsMap* settings =
    DownloadRequestLimiter::GetContentSettings(web_contents_);
  ContentSettingsPattern pattern(
      ContentSettingsPattern::FromURL(web_contents_->GetURL()));
  if (!settings || !pattern.IsValid())
    return;
  settings->SetContentSetting(
      pattern,
      ContentSettingsPattern::Wildcard(),
      CONTENT_SETTINGS_TYPE_AUTOMATIC_DOWNLOADS,
      std::string(),
      setting);
}

void DownloadRequestLimiter::TabDownloadState::Cancel() {
  SetContentSetting(CONTENT_SETTING_BLOCK);
  NotifyCallbacks(false);
}

void DownloadRequestLimiter::TabDownloadState::CancelOnce() {
  NotifyCallbacks(false);
}

void DownloadRequestLimiter::TabDownloadState::Accept() {
  SetContentSetting(CONTENT_SETTING_ALLOW);
  NotifyCallbacks(true);
}

DownloadRequestLimiter::TabDownloadState::TabDownloadState()
    : web_contents_(NULL),
      host_(NULL),
      status_(DownloadRequestLimiter::ALLOW_ONE_DOWNLOAD),
      download_count_(0),
      factory_(this) {
}

void DownloadRequestLimiter::TabDownloadState::Observe(
    int type,
    const content::NotificationSource& source,
    const content::NotificationDetails& details) {
  DCHECK_EQ(content::NOTIFICATION_NAV_ENTRY_PENDING, type);
  content::NavigationController* controller = &web_contents()->GetController();
  DCHECK_EQ(controller, content::Source<NavigationController>(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;
  }

  NotifyCallbacks(false);
  host_->Remove(this, web_contents());
}

void DownloadRequestLimiter::TabDownloadState::NotifyCallbacks(bool allow) {
  set_download_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)) {
    // Null the generated weak pointer so we don't get notified again.
    factory_.InvalidateWeakPtrs();
    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)
    set_download_status(DownloadRequestLimiter::PROMPT_BEFORE_DOWNLOAD);
}

// DownloadRequestLimiter ------------------------------------------------------

HostContentSettingsMap* DownloadRequestLimiter::content_settings_ = NULL;

void DownloadRequestLimiter::SetContentSettingsForTesting(
    HostContentSettingsMap* content_settings) {
  content_settings_ = content_settings;
}

DownloadRequestLimiter::DownloadRequestLimiter()
    : factory_(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(content::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(
    content::WebContents* web_contents,
    content::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));

  content::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<void(bool)> 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));
  content::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);
}

HostContentSettingsMap* DownloadRequestLimiter::GetContentSettings(
    content::WebContents* contents) {
  return content_settings_ ? content_settings_ : Profile::FromBrowserContext(
      contents->GetBrowserContext())->GetHostContentSettingsMap();
}

void DownloadRequestLimiter::CanDownloadImpl(
    content::WebContents* originating_contents,
    int request_id,
    const std::string& request_method,
    const Callback& callback) {
  DCHECK(originating_contents);

  TabDownloadState* state = GetDownloadState(
      originating_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);
      state->increment_download_count();
      break;

    case DOWNLOADS_NOT_ALLOWED:
      ScheduleNotification(callback, false);
      break;

    case PROMPT_BEFORE_DOWNLOAD: {
      HostContentSettingsMap* content_settings = GetContentSettings(
          originating_contents);
      ContentSetting setting = CONTENT_SETTING_ASK;
      if (content_settings)
        setting = content_settings->GetContentSetting(
            originating_contents->GetURL(),
            originating_contents->GetURL(),
            CONTENT_SETTINGS_TYPE_AUTOMATIC_DOWNLOADS,
            std::string());
      switch (setting) {
        case CONTENT_SETTING_ALLOW: {
          TabSpecificContentSettings* settings =
              TabSpecificContentSettings::FromWebContents(
                  originating_contents);
          if (settings)
            settings->SetDownloadsBlocked(false);
          ScheduleNotification(callback, true);
          state->increment_download_count();
          return;
        }
        case CONTENT_SETTING_BLOCK: {
          TabSpecificContentSettings* settings =
              TabSpecificContentSettings::FromWebContents(
                  originating_contents);
          if (settings)
            settings->SetDownloadsBlocked(true);
          ScheduleNotification(callback, false);
          return;
        }
        case CONTENT_SETTING_DEFAULT:
        case CONTENT_SETTING_ASK:
        case CONTENT_SETTING_SESSION_ONLY:
          state->PromptUserForDownload(callback);
          state->increment_download_count();
          break;
        case CONTENT_SETTING_NUM_SETTINGS:
        default:
          NOTREACHED();
          return;
      }
      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,
                                    content::WebContents* contents) {
  DCHECK(ContainsKey(state_map_, contents));
  state_map_.erase(contents);
  delete state;
}