// 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_item.h"
#include "base/basictypes.h"
#include "base/file_util.h"
#include "base/format_macros.h"
#include "base/logging.h"
#include "base/metrics/histogram.h"
#include "base/stringprintf.h"
#include "base/timer.h"
#include "base/utf_string_conversions.h"
#include "net/base/net_util.h"
#include "chrome/browser/download/download_extensions.h"
#include "chrome/browser/download/download_file_manager.h"
#include "chrome/browser/download/download_history.h"
#include "chrome/browser/download/download_manager.h"
#include "chrome/browser/download/download_prefs.h"
#include "chrome/browser/download/download_util.h"
#include "chrome/browser/history/download_create_info.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/prefs/pref_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/extensions/extension.h"
#include "chrome/common/pref_names.h"
#include "content/browser/browser_thread.h"
#include "ui/base/l10n/l10n_util.h"
// A DownloadItem normally goes through the following states:
// * Created (when download starts)
// * Made visible to consumers (e.g. Javascript) after the
// destination file has been determined.
// * Entered into the history database.
// * Made visible in the download shelf.
// * All data is saved. Note that the actual data download occurs
// in parallel with the above steps, but until those steps are
// complete, completion of the data download will be ignored.
// * Download file is renamed to its final name, and possibly
// auto-opened.
// TODO(rdsmith): This progress should be reflected in
// DownloadItem::DownloadState and a state transition table/state diagram.
//
// TODO(rdsmith): This description should be updated to reflect the cancel
// pathways.
namespace {
// Update frequency (milliseconds).
const int kUpdateTimeMs = 1000;
void DeleteDownloadedFile(const FilePath& path) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
// Make sure we only delete files.
if (!file_util::DirectoryExists(path))
file_util::Delete(path, false);
}
const char* DebugSafetyStateString(DownloadItem::SafetyState state) {
switch (state) {
case DownloadItem::SAFE:
return "SAFE";
case DownloadItem::DANGEROUS:
return "DANGEROUS";
case DownloadItem::DANGEROUS_BUT_VALIDATED:
return "DANGEROUS_BUT_VALIDATED";
default:
NOTREACHED() << "Unknown safety state " << state;
return "unknown";
};
}
const char* DebugDownloadStateString(DownloadItem::DownloadState state) {
switch (state) {
case DownloadItem::IN_PROGRESS:
return "IN_PROGRESS";
case DownloadItem::COMPLETE:
return "COMPLETE";
case DownloadItem::CANCELLED:
return "CANCELLED";
case DownloadItem::REMOVING:
return "REMOVING";
case DownloadItem::INTERRUPTED:
return "INTERRUPTED";
default:
NOTREACHED() << "Unknown download state " << state;
return "unknown";
};
}
DownloadItem::SafetyState GetSafetyState(bool dangerous_file,
bool dangerous_url) {
return (dangerous_url || dangerous_file) ?
DownloadItem::DANGEROUS : DownloadItem::SAFE;
}
// Note: When a download has both |dangerous_file| and |dangerous_url| set,
// danger type is set to DANGEROUS_URL since the risk of dangerous URL
// overweights that of dangerous file type.
DownloadItem::DangerType GetDangerType(bool dangerous_file,
bool dangerous_url) {
if (dangerous_url) {
// dangerous URL overweights dangerous file. We check dangerous URL first.
return DownloadItem::DANGEROUS_URL;
} else if (dangerous_file) {
return DownloadItem::DANGEROUS_FILE;
}
return DownloadItem::NOT_DANGEROUS;
}
} // namespace
// Constructor for reading from the history service.
DownloadItem::DownloadItem(DownloadManager* download_manager,
const DownloadCreateInfo& info)
: id_(-1),
full_path_(info.path),
path_uniquifier_(0),
url_chain_(info.url_chain),
referrer_url_(info.referrer_url),
mime_type_(info.mime_type),
original_mime_type_(info.original_mime_type),
total_bytes_(info.total_bytes),
received_bytes_(info.received_bytes),
start_tick_(base::TimeTicks()),
state_(static_cast<DownloadState>(info.state)),
start_time_(info.start_time),
db_handle_(info.db_handle),
download_manager_(download_manager),
is_paused_(false),
open_when_complete_(false),
safety_state_(SAFE),
danger_type_(NOT_DANGEROUS),
auto_opened_(false),
target_name_(info.original_name),
render_process_id_(-1),
request_id_(-1),
save_as_(false),
is_otr_(false),
is_extension_install_(info.is_extension_install),
name_finalized_(false),
is_temporary_(false),
all_data_saved_(false),
opened_(false) {
if (IsInProgress())
state_ = CANCELLED;
if (IsComplete())
all_data_saved_ = true;
Init(false /* don't start progress timer */);
}
// Constructing for a regular download:
DownloadItem::DownloadItem(DownloadManager* download_manager,
const DownloadCreateInfo& info,
bool is_otr)
: id_(info.download_id),
full_path_(info.path),
path_uniquifier_(info.path_uniquifier),
url_chain_(info.url_chain),
referrer_url_(info.referrer_url),
mime_type_(info.mime_type),
original_mime_type_(info.original_mime_type),
total_bytes_(info.total_bytes),
received_bytes_(0),
last_os_error_(0),
start_tick_(base::TimeTicks::Now()),
state_(IN_PROGRESS),
start_time_(info.start_time),
db_handle_(DownloadHistory::kUninitializedHandle),
download_manager_(download_manager),
is_paused_(false),
open_when_complete_(false),
safety_state_(GetSafetyState(info.is_dangerous_file,
info.is_dangerous_url)),
danger_type_(GetDangerType(info.is_dangerous_file,
info.is_dangerous_url)),
auto_opened_(false),
target_name_(info.original_name),
render_process_id_(info.child_id),
request_id_(info.request_id),
save_as_(info.prompt_user_for_save_location),
is_otr_(is_otr),
is_extension_install_(info.is_extension_install),
name_finalized_(false),
is_temporary_(!info.save_info.file_path.empty()),
all_data_saved_(false),
opened_(false) {
Init(true /* start progress timer */);
}
// Constructing for the "Save Page As..." feature:
DownloadItem::DownloadItem(DownloadManager* download_manager,
const FilePath& path,
const GURL& url,
bool is_otr)
: id_(1),
full_path_(path),
path_uniquifier_(0),
url_chain_(1, url),
referrer_url_(GURL()),
mime_type_(std::string()),
original_mime_type_(std::string()),
total_bytes_(0),
received_bytes_(0),
last_os_error_(0),
start_tick_(base::TimeTicks::Now()),
state_(IN_PROGRESS),
start_time_(base::Time::Now()),
db_handle_(DownloadHistory::kUninitializedHandle),
download_manager_(download_manager),
is_paused_(false),
open_when_complete_(false),
safety_state_(SAFE),
danger_type_(NOT_DANGEROUS),
auto_opened_(false),
render_process_id_(-1),
request_id_(-1),
save_as_(false),
is_otr_(is_otr),
is_extension_install_(false),
name_finalized_(false),
is_temporary_(false),
all_data_saved_(false),
opened_(false) {
Init(true /* start progress timer */);
}
DownloadItem::~DownloadItem() {
state_ = REMOVING;
UpdateObservers();
}
void DownloadItem::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void DownloadItem::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
void DownloadItem::UpdateObservers() {
FOR_EACH_OBSERVER(Observer, observers_, OnDownloadUpdated(this));
}
bool DownloadItem::CanOpenDownload() {
return !Extension::IsExtension(target_name_);
}
bool DownloadItem::ShouldOpenFileBasedOnExtension() {
return download_manager_->ShouldOpenFileBasedOnExtension(
GetUserVerifiedFilePath());
}
void DownloadItem::OpenFilesBasedOnExtension(bool open) {
DownloadPrefs* prefs = download_manager_->download_prefs();
if (open)
prefs->EnableAutoOpenBasedOnExtension(GetUserVerifiedFilePath());
else
prefs->DisableAutoOpenBasedOnExtension(GetUserVerifiedFilePath());
}
void DownloadItem::OpenDownload() {
if (IsPartialDownload()) {
open_when_complete_ = !open_when_complete_;
} else if (IsComplete()) {
opened_ = true;
FOR_EACH_OBSERVER(Observer, observers_, OnDownloadOpened(this));
if (is_extension_install()) {
download_util::OpenChromeExtension(download_manager_->profile(),
download_manager_,
*this);
return;
}
#if defined(OS_MACOSX)
// Mac OS X requires opening downloads on the UI thread.
platform_util::OpenItem(full_path());
#else
BrowserThread::PostTask(
BrowserThread::FILE, FROM_HERE,
NewRunnableFunction(&platform_util::OpenItem, full_path()));
#endif
}
}
void DownloadItem::ShowDownloadInShell() {
#if defined(OS_MACOSX)
// Mac needs to run this operation on the UI thread.
platform_util::ShowItemInFolder(full_path());
#else
BrowserThread::PostTask(
BrowserThread::FILE, FROM_HERE,
NewRunnableFunction(&platform_util::ShowItemInFolder,
full_path()));
#endif
}
void DownloadItem::DangerousDownloadValidated() {
UMA_HISTOGRAM_ENUMERATION("Download.DangerousDownloadValidated",
danger_type_,
DANGEROUS_TYPE_MAX);
download_manager_->DangerousDownloadValidated(this);
}
void DownloadItem::UpdateSize(int64 bytes_so_far) {
received_bytes_ = bytes_so_far;
// If we've received more data than we were expecting (bad server info?),
// revert to 'unknown size mode'.
if (received_bytes_ > total_bytes_)
total_bytes_ = 0;
}
void DownloadItem::StartProgressTimer() {
update_timer_.Start(base::TimeDelta::FromMilliseconds(kUpdateTimeMs), this,
&DownloadItem::UpdateObservers);
}
void DownloadItem::StopProgressTimer() {
update_timer_.Stop();
}
// Updates from the download thread may have been posted while this download
// was being cancelled in the UI thread, so we'll accept them unless we're
// complete.
void DownloadItem::Update(int64 bytes_so_far) {
if (!IsInProgress()) {
NOTREACHED();
return;
}
UpdateSize(bytes_so_far);
UpdateObservers();
}
// Triggered by a user action.
void DownloadItem::Cancel(bool update_history) {
VLOG(20) << __FUNCTION__ << "()" << " download = " << DebugString(true);
if (!IsPartialDownload()) {
// Small downloads might be complete before this method has
// a chance to run.
return;
}
download_util::RecordDownloadCount(download_util::CANCELLED_COUNT);
state_ = CANCELLED;
UpdateObservers();
StopProgressTimer();
if (update_history)
download_manager_->DownloadCancelled(id_);
}
void DownloadItem::MarkAsComplete() {
DCHECK(all_data_saved_);
state_ = COMPLETE;
UpdateObservers();
}
void DownloadItem::OnAllDataSaved(int64 size) {
DCHECK(!all_data_saved_);
all_data_saved_ = true;
UpdateSize(size);
StopProgressTimer();
}
void DownloadItem::Completed() {
VLOG(20) << " " << __FUNCTION__ << "() "
<< DebugString(false);
download_util::RecordDownloadCount(download_util::COMPLETED_COUNT);
// Handle chrome extensions explicitly and skip the shell execute.
if (is_extension_install()) {
download_util::OpenChromeExtension(download_manager_->profile(),
download_manager_,
*this);
auto_opened_ = true;
} else if (open_when_complete() ||
download_manager_->ShouldOpenFileBasedOnExtension(
GetUserVerifiedFilePath()) ||
is_temporary()) {
// If the download is temporary, like in drag-and-drop, do not open it but
// we still need to set it auto-opened so that it can be removed from the
// download shelf.
if (!is_temporary())
OpenDownload();
auto_opened_ = true;
}
DCHECK(all_data_saved_);
state_ = COMPLETE;
UpdateObservers();
download_manager_->DownloadCompleted(id());
}
void DownloadItem::Interrupted(int64 size, int os_error) {
if (!IsInProgress())
return;
state_ = INTERRUPTED;
last_os_error_ = os_error;
UpdateSize(size);
StopProgressTimer();
UpdateObservers();
}
void DownloadItem::Delete(DeleteReason reason) {
switch (reason) {
case DELETE_DUE_TO_USER_DISCARD:
UMA_HISTOGRAM_ENUMERATION("Download.UserDiscard",
danger_type_,
DANGEROUS_TYPE_MAX);
break;
case DELETE_DUE_TO_BROWSER_SHUTDOWN:
UMA_HISTOGRAM_ENUMERATION("Download.Discard",
danger_type_,
DANGEROUS_TYPE_MAX);
break;
default:
NOTREACHED();
}
BrowserThread::PostTask(
BrowserThread::FILE, FROM_HERE,
NewRunnableFunction(&DeleteDownloadedFile, full_path_));
Remove();
// We have now been deleted.
}
void DownloadItem::Remove() {
Cancel(true);
state_ = REMOVING;
download_manager_->RemoveDownload(db_handle_);
// We have now been deleted.
}
bool DownloadItem::TimeRemaining(base::TimeDelta* remaining) const {
if (total_bytes_ <= 0)
return false; // We never received the content_length for this download.
int64 speed = CurrentSpeed();
if (speed == 0)
return false;
*remaining =
base::TimeDelta::FromSeconds((total_bytes_ - received_bytes_) / speed);
return true;
}
int64 DownloadItem::CurrentSpeed() const {
if (is_paused_)
return 0;
base::TimeDelta diff = base::TimeTicks::Now() - start_tick_;
int64 diff_ms = diff.InMilliseconds();
return diff_ms == 0 ? 0 : received_bytes_ * 1000 / diff_ms;
}
int DownloadItem::PercentComplete() const {
int percent = -1;
if (total_bytes_ > 0)
percent = static_cast<int>(received_bytes_ * 100.0 / total_bytes_);
return percent;
}
void DownloadItem::Rename(const FilePath& full_path) {
VLOG(20) << " " << __FUNCTION__ << "()"
<< " full_path = \"" << full_path.value() << "\""
<< DebugString(true);
DCHECK(!full_path.empty());
full_path_ = full_path;
}
void DownloadItem::TogglePause() {
DCHECK(IsInProgress());
download_manager_->PauseDownload(id_, !is_paused_);
is_paused_ = !is_paused_;
UpdateObservers();
}
void DownloadItem::OnNameFinalized() {
VLOG(20) << " " << __FUNCTION__ << "() "
<< DebugString(true);
name_finalized_ = true;
// We can't reach this point in the code without having received all the
// data, so it's safe to move to the COMPLETE state.
DCHECK(all_data_saved_);
state_ = COMPLETE;
UpdateObservers();
download_manager_->DownloadCompleted(id());
}
void DownloadItem::OnDownloadCompleting(DownloadFileManager* file_manager) {
VLOG(20) << " " << __FUNCTION__ << "() "
<< " needs rename = " << NeedsRename()
<< " " << DebugString(true);
DCHECK_NE(DANGEROUS, safety_state());
DCHECK(file_manager);
if (NeedsRename()) {
BrowserThread::PostTask(
BrowserThread::FILE, FROM_HERE,
NewRunnableMethod(
file_manager, &DownloadFileManager::RenameCompletingDownloadFile,
id(), GetTargetFilePath(), safety_state() == SAFE));
return;
} else {
name_finalized_ = true;
}
Completed();
BrowserThread::PostTask(
BrowserThread::FILE, FROM_HERE,
NewRunnableMethod(
file_manager, &DownloadFileManager::CompleteDownload, id()));
}
void DownloadItem::OnDownloadRenamedToFinalName(const FilePath& full_path) {
VLOG(20) << " " << __FUNCTION__ << "()"
<< " full_path = " << full_path.value()
<< " needed rename = " << NeedsRename()
<< " " << DebugString(false);
DCHECK(NeedsRename());
Rename(full_path);
OnNameFinalized();
Completed();
}
bool DownloadItem::MatchesQuery(const string16& query) const {
if (query.empty())
return true;
DCHECK_EQ(query, l10n_util::ToLower(query));
string16 url_raw(l10n_util::ToLower(UTF8ToUTF16(url().spec())));
if (url_raw.find(query) != string16::npos)
return true;
// TODO(phajdan.jr): write a test case for the following code.
// A good test case would be:
// "/\xe4\xbd\xa0\xe5\xa5\xbd\xe4\xbd\xa0\xe5\xa5\xbd",
// L"/\x4f60\x597d\x4f60\x597d",
// "/%E4%BD%A0%E5%A5%BD%E4%BD%A0%E5%A5%BD"
PrefService* prefs = download_manager_->profile()->GetPrefs();
std::string languages(prefs->GetString(prefs::kAcceptLanguages));
string16 url_formatted(l10n_util::ToLower(net::FormatUrl(url(), languages)));
if (url_formatted.find(query) != string16::npos)
return true;
string16 path(l10n_util::ToLower(full_path().LossyDisplayName()));
// This shouldn't just do a substring match; it is wrong for Unicode
// due to normalization and we have a fancier search-query system
// used elsewhere.
// http://code.google.com/p/chromium/issues/detail?id=71982
if (path.find(query) != string16::npos)
return true;
return false;
}
void DownloadItem::SetFileCheckResults(const FilePath& path,
bool is_dangerous_file,
bool is_dangerous_url,
int path_uniquifier,
bool prompt,
bool is_extension_install,
const FilePath& original_name) {
VLOG(20) << " " << __FUNCTION__ << "()"
<< " path = \"" << path.value() << "\""
<< " is_dangerous_file = " << is_dangerous_file
<< " is_dangerous_url = " << is_dangerous_url
<< " path_uniquifier = " << path_uniquifier
<< " prompt = " << prompt
<< " is_extension_install = " << is_extension_install
<< " path = \"" << path.value() << "\""
<< " original_name = \"" << original_name.value() << "\""
<< " " << DebugString(true);
// Make sure the initial file name is set only once.
DCHECK(full_path_.empty());
DCHECK(!path.empty());
full_path_ = path;
safety_state_ = GetSafetyState(is_dangerous_file, is_dangerous_url);
danger_type_ = GetDangerType(is_dangerous_file, is_dangerous_url);
path_uniquifier_ = path_uniquifier;
save_as_ = prompt;
is_extension_install_ = is_extension_install;
target_name_ = original_name;
if (target_name_.value().empty())
target_name_ = full_path_.BaseName();
}
FilePath DownloadItem::GetTargetFilePath() const {
return full_path_.DirName().Append(target_name_);
}
FilePath DownloadItem::GetFileNameToReportUser() const {
if (path_uniquifier_ > 0) {
FilePath name(target_name_);
download_util::AppendNumberToPath(&name, path_uniquifier_);
return name;
}
return target_name_;
}
FilePath DownloadItem::GetUserVerifiedFilePath() const {
if (safety_state_ == DownloadItem::SAFE)
return GetTargetFilePath();
return full_path_;
}
void DownloadItem::Init(bool start_timer) {
if (target_name_.value().empty())
target_name_ = full_path_.BaseName();
if (start_timer)
StartProgressTimer();
VLOG(20) << " " << __FUNCTION__ << "() " << DebugString(true);
}
// TODO(ahendrickson) -- Move |INTERRUPTED| from |IsCancelled()| to
// |IsPartialDownload()|, when resuming interrupted downloads is implemented.
bool DownloadItem::IsPartialDownload() const {
return (state_ == IN_PROGRESS);
}
bool DownloadItem::IsInProgress() const {
return (state_ == IN_PROGRESS);
}
bool DownloadItem::IsCancelled() const {
return (state_ == CANCELLED) || (state_ == INTERRUPTED);
}
bool DownloadItem::IsInterrupted() const {
return (state_ == INTERRUPTED);
}
bool DownloadItem::IsComplete() const {
return state() == COMPLETE;
}
std::string DownloadItem::DebugString(bool verbose) const {
std::string description =
base::StringPrintf("{ id_ = %d"
" state = %s",
id_,
DebugDownloadStateString(state()));
if (verbose) {
description += base::StringPrintf(
" db_handle = %" PRId64
" total_bytes = %" PRId64
" is_paused = " "%c"
" is_extension_install = " "%c"
" is_otr = " "%c"
" safety_state = " "%s"
" url = " "\"%s\""
" target_name_ = \"%" PRFilePath "\""
" full_path = \"%" PRFilePath "\"",
db_handle(),
total_bytes(),
is_paused() ? 'T' : 'F',
is_extension_install() ? 'T' : 'F',
is_otr() ? 'T' : 'F',
DebugSafetyStateString(safety_state()),
url().spec().c_str(),
target_name_.value().c_str(),
full_path().value().c_str());
} else {
description += base::StringPrintf(" url = \"%s\"", url().spec().c_str());
}
description += " }";
return description;
}