// 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; }