diff --git a/infra/THORIUM_DEV_BOOKMARKS.html b/infra/THORIUM_DEV_BOOKMARKS.html index 2e73a5ac..1480b7bf 100644 --- a/infra/THORIUM_DEV_BOOKMARKS.html +++ b/infra/THORIUM_DEV_BOOKMARKS.html @@ -210,6 +210,7 @@
theme_helper_win.cc - Chromium Code Search
obsolete_system_linux.cc - Chromium Code Search
insecure_download_blocking.cc - Chromium Code Search +
download_target_determiner.cc - Chromium Code Search
feature_promo_controller.cc - Chromium Code Search
gpu_pre_sandbox_hook_linux.cc - Chromium Code Search
download_bubble_prefs.cc - Chromium Code Search diff --git a/src/chrome/browser/download/download_target_determiner.cc b/src/chrome/browser/download/download_target_determiner.cc new file mode 100644 index 00000000..888d1a81 --- /dev/null +++ b/src/chrome/browser/download/download_target_determiner.cc @@ -0,0 +1,1368 @@ +// Copyright 2024 The Chromium Authors and Alex313031 +// 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_target_determiner.h" + +#include +#include +#include + +#include "base/command_line.h" +#include "base/files/file_path.h" +#include "base/functional/bind.h" +#include "base/location.h" +#include "base/rand_util.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "base/task/single_thread_task_runner.h" +#include "base/task/thread_pool.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "chrome/browser/download/chrome_download_manager_delegate.h" +#include "chrome/browser/download/download_confirmation_reason.h" +#include "chrome/browser/download/download_crx_util.h" +#include "chrome/browser/download/download_prefs.h" +#include "chrome/browser/download/download_stats.h" +#include "chrome/browser/history/history_service_factory.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/safe_browsing/safe_browsing_metrics_collector_factory.h" +#include "chrome/common/chrome_features.h" +#include "chrome/common/pref_names.h" +#include "chrome/grit/generated_resources.h" +#include "components/download/public/common/download_interrupt_reasons.h" +#include "components/download/public/common/download_item.h" +#include "components/download/public/common/download_target_info.h" +#include "components/history/core/browser/history_service.h" +#include "components/prefs/pref_service.h" +#include "components/safe_browsing/content/browser/download/download_stats.h" +#include "components/safe_browsing/content/common/file_type_policies.h" +#include "components/safe_browsing/core/browser/safe_browsing_metrics_collector.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/download_item_utils.h" +#include "extensions/buildflags/buildflags.h" +#include "extensions/common/constants.h" +#include "net/base/filename_util.h" +#include "net/http/http_content_disposition.h" +#include "ppapi/buildflags/buildflags.h" +#include "third_party/blink/public/common/mime_util/mime_util.h" +#include "ui/base/l10n/l10n_util.h" +#include "url/origin.h" + +#if BUILDFLAG(ENABLE_EXTENSIONS) +#include "chrome/browser/extensions/webstore_installer.h" +#include "extensions/common/feature_switch.h" +#endif + +#if BUILDFLAG(ENABLE_PLUGINS) +#include "chrome/browser/plugins/plugin_prefs.h" +#include "content/public/browser/plugin_service.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/common/webplugininfo.h" +#endif + +#if BUILDFLAG(IS_WIN) +#include "chrome/browser/ui/pdf/adobe_reader_info_win.h" +#include "ui/shell_dialogs/select_file_utils_win.h" +#endif + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/ash/policy/dlp/dlp_files_controller_ash.h" +#include "chrome/browser/chromeos/policy/dlp/dlp_file_destination.h" +#include "chrome/browser/chromeos/policy/dlp/dlp_rules_manager.h" +#include "chrome/browser/chromeos/policy/dlp/dlp_rules_manager_factory.h" +#endif + +using content::BrowserThread; +using download::DownloadItem; +using download::DownloadPathReservationTracker; +using safe_browsing::DownloadFileType; + +namespace { + +const base::FilePath::CharType kCrdownloadSuffix[] = + FILE_PATH_LITERAL(".crdownload"); + +// Condenses the results from HistoryService::GetVisibleVisitCountToHost() to a +// single bool. A host is considered visited before if prior visible visits were +// found in history and the first such visit was earlier than the most recent +// midnight. +void VisitCountsToVisitedBefore(base::OnceCallback callback, + history::VisibleVisitCountToHostResult result) { + std::move(callback).Run( + result.success && result.count > 0 && + (result.first_visit.LocalMidnight() < base::Time::Now().LocalMidnight())); +} + +#if BUILDFLAG(IS_WIN) +// Keeps track of whether Adobe Reader is up to date. +bool g_is_adobe_reader_up_to_date_ = false; +#endif + +// For the `new_path`, generates a new safe file name if needed. Keep its +// extension if it is empty or matches that of the `old_extension`. Otherwise, +// suggest a new safe extension. +void GenerateSafeFileName(base::FilePath* new_path, + const base::FilePath::StringType& old_extension, + const std::string& mime_type) { + DCHECK(new_path); + if (new_path->Extension().empty() || new_path->Extension() == old_extension) { + net::GenerateSafeFileName(std::string() /*mime_type*/, + false /*ignore_extension*/, new_path); + } else { + net::GenerateSafeFileName(mime_type, true /*ignore_extension*/, new_path); + } +} + +} // namespace + +DownloadTargetDeterminerDelegate::~DownloadTargetDeterminerDelegate() = default; + +DownloadTargetDeterminer::DownloadTargetDeterminer( + DownloadItem* download, + const base::FilePath& initial_virtual_path, + DownloadPathReservationTracker::FilenameConflictAction conflict_action, + DownloadPrefs* download_prefs, + DownloadTargetDeterminerDelegate* delegate, + CompletionCallback callback) + : next_state_(STATE_GENERATE_TARGET_PATH), + confirmation_reason_(DownloadConfirmationReason::NONE), + should_notify_extensions_(false), + create_target_directory_(false), + conflict_action_(conflict_action), + danger_type_(download->GetDangerType()), + danger_level_(DownloadFileType::NOT_DANGEROUS), + virtual_path_(initial_virtual_path), + is_filetype_handled_safely_(false), +#if BUILDFLAG(IS_ANDROID) + is_checking_dialog_confirmed_path_(false), +#endif + download_(download), + is_resumption_(download_->GetLastReason() != + download::DOWNLOAD_INTERRUPT_REASON_NONE && + !initial_virtual_path.empty()), + download_prefs_(download_prefs), + delegate_(delegate), + completion_callback_(std::move(callback)) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(download_); + DCHECK(delegate); + download_->AddObserver(this); + + DoLoop(); +} + +DownloadTargetDeterminer::~DownloadTargetDeterminer() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(download_); + DCHECK(!completion_callback_); + download_->RemoveObserver(this); +} + +void DownloadTargetDeterminer::DoLoop() { + Result result = CONTINUE; + do { + State current_state = next_state_; + next_state_ = STATE_NONE; + + switch (current_state) { + case STATE_GENERATE_TARGET_PATH: + result = DoGenerateTargetPath(); + break; + case STATE_SET_INSECURE_DOWNLOAD_STATUS: + result = DoSetInsecureDownloadStatus(); + break; + case STATE_NOTIFY_EXTENSIONS: + result = DoNotifyExtensions(); + break; + case STATE_RESERVE_VIRTUAL_PATH: + result = DoReserveVirtualPath(); + break; + case STATE_PROMPT_USER_FOR_DOWNLOAD_PATH: + result = DoRequestConfirmation(); + break; + case STATE_DETERMINE_LOCAL_PATH: + result = DoDetermineLocalPath(); + break; + case STATE_DETERMINE_MIME_TYPE: + result = DoDetermineMimeType(); + break; + case STATE_DETERMINE_IF_HANDLED_SAFELY_BY_BROWSER: + result = DoDetermineIfHandledSafely(); + break; + case STATE_DETERMINE_IF_ADOBE_READER_UP_TO_DATE: + result = DoDetermineIfAdobeReaderUpToDate(); + break; + case STATE_CHECK_DOWNLOAD_URL: + result = DoCheckDownloadUrl(); + break; + case STATE_CHECK_VISITED_REFERRER_BEFORE: + result = DoCheckVisitedReferrerBefore(); + break; + case STATE_DETERMINE_INTERMEDIATE_PATH: + result = DoDetermineIntermediatePath(); + break; + case STATE_NONE: + NOTREACHED(); + return; + } + } while (result == CONTINUE); + // Note that if a callback completes synchronously, the handler will still + // return QUIT_DOLOOP. In this case, an inner DoLoop() may complete the target + // determination and delete |this|. + + if (result == COMPLETE) + ScheduleCallbackAndDeleteSelf(download::DOWNLOAD_INTERRUPT_REASON_NONE); +} + +DownloadTargetDeterminer::Result + DownloadTargetDeterminer::DoGenerateTargetPath() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(local_path_.empty()); + DCHECK_EQ(confirmation_reason_, DownloadConfirmationReason::NONE); + DCHECK(!should_notify_extensions_); + bool is_forced_path = !download_->GetForcedFilePath().empty(); + + next_state_ = STATE_SET_INSECURE_DOWNLOAD_STATUS; + + // Transient download should use the existing path. + if (download_->IsTransient()) { + if (is_forced_path) { + RecordDownloadPathGeneration(DownloadPathGenerationEvent::USE_FORCE_PATH, + true); + virtual_path_ = download_->GetForcedFilePath(); + } else if (!virtual_path_.empty()) { + RecordDownloadPathGeneration( + DownloadPathGenerationEvent::USE_EXISTING_VIRTUAL_PATH, true); + } else { + // No path is provided, we have no idea what the target path is. Stop the + // target determination process and wait for self deletion. + RecordDownloadPathGeneration(DownloadPathGenerationEvent::NO_VALID_PATH, + true); + RecordDownloadCancelReason(DownloadCancelReason::kNoValidPath); + ScheduleCallbackAndDeleteSelf( + download::DOWNLOAD_INTERRUPT_REASON_USER_CANCELED); + return QUIT_DOLOOP; + } + + conflict_action_ = DownloadPathReservationTracker::OVERWRITE; + DCHECK(virtual_path_.IsAbsolute()); + return CONTINUE; + } + + bool no_prompt_needed = HasPromptedForPath(); +#if BUILDFLAG(IS_ANDROID) + // If |virtual_path_| is content URI, there is no need to prompt the user. + no_prompt_needed |= virtual_path_.IsContentUri(); +#endif + if (!virtual_path_.empty() && no_prompt_needed && !is_forced_path) { + // The download is being resumed and the user has already been prompted for + // a path. Assume that it's okay to overwrite the file if there's a conflict + // and reuse the selection. + confirmation_reason_ = NeedsConfirmation(virtual_path_); + conflict_action_ = DownloadPathReservationTracker::OVERWRITE; + RecordDownloadPathGeneration( + DownloadPathGenerationEvent::USE_EXISTING_VIRTUAL_PATH, false); + } else if (!is_forced_path) { + // If we don't have a forced path, we should construct a path for the + // download. Forced paths are only specified for programmatic downloads + // (WebStore, Drag&Drop). Treat the path as a virtual path. We will + // eventually determine whether this is a local path and if not, figure out + // a local path. + base::FilePath generated_filename = GenerateFileName(); + confirmation_reason_ = NeedsConfirmation(generated_filename); + base::FilePath target_directory; + if (confirmation_reason_ != DownloadConfirmationReason::NONE) { + if (download_prefs_->IsDownloadPathManaged()) + DCHECK(confirmation_reason_ == DownloadConfirmationReason::DLP_BLOCKED); + // If the user is going to be prompted and the user has been prompted + // before, then always prefer the last directory that the user selected. + target_directory = download_prefs_->SaveFilePath(); + RecordDownloadPathGeneration( + DownloadPathGenerationEvent::USE_LAST_PROMPT_DIRECTORY, false); + } else { + target_directory = download_prefs_->DownloadPath(); + RecordDownloadPathGeneration( + DownloadPathGenerationEvent::USE_DEFAULTL_DOWNLOAD_DIRECTORY, false); + } + should_notify_extensions_ = true; + virtual_path_ = target_directory.Append(generated_filename); + DCHECK(virtual_path_.IsAbsolute()); + } else { + conflict_action_ = DownloadPathReservationTracker::OVERWRITE; + virtual_path_ = download_->GetForcedFilePath(); + RecordDownloadPathGeneration(DownloadPathGenerationEvent::USE_FORCE_PATH, + false); + // If this is a resumed download which was previously interrupted due to an + // issue with the forced path, the user is still not prompted. If the path + // supplied to a programmatic download is invalid, then the caller needs to + // intervene. + DCHECK(virtual_path_.IsAbsolute()); + } + DVLOG(20) << "Generated virtual path: " << virtual_path_.AsUTF8Unsafe(); + + return CONTINUE; +} + +base::FilePath DownloadTargetDeterminer::GenerateFileName() const { + std::string suggested_filename = download_->GetSuggestedFilename(); + std::string sniffed_mime_type = download_->GetMimeType(); + + if (suggested_filename.empty() && + sniffed_mime_type == "application/x-x509-user-cert") { + suggested_filename = "user.crt"; + } + + // Generate the file name, we may replace the file extension based on mime + // type under certain condition. + std::string default_filename( + l10n_util::GetStringUTF8(IDS_DEFAULT_DOWNLOAD_FILENAME)); + std::string referrer_charset = + GetProfile()->GetPrefs()->GetString(prefs::kDefaultCharset); + base::FilePath generated_filename = net::GenerateFileName( + download_->GetURL(), download_->GetContentDisposition(), referrer_charset, + suggested_filename, sniffed_mime_type, default_filename); + + // We don't replace the file extension if sfafe browsing consider the file + // extension to be unsafe. Just let safe browsing scan the generated file. + if (safe_browsing::FileTypePolicies::GetInstance()->IsCheckedBinaryFile( + generated_filename)) { + return generated_filename; + } + + // If no mime type or explicitly specified a name, don't replace file + // extension. + if (sniffed_mime_type.empty() || !suggested_filename.empty()) + return generated_filename; + + // Trust content disposition header filename attribute. + net::HttpContentDisposition content_disposition_header( + download_->GetContentDisposition(), referrer_charset); + if (!content_disposition_header.filename().empty()) + return generated_filename; + + // When headers have X-Content-Type-Options:nosniff, or for many text file + // types like csv, sniffed mime type will be text/plain. Prefer the extension + // generated by the URL here. + if (sniffed_mime_type == "text/plain" && + download_->GetOriginalMimeType() != "text/plain") { + return generated_filename; + } + + // Replaces file extension based on sniffed mime type in network layer. + generated_filename = net::GenerateFileName( + download_->GetURL(), std::string() /* content_disposition */, + referrer_charset, std::string() /* suggested_filename */, + sniffed_mime_type, default_filename, true /* should_replace_extension */); + return generated_filename; +} + +DownloadTargetDeterminer::Result +DownloadTargetDeterminer::DoSetInsecureDownloadStatus() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(!virtual_path_.empty()); + + next_state_ = STATE_NOTIFY_EXTENSIONS; + + delegate_->GetInsecureDownloadStatus( + download_, virtual_path_, + base::BindOnce(&DownloadTargetDeterminer::GetInsecureDownloadStatusDone, + weak_ptr_factory_.GetWeakPtr())); + return QUIT_DOLOOP; +} + +void DownloadTargetDeterminer::GetInsecureDownloadStatusDone( + download::DownloadItem::InsecureDownloadStatus status) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + // Delegate should not call back here more than once. + DCHECK_EQ(STATE_NOTIFY_EXTENSIONS, next_state_); + + insecure_download_status_ = status; + + if (status == download::DownloadItem::InsecureDownloadStatus::SILENT_BLOCK) { + RecordDownloadCancelReason(DownloadCancelReason::kInsecureDownload); + ScheduleCallbackAndDeleteSelf( + download::DOWNLOAD_INTERRUPT_REASON_FILE_BLOCKED); + return; + } + + DoLoop(); +} + +DownloadTargetDeterminer::Result + DownloadTargetDeterminer::DoNotifyExtensions() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(!virtual_path_.empty()); + + next_state_ = STATE_RESERVE_VIRTUAL_PATH; + + if (!should_notify_extensions_ || + download_->GetState() != DownloadItem::IN_PROGRESS) + return CONTINUE; + + delegate_->NotifyExtensions( + download_, virtual_path_, + base::BindOnce(&DownloadTargetDeterminer::NotifyExtensionsDone, + weak_ptr_factory_.GetWeakPtr())); + return QUIT_DOLOOP; +} + +void DownloadTargetDeterminer::NotifyExtensionsDone( + const base::FilePath& suggested_path, + DownloadPathReservationTracker::FilenameConflictAction conflict_action) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DVLOG(20) << "Extension suggested path: " << suggested_path.AsUTF8Unsafe(); + + // Extensions should not call back here more than once. + DCHECK_EQ(STATE_RESERVE_VIRTUAL_PATH, next_state_); + + // Ignore path suggestion for file URLs. + if (download_->GetURL().SchemeIsFile()) { + DoLoop(); + return; + } + + if (!suggested_path.empty()) { + // If an extension overrides the filename, then the target directory will be + // forced to download_prefs_->DownloadPath() since extensions cannot place + // downloaded files anywhere except there. This prevents subdirectories from + // accumulating: if an extension is allowed to say that a file should go in + // last_download_path/music/foo.mp3, then last_download_path will accumulate + // the subdirectory /music/ so that the next download may end up in + // Downloads/music/music/music/bar.mp3. + base::FilePath new_path(download_prefs_->DownloadPath().Append( + suggested_path).NormalizePathSeparators()); + + // If the (Chrome) extension does not suggest an file extension, or if the + // suggested extension matches that of the |virtual_path_|, do not + // pass a mime type to GenerateSafeFileName so that it does not force the + // filename to have an extension or generate a different one. Otherwise, + // correct the file extension in case it is wrongly given. + GenerateSafeFileName(&new_path, virtual_path_.Extension(), + download_->GetMimeType()); + + virtual_path_ = new_path; + create_target_directory_ = true; + } + // An extension may set conflictAction without setting filename. + if (conflict_action != DownloadPathReservationTracker::UNIQUIFY) + conflict_action_ = conflict_action; + + DoLoop(); +} + +DownloadTargetDeterminer::Result + DownloadTargetDeterminer::DoReserveVirtualPath() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(!virtual_path_.empty()); + + next_state_ = STATE_PROMPT_USER_FOR_DOWNLOAD_PATH; + if (download_->GetState() != DownloadItem::IN_PROGRESS) + return CONTINUE; + + delegate_->ReserveVirtualPath( + download_, virtual_path_, create_target_directory_, conflict_action_, + base::BindOnce(&DownloadTargetDeterminer::ReserveVirtualPathDone, + weak_ptr_factory_.GetWeakPtr())); + return QUIT_DOLOOP; +} + +void DownloadTargetDeterminer::ReserveVirtualPathDone( + download::PathValidationResult result, + const base::FilePath& path) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + DVLOG(20) << "Reserved path: " << path.AsUTF8Unsafe() + << " Result:" << static_cast(result); + DCHECK_EQ(STATE_PROMPT_USER_FOR_DOWNLOAD_PATH, next_state_); + RecordDownloadPathValidation(result, download_->IsTransient()); + if (download_->IsTransient()) { + DCHECK_EQ(DownloadConfirmationReason::NONE, confirmation_reason_) + << "Transient download should not ask the user for confirmation."; + DCHECK(result != download::PathValidationResult::CONFLICT) + << "Transient download" + "should always overwrite the file."; + switch (result) { + case download::PathValidationResult::PATH_NOT_WRITABLE: + case download::PathValidationResult::NAME_TOO_LONG: + case download::PathValidationResult::CONFLICT: + RecordDownloadCancelReason( + DownloadCancelReason::kFailedPathReservation); + ScheduleCallbackAndDeleteSelf( + download::DOWNLOAD_INTERRUPT_REASON_USER_CANCELED); + return; + case download::PathValidationResult::SUCCESS: + case download::PathValidationResult::SUCCESS_RESOLVED_CONFLICT: + case download::PathValidationResult::SAME_AS_SOURCE: + DCHECK_EQ(virtual_path_, path) << "Transient download path should not" + "be changed."; + break; + case download::PathValidationResult::COUNT: + NOTREACHED(); + } + } else { + virtual_path_ = path; + + switch (result) { + case download::PathValidationResult::SUCCESS: + case download::PathValidationResult::SAME_AS_SOURCE: + break; + + // TODO(crbug.com/1361503): This should trigger a duplicate download + // prompt. + case download::PathValidationResult::SUCCESS_RESOLVED_CONFLICT: + break; + + case download::PathValidationResult::PATH_NOT_WRITABLE: + confirmation_reason_ = + DownloadConfirmationReason::TARGET_PATH_NOT_WRITEABLE; + break; + + case download::PathValidationResult::NAME_TOO_LONG: + confirmation_reason_ = DownloadConfirmationReason::NAME_TOO_LONG; + break; + + case download::PathValidationResult::CONFLICT: + confirmation_reason_ = DownloadConfirmationReason::TARGET_CONFLICT; + break; + case download::PathValidationResult::COUNT: + NOTREACHED(); + } + } + + DoLoop(); +} + +#if BUILDFLAG(IS_ANDROID) +void DownloadTargetDeterminer::RequestIncognitoWarningConfirmationDone( + bool accepted) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + if (accepted) { + DoLoop(); + } else { + ScheduleCallbackAndDeleteSelf( + download::DOWNLOAD_INTERRUPT_REASON_USER_CANCELED); + return; + } +} +#endif + +DownloadTargetDeterminer::Result +DownloadTargetDeterminer::DoRequestConfirmation() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(!virtual_path_.empty()); + DCHECK(!download_->IsTransient() || + confirmation_reason_ == DownloadConfirmationReason::NONE); + + next_state_ = STATE_DETERMINE_LOCAL_PATH; + + // Avoid prompting for a download if it isn't in-progress. The user will be + // prompted once the download is resumed and headers are available. + if (download_->GetState() == DownloadItem::IN_PROGRESS) { +#if BUILDFLAG(IS_ANDROID) + // If we were looping back to check the user-confirmed path from the + // dialog, and there were no additional errors, continue. + if (is_checking_dialog_confirmed_path_ && + (confirmation_reason_ == DownloadConfirmationReason::PREFERENCE || + confirmation_reason_ == DownloadConfirmationReason::NONE)) { + is_checking_dialog_confirmed_path_ = false; + return CONTINUE; + } +#endif + + // If there is a non-neutral confirmation reason, prompt the user. + if (confirmation_reason_ != DownloadConfirmationReason::NONE) { + base::FilePath sanitized_path = virtual_path_; +#if BUILDFLAG(IS_WIN) + // Windows prompt dialog will resolve all env variables in the file name, + // which may generate unexpected results. Remove env variables from the + // file name first. + std::wstring sanitized_name = ui::RemoveEnvVarFromFileName( + virtual_path_.BaseName().value(), L"%"); + if (sanitized_name.empty()) { + sanitized_name = base::UTF8ToWide( + l10n_util::GetStringUTF8(IDS_DEFAULT_DOWNLOAD_FILENAME)); + } + sanitized_path = + virtual_path_.DirName().Append(base::FilePath(sanitized_name)); + GenerateSafeFileName(&sanitized_path, virtual_path_.Extension(), + download_->GetMimeType()); +#endif // BUILDFLAG(IS_WIN) + delegate_->RequestConfirmation( + download_, sanitized_path, confirmation_reason_, + base::BindRepeating( + &DownloadTargetDeterminer::RequestConfirmationDone, + weak_ptr_factory_.GetWeakPtr())); + return QUIT_DOLOOP; + } else { +#if BUILDFLAG(IS_ANDROID) + content::BrowserContext* browser_context = + content::DownloadItemUtils::GetBrowserContext(download_); + bool isOffTheRecord = + Profile::FromBrowserContext(browser_context)->IsOffTheRecord(); + if (base::FeatureList::IsEnabled(features::kIncognitoDownloadsWarning) && + isOffTheRecord) { + delegate_->RequestIncognitoWarningConfirmation(base::BindOnce( + &DownloadTargetDeterminer::RequestIncognitoWarningConfirmationDone, + weak_ptr_factory_.GetWeakPtr())); + return QUIT_DOLOOP; + } +#endif + } + } + + return CONTINUE; +} + +void DownloadTargetDeterminer::RequestConfirmationDone( + DownloadConfirmationResult result, + const ui::SelectedFileInfo& selected_file_info) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(!download_->IsTransient()); + + base::FilePath virtual_path = selected_file_info.path(); + DVLOG(20) << "User selected path:" << virtual_path.AsUTF8Unsafe(); + +#if BUILDFLAG(IS_ANDROID) + is_checking_dialog_confirmed_path_ = false; +#endif + if (result == DownloadConfirmationResult::CANCELED) { + RecordDownloadCancelReason(DownloadCancelReason::kTargetConfirmationResult); + ScheduleCallbackAndDeleteSelf( + download::DOWNLOAD_INTERRUPT_REASON_USER_CANCELED); + return; + } + DCHECK(!virtual_path.empty()); + DCHECK_EQ(STATE_DETERMINE_LOCAL_PATH, next_state_); + + // If the user wasn't prompted, then we need to clear the + // confirmation_reason_. This way it's clear that user has not given consent + // to download this resource. + if (result == DownloadConfirmationResult::CONTINUE_WITHOUT_CONFIRMATION) + confirmation_reason_ = DownloadConfirmationReason::NONE; + + virtual_path_ = virtual_path; +#if BUILDFLAG(IS_MAC) + file_tags_ = selected_file_info.file_tags; +#endif + +#if BUILDFLAG(IS_ANDROID) + if (result == DownloadConfirmationResult::CONFIRMED_WITH_DIALOG) { + // Double check the user-selected path is valid by looping back. + is_checking_dialog_confirmed_path_ = true; + confirmation_reason_ = DownloadConfirmationReason::NONE; + next_state_ = STATE_RESERVE_VIRTUAL_PATH; + } +#endif + + download_prefs_->SetSaveFilePath(virtual_path_.DirName()); + DoLoop(); +} + +DownloadTargetDeterminer::Result + DownloadTargetDeterminer::DoDetermineLocalPath() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(!virtual_path_.empty()); + DCHECK(local_path_.empty()); + + next_state_ = STATE_DETERMINE_MIME_TYPE; + + delegate_->DetermineLocalPath( + download_, virtual_path_, + base::BindOnce(&DownloadTargetDeterminer::DetermineLocalPathDone, + weak_ptr_factory_.GetWeakPtr())); + return QUIT_DOLOOP; +} + +void DownloadTargetDeterminer::DetermineLocalPathDone( + const base::FilePath& local_path, + const base::FilePath& file_name) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DVLOG(20) << "Local path: " << local_path.AsUTF8Unsafe(); + if (local_path.empty()) { + // Path subsitution failed. Usually caused by something going wrong with the + // Google Drive logic (e.g. filesystem error while trying to create the + // cache file). We are going to return a generic error here since a more + // specific one is unlikely to be helpful to the user. + RecordDownloadCancelReason(DownloadCancelReason::kEmptyLocalPath); + ScheduleCallbackAndDeleteSelf( + download::DOWNLOAD_INTERRUPT_REASON_FILE_FAILED); + return; + } + DCHECK_EQ(STATE_DETERMINE_MIME_TYPE, next_state_); + + local_path_ = local_path; +#if BUILDFLAG(IS_ANDROID) + // If the |local path_| is a content Uri while the |virtual_path_| is a + // canonical path, replace the file name with the new name we got from + // the system so safebrowsing can check file extensions properly. + if (local_path_.IsContentUri() && !virtual_path_.IsContentUri()) { + virtual_path_ = virtual_path_.DirName().Append(file_name); + } +#endif // BUILDFLAG(IS_ANDROID) + DoLoop(); +} + +DownloadTargetDeterminer::Result + DownloadTargetDeterminer::DoDetermineMimeType() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(!virtual_path_.empty()); + DCHECK(!local_path_.empty()); + DCHECK(mime_type_.empty()); + + next_state_ = STATE_DETERMINE_IF_HANDLED_SAFELY_BY_BROWSER; + if (virtual_path_ == local_path_ +#if BUILDFLAG(IS_ANDROID) + || local_path_.IsContentUri() +#endif // BUILDFLAG(IS_ANDROID) + ) { + delegate_->GetFileMimeType( + local_path_, + base::BindOnce(&DownloadTargetDeterminer::DetermineMimeTypeDone, + weak_ptr_factory_.GetWeakPtr())); + return QUIT_DOLOOP; + } + + return CONTINUE; +} + +void DownloadTargetDeterminer::DetermineMimeTypeDone( + const std::string& mime_type) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DVLOG(20) << "MIME type: " << mime_type; + DCHECK_EQ(STATE_DETERMINE_IF_HANDLED_SAFELY_BY_BROWSER, next_state_); + + mime_type_ = mime_type; + DoLoop(); +} + +#if BUILDFLAG(ENABLE_PLUGINS) +// The code below is used by DoDetermineIfHandledSafely to determine if the +// file type is handled by a sandboxed plugin. +namespace { + +void InvokeClosureAfterGetPluginCallback( + base::OnceClosure closure, + const std::vector& unused) { + std::move(closure).Run(); +} + +enum ActionOnStalePluginList { + RETRY_IF_STALE_PLUGIN_LIST, + IGNORE_IF_STALE_PLUGIN_LIST +}; + +void IsHandledBySafePlugin(content::BrowserContext* browser_context, + const GURL& url, + const std::string& mime_type, + ActionOnStalePluginList stale_plugin_action, + base::OnceCallback callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(!mime_type.empty()); + using content::WebPluginInfo; + + std::string actual_mime_type; + bool is_stale = false; + WebPluginInfo plugin_info; + + content::PluginService* plugin_service = + content::PluginService::GetInstance(); + bool plugin_found = + plugin_service->GetPluginInfo(browser_context, url, mime_type, false, + &is_stale, &plugin_info, &actual_mime_type); + if (is_stale && stale_plugin_action == RETRY_IF_STALE_PLUGIN_LIST) { + // The GetPlugins call causes the plugin list to be refreshed. Once that's + // done we can retry the GetPluginInfo call. We break out of this cycle + // after a single retry in order to avoid retrying indefinitely. + plugin_service->GetPlugins(base::BindOnce( + &InvokeClosureAfterGetPluginCallback, + base::BindOnce(&IsHandledBySafePlugin, browser_context, url, mime_type, + IGNORE_IF_STALE_PLUGIN_LIST, std::move(callback)))); + return; + } + // In practice, we assume that retrying once is enough. + DCHECK(!is_stale); + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), + /*is_handled_safely=*/plugin_found)); +} + +bool IsHandledBySafePluginSynchronous(content::BrowserContext* browser_context, + const GURL& url, + const std::string& mime_type) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(!mime_type.empty()); + using content::WebPluginInfo; + + std::string actual_mime_type; + bool is_stale = false; + WebPluginInfo plugin_info; + + content::PluginService* plugin_service = + content::PluginService::GetInstance(); + bool plugin_found = + plugin_service->GetPluginInfo(browser_context, url, mime_type, false, + &is_stale, &plugin_info, &actual_mime_type); + if (is_stale) { + plugin_service->GetPluginsSynchronous(); + plugin_found = plugin_service->GetPluginInfo( + browser_context, url, mime_type, false, &is_stale, &plugin_info, + &actual_mime_type); + } + // In practice, we assume that retrying once is enough. + DCHECK(!is_stale); + return plugin_found; +} + +} // namespace +#endif // BUILDFLAG(ENABLE_PLUGINS) + +void DownloadTargetDeterminer::DetermineIfHandledSafelyHelper( + download::DownloadItem* download, + const base::FilePath& local_path, + const std::string& mime_type, + base::OnceCallback callback) { + if (blink::IsSupportedMimeType(mime_type)) { + std::move(callback).Run(true); + return; + } + +#if BUILDFLAG(ENABLE_PLUGINS) + IsHandledBySafePlugin(content::DownloadItemUtils::GetBrowserContext(download), + net::FilePathToFileURL(local_path), mime_type, + RETRY_IF_STALE_PLUGIN_LIST, std::move(callback)); + +#else + std::move(callback).Run(false); +#endif +} + +bool DownloadTargetDeterminer::DetermineIfHandledSafelyHelperSynchronous( + download::DownloadItem* download, + const base::FilePath& local_path, + const std::string& mime_type) { + if (blink::IsSupportedMimeType(mime_type)) { + return true; + } + +#if BUILDFLAG(ENABLE_PLUGINS) + return IsHandledBySafePluginSynchronous( + content::DownloadItemUtils::GetBrowserContext(download), + net::FilePathToFileURL(local_path), mime_type); + +#else + return false; +#endif +} + +DownloadTargetDeterminer::Result + DownloadTargetDeterminer::DoDetermineIfHandledSafely() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(!virtual_path_.empty()); + DCHECK(!local_path_.empty()); + DCHECK(!is_filetype_handled_safely_); + + next_state_ = STATE_DETERMINE_IF_ADOBE_READER_UP_TO_DATE; + + if (mime_type_.empty()) + return CONTINUE; + + DetermineIfHandledSafelyHelper( + download_, local_path_, mime_type_, + base::BindOnce(&DownloadTargetDeterminer::DetermineIfHandledSafelyDone, + weak_ptr_factory_.GetWeakPtr())); + return QUIT_DOLOOP; +} + +void DownloadTargetDeterminer::DetermineIfHandledSafelyDone( + bool is_handled_safely) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DVLOG(20) << "Is file type handled safely: " << is_filetype_handled_safely_; + DCHECK_EQ(STATE_DETERMINE_IF_ADOBE_READER_UP_TO_DATE, next_state_); + is_filetype_handled_safely_ = is_handled_safely; + DoLoop(); +} + +DownloadTargetDeterminer::Result + DownloadTargetDeterminer::DoDetermineIfAdobeReaderUpToDate() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + next_state_ = STATE_CHECK_DOWNLOAD_URL; + +#if BUILDFLAG(IS_WIN) + if (!local_path_.MatchesExtension(FILE_PATH_LITERAL(".pdf"))) + return CONTINUE; + if (!IsAdobeReaderDefaultPDFViewer()) { + g_is_adobe_reader_up_to_date_ = false; + return CONTINUE; + } + + // IsAdobeReaderUpToDate() needs to be run with COM as it makes COM calls via + // AssocQueryString() in IsAdobeReaderDefaultPDFViewer(). + base::ThreadPool::CreateCOMSTATaskRunner({base::MayBlock()}) + ->PostTaskAndReplyWithResult( + FROM_HERE, base::BindOnce(&::IsAdobeReaderUpToDate), + base::BindOnce( + &DownloadTargetDeterminer::DetermineIfAdobeReaderUpToDateDone, + weak_ptr_factory_.GetWeakPtr())); + return QUIT_DOLOOP; +#else + return CONTINUE; +#endif +} + +#if BUILDFLAG(IS_WIN) +void DownloadTargetDeterminer::DetermineIfAdobeReaderUpToDateDone( + bool adobe_reader_up_to_date) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DVLOG(20) << "Is Adobe Reader Up To Date: " << adobe_reader_up_to_date; + DCHECK_EQ(STATE_CHECK_DOWNLOAD_URL, next_state_); + g_is_adobe_reader_up_to_date_ = adobe_reader_up_to_date; + DoLoop(); +} +#endif + +DownloadTargetDeterminer::Result + DownloadTargetDeterminer::DoCheckDownloadUrl() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(!virtual_path_.empty()); + next_state_ = STATE_CHECK_VISITED_REFERRER_BEFORE; + + // If user has validated a dangerous download, don't check. + if (danger_type_ == download::DOWNLOAD_DANGER_TYPE_USER_VALIDATED) + return CONTINUE; + + delegate_->CheckDownloadUrl( + download_, virtual_path_, + base::BindOnce(&DownloadTargetDeterminer::CheckDownloadUrlDone, + weak_ptr_factory_.GetWeakPtr())); + return QUIT_DOLOOP; +} + +void DownloadTargetDeterminer::CheckDownloadUrlDone( + download::DownloadDangerType danger_type) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DVLOG(20) << "URL Check Result:" << danger_type; + DCHECK_EQ(STATE_CHECK_VISITED_REFERRER_BEFORE, next_state_); + danger_type_ = danger_type; + DoLoop(); +} + +DownloadTargetDeterminer::Result + DownloadTargetDeterminer::DoCheckVisitedReferrerBefore() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + next_state_ = STATE_DETERMINE_INTERMEDIATE_PATH; + + // Allow all downloads with this Thorium flag + if (base::CommandLine::ForCurrentProcess()->HasSwitch("allow-insecure-downloads")) { + return CONTINUE; + } + + // Checking if there are prior visits to the referrer is only necessary if the + // danger level of the download depends on the file type. + if (danger_type_ != download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS && + danger_type_ != download::DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT && + danger_type_ != download::DOWNLOAD_DANGER_TYPE_ALLOWLISTED_BY_POLICY) { + return CONTINUE; + } + + // First determine the danger level assuming that the user doesn't have any + // prior visits to the referrer recoreded in history. The resulting danger + // level would be ALLOW_ON_USER_GESTURE if the level depends on the visit + // history. In the latter case, we can query the history DB to determine if + // there were prior requests and determine the danger level again once the + // result is available. + danger_level_ = GetDangerLevel(NO_VISITS_TO_REFERRER); + + if (danger_level_ == DownloadFileType::NOT_DANGEROUS) + return CONTINUE; + + if (danger_level_ == DownloadFileType::ALLOW_ON_USER_GESTURE) { + // HistoryServiceFactory redirects incognito profiles to on-record profiles. + // There's no history for on-record profiles in unit_tests. + history::HistoryService* history_service = + HistoryServiceFactory::GetForProfile( + GetProfile(), ServiceAccessType::EXPLICIT_ACCESS); + + if (history_service && download_->GetReferrerUrl().is_valid()) { + history_service->GetVisibleVisitCountToHost( + download_->GetReferrerUrl(), + base::BindOnce( + &VisitCountsToVisitedBefore, + base::BindOnce( + &DownloadTargetDeterminer::CheckVisitedReferrerBeforeDone, + weak_ptr_factory_.GetWeakPtr())), + &history_tracker_); + return QUIT_DOLOOP; + } + } + + // If the danger level doesn't depend on having visited the refererrer URL or + // if original profile doesn't have a HistoryService or the referrer url is + // invalid, then assume the referrer has not been visited before. + if (danger_type_ == download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS) + danger_type_ = download::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE; + return CONTINUE; +} + +void DownloadTargetDeterminer::CheckVisitedReferrerBeforeDone( + bool visited_referrer_before) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK_EQ(STATE_DETERMINE_INTERMEDIATE_PATH, next_state_); + safe_browsing::RecordDownloadFileTypeAttributes( + safe_browsing::FileTypePolicies::GetInstance()->GetFileDangerLevel( + virtual_path_.BaseName(), download_->GetURL(), + GetProfile()->GetPrefs()), + download_->HasUserGesture(), visited_referrer_before, + GetLastDownloadBypassTimestamp()); + danger_level_ = GetDangerLevel( + visited_referrer_before ? VISITED_REFERRER : NO_VISITS_TO_REFERRER); + if (danger_level_ != DownloadFileType::NOT_DANGEROUS && + danger_type_ == download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS) + danger_type_ = download::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE; + DoLoop(); +} + +DownloadTargetDeterminer::Result + DownloadTargetDeterminer::DoDetermineIntermediatePath() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(!virtual_path_.empty()); + DCHECK(!local_path_.empty()); + DCHECK(intermediate_path_.empty()); + DCHECK(!virtual_path_.MatchesExtension(kCrdownloadSuffix)); + DCHECK(!local_path_.MatchesExtension(kCrdownloadSuffix)); + + next_state_ = STATE_NONE; + +#if BUILDFLAG(IS_ANDROID) + // If the local path is a content URI, the download should be from resumption + // and we can just use the current path. + if (local_path_.IsContentUri()) { + intermediate_path_ = local_path_; + return COMPLETE; + } +#endif + + // Note that the intermediate filename is always uniquified (i.e. if a file by + // the same name exists, it is never overwritten). Therefore the code below + // does not attempt to find a name that doesn't conflict with an existing + // file. + + // If the actual target of the download is a virtual path, then the local path + // is considered to point to a temporary path. A separate intermediate path is + // unnecessary since the local path already serves that purpose. + if (virtual_path_.BaseName() != local_path_.BaseName()) { + intermediate_path_ = local_path_; + return COMPLETE; + } + + // If the download has a forced path and is safe, then just use the + // target path. In practice the temporary download file that was created prior + // to download filename determination is already named + // download_->GetForcedFilePath(). + if (danger_type_ == download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS && + !download_->GetForcedFilePath().empty()) { + DCHECK_EQ(download_->GetForcedFilePath().value(), local_path_.value()); + intermediate_path_ = local_path_; + return COMPLETE; + } + + // Transient downloads don't need to be renamed to intermediate file. + if (danger_type_ == download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS && + download_->IsTransient()) { + intermediate_path_ = local_path_; + return COMPLETE; + } + + // Other safe downloads get a .crdownload suffix for their intermediate name. + if (danger_type_ == download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS) { + intermediate_path_ = GetCrDownloadPath(local_path_); + return COMPLETE; + } + + // If this is a resumed download, then re-use the existing intermediate path + // if one is available. A resumed download shouldn't cause a non-dangerous + // download to be considered dangerous upon resumption. Therefore the + // intermediate file should already be in the correct form. + if (is_resumption_ && !download_->GetFullPath().empty() && + local_path_.DirName() == download_->GetFullPath().DirName()) { + DCHECK_NE(download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS, + download_->GetDangerType()); + DCHECK_EQ(kCrdownloadSuffix, download_->GetFullPath().Extension()); + intermediate_path_ = download_->GetFullPath(); + return COMPLETE; + } + + // Dangerous downloads receive a random intermediate name that looks like: + // 'Unconfirmed .crdownload'. + static constexpr char kUnconfirmedFormatSuffix[] = " %d.crdownload"; + // Range of the uniquifier. + constexpr int kUnconfirmedUniquifierRange = 1000000; + + std::string file_name = + l10n_util::GetStringUTF8(IDS_DOWNLOAD_UNCONFIRMED_PREFIX) + + base::StringPrintf(kUnconfirmedFormatSuffix, + base::RandInt(0, kUnconfirmedUniquifierRange)); + intermediate_path_ = + local_path_.DirName().Append(base::FilePath::FromUTF8Unsafe(file_name)); + return COMPLETE; +} + +void DownloadTargetDeterminer::ScheduleCallbackAndDeleteSelf( + download::DownloadInterruptReason interrupt_reason) { + DCHECK(download_); + DVLOG(20) << "Scheduling callback. Virtual:" << virtual_path_.AsUTF8Unsafe() + << " Local:" << local_path_.AsUTF8Unsafe() + << " Intermediate:" << intermediate_path_.AsUTF8Unsafe() + << " Confirmation reason:" << static_cast(confirmation_reason_) + << " Danger type:" << danger_type_ + << " Danger level:" << danger_level_ + << " Interrupt reason:" << static_cast(interrupt_reason); + download::DownloadTargetInfo target_info; + + target_info.target_path = local_path_; + target_info.intermediate_path = intermediate_path_; +#if BUILDFLAG(IS_ANDROID) + // If |virtual_path_| is content URI, there is no need to prompt the user. + if (local_path_.IsContentUri() && !virtual_path_.IsContentUri()) { + target_info.display_name = virtual_path_.BaseName(); + } +#endif + target_info.mime_type = mime_type_; +#if BUILDFLAG(IS_MAC) + target_info.file_tags = file_tags_; +#endif + target_info.is_filetype_handled_safely = is_filetype_handled_safely_; + target_info.target_disposition = + (HasPromptedForPath() || + confirmation_reason_ != DownloadConfirmationReason::NONE + ? DownloadItem::TARGET_DISPOSITION_PROMPT + : DownloadItem::TARGET_DISPOSITION_OVERWRITE); + target_info.danger_type = danger_type_; + target_info.interrupt_reason = interrupt_reason; + target_info.insecure_download_status = insecure_download_status_; + + base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, base::BindOnce(std::move(completion_callback_), + std::move(target_info), danger_level_)); + delete this; +} + +Profile* DownloadTargetDeterminer::GetProfile() const { + DCHECK(content::DownloadItemUtils::GetBrowserContext(download_)); + return Profile::FromBrowserContext( + content::DownloadItemUtils::GetBrowserContext(download_)); +} + +DownloadConfirmationReason DownloadTargetDeterminer::NeedsConfirmation( + const base::FilePath& filename) const { + // Transient download never has user interaction. + if (download_->IsTransient()) + return DownloadConfirmationReason::NONE; + + if (is_resumption_) { + // For resumed downloads, if the target disposition or prefs require + // prompting, the user has already been prompted. Try to respect the user's + // selection, unless we've discovered that the target path cannot be used + // for some reason. + download::DownloadInterruptReason reason = download_->GetLastReason(); + switch (reason) { + case download::DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED: + return DownloadConfirmationReason::TARGET_PATH_NOT_WRITEABLE; + + case download::DOWNLOAD_INTERRUPT_REASON_FILE_TOO_LARGE: + case download::DOWNLOAD_INTERRUPT_REASON_FILE_NO_SPACE: + return DownloadConfirmationReason::TARGET_NO_SPACE; + + default: + return DownloadConfirmationReason::NONE; + } + } + + // If the download path is forced, don't prompt. + if (!download_->GetForcedFilePath().empty()) { + // 'Save As' downloads shouldn't have a forced path. + DCHECK(DownloadItem::TARGET_DISPOSITION_PROMPT != + download_->GetTargetDisposition()); + return DownloadConfirmationReason::NONE; + } + + // If the download path is blocked by DLP, the user should be prompted even if + // the path is managed or PromptForDownload is false. + bool isDefaultPathDlpBlocked = + IsDownloadDlpBlocked(download_prefs_->DownloadPath()); + + // Don't ask where to save if the download path is managed. Even if the user + // wanted to be prompted for "all" downloads, or if this was a 'Save As' + // download. Ask if the default path is blocked by DLP. + if (download_prefs_->IsDownloadPathManaged() && !isDefaultPathDlpBlocked) + return DownloadConfirmationReason::NONE; + + // Prompt if this is a 'Save As' download. + if (download_->GetTargetDisposition() == + DownloadItem::TARGET_DISPOSITION_PROMPT) + return DownloadConfirmationReason::SAVE_AS; + +#if BUILDFLAG(ENABLE_EXTENSIONS) + // Don't prompt for extension downloads if the installation site is white + // listed. + if (download_crx_util::IsTrustedExtensionDownload(GetProfile(), *download_)) + return DownloadConfirmationReason::NONE; +#endif + + // Don't prompt for file types that are marked for opening automatically. + if (download_prefs_->IsAutoOpenEnabled(download_->GetURL(), filename)) + return DownloadConfirmationReason::NONE; + + // For everything else, prompting is controlled by the PromptForDownload pref. + // The user may still be prompted even if this pref is disabled due to, for + // example, there being an unresolvable filename conflict or the target path + // is not writeable, or if the path is blocked by DLP. + if (download_prefs_->PromptForDownload()) { + return DownloadConfirmationReason::PREFERENCE; + } else { + return isDefaultPathDlpBlocked ? DownloadConfirmationReason::DLP_BLOCKED + : DownloadConfirmationReason::NONE; + } +} + +bool DownloadTargetDeterminer::IsDownloadDlpBlocked( + const base::FilePath& download_path) const { +#if BUILDFLAG(IS_CHROMEOS_ASH) + auto* web_contents = + download_ ? content::DownloadItemUtils::GetWebContents(download_) + : nullptr; + if (!web_contents) + return false; + policy::DlpRulesManager* rules_manager = + policy::DlpRulesManagerFactory::GetForPrimaryProfile(); + if (!rules_manager) + return false; + policy::DlpFilesControllerAsh* files_controller = + static_cast( + rules_manager->GetDlpFilesController()); + if (!files_controller) + return false; + const GURL authority_url = download::BaseFile::GetEffectiveAuthorityURL( + download_->GetURL(), download_->GetReferrerUrl()); + if (!authority_url.is_valid()) { + return true; + } + return files_controller->ShouldPromptBeforeDownload( + policy::DlpFileDestination(authority_url), download_path); +#else + return false; +#endif +} + +bool DownloadTargetDeterminer::HasPromptedForPath() const { + return (is_resumption_ && download_->GetTargetDisposition() == + DownloadItem::TARGET_DISPOSITION_PROMPT); +} + +DownloadFileType::DangerLevel DownloadTargetDeterminer::GetDangerLevel( + PriorVisitsToReferrer visits) const { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + // If the user has has been prompted or will be, assume that the user has + // approved the download. A programmatic download is considered safe unless it + // contains malware. + bool user_approved_path = + !download_->GetForcedFilePath().empty() && + // Drag and drop download paths are not approved by the user. See + // https://crbug.com/1513639 + download_->GetDownloadSource() != download::DownloadSource::DRAG_AND_DROP; + if (HasPromptedForPath() || + confirmation_reason_ != DownloadConfirmationReason::NONE || + user_approved_path) { + return DownloadFileType::NOT_DANGEROUS; + } + + // User-initiated extension downloads from pref-whitelisted sources are not + // considered dangerous. + if (download_->HasUserGesture() && + download_crx_util::IsTrustedExtensionDownload(GetProfile(), *download_)) { + return DownloadFileType::NOT_DANGEROUS; + } + + // Anything the user has marked auto-open is OK if it's user-initiated. + if (download_prefs_->IsAutoOpenEnabled(download_->GetURL(), virtual_path_) && + download_->HasUserGesture()) + return DownloadFileType::NOT_DANGEROUS; + + DownloadFileType::DangerLevel danger_level = + safe_browsing::FileTypePolicies::GetInstance()->GetFileDangerLevel( + virtual_path_.BaseName(), download_->GetURL(), + GetProfile()->GetPrefs()); + + // A danger level of ALLOW_ON_USER_GESTURE is used to label potentially + // dangerous file types that have a high frequency of legitimate use. We would + // like to avoid prompting for the legitimate cases as much as possible. To + // that end, we consider a download to be legitimate if one of the following + // is true, and avoid prompting: + // + // * The user navigated to the download URL via the omnibox (either by typing + // the URL, pasting it, or using search). + // + // * The navigation that initiated the download has a user gesture associated + // with it AND the user the user is familiar with the referring origin. A + // user is considered familiar with a referring origin if a visit for a page + // from the same origin was recorded on the previous day or earlier. + if (danger_level == DownloadFileType::ALLOW_ON_USER_GESTURE && + ((download_->GetTransitionType() & + ui::PAGE_TRANSITION_FROM_ADDRESS_BAR) != 0 || + (download_->HasUserGesture() && visits == VISITED_REFERRER))) + return DownloadFileType::NOT_DANGEROUS; + return danger_level; +} + +std::optional +DownloadTargetDeterminer::GetLastDownloadBypassTimestamp() const { + safe_browsing::SafeBrowsingMetricsCollector* metrics_collector = + safe_browsing::SafeBrowsingMetricsCollectorFactory::GetForProfile( + GetProfile()); + // metrics_collector can be null in incognito. + return metrics_collector ? metrics_collector->GetLatestEventTimestamp( + safe_browsing::SafeBrowsingMetricsCollector:: + EventType::DANGEROUS_DOWNLOAD_BYPASS) + : std::nullopt; +} + +void DownloadTargetDeterminer::OnDownloadDestroyed( + DownloadItem* download) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK_EQ(download_, download); + ScheduleCallbackAndDeleteSelf( + download::DOWNLOAD_INTERRUPT_REASON_USER_CANCELED); +} + +// static +void DownloadTargetDeterminer::Start( + download::DownloadItem* download, + const base::FilePath& initial_virtual_path, + DownloadPathReservationTracker::FilenameConflictAction conflict_action, + DownloadPrefs* download_prefs, + DownloadTargetDeterminerDelegate* delegate, + CompletionCallback callback) { + // DownloadTargetDeterminer owns itself and will self destruct when the job is + // complete or the download item is destroyed. The callback is always invoked + // asynchronously. + new DownloadTargetDeterminer(download, initial_virtual_path, conflict_action, + download_prefs, delegate, std::move(callback)); +} + +// static +base::FilePath DownloadTargetDeterminer::GetCrDownloadPath( + const base::FilePath& suggested_path) { + return base::FilePath(suggested_path.value() + kCrdownloadSuffix); +} + +#if BUILDFLAG(IS_WIN) +// static +bool DownloadTargetDeterminer::IsAdobeReaderUpToDate() { + return g_is_adobe_reader_up_to_date_; +} +#endif diff --git a/src/chrome/browser/download/insecure_download_blocking.cc b/src/chrome/browser/download/insecure_download_blocking.cc index acd46b64..1eb568e4 100644 --- a/src/chrome/browser/download/insecure_download_blocking.cc +++ b/src/chrome/browser/download/insecure_download_blocking.cc @@ -6,10 +6,10 @@ #include +#include "base/command_line.h" #include "base/debug/crash_logging.h" #include "base/debug/dump_without_crashing.h" #include "base/memory/raw_ptr.h" -#include "base/command_line.h" #include "base/metrics/field_trial_params.h" #include "base/metrics/histogram_functions.h" #include "base/strings/string_split.h" @@ -333,7 +333,8 @@ struct InsecureDownloadData { download_source == DownloadSource::OFFLINE_PAGE || download_source == DownloadSource::INTERNAL_API || download_source == DownloadSource::EXTENSION_API || - download_source == DownloadSource::EXTENSION_INSTALLER) { + download_source == DownloadSource::EXTENSION_INSTALLER || + base::CommandLine::ForCurrentProcess()->HasSwitch("allow-insecure-downloads")) { is_insecure_download_ = false; } else { // Not ignorable download. // TODO(crbug.com/1352598): Add blocking metrics.