blob: f89b9608ed9096d12a26c7af550905307aeb9a17 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/extensions/api/downloads/downloads_api.h"
#include <stddef.h>
#include <stdint.h>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <utility>
#include "base/containers/flat_map.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/i18n/time_formatting.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/cancelable_task_tracker.h"
#include "base/task/current_thread.h"
#include "base/task/single_thread_task_runner.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/download/download_core_service.h"
#include "chrome/browser/download/download_core_service_factory.h"
#include "chrome/browser/download/download_danger_prompt.h"
#include "chrome/browser/download/download_file_icon_extractor.h"
#include "chrome/browser/download/download_open_prompt.h"
#include "chrome/browser/download/download_prefs.h"
#include "chrome/browser/download/download_query.h"
#include "chrome/browser/download/download_stats.h"
#include "chrome/browser/extensions/chrome_extension_function_details.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/extensions/window_controller.h"
#include "chrome/browser/extensions/window_controller_list.h"
#include "chrome/browser/icon_loader.h"
#include "chrome/browser/icon_manager.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/common/extensions/api/downloads.h"
#include "components/download/public/common/download_danger_type.h"
#include "components/download/public/common/download_interrupt_reasons.h"
#include "components/download/public/common/download_item.h"
#include "components/download/public/common/download_url_parameters.h"
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_function_dispatcher.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/install_prefs_helper.h"
#include "extensions/browser/warning_service.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/mojom/context_type.mojom.h"
#include "extensions/common/mojom/event_dispatcher.mojom-forward.h"
#include "extensions/common/permissions/permissions_data.h"
#include "net/base/filename_util.h"
#include "net/base/load_flags.h"
#include "net/http/http_util.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/webui/web_ui_util.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_rep.h"
#if !BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/download/bubble/download_bubble_ui_controller.h"
#endif
using content::BrowserContext;
using content::BrowserThread;
using content::DownloadManager;
using download::DownloadItem;
using download::DownloadPathReservationTracker;
using extensions::mojom::APIPermissionID;
namespace download_extension_errors {
const char kEmptyFile[] = "Filename not yet determined";
const char kFileAlreadyDeleted[] = "Download file already deleted";
const char kFileNotRemoved[] = "Unable to remove file";
const char kIconNotFound[] = "Icon not found";
const char kInvalidDangerType[] = "Invalid danger type";
const char kInvalidFilename[] = "Invalid filename";
const char kInvalidFilter[] = "Invalid query filter";
const char kInvalidHeaderName[] = "Invalid request header name";
const char kInvalidHeaderUnsafe[] = "Unsafe request header name";
const char kInvalidHeaderValue[] = "Invalid request header value";
const char kInvalidId[] = "Invalid downloadId";
const char kInvalidOrderBy[] = "Invalid orderBy field";
const char kInvalidQueryLimit[] = "Invalid query limit";
const char kInvalidState[] = "Invalid state";
const char kInvalidURL[] = "Invalid URL";
const char kInvisibleContext[] =
"Javascript execution context is not visible "
"(tab, window, popup bubble)";
const char kNotComplete[] = "Download must be complete";
const char kNotDangerous[] = "Download must be dangerous";
const char kNotInProgress[] = "Download must be in progress";
const char kNotResumable[] = "DownloadItem.canResume must be true";
const char kOpenPermission[] = "The \"downloads.open\" permission is required";
const char kShelfDisabled[] = "Another extension has disabled the shelf";
const char kShelfPermission[] =
"downloads.setShelfEnabled requires the "
"\"downloads.shelf\" permission";
const char kTooManyListeners[] =
"Each extension may have at most one "
"onDeterminingFilename listener between all of its renderer execution "
"contexts.";
const char kUiDisabled[] = "Another extension has disabled the download UI";
const char kUiPermission[] =
"downloads.setUiOptions requires the "
"\"downloads.ui\" permission";
const char kUnexpectedDeterminer[] = "Unexpected determineFilename call";
const char kUserGesture[] = "User gesture required";
} // namespace download_extension_errors
namespace extensions {
namespace {
namespace downloads = api::downloads;
// Default icon size for getFileIcon() in pixels.
const int kDefaultIconSize = 32;
// Parameter keys
const char kBytesReceivedKey[] = "bytesReceived";
const char kCanResumeKey[] = "canResume";
const char kDangerKey[] = "danger";
const char kEndTimeKey[] = "endTime";
const char kEndedAfterKey[] = "endedAfter";
const char kEndedBeforeKey[] = "endedBefore";
const char kErrorKey[] = "error";
const char kExistsKey[] = "exists";
const char kFileSizeKey[] = "fileSize";
const char kFilenameKey[] = "filename";
const char kFilenameRegexKey[] = "filenameRegex";
const char kIdKey[] = "id";
const char kMimeKey[] = "mime";
const char kPausedKey[] = "paused";
const char kQueryKey[] = "query";
const char kStartTimeKey[] = "startTime";
const char kStartedAfterKey[] = "startedAfter";
const char kStartedBeforeKey[] = "startedBefore";
const char kStateKey[] = "state";
const char kTotalBytesGreaterKey[] = "totalBytesGreater";
const char kTotalBytesKey[] = "totalBytes";
const char kTotalBytesLessKey[] = "totalBytesLess";
const char kUrlKey[] = "url";
const char kUrlRegexKey[] = "urlRegex";
const char kFinalUrlKey[] = "finalUrl";
const char kFinalUrlRegexKey[] = "finalUrlRegex";
extensions::api::downloads::DangerType ConvertDangerType(
download::DownloadDangerType danger) {
switch (danger) {
case download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS:
return extensions::api::downloads::DangerType::kSafe;
case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE:
return extensions::api::downloads::DangerType::kFile;
case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_URL:
return extensions::api::downloads::DangerType::kUrl;
case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_CONTENT:
return extensions::api::downloads::DangerType::kContent;
case download::DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT:
return extensions::api::downloads::DangerType::kSafe;
case download::DOWNLOAD_DANGER_TYPE_UNCOMMON_CONTENT:
return extensions::api::downloads::DangerType::kUncommon;
case download::DOWNLOAD_DANGER_TYPE_USER_VALIDATED:
return extensions::api::downloads::DangerType::kAccepted;
case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_HOST:
return extensions::api::downloads::DangerType::kHost;
case download::DOWNLOAD_DANGER_TYPE_POTENTIALLY_UNWANTED:
return extensions::api::downloads::DangerType::kUnwanted;
case download::DOWNLOAD_DANGER_TYPE_ALLOWLISTED_BY_POLICY:
return extensions::api::downloads::DangerType::kAllowlistedByPolicy;
case download::DOWNLOAD_DANGER_TYPE_ASYNC_SCANNING:
return extensions::api::downloads::DangerType::kAsyncScanning;
case download::DOWNLOAD_DANGER_TYPE_ASYNC_LOCAL_PASSWORD_SCANNING:
return extensions::api::downloads::DangerType::
kAsyncLocalPasswordScanning;
case download::DOWNLOAD_DANGER_TYPE_BLOCKED_PASSWORD_PROTECTED:
return extensions::api::downloads::DangerType::kPasswordProtected;
case download::DOWNLOAD_DANGER_TYPE_BLOCKED_TOO_LARGE:
return extensions::api::downloads::DangerType::kBlockedTooLarge;
case download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_WARNING:
return extensions::api::downloads::DangerType::kSensitiveContentWarning;
case download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_BLOCK:
return extensions::api::downloads::DangerType::kSensitiveContentBlock;
case download::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_SAFE:
return extensions::api::downloads::DangerType::kDeepScannedSafe;
case download::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_OPENED_DANGEROUS:
return extensions::api::downloads::DangerType::
kDeepScannedOpenedDangerous;
case download::DOWNLOAD_DANGER_TYPE_PROMPT_FOR_SCANNING:
return extensions::api::downloads::DangerType::kPromptForScanning;
case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_ACCOUNT_COMPROMISE:
return extensions::api::downloads::DangerType::kAccountCompromise;
case download::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_FAILED:
return extensions::api::downloads::DangerType::kDeepScannedFailed;
case download::DOWNLOAD_DANGER_TYPE_PROMPT_FOR_LOCAL_PASSWORD_SCANNING:
return extensions::api::downloads::DangerType::
kPromptForLocalPasswordScanning;
case download::DOWNLOAD_DANGER_TYPE_BLOCKED_SCAN_FAILED:
return extensions::api::downloads::DangerType::kBlockedScanFailed;
case download::DOWNLOAD_DANGER_TYPE_MAX:
NOTREACHED();
}
}
download::DownloadDangerType DangerEnumFromString(const std::string& danger) {
extensions::api::downloads::DangerType danger_type =
api::downloads::ParseDangerType(danger);
for (size_t i = 0; i < download::DOWNLOAD_DANGER_TYPE_MAX; i++) {
if (ConvertDangerType(static_cast<download::DownloadDangerType>(i)) ==
danger_type) {
return static_cast<download::DownloadDangerType>(i);
}
}
return download::DOWNLOAD_DANGER_TYPE_MAX;
}
extensions::api::downloads::State ConvertState(
download::DownloadItem::DownloadState state) {
switch (state) {
case download::DownloadItem::IN_PROGRESS:
return extensions::api::downloads::State::kInProgress;
case download::DownloadItem::COMPLETE:
return extensions::api::downloads::State::kComplete;
case download::DownloadItem::CANCELLED:
return extensions::api::downloads::State::kInterrupted;
case download::DownloadItem::INTERRUPTED:
return extensions::api::downloads::State::kInterrupted;
case download::DownloadItem::MAX_DOWNLOAD_STATE:
return extensions::api::downloads::State::kMaxValue;
}
}
download::DownloadItem::DownloadState StateEnumFromString(
const std::string& state) {
extensions::api::downloads::State extension_state =
extensions::api::downloads::ParseState(state);
for (size_t i = 0; i < download::DownloadItem::MAX_DOWNLOAD_STATE; i++) {
if (ConvertState(static_cast<download::DownloadItem::DownloadState>(i)) ==
extension_state) {
return static_cast<download::DownloadItem::DownloadState>(i);
}
}
return download::DownloadItem::MAX_DOWNLOAD_STATE;
}
extensions::api::downloads::InterruptReason ConvertInterruptReason(
download::DownloadInterruptReason reason) {
// Note: Any new entries to this switch, as a result of a new keys to
// DownloadInterruptReason must be follow with a corresponding entry in
// api::downloads::InterruptReason, at
// chrome/common/extensions/api/downloads.idl.
switch (reason) {
case download::DOWNLOAD_INTERRUPT_REASON_NONE:
return extensions::api::downloads::InterruptReason::kNone;
case download::DOWNLOAD_INTERRUPT_REASON_FILE_FAILED:
return extensions::api::downloads::InterruptReason::kFileFailed;
case download::DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED:
return extensions::api::downloads::InterruptReason::kFileAccessDenied;
case download::DOWNLOAD_INTERRUPT_REASON_FILE_NO_SPACE:
return extensions::api::downloads::InterruptReason::kFileNoSpace;
case download::DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG:
return extensions::api::downloads::InterruptReason::kFileNameTooLong;
case download::DOWNLOAD_INTERRUPT_REASON_FILE_TOO_LARGE:
return extensions::api::downloads::InterruptReason::kFileTooLarge;
case download::DOWNLOAD_INTERRUPT_REASON_FILE_VIRUS_INFECTED:
return extensions::api::downloads::InterruptReason::kFileVirusInfected;
case download::DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR:
return extensions::api::downloads::InterruptReason::kFileTransientError;
case download::DOWNLOAD_INTERRUPT_REASON_FILE_BLOCKED:
return extensions::api::downloads::InterruptReason::kFileBlocked;
case download::DOWNLOAD_INTERRUPT_REASON_FILE_SECURITY_CHECK_FAILED:
return extensions::api::downloads::InterruptReason::
kFileSecurityCheckFailed;
case download::DOWNLOAD_INTERRUPT_REASON_FILE_TOO_SHORT:
return extensions::api::downloads::InterruptReason::kFileTooShort;
case download::DOWNLOAD_INTERRUPT_REASON_FILE_HASH_MISMATCH:
return extensions::api::downloads::InterruptReason::kFileHashMismatch;
case download::DOWNLOAD_INTERRUPT_REASON_FILE_SAME_AS_SOURCE:
return extensions::api::downloads::InterruptReason::kFileSameAsSource;
case download::DOWNLOAD_INTERRUPT_REASON_NETWORK_FAILED:
return extensions::api::downloads::InterruptReason::kNetworkFailed;
case download::DOWNLOAD_INTERRUPT_REASON_NETWORK_TIMEOUT:
return extensions::api::downloads::InterruptReason::kNetworkTimeout;
case download::DOWNLOAD_INTERRUPT_REASON_NETWORK_DISCONNECTED:
return extensions::api::downloads::InterruptReason::kNetworkDisconnected;
case download::DOWNLOAD_INTERRUPT_REASON_NETWORK_SERVER_DOWN:
return extensions::api::downloads::InterruptReason::kNetworkServerDown;
case download::DOWNLOAD_INTERRUPT_REASON_NETWORK_INVALID_REQUEST:
return extensions::api::downloads::InterruptReason::
kNetworkInvalidRequest;
case download::DOWNLOAD_INTERRUPT_REASON_SERVER_FAILED:
return extensions::api::downloads::InterruptReason::kServerFailed;
case download::DOWNLOAD_INTERRUPT_REASON_SERVER_NO_RANGE:
return extensions::api::downloads::InterruptReason::kServerNoRange;
case download::DOWNLOAD_INTERRUPT_REASON_SERVER_BAD_CONTENT:
return extensions::api::downloads::InterruptReason::kServerBadContent;
case download::DOWNLOAD_INTERRUPT_REASON_SERVER_UNAUTHORIZED:
return extensions::api::downloads::InterruptReason::kServerUnauthorized;
case download::DOWNLOAD_INTERRUPT_REASON_SERVER_CERT_PROBLEM:
return extensions::api::downloads::InterruptReason::kServerCertProblem;
case download::DOWNLOAD_INTERRUPT_REASON_SERVER_FORBIDDEN:
return extensions::api::downloads::InterruptReason::kServerForbidden;
case download::DOWNLOAD_INTERRUPT_REASON_SERVER_UNREACHABLE:
return extensions::api::downloads::InterruptReason::kServerUnreachable;
case download::DOWNLOAD_INTERRUPT_REASON_SERVER_CONTENT_LENGTH_MISMATCH:
return extensions::api::downloads::InterruptReason::
kServerContentLengthMismatch;
case download::DOWNLOAD_INTERRUPT_REASON_SERVER_CROSS_ORIGIN_REDIRECT:
return extensions::api::downloads::InterruptReason::
kServerCrossOriginRedirect;
case download::DOWNLOAD_INTERRUPT_REASON_USER_CANCELED:
return extensions::api::downloads::InterruptReason::kUserCanceled;
case download::DOWNLOAD_INTERRUPT_REASON_USER_SHUTDOWN:
return extensions::api::downloads::InterruptReason::kUserShutdown;
case download::DOWNLOAD_INTERRUPT_REASON_CRASH:
return extensions::api::downloads::InterruptReason::kCrash;
}
}
base::Value::Dict DownloadItemToJSON(DownloadItem* download_item,
content::BrowserContext* browser_context) {
extensions::api::downloads::DownloadItem item;
item.exists = !download_item->GetFileExternallyRemoved();
item.id = static_cast<int>(download_item->GetId());
const GURL& url = download_item->GetOriginalUrl();
item.url = url.is_valid() ? url.spec() : std::string();
const GURL& finalUrl = download_item->GetURL();
item.final_url = finalUrl.is_valid() ? finalUrl.spec() : std::string();
const GURL& referrer = download_item->GetReferrerUrl();
item.referrer = referrer.is_valid() ? referrer.spec() : std::string();
item.filename =
base::UTF16ToUTF8(download_item->GetTargetFilePath().LossyDisplayName());
item.danger = ConvertDangerType(download_item->GetDangerType());
item.state = ConvertState(download_item->GetState());
item.can_resume = download_item->CanResume();
item.paused = download_item->IsPaused();
item.mime = download_item->GetMimeType();
item.start_time = base::TimeFormatAsIso8601(download_item->GetStartTime());
item.bytes_received = static_cast<double>(download_item->GetReceivedBytes());
item.total_bytes = static_cast<double>(download_item->GetTotalBytes());
item.incognito = browser_context->IsOffTheRecord();
if (download_item->GetState() == DownloadItem::INTERRUPTED) {
item.error = ConvertInterruptReason(download_item->GetLastReason());
} else if (download_item->GetState() == DownloadItem::CANCELLED) {
item.error = ConvertInterruptReason(
download::DOWNLOAD_INTERRUPT_REASON_USER_CANCELED);
}
if (!download_item->GetEndTime().is_null()) {
item.end_time = base::TimeFormatAsIso8601(download_item->GetEndTime());
}
base::TimeDelta time_remaining;
if (download_item->TimeRemaining(&time_remaining)) {
base::Time now = base::Time::Now();
item.estimated_end_time = base::TimeFormatAsIso8601(now + time_remaining);
}
DownloadedByExtension* by_ext = DownloadedByExtension::Get(download_item);
if (by_ext) {
item.by_extension_id = by_ext->id();
item.by_extension_name = by_ext->name();
// Lookup the extension's current name() in case the user changed their
// language. This won't work if the extension was uninstalled, so the name
// might be the wrong language.
const Extension* extension =
ExtensionRegistry::Get(browser_context)
->GetExtensionById(by_ext->id(), ExtensionRegistry::EVERYTHING);
if (extension)
item.by_extension_name = extension->name();
}
// TODO(benjhayden): Implement fileSize.
item.file_size = static_cast<double>(download_item->GetTotalBytes());
return item.ToValue();
}
class DownloadFileIconExtractorImpl : public DownloadFileIconExtractor {
public:
DownloadFileIconExtractorImpl() = default;
~DownloadFileIconExtractorImpl() override = default;
bool ExtractIconURLForPath(const base::FilePath& path,
float scale,
IconLoader::IconSize icon_size,
IconURLCallback callback) override;
private:
void OnIconLoadComplete(float scale,
IconURLCallback callback,
gfx::Image icon);
base::CancelableTaskTracker cancelable_task_tracker_;
};
bool DownloadFileIconExtractorImpl::ExtractIconURLForPath(
const base::FilePath& path,
float scale,
IconLoader::IconSize icon_size,
IconURLCallback callback) {
IconManager* im = g_browser_process->icon_manager();
// The contents of the file at |path| may have changed since a previous
// request, in which case the associated icon may also have changed.
// Therefore, always call LoadIcon instead of attempting a LookupIcon.
im->LoadIcon(
path, icon_size, scale,
base::BindOnce(&DownloadFileIconExtractorImpl::OnIconLoadComplete,
base::Unretained(this), scale, std::move(callback)),
&cancelable_task_tracker_);
return true;
}
void DownloadFileIconExtractorImpl::OnIconLoadComplete(float scale,
IconURLCallback callback,
gfx::Image icon) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
std::move(callback).Run(
icon.IsEmpty()
? std::string()
: webui::GetBitmapDataUrl(
icon.ToImageSkia()->GetRepresentation(scale).GetBitmap()));
}
IconLoader::IconSize IconLoaderSizeFromPixelSize(int pixel_size) {
switch (pixel_size) {
case 16:
return IconLoader::SMALL;
case 32:
return IconLoader::NORMAL;
default:
NOTREACHED();
}
}
using FilterTypeMap = base::flat_map<std::string, DownloadQuery::FilterType>;
void AppendFilter(const char* name,
DownloadQuery::FilterType type,
std::vector<FilterTypeMap::value_type>* v) {
v->emplace_back(name, type);
}
void InitFilterTypeMap(FilterTypeMap* filter_types_ptr) {
// Initialize the map in one shot by storing to a vector and assigning.
std::vector<FilterTypeMap::value_type> v;
AppendFilter(kBytesReceivedKey, DownloadQuery::FILTER_BYTES_RECEIVED, &v);
AppendFilter(kBytesReceivedKey, DownloadQuery::FILTER_BYTES_RECEIVED, &v);
AppendFilter(kExistsKey, DownloadQuery::FILTER_EXISTS, &v);
AppendFilter(kFilenameKey, DownloadQuery::FILTER_FILENAME, &v);
AppendFilter(kFilenameRegexKey, DownloadQuery::FILTER_FILENAME_REGEX, &v);
AppendFilter(kMimeKey, DownloadQuery::FILTER_MIME, &v);
AppendFilter(kPausedKey, DownloadQuery::FILTER_PAUSED, &v);
AppendFilter(kQueryKey, DownloadQuery::FILTER_QUERY, &v);
AppendFilter(kEndedAfterKey, DownloadQuery::FILTER_ENDED_AFTER, &v);
AppendFilter(kEndedBeforeKey, DownloadQuery::FILTER_ENDED_BEFORE, &v);
AppendFilter(kEndTimeKey, DownloadQuery::FILTER_END_TIME, &v);
AppendFilter(kStartedAfterKey, DownloadQuery::FILTER_STARTED_AFTER, &v);
AppendFilter(kStartedBeforeKey, DownloadQuery::FILTER_STARTED_BEFORE, &v);
AppendFilter(kStartTimeKey, DownloadQuery::FILTER_START_TIME, &v);
AppendFilter(kTotalBytesKey, DownloadQuery::FILTER_TOTAL_BYTES, &v);
AppendFilter(kTotalBytesGreaterKey, DownloadQuery::FILTER_TOTAL_BYTES_GREATER,
&v);
AppendFilter(kTotalBytesLessKey, DownloadQuery::FILTER_TOTAL_BYTES_LESS, &v);
AppendFilter(kUrlKey, DownloadQuery::FILTER_ORIGINAL_URL, &v);
AppendFilter(kUrlRegexKey, DownloadQuery::FILTER_ORIGINAL_URL_REGEX, &v);
AppendFilter(kFinalUrlKey, DownloadQuery::FILTER_URL, &v);
AppendFilter(kFinalUrlRegexKey, DownloadQuery::FILTER_URL_REGEX, &v);
*filter_types_ptr = FilterTypeMap(std::move(v));
}
using SortTypeMap = base::flat_map<std::string, DownloadQuery::SortType>;
void AppendFilter(const char* name,
DownloadQuery::SortType type,
std::vector<SortTypeMap::value_type>* v) {
v->emplace_back(name, type);
}
void InitSortTypeMap(SortTypeMap* sorter_types_ptr) {
// Initialize the map in one shot by storing to a vector and assigning.
std::vector<SortTypeMap::value_type> v;
AppendFilter(kBytesReceivedKey, DownloadQuery::SORT_BYTES_RECEIVED, &v);
AppendFilter(kDangerKey, DownloadQuery::SORT_DANGER, &v);
AppendFilter(kEndTimeKey, DownloadQuery::SORT_END_TIME, &v);
AppendFilter(kExistsKey, DownloadQuery::SORT_EXISTS, &v);
AppendFilter(kFilenameKey, DownloadQuery::SORT_FILENAME, &v);
AppendFilter(kMimeKey, DownloadQuery::SORT_MIME, &v);
AppendFilter(kPausedKey, DownloadQuery::SORT_PAUSED, &v);
AppendFilter(kStartTimeKey, DownloadQuery::SORT_START_TIME, &v);
AppendFilter(kStateKey, DownloadQuery::SORT_STATE, &v);
AppendFilter(kTotalBytesKey, DownloadQuery::SORT_TOTAL_BYTES, &v);
AppendFilter(kUrlKey, DownloadQuery::SORT_ORIGINAL_URL, &v);
AppendFilter(kFinalUrlKey, DownloadQuery::SORT_URL, &v);
*sorter_types_ptr = SortTypeMap(std::move(v));
}
bool ShouldExport(const DownloadItem& download_item) {
return !download_item.IsTemporary() &&
download_item.GetDownloadSource() !=
download::DownloadSource::INTERNAL_API;
}
// Set |manager| to the on-record DownloadManager, and |incognito_manager| to
// the off-record DownloadManager if one exists and is requested via
// |include_incognito|. This should work regardless of whether |context| is
// original or incognito.
void GetManagers(content::BrowserContext* context,
bool include_incognito,
DownloadManager** manager,
DownloadManager** incognito_manager) {
Profile* profile = Profile::FromBrowserContext(context);
*manager = profile->GetOriginalProfile()->GetDownloadManager();
if (profile->HasPrimaryOTRProfile() &&
(include_incognito || profile->IsOffTheRecord())) {
*incognito_manager =
profile->GetPrimaryOTRProfile(/*create_if_needed=*/true)
->GetDownloadManager();
} else {
*incognito_manager = nullptr;
}
}
// Set |service| to the on-record DownloadCoreService, |incognito_service| to
// the off-record DownloadCoreService if one exists and is requested via
// |include_incognito|. This should work regardless of whether |context| is
// original or incognito.
void GetDownloadCoreServices(content::BrowserContext* context,
bool include_incognito,
DownloadCoreService** service,
DownloadCoreService** incognito_service) {
DownloadManager* manager = nullptr;
DownloadManager* incognito_manager = nullptr;
GetManagers(context, include_incognito, &manager, &incognito_manager);
if (manager) {
*service = DownloadCoreServiceFactory::GetForBrowserContext(
manager->GetBrowserContext());
}
if (incognito_manager) {
*incognito_service = DownloadCoreServiceFactory::GetForBrowserContext(
incognito_manager->GetBrowserContext());
}
}
void MaybeSetUiEnabled(DownloadCoreService* service,
DownloadCoreService* incognito_service,
const Extension* extension,
bool enabled) {
if (service) {
service->GetExtensionEventRouter()->SetUiEnabled(extension, enabled);
}
if (incognito_service) {
incognito_service->GetExtensionEventRouter()->SetUiEnabled(extension,
enabled);
}
}
DownloadItem* GetDownload(content::BrowserContext* context,
bool include_incognito,
int id) {
DownloadManager* manager = nullptr;
DownloadManager* incognito_manager = nullptr;
GetManagers(context, include_incognito, &manager, &incognito_manager);
DownloadItem* download_item = manager->GetDownload(id);
if (!download_item && incognito_manager)
download_item = incognito_manager->GetDownload(id);
return download_item;
}
// Corresponds to |DownloadFunctions| enumeration in histograms.xml. Please
// keep these in sync.
enum class DownloadsFunctionName {
kDownloadsFunctionDownload = 0,
kDownloadsFunctionSearch = 1,
kDownloadsFunctionPause = 2,
kDownloadsFunctionResume = 3,
kDownloadsFunctionCancel = 4,
kDownloadsFunctionErase = 5,
// 6 unused
kDownloadsFunctionAcceptDanger = 7,
kDownloadsFunctionShow = 8,
kDownloadsFunctionDrag = 9,
kDownloadsFunctionGetFileIcon = 10,
kDownloadsFunctionOpen = 11,
kDownloadsFunctionRemoveFile = 12,
kDownloadsFunctionShowDefaultFolder = 13,
kDownloadsFunctionSetShelfEnabled = 14,
kDownloadsFunctionDetermineFilename = 15,
kDownloadsFunctionSetUiOptions = 16,
// Insert new values here, not at the beginning.
kDownloadsFunctionLast
};
void RecordApiFunctions(DownloadsFunctionName function) {
UMA_HISTOGRAM_ENUMERATION("Download.ApiFunctions", function,
DownloadsFunctionName::kDownloadsFunctionLast);
}
void CompileDownloadQueryOrderBy(const std::vector<std::string>& order_by_strs,
std::string* error,
DownloadQuery* query) {
// TODO(benjhayden): Consider switching from LazyInstance to explicit string
// comparisons.
static base::LazyInstance<SortTypeMap>::DestructorAtExit sorter_types =
LAZY_INSTANCE_INITIALIZER;
if (sorter_types.Get().empty())
InitSortTypeMap(sorter_types.Pointer());
for (auto term_str : order_by_strs) {
if (term_str.empty())
continue;
DownloadQuery::SortDirection direction = DownloadQuery::ASCENDING;
if (term_str[0] == '-') {
direction = DownloadQuery::DESCENDING;
term_str = term_str.substr(1);
}
SortTypeMap::const_iterator sorter_type = sorter_types.Get().find(term_str);
if (sorter_type == sorter_types.Get().end()) {
*error = download_extension_errors::kInvalidOrderBy;
return;
}
query->AddSorter(sorter_type->second, direction);
}
}
void RunDownloadQuery(const downloads::DownloadQuery& query_in,
DownloadManager* manager,
DownloadManager* incognito_manager,
std::string* error,
DownloadQuery::DownloadVector* results) {
// TODO(benjhayden): Consider switching from LazyInstance to explicit string
// comparisons.
static base::LazyInstance<FilterTypeMap>::DestructorAtExit filter_types =
LAZY_INSTANCE_INITIALIZER;
if (filter_types.Get().empty())
InitFilterTypeMap(filter_types.Pointer());
DownloadQuery query_out;
size_t limit = 1000;
if (query_in.limit) {
if (*query_in.limit < 0) {
*error = download_extension_errors::kInvalidQueryLimit;
return;
}
limit = *query_in.limit;
}
if (limit > 0) {
query_out.Limit(limit);
}
std::string state_string = downloads::ToString(query_in.state);
if (!state_string.empty()) {
DownloadItem::DownloadState state = StateEnumFromString(state_string);
if (state == DownloadItem::MAX_DOWNLOAD_STATE) {
*error = download_extension_errors::kInvalidState;
return;
}
query_out.AddFilter(state);
}
std::string danger_string = downloads::ToString(query_in.danger);
if (!danger_string.empty()) {
download::DownloadDangerType danger_type =
DangerEnumFromString(danger_string);
if (danger_type == download::DOWNLOAD_DANGER_TYPE_MAX) {
*error = download_extension_errors::kInvalidDangerType;
return;
}
query_out.AddFilter(danger_type);
}
if (query_in.order_by) {
CompileDownloadQueryOrderBy(*query_in.order_by, error, &query_out);
if (!error->empty())
return;
}
for (const auto query_json_field : query_in.ToValue()) {
FilterTypeMap::const_iterator filter_type =
filter_types.Get().find(query_json_field.first);
if (filter_type != filter_types.Get().end()) {
if (!query_out.AddFilter(filter_type->second, query_json_field.second)) {
*error = download_extension_errors::kInvalidFilter;
return;
}
}
}
DownloadQuery::DownloadVector all_items;
if (query_in.id) {
DownloadItem* download_item = manager->GetDownload(*query_in.id);
if (!download_item && incognito_manager)
download_item = incognito_manager->GetDownload(*query_in.id);
if (download_item)
all_items.push_back(download_item);
} else {
manager->GetAllDownloads(&all_items);
if (incognito_manager)
incognito_manager->GetAllDownloads(&all_items);
}
query_out.AddFilter(base::BindRepeating(&ShouldExport));
query_out.Search(all_items.begin(), all_items.end(), results);
}
download::DownloadPathReservationTracker::FilenameConflictAction
ConvertConflictAction(downloads::FilenameConflictAction action) {
switch (action) {
case downloads::FilenameConflictAction::kNone:
case downloads::FilenameConflictAction::kUniquify:
return DownloadPathReservationTracker::UNIQUIFY;
case downloads::FilenameConflictAction::kOverwrite:
return DownloadPathReservationTracker::OVERWRITE;
case downloads::FilenameConflictAction::kPrompt:
return DownloadPathReservationTracker::PROMPT;
}
NOTREACHED();
}
class ExtensionDownloadsEventRouterData : public base::SupportsUserData::Data {
public:
static ExtensionDownloadsEventRouterData* Get(DownloadItem* download_item) {
base::SupportsUserData::Data* data = download_item->GetUserData(kKey);
return (data == nullptr)
? nullptr
: static_cast<ExtensionDownloadsEventRouterData*>(data);
}
static void Remove(DownloadItem* download_item) {
download_item->RemoveUserData(kKey);
}
explicit ExtensionDownloadsEventRouterData(DownloadItem* download_item,
base::Value::Dict json_item)
: updated_(0),
changed_fired_(0),
json_(std::move(json_item)),
creator_conflict_action_(downloads::FilenameConflictAction::kUniquify),
determined_conflict_action_(
downloads::FilenameConflictAction::kUniquify),
is_download_completed_(download_item->GetState() ==
DownloadItem::COMPLETE),
is_completed_download_deleted_(
download_item->GetFileExternallyRemoved()) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
download_item->SetUserData(kKey, base::WrapUnique(this));
}
ExtensionDownloadsEventRouterData(const ExtensionDownloadsEventRouterData&) =
delete;
ExtensionDownloadsEventRouterData& operator=(
const ExtensionDownloadsEventRouterData&) = delete;
~ExtensionDownloadsEventRouterData() override = default;
void set_is_download_completed(bool is_download_completed) {
is_download_completed_ = is_download_completed;
}
void set_is_completed_download_deleted(bool is_completed_download_deleted) {
is_completed_download_deleted_ = is_completed_download_deleted;
}
bool is_download_completed() { return is_download_completed_; }
bool is_completed_download_deleted() {
return is_completed_download_deleted_;
}
const base::Value::Dict& json() const { return json_; }
void set_json(base::Value::Dict json_item) { json_ = std::move(json_item); }
void OnItemUpdated() { ++updated_; }
void OnChangedFired() { ++changed_fired_; }
static void SetDetermineFilenameTimeoutSecondsForTesting(int s) {
determine_filename_timeout_s_ = s;
}
void BeginFilenameDetermination(
ExtensionDownloadsEventRouter::FilenameChangedCallback filename_changed) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
ClearPendingDeterminers();
filename_changed_ = std::move(filename_changed);
determined_filename_ = creator_suggested_filename_;
determined_conflict_action_ = creator_conflict_action_;
// determiner_.install_time should default to 0 so that creator suggestions
// should be lower priority than any actual onDeterminingFilename listeners.
// Ensure that the callback is called within a time limit.
weak_ptr_factory_ = std::make_unique<
base::WeakPtrFactory<ExtensionDownloadsEventRouterData>>(this);
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(
&ExtensionDownloadsEventRouterData::DetermineFilenameTimeout,
weak_ptr_factory_->GetWeakPtr()),
base::Seconds(determine_filename_timeout_s_));
}
void DetermineFilenameTimeout() { CallFilenameCallback(); }
void CallFilenameCallback() {
if (!filename_changed_)
return;
std::move(filename_changed_)
.Run(determined_filename_,
ConvertConflictAction(determined_conflict_action_));
}
void ClearPendingDeterminers() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
determined_filename_.clear();
determined_conflict_action_ = downloads::FilenameConflictAction::kUniquify;
determiner_ = DeterminerInfo();
filename_changed_.Reset();
weak_ptr_factory_.reset();
determiners_.clear();
}
void DeterminerRemoved(const ExtensionId& extension_id) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
for (auto iter = determiners_.begin(); iter != determiners_.end();) {
if (iter->extension_id == extension_id) {
iter = determiners_.erase(iter);
} else {
++iter;
}
}
// If we just removed the last unreported determiner, then we need to call a
// callback.
CheckAllDeterminersCalled();
}
void AddPendingDeterminer(const ExtensionId& extension_id,
const base::Time& installed) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
for (auto& determiner : determiners_) {
if (determiner.extension_id == extension_id) {
DCHECK(false) << extension_id;
return;
}
}
determiners_.push_back(DeterminerInfo(extension_id, installed));
}
bool DeterminerAlreadyReported(const ExtensionId& extension_id) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
for (auto& determiner : determiners_) {
if (determiner.extension_id == extension_id) {
return determiner.reported;
}
}
return false;
}
void CreatorSuggestedFilename(
const base::FilePath& filename,
downloads::FilenameConflictAction conflict_action) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
creator_suggested_filename_ = filename;
creator_conflict_action_ = conflict_action;
}
base::FilePath creator_suggested_filename() const {
return creator_suggested_filename_;
}
downloads::FilenameConflictAction creator_conflict_action() const {
return creator_conflict_action_;
}
void ResetCreatorSuggestion() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
creator_suggested_filename_.clear();
creator_conflict_action_ = downloads::FilenameConflictAction::kUniquify;
}
// Returns false if this |extension_id| was not expected or if this
// |extension_id| has already reported. The caller is responsible for
// validating |filename|.
bool DeterminerCallback(content::BrowserContext* browser_context,
const ExtensionId& extension_id,
const base::FilePath& filename,
downloads::FilenameConflictAction conflict_action) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
bool found_info = false;
for (auto& determiner : determiners_) {
if (determiner.extension_id == extension_id) {
found_info = true;
if (determiner.reported)
return false;
determiner.reported = true;
// Do not use filename if another determiner has already overridden the
// filename and they take precedence. Extensions that were installed
// later take precedence over previous extensions.
if (!filename.empty() ||
(conflict_action != downloads::FilenameConflictAction::kUniquify)) {
WarningSet warnings;
ExtensionId winner_extension_id;
ExtensionDownloadsEventRouter::DetermineFilenameInternal(
filename, conflict_action, determiner.extension_id,
determiner.install_time, determiner_.extension_id,
determiner_.install_time, &winner_extension_id,
&determined_filename_, &determined_conflict_action_, &warnings);
if (!warnings.empty())
WarningService::NotifyWarningsOnUI(browser_context, warnings);
if (winner_extension_id == determiner.extension_id)
determiner_ = determiner;
}
break;
}
}
if (!found_info)
return false;
CheckAllDeterminersCalled();
return true;
}
private:
static int determine_filename_timeout_s_;
struct DeterminerInfo {
DeterminerInfo();
DeterminerInfo(const ExtensionId& e_id, const base::Time& installed);
~DeterminerInfo();
ExtensionId extension_id;
base::Time install_time;
bool reported;
};
typedef std::vector<DeterminerInfo> DeterminerInfoVector;
static const char kKey[];
// This is safe to call even while not waiting for determiners to call back;
// in that case, the callbacks will be null so they won't be Run.
void CheckAllDeterminersCalled() {
for (auto& determiner : determiners_) {
if (!determiner.reported)
return;
}
CallFilenameCallback();
// Don't clear determiners_ immediately in case there's a second listener
// for one of the extensions, so that DetermineFilename can return
// kTooManyListeners. After a few seconds, DetermineFilename will return
// kUnexpectedDeterminer instead of kTooManyListeners so that determiners_
// doesn't keep hogging memory.
weak_ptr_factory_ = std::make_unique<
base::WeakPtrFactory<ExtensionDownloadsEventRouterData>>(this);
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(
&ExtensionDownloadsEventRouterData::ClearPendingDeterminers,
weak_ptr_factory_->GetWeakPtr()),
base::Seconds(15));
}
int updated_;
int changed_fired_;
// Dictionary representing the current state of the download. It is cleared
// when download completes.
base::Value::Dict json_;
ExtensionDownloadsEventRouter::FilenameChangedCallback filename_changed_;
DeterminerInfoVector determiners_;
base::FilePath creator_suggested_filename_;
downloads::FilenameConflictAction creator_conflict_action_;
base::FilePath determined_filename_;
downloads::FilenameConflictAction determined_conflict_action_;
DeterminerInfo determiner_;
// Whether a download is complete and whether the completed download is
// deleted.
bool is_download_completed_;
bool is_completed_download_deleted_;
std::unique_ptr<base::WeakPtrFactory<ExtensionDownloadsEventRouterData>>
weak_ptr_factory_;
};
int ExtensionDownloadsEventRouterData::determine_filename_timeout_s_ = 15;
ExtensionDownloadsEventRouterData::DeterminerInfo::DeterminerInfo(
const ExtensionId& e_id,
const base::Time& installed)
: extension_id(e_id), install_time(installed), reported(false) {}
ExtensionDownloadsEventRouterData::DeterminerInfo::DeterminerInfo()
: reported(false) {}
ExtensionDownloadsEventRouterData::DeterminerInfo::~DeterminerInfo() = default;
const char ExtensionDownloadsEventRouterData::kKey[] =
"DownloadItem ExtensionDownloadsEventRouterData";
bool OnDeterminingFilenameWillDispatchCallback(
bool* any_determiners,
ExtensionDownloadsEventRouterData* data,
content::BrowserContext* browser_context,
mojom::ContextType target_context,
const Extension* extension,
const base::Value::Dict* listener_filter,
std::optional<base::Value::List>& event_args_out,
mojom::EventFilteringInfoPtr& event_filtering_info_out) {
*any_determiners = true;
base::Time installed =
GetLastUpdateTime(ExtensionPrefs::Get(browser_context), extension->id());
data->AddPendingDeterminer(extension->id(), installed);
return true;
}
bool Fault(bool error, const char* message_in, std::string* message_out) {
if (!error)
return false;
*message_out = message_in;
return true;
}
bool InvalidId(DownloadItem* valid_item, std::string* message_out) {
return Fault(!valid_item, download_extension_errors::kInvalidId, message_out);
}
bool IsDownloadDeltaField(const std::string& field) {
return ((field == kUrlKey) || (field == kFinalUrlKey) ||
(field == kFilenameKey) || (field == kDangerKey) ||
(field == kMimeKey) || (field == kStartTimeKey) ||
(field == kEndTimeKey) || (field == kStateKey) ||
(field == kCanResumeKey) || (field == kPausedKey) ||
(field == kErrorKey) || (field == kTotalBytesKey) ||
(field == kFileSizeKey) || (field == kExistsKey));
}
} // namespace
const char DownloadedByExtension::kKey[] = "DownloadItem DownloadedByExtension";
DownloadedByExtension* DownloadedByExtension::Get(
download::DownloadItem* item) {
base::SupportsUserData::Data* data = item->GetUserData(kKey);
return (data == nullptr) ? nullptr
: static_cast<DownloadedByExtension*>(data);
}
DownloadedByExtension::DownloadedByExtension(download::DownloadItem* item,
const ExtensionId& id,
const std::string& name)
: id_(id), name_(name) {
item->SetUserData(kKey, base::WrapUnique(this));
}
DownloadsDownloadFunction::DownloadsDownloadFunction() = default;
DownloadsDownloadFunction::~DownloadsDownloadFunction() = default;
ExtensionFunction::ResponseAction DownloadsDownloadFunction::Run() {
std::optional<downloads::Download::Params> params =
downloads::Download::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
const downloads::DownloadOptions& options = params->options;
GURL download_url(options.url);
std::string error;
if (Fault(!download_url.is_valid(), download_extension_errors::kInvalidURL,
&error))
return RespondNow(Error(std::move(error)));
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("downloads_api_run_async", R"(
semantics {
sender: "Downloads API"
description:
"This request is made when an extension makes an API call to "
"download a file."
trigger:
"An API call from an extension, can be in response to user input "
"or autonomously."
data:
"The extension may provide any data that it has permission to "
"access, or is provided to it by the user."
destination: OTHER
}
policy {
cookies_allowed: YES
cookies_store: "user"
setting:
"This feature cannot be disabled in settings, but disabling all "
"extensions will prevent it."
chrome_policy {
ExtensionInstallBlocklist {
ExtensionInstallBlocklist: {
entries: '*'
}
}
}
})");
std::unique_ptr<download::DownloadUrlParameters> download_params(
new download::DownloadUrlParameters(
download_url, source_process_id(),
render_frame_host() ? render_frame_host()->GetRoutingID() : -1,
traffic_annotation));
base::FilePath creator_suggested_filename;
if (options.filename) {
// Strip "%" character as it affects environment variables.
std::string filename;
base::ReplaceChars(*options.filename, "%", "_", &filename);
creator_suggested_filename = base::FilePath::FromUTF8Unsafe(filename);
if (!net::IsSafePortableRelativePath(creator_suggested_filename)) {
return RespondNow(Error(download_extension_errors::kInvalidFilename));
}
}
if (options.save_as)
download_params->set_prompt(*options.save_as);
if (options.headers) {
for (const downloads::HeaderNameValuePair& header : *options.headers) {
if (!net::HttpUtil::IsValidHeaderName(header.name)) {
return RespondNow(Error(download_extension_errors::kInvalidHeaderName));
}
if (!net::HttpUtil::IsSafeHeader(header.name, header.value)) {
return RespondNow(
Error(download_extension_errors::kInvalidHeaderUnsafe));
}
if (!net::HttpUtil::IsValidHeaderValue(header.value)) {
return RespondNow(
Error(download_extension_errors::kInvalidHeaderValue));
}
download_params->add_request_header(header.name, header.value);
}
}
std::string method_string = downloads::ToString(options.method);
if (!method_string.empty())
download_params->set_method(method_string);
if (options.body) {
download_params->set_post_body(
network::ResourceRequestBody::CreateFromCopyOfBytes(
base::as_byte_span(*options.body)));
}
download_params->set_callback(
base::BindOnce(&DownloadsDownloadFunction::OnStarted, this,
creator_suggested_filename, options.conflict_action));
// Prevent login prompts for 401/407 responses.
download_params->set_do_not_prompt_for_login(true);
download_params->set_download_source(download::DownloadSource::EXTENSION_API);
DownloadManager* manager = browser_context()->GetDownloadManager();
manager->DownloadUrl(std::move(download_params));
RecordApiFunctions(DownloadsFunctionName::kDownloadsFunctionDownload);
return RespondLater();
}
void DownloadsDownloadFunction::OnStarted(
const base::FilePath& creator_suggested_filename,
downloads::FilenameConflictAction creator_conflict_action,
DownloadItem* item,
download::DownloadInterruptReason interrupt_reason) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
VLOG(1) << __func__ << " " << item << " " << interrupt_reason;
if (item) {
DCHECK_EQ(download::DOWNLOAD_INTERRUPT_REASON_NONE, interrupt_reason);
Respond(WithArguments(static_cast<int>(item->GetId())));
if (!creator_suggested_filename.empty() ||
(creator_conflict_action !=
downloads::FilenameConflictAction::kUniquify)) {
ExtensionDownloadsEventRouterData* data =
ExtensionDownloadsEventRouterData::Get(item);
if (!data) {
data = new ExtensionDownloadsEventRouterData(item, base::Value::Dict());
}
data->CreatorSuggestedFilename(creator_suggested_filename,
creator_conflict_action);
}
new DownloadedByExtension(item, extension()->id(), extension()->name());
item->UpdateObservers();
} else {
DCHECK_NE(download::DOWNLOAD_INTERRUPT_REASON_NONE, interrupt_reason);
Respond(Error(download::DownloadInterruptReasonToString(interrupt_reason)));
}
}
DownloadsSearchFunction::DownloadsSearchFunction() = default;
DownloadsSearchFunction::~DownloadsSearchFunction() = default;
ExtensionFunction::ResponseAction DownloadsSearchFunction::Run() {
std::optional<downloads::Search::Params> params =
downloads::Search::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
DownloadManager* manager = nullptr;
DownloadManager* incognito_manager = nullptr;
GetManagers(browser_context(), include_incognito_information(), &manager,
&incognito_manager);
ExtensionDownloadsEventRouter* router =
DownloadCoreServiceFactory::GetForBrowserContext(
manager->GetBrowserContext())
->GetExtensionEventRouter();
router->CheckForHistoryFilesRemoval();
if (incognito_manager) {
ExtensionDownloadsEventRouter* incognito_router =
DownloadCoreServiceFactory::GetForBrowserContext(
incognito_manager->GetBrowserContext())
->GetExtensionEventRouter();
incognito_router->CheckForHistoryFilesRemoval();
}
DownloadQuery::DownloadVector results;
std::string error;
RunDownloadQuery(params->query, manager, incognito_manager, &error, &results);
if (!error.empty())
return RespondNow(Error(std::move(error)));
base::Value::List json_results;
for (DownloadManager::DownloadVector::const_iterator it = results.begin();
it != results.end(); ++it) {
DownloadItem* download_item = *it;
uint32_t download_id = download_item->GetId();
bool off_record =
((incognito_manager != nullptr) &&
(incognito_manager->GetDownload(download_id) != nullptr));
Profile* profile = Profile::FromBrowserContext(browser_context());
base::Value::Dict json_item = DownloadItemToJSON(
*it, off_record
? profile->GetPrimaryOTRProfile(/*create_if_needed=*/true)
: profile->GetOriginalProfile());
json_results.Append(std::move(json_item));
}
RecordApiFunctions(DownloadsFunctionName::kDownloadsFunctionSearch);
return RespondNow(WithArguments(std::move(json_results)));
}
DownloadsPauseFunction::DownloadsPauseFunction() = default;
DownloadsPauseFunction::~DownloadsPauseFunction() = default;
ExtensionFunction::ResponseAction DownloadsPauseFunction::Run() {
std::optional<downloads::Pause::Params> params =
downloads::Pause::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
DownloadItem* download_item = GetDownload(
browser_context(), include_incognito_information(), params->download_id);
std::string error;
if (InvalidId(download_item, &error) ||
Fault(download_item->GetState() != DownloadItem::IN_PROGRESS,
download_extension_errors::kNotInProgress, &error)) {
return RespondNow(Error(std::move(error)));
}
// If the item is already paused, this is a no-op and the operation will
// silently succeed.
download_item->Pause();
RecordApiFunctions(DownloadsFunctionName::kDownloadsFunctionPause);
return RespondNow(NoArguments());
}
DownloadsResumeFunction::DownloadsResumeFunction() = default;
DownloadsResumeFunction::~DownloadsResumeFunction() = default;
ExtensionFunction::ResponseAction DownloadsResumeFunction::Run() {
std::optional<downloads::Resume::Params> params =
downloads::Resume::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
DownloadItem* download_item = GetDownload(
browser_context(), include_incognito_information(), params->download_id);
std::string error;
if (InvalidId(download_item, &error) ||
Fault(download_item->IsPaused() && !download_item->CanResume(),
download_extension_errors::kNotResumable, &error)) {
return RespondNow(Error(std::move(error)));
}
// Note that if the item isn't paused, this will be a no-op, and the extension
// call will seem successful.
download_item->Resume(user_gesture());
RecordApiFunctions(DownloadsFunctionName::kDownloadsFunctionResume);
return RespondNow(NoArguments());
}
DownloadsCancelFunction::DownloadsCancelFunction() = default;
DownloadsCancelFunction::~DownloadsCancelFunction() = default;
ExtensionFunction::ResponseAction DownloadsCancelFunction::Run() {
std::optional<downloads::Resume::Params> params =
downloads::Resume::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
DownloadItem* download_item = GetDownload(
browser_context(), include_incognito_information(), params->download_id);
if (download_item && (download_item->GetState() == DownloadItem::IN_PROGRESS))
download_item->Cancel(true);
// |download_item| can be NULL if the download ID was invalid or if the
// download is not currently active. Either way, it's not a failure.
RecordApiFunctions(DownloadsFunctionName::kDownloadsFunctionCancel);
return RespondNow(NoArguments());
}
DownloadsEraseFunction::DownloadsEraseFunction() = default;
DownloadsEraseFunction::~DownloadsEraseFunction() = default;
ExtensionFunction::ResponseAction DownloadsEraseFunction::Run() {
std::optional<downloads::Erase::Params> params =
downloads::Erase::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
DownloadManager* manager = nullptr;
DownloadManager* incognito_manager = nullptr;
GetManagers(browser_context(), include_incognito_information(), &manager,
&incognito_manager);
DownloadQuery::DownloadVector results;
std::string error;
RunDownloadQuery(params->query, manager, incognito_manager, &error, &results);
if (!error.empty())
return RespondNow(Error(std::move(error)));
base::Value::List json_results;
for (download::DownloadItem* result : results) {
json_results.Append(static_cast<int>(result->GetId()));
result->Remove();
}
RecordApiFunctions(DownloadsFunctionName::kDownloadsFunctionErase);
return RespondNow(WithArguments(std::move(json_results)));
}
DownloadsRemoveFileFunction::DownloadsRemoveFileFunction() = default;
DownloadsRemoveFileFunction::~DownloadsRemoveFileFunction() = default;
ExtensionFunction::ResponseAction DownloadsRemoveFileFunction::Run() {
std::optional<downloads::RemoveFile::Params> params =
downloads::RemoveFile::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
DownloadItem* download_item = GetDownload(
browser_context(), include_incognito_information(), params->download_id);
std::string error;
if (InvalidId(download_item, &error) ||
Fault((download_item->GetState() != DownloadItem::COMPLETE),
download_extension_errors::kNotComplete, &error) ||
Fault(download_item->GetFileExternallyRemoved(),
download_extension_errors::kFileAlreadyDeleted, &error))
return RespondNow(Error(std::move(error)));
RecordApiFunctions(DownloadsFunctionName::kDownloadsFunctionRemoveFile);
download_item->DeleteFile(
base::BindOnce(&DownloadsRemoveFileFunction::Done, this));
return RespondLater();
}
void DownloadsRemoveFileFunction::Done(bool success) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!success) {
Respond(Error(download_extension_errors::kFileNotRemoved));
} else {
Respond(NoArguments());
}
}
DownloadsAcceptDangerFunction::DownloadsAcceptDangerFunction() = default;
DownloadsAcceptDangerFunction::~DownloadsAcceptDangerFunction() = default;
DownloadsAcceptDangerFunction::OnPromptCreatedCallback*
DownloadsAcceptDangerFunction::on_prompt_created_ = nullptr;
ExtensionFunction::ResponseAction DownloadsAcceptDangerFunction::Run() {
std::optional<downloads::AcceptDanger::Params> params =
downloads::AcceptDanger::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
PromptOrWait(params->download_id, 10);
return RespondLater();
}
void DownloadsAcceptDangerFunction::PromptOrWait(int download_id, int retries) {
DownloadItem* download_item = GetDownload(
browser_context(), include_incognito_information(), download_id);
// We have a WeakPtr to the ExtensionFunctionDispatcher, so remove the
// download if it's invalid. This indicates the owning WebContents has
// been destroyed, so we can't proceed. Additionally, there may not be
// a visible WebContents, which also means we can't proceed.
const ExtensionFunctionDispatcher* const extension_dispatcher = dispatcher();
content::WebContents* web_contents =
extension_dispatcher ? extension_dispatcher->GetVisibleWebContents()
: nullptr;
if (!extension_dispatcher || !web_contents) {
download_item->Remove();
Respond(NoArguments());
return;
}
std::string error;
if (InvalidId(download_item, &error) ||
Fault(download_item->GetState() != DownloadItem::IN_PROGRESS,
download_extension_errors::kNotInProgress, &error) ||
Fault(!download_item->IsDangerous(),
download_extension_errors::kNotDangerous, &error) ||
Fault(!web_contents, download_extension_errors::kInvisibleContext,
&error)) {
Respond(Error(std::move(error)));
return;
}
bool visible = platform_util::IsVisible(web_contents->GetNativeView());
if (!visible) {
if (retries > 0) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&DownloadsAcceptDangerFunction::PromptOrWait, this,
download_id, retries - 1),
base::Milliseconds(100));
return;
}
Respond(Error(download_extension_errors::kInvisibleContext));
return;
}
RecordApiFunctions(DownloadsFunctionName::kDownloadsFunctionAcceptDanger);
// DownloadDangerPrompt displays a modal dialog using native widgets that the
// user must either accept or cancel. It cannot be scripted.
DownloadDangerPrompt* prompt = DownloadDangerPrompt::Create(
download_item, web_contents,
base::BindOnce(&DownloadsAcceptDangerFunction::DangerPromptCallback, this,
download_id));
// DownloadDangerPrompt deletes itself
if (on_prompt_created_ && !on_prompt_created_->is_null()) {
std::move(*on_prompt_created_).Run(prompt);
on_prompt_created_ = nullptr;
}
// Function finishes in DangerPromptCallback().
}
void DownloadsAcceptDangerFunction::DangerPromptCallback(
int download_id,
DownloadDangerPrompt::Action action) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DownloadItem* download_item = GetDownload(
browser_context(), include_incognito_information(), download_id);
std::string error;
if (InvalidId(download_item, &error) ||
Fault(download_item->GetState() != DownloadItem::IN_PROGRESS,
download_extension_errors::kNotInProgress, &error)) {
Respond(Error(std::move(error)));
return;
}
switch (action) {
case DownloadDangerPrompt::ACCEPT:
download_item->ValidateDangerousDownload();
break;
case DownloadDangerPrompt::CANCEL:
download_item->Remove();
break;
case DownloadDangerPrompt::DISMISS:
break;
}
Respond(NoArguments());
}
DownloadsShowFunction::DownloadsShowFunction() = default;
DownloadsShowFunction::~DownloadsShowFunction() = default;
ExtensionFunction::ResponseAction DownloadsShowFunction::Run() {
std::optional<downloads::Show::Params> params =
downloads::Show::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
DownloadItem* download_item = GetDownload(
browser_context(), include_incognito_information(), params->download_id);
std::string error;
if (InvalidId(download_item, &error))
return RespondNow(Error(std::move(error)));
download_item->ShowDownloadInShell();
RecordApiFunctions(DownloadsFunctionName::kDownloadsFunctionShow);
return RespondNow(NoArguments());
}
DownloadsShowDefaultFolderFunction::DownloadsShowDefaultFolderFunction() =
default;
DownloadsShowDefaultFolderFunction::~DownloadsShowDefaultFolderFunction() =
default;
ExtensionFunction::ResponseAction DownloadsShowDefaultFolderFunction::Run() {
DownloadManager* manager = nullptr;
DownloadManager* incognito_manager = nullptr;
GetManagers(browser_context(), include_incognito_information(), &manager,
&incognito_manager);
platform_util::OpenItem(
Profile::FromBrowserContext(browser_context()),
DownloadPrefs::FromDownloadManager(manager)->DownloadPath(),
platform_util::OPEN_FOLDER, platform_util::OpenOperationCallback());
RecordApiFunctions(
DownloadsFunctionName::kDownloadsFunctionShowDefaultFolder);
return RespondNow(NoArguments());
}
DownloadsOpenFunction::OnPromptCreatedCallback*
DownloadsOpenFunction::on_prompt_created_cb_ = nullptr;
DownloadsOpenFunction::DownloadsOpenFunction() = default;
DownloadsOpenFunction::~DownloadsOpenFunction() = default;
ExtensionFunction::ResponseAction DownloadsOpenFunction::Run() {
std::optional<downloads::Open::Params> params =
downloads::Open::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
DownloadItem* download_item = GetDownload(
browser_context(), include_incognito_information(), params->download_id);
std::string error;
if (InvalidId(download_item, &error) ||
Fault(!user_gesture(), download_extension_errors::kUserGesture, &error) ||
Fault(download_item->GetState() != DownloadItem::COMPLETE,
download_extension_errors::kNotComplete, &error) ||
Fault(download_item->GetFileExternallyRemoved(),
download_extension_errors::kFileAlreadyDeleted, &error) ||
Fault(!extension()->permissions_data()->HasAPIPermission(
APIPermissionID::kDownloadsOpen),
download_extension_errors::kOpenPermission, &error)) {
return RespondNow(Error(std::move(error)));
}
WindowController* window_controller =
ChromeExtensionFunctionDetails(this).GetCurrentWindowController();
if (!window_controller) {
return RespondNow(Error(download_extension_errors::kInvisibleContext));
}
content::WebContents* active_contents = window_controller->GetActiveTab();
if (!active_contents) {
return RespondNow(Error(download_extension_errors::kInvisibleContext));
}
// Extensions with debugger permission could fake user gestures and should
// not be trusted.
if (GetSenderWebContents() &&
GetSenderWebContents()->HasRecentInteraction() &&
!extension()->permissions_data()->HasAPIPermission(
APIPermissionID::kDebugger)) {
download_item->OpenDownload();
return RespondNow(NoArguments());
}
// Prompt user for ack to open the download.
// TODO(qinmin): check if user prefers to open all download using the same
// extension, or check the recent user gesture on the originating webcontents
// to avoid showing the prompt.
DownloadOpenPrompt* download_open_prompt =
DownloadOpenPrompt::CreateDownloadOpenConfirmationDialog(
active_contents,
util::GetFixupExtensionNameForUIDisplay(extension()->name()),
download_item->GetFullPath(),
base::BindOnce(&DownloadsOpenFunction::OpenPromptDone, this,
params->download_id));
if (on_prompt_created_cb_)
std::move(*on_prompt_created_cb_).Run(download_open_prompt);
RecordApiFunctions(DownloadsFunctionName::kDownloadsFunctionOpen);
return RespondLater();
}
void DownloadsOpenFunction::OpenPromptDone(int download_id, bool accept) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
std::string error;
if (Fault(!accept, download_extension_errors::kOpenPermission, &error)) {
Respond(Error(std::move(error)));
return;
}
DownloadItem* download_item = GetDownload(
browser_context(), include_incognito_information(), download_id);
if (Fault(!download_item, download_extension_errors::kFileAlreadyDeleted,
&error)) {
Respond(Error(std::move(error)));
return;
}
download_item->OpenDownload();
Respond(NoArguments());
}
DownloadsSetShelfEnabledFunction::DownloadsSetShelfEnabledFunction() = default;
DownloadsSetShelfEnabledFunction::~DownloadsSetShelfEnabledFunction() = default;
ExtensionFunction::ResponseAction DownloadsSetShelfEnabledFunction::Run() {
std::optional<downloads::SetShelfEnabled::Params> params =
downloads::SetShelfEnabled::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
// TODO(devlin): Solve this with the feature system.
if (!extension()->permissions_data()->HasAPIPermission(
APIPermissionID::kDownloadsShelf)) {
return RespondNow(Error(download_extension_errors::kShelfPermission));
}
RecordApiFunctions(DownloadsFunctionName::kDownloadsFunctionSetShelfEnabled);
DownloadCoreService* service = nullptr;
DownloadCoreService* incognito_service = nullptr;
GetDownloadCoreServices(browser_context(), include_incognito_information(),
&service, &incognito_service);
MaybeSetUiEnabled(service, incognito_service, extension(), params->enabled);
for (WindowController* window : *WindowControllerList::GetInstance()) {
DownloadCoreService* current_service =
DownloadCoreServiceFactory::GetForBrowserContext(window->profile());
// The following code is to hide the download UI explicitly if the UI is
// set to disabled.
bool match_current_service =
(current_service == service) || (current_service == incognito_service);
if (!match_current_service || current_service->IsDownloadUiEnabled()) {
continue;
}
#if !BUILDFLAG(IS_CHROMEOS)
// Calling this API affects the download bubble as well, so extensions
// using this API is still compatible with the new download bubble. This
// API will eventually be deprecated (replaced by the SetUiOptions API
// below).
Browser* browser = window->GetBrowser();
if (browser->window()->GetDownloadBubbleUIController()) {
browser->window()->GetDownloadBubbleUIController()->HideDownloadUi();
}
#endif
}
if (params->enabled &&
((service && !service->IsDownloadUiEnabled()) ||
(incognito_service && !incognito_service->IsDownloadUiEnabled()))) {
return RespondNow(Error(download_extension_errors::kShelfDisabled));
}
return RespondNow(NoArguments());
}
DownloadsSetUiOptionsFunction::DownloadsSetUiOptionsFunction() = default;
DownloadsSetUiOptionsFunction::~DownloadsSetUiOptionsFunction() = default;
ExtensionFunction::ResponseAction DownloadsSetUiOptionsFunction::Run() {
std::optional<downloads::SetUiOptions::Params> params =
downloads::SetUiOptions::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
const downloads::UiOptions& options = params->options;
if (!extension()->permissions_data()->HasAPIPermission(
APIPermissionID::kDownloadsUi)) {
return RespondNow(Error(download_extension_errors::kUiPermission));
}
RecordApiFunctions(DownloadsFunctionName::kDownloadsFunctionSetUiOptions);
DownloadCoreService* service = nullptr;
DownloadCoreService* incognito_service = nullptr;
GetDownloadCoreServices(browser_context(), include_incognito_information(),
&service, &incognito_service);
MaybeSetUiEnabled(service, incognito_service, extension(), options.enabled);
for (WindowController* window : *WindowControllerList::GetInstance()) {
DownloadCoreService* current_service =
DownloadCoreServiceFactory::GetForBrowserContext(window->profile());
// The following code is to hide the download UI explicitly if the UI is
// set to disabled.
bool match_current_service =
(current_service == service) || (current_service == incognito_service);
if (!match_current_service || current_service->IsDownloadUiEnabled()) {
continue;
}
#if !BUILDFLAG(IS_CHROMEOS)
Browser* browser = window->GetBrowser();
if (browser->window()->GetDownloadBubbleUIController()) {
browser->window()->GetDownloadBubbleUIController()->HideDownloadUi();
}
#endif
}
if (options.enabled &&
((service && !service->IsDownloadUiEnabled()) ||
(incognito_service && !incognito_service->IsDownloadUiEnabled()))) {
return RespondNow(Error(download_extension_errors::kUiDisabled));
}
return RespondNow(NoArguments());
}
DownloadsGetFileIconFunction::DownloadsGetFileIconFunction()
: icon_extractor_(new DownloadFileIconExtractorImpl()) {}
DownloadsGetFileIconFunction::~DownloadsGetFileIconFunction() = default;
void DownloadsGetFileIconFunction::SetIconExtractorForTesting(
DownloadFileIconExtractor* extractor) {
DCHECK(extractor);
icon_extractor_.reset(extractor);
}
ExtensionFunction::ResponseAction DownloadsGetFileIconFunction::Run() {
std::optional<downloads::GetFileIcon::Params> params =
downloads::GetFileIcon::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
const std::optional<downloads::GetFileIconOptions>& options = params->options;
DownloadItem* download_item = GetDownload(
browser_context(), include_incognito_information(), params->download_id);
std::string error;
if (InvalidId(download_item, &error) ||
Fault(download_item->GetTargetFilePath().empty(),
download_extension_errors::kEmptyFile, &error))
return RespondNow(Error(std::move(error)));
int icon_size = kDefaultIconSize;
if (options && options->size) {
icon_size = *options->size;
if (icon_size != 16 && icon_size != 32) {
return RespondNow(Error("Invalid `size`. Must be either `16` or `32`."));
}
}
// In-progress downloads return the intermediate filename for GetFullPath()
// which doesn't have the final extension. Therefore a good file icon can't be
// found, so use GetTargetFilePath() instead.
DCHECK(icon_extractor_.get());
DCHECK(icon_size == 16 || icon_size == 32);
float scale = 1.0;
// We have a WeakPtr to the ExtensionFunctionDispatcher, so validate it
// before attempting to use it.
const ExtensionFunctionDispatcher* const extension_dispatcher = dispatcher();
EXTENSION_FUNCTION_VALIDATE(extension_dispatcher);
content::WebContents* web_contents =
extension_dispatcher->GetVisibleWebContents();
if (web_contents && web_contents->GetRenderWidgetHostView())
scale = web_contents->GetRenderWidgetHostView()->GetDeviceScaleFactor();
EXTENSION_FUNCTION_VALIDATE(icon_extractor_->ExtractIconURLForPath(
download_item->GetTargetFilePath(), scale,
IconLoaderSizeFromPixelSize(icon_size),
base::BindOnce(&DownloadsGetFileIconFunction::OnIconURLExtracted, this)));
return RespondLater();
}
void DownloadsGetFileIconFunction::OnIconURLExtracted(const std::string& url) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
std::string error;
if (Fault(url.empty(), download_extension_errors::kIconNotFound, &error)) {
Respond(Error(std::move(error)));
return;
}
RecordApiFunctions(DownloadsFunctionName::kDownloadsFunctionGetFileIcon);
Respond(WithArguments(url));
}
ExtensionDownloadsEventRouter::ExtensionDownloadsEventRouter(
Profile* profile,
DownloadManager* manager)
: profile_(profile), notifier_(manager, this) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(profile_);
extension_registry_observation_.Observe(ExtensionRegistry::Get(profile_));
EventRouter* router = EventRouter::Get(profile_);
if (router)
router->RegisterObserver(this,
downloads::OnDeterminingFilename::kEventName);
}
ExtensionDownloadsEventRouter::~ExtensionDownloadsEventRouter() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
EventRouter* router = EventRouter::Get(profile_);
if (router)
router->UnregisterObserver(this);
}
void ExtensionDownloadsEventRouter::
SetDetermineFilenameTimeoutSecondsForTesting(int s) {
ExtensionDownloadsEventRouterData::
SetDetermineFilenameTimeoutSecondsForTesting(s);
}
void ExtensionDownloadsEventRouter::SetUiEnabled(const Extension* extension,
bool enabled) {
auto iter = ui_disabling_extensions_.find(extension);
if (iter == ui_disabling_extensions_.end()) {
if (!enabled)
ui_disabling_extensions_.insert(extension);
} else if (enabled) {
ui_disabling_extensions_.erase(extension);
}
}
bool ExtensionDownloadsEventRouter::IsUiEnabled() const {
return ui_disabling_extensions_.empty();
}
// The method by which extensions hook into the filename determination process
// is based on the method by which the omnibox API allows extensions to hook
// into the omnibox autocompletion process. Extensions that wish to play a part
// in the filename determination process call
// chrome.downloads.onDeterminingFilename.addListener, which adds an
// EventListener object to ExtensionEventRouter::listeners().
//
// When a download's filename is being determined, DownloadTargetDeterminer (via
// ChromeDownloadManagerDelegate (CDMD) ::NotifyExtensions()) passes a callback
// to ExtensionDownloadsEventRouter::OnDeterminingFilename (ODF), which stores
// the callback in the item's ExtensionDownloadsEventRouterData (EDERD) along
// with all of the extension IDs that are listening for onDeterminingFilename
// events. ODF dispatches chrome.downloads.onDeterminingFilename.
//
// When the extension's event handler calls |suggestCallback|,
// downloads_custom_bindings.js calls
// DownloadsInternalDetermineFilenameFunction::RunAsync, which calls
// EDER::DetermineFilename, which notifies the item's EDERD.
//
// When the last extension's event handler returns, EDERD invokes the callback
// that CDMD passed to ODF, allowing DownloadTargetDeterminer to continue the
// filename determination process. If multiple extensions wish to override the
// filename, then the extension that was last installed wins.
void ExtensionDownloadsEventRouter::OnDeterminingFilename(
DownloadItem* item,
const base::FilePath& suggested_path,
FilenameChangedCallback filename_changed_callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
ExtensionDownloadsEventRouterData* data =
ExtensionDownloadsEventRouterData::Get(item);
if (!data) {
std::move(filename_changed_callback)
.Run({}, DownloadPathReservationTracker::UNIQUIFY);
return;
}
data->BeginFilenameDetermination(std::move(filename_changed_callback));
bool any_determiners = false;
base::Value::Dict json = DownloadItemToJSON(item, profile_);
json.Set(kFilenameKey, suggested_path.LossyDisplayName());
DispatchEvent(events::DOWNLOADS_ON_DETERMINING_FILENAME,
downloads::OnDeterminingFilename::kEventName, false,
base::BindRepeating(&OnDeterminingFilenameWillDispatchCallback,
&any_determiners, data),
base::Value(std::move(json)));
if (!any_determiners) {
data->CallFilenameCallback();
data->ClearPendingDeterminers();
data->ResetCreatorSuggestion();
}
}
void ExtensionDownloadsEventRouter::DetermineFilenameInternal(
const base::FilePath& filename,
downloads::FilenameConflictAction conflict_action,
const ExtensionId& suggesting_extension_id,
const base::Time& suggesting_install_time,
const ExtensionId& incumbent_extension_id,
const base::Time& incumbent_install_time,
ExtensionId* winner_extension_id,
base::FilePath* determined_filename,
downloads::FilenameConflictAction* determined_conflict_action,
WarningSet* warnings) {
DCHECK(!filename.empty() ||
(conflict_action != downloads::FilenameConflictAction::kUniquify));
DCHECK(!suggesting_extension_id.empty());
if (incumbent_extension_id.empty()) {
*winner_extension_id = suggesting_extension_id;
*determined_filename = filename;
*determined_conflict_action = conflict_action;
return;
}
if (suggesting_install_time < incumbent_install_time) {
*winner_extension_id = incumbent_extension_id;
warnings->insert(Warning::CreateDownloadFilenameConflictWarning(
suggesting_extension_id, incumbent_extension_id, filename,
*determined_filename));
return;
}
*winner_extension_id = suggesting_extension_id;
warnings->insert(Warning::CreateDownloadFilenameConflictWarning(
incumbent_extension_id, suggesting_extension_id, *determined_filename,
filename));
*determined_filename = filename;
*determined_conflict_action = conflict_action;
}
bool ExtensionDownloadsEventRouter::DetermineFilename(
content::BrowserContext* browser_context,
bool include_incognito,
const ExtensionId& ext_id,
int download_id,
const base::FilePath& const_filename,
downloads::FilenameConflictAction conflict_action,
std::string* error) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
RecordApiFunctions(
DownloadsFunctionName::kDownloadsFunctionDetermineFilename);
DownloadItem* item =
GetDownload(browser_context, include_incognito, download_id);
ExtensionDownloadsEventRouterData* data =
item ? ExtensionDownloadsEventRouterData::Get(item) : nullptr;
// maxListeners=1 in downloads.idl and suggestCallback in
// downloads_custom_bindings.js should prevent duplicate DeterminerCallback
// calls from the same renderer, but an extension may have more than one
// renderer, so don't DCHECK(!reported).
if (InvalidId(item, error) ||
Fault(item->GetState() != DownloadItem::IN_PROGRESS,
download_extension_errors::kNotInProgress, error) ||
Fault(!data, download_extension_errors::kUnexpectedDeterminer, error) ||
Fault(data->DeterminerAlreadyReported(ext_id),
download_extension_errors::kTooManyListeners, error))
return false;
base::FilePath::StringType filename_str(const_filename.value());
// Allow windows-style directory separators on all platforms.
std::replace(filename_str.begin(), filename_str.end(),
FILE_PATH_LITERAL('\\'), FILE_PATH_LITERAL('/'));
base::FilePath filename(filename_str);
bool valid_filename = net::IsSafePortableRelativePath(filename);
filename =
(valid_filename ? filename.NormalizePathSeparators() : base::FilePath());
// If the invalid filename check is moved to before DeterminerCallback(), then
// it will block forever waiting for this ext_id to report.
if (Fault(!data->DeterminerCallback(browser_context, ext_id, filename,
conflict_action),
download_extension_errors::kUnexpectedDeterminer, error) ||
Fault((!const_filename.empty() && !valid_filename),
download_extension_errors::kInvalidFilename, error))
return false;
return true;
}
void ExtensionDownloadsEventRouter::OnListenerRemoved(
const EventListenerInfo& details) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DownloadManager* manager = notifier_.GetManager();
if (!manager)
return;
bool determiner_removed =
(details.event_name == downloads::OnDeterminingFilename::kEventName);
EventRouter* router = EventRouter::Get(profile_);
bool any_listeners =
router->HasEventListener(downloads::OnChanged::kEventName) ||
router->HasEventListener(downloads::OnDeterminingFilename::kEventName);
if (!determiner_removed && any_listeners)
return;
DownloadManager::DownloadVector items;
manager->GetAllDownloads(&items);
for (DownloadManager::DownloadVector::const_iterator iter = items.begin();
iter != items.end(); ++iter) {
ExtensionDownloadsEventRouterData* data =
ExtensionDownloadsEventRouterData::Get(*iter);
if (!data)
continue;
if (determiner_removed) {
// Notify any items that may be waiting for callbacks from this
// extension/determiner. This will almost always be a no-op, however, it
// is possible for an extension renderer to be unloaded while a download
// item is waiting for a determiner. In that case, the download item
// should proceed.
data->DeterminerRemoved(details.extension_id);
}
if (!any_listeners && data->creator_suggested_filename().empty()) {
ExtensionDownloadsEventRouterData::Remove(*iter);
}
}
}
// That's all the methods that have to do with filename determination. The rest
// have to do with the other, less special events.
void ExtensionDownloadsEventRouter::OnDownloadCreated(
DownloadManager* manager,
DownloadItem* download_item) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!ShouldExport(*download_item))
return;
EventRouter* router = EventRouter::Get(profile_);
// Avoid allocating a bunch of memory in DownloadItemToJSON if it isn't going
// to be used.
if (!router || (!router->HasEventListener(downloads::OnCreated::kEventName) &&
!router->HasEventListener(downloads::OnChanged::kEventName) &&
!router->HasEventListener(
downloads::OnDeterminingFilename::kEventName))) {
return;
}
// download_item->GetFileExternallyRemoved() should always return false for
// unfinished download.
base::Value::Dict json_item = DownloadItemToJSON(download_item, profile_);
DispatchEvent(events::DOWNLOADS_ON_CREATED, downloads::OnCreated::kEventName,
true, Event::WillDispatchCallback(),
base::Value(json_item.Clone()));
if (!ExtensionDownloadsEventRouterData::Get(download_item) &&
(router->HasEventListener(downloads::OnChanged::kEventName) ||
router->HasEventListener(
downloads::OnDeterminingFilename::kEventName))) {
new ExtensionDownloadsEventRouterData(
download_item, download_item->GetState() == DownloadItem::COMPLETE
? base::Value::Dict()
: std::move(json_item));
}
}
void ExtensionDownloadsEventRouter::OnDownloadUpdated(
DownloadManager* manager,
DownloadItem* download_item) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
EventRouter* router = EventRouter::Get(profile_);
ExtensionDownloadsEventRouterData* data =
ExtensionDownloadsEventRouterData::Get(download_item);
if (!ShouldExport(*download_item) ||
!router->HasEventListener(downloads::OnChanged::kEventName)) {
return;
}
if (!data) {
// The download_item probably transitioned from temporary to not temporary,
// or else an event listener was added.
data = new ExtensionDownloadsEventRouterData(download_item,
base::Value::Dict());
}
base::Value::Dict new_json;
base::Value::Dict delta;
delta.Set(kIdKey, static_cast<int>(download_item->GetId()));
bool changed = false;
// For completed downloads, update can only happen when file is removed.
if (data->is_download_completed()) {
if (data->is_completed_download_deleted() !=
download_item->GetFileExternallyRemoved()) {
DCHECK(!data->is_completed_download_deleted());
DCHECK(download_item->GetFileExternallyRemoved());
std::string exists = kExistsKey;
delta.SetByDottedPath(exists + ".current", false);
delta.SetByDottedPath(exists + ".previous", true);
changed = true;
}
} else {
new_json = DownloadItemToJSON(download_item, profile_);
std::set<std::string> new_fields;
// For each field in the new json representation of the download_item except
// the bytesReceived field, if the field has changed from the previous old
// json, set the differences in the |delta| object and remember that
// something significant changed.
for (auto kv : new_json) {
new_fields.insert(kv.first);
if (IsDownloadDeltaField(kv.first)) {
const base::Value* old_value = data->json().Find(kv.first);
if (!old_value || kv.second != *old_value) {
delta.SetByDottedPath(kv.first + ".current", kv.second.Clone());
if (old_value) {
delta.SetByDottedPath(kv.first + ".previous", old_value->Clone());
}
changed = true;
}
}
}
// If a field was in the previous json but is not in the new json, set the
// difference in |delta|.
for (auto kv : data->json()) {
if ((new_fields.find(kv.first) == new_fields.end()) &&
IsDownloadDeltaField(kv.first)) {
// estimatedEndTime disappears after completion, but bytesReceived
// stays.
delta.SetByDottedPath(kv.first + ".previous", kv.second.Clone());
changed = true;
}
}
}
data->set_is_download_completed(download_item->GetState() ==
DownloadItem::COMPLETE);
// download_item->GetFileExternallyRemoved() should always return false for
// unfinished download.
data->set_is_completed_download_deleted(
download_item->GetFileExternallyRemoved());
data->set_json(std::move(new_json));
// Update the OnChangedStat and dispatch the event if something significant
// changed. Replace the stored json with the new json.
data->OnItemUpdated();
if (changed) {
DispatchEvent(events::DOWNLOADS_ON_CHANGED,
downloads::OnChanged::kEventName, true,
Event::WillDispatchCallback(), base::Value(std::move(delta)));
data->OnChangedFired();
}
}
void ExtensionDownloadsEventRouter::OnDownloadRemoved(
DownloadManager* manager,
DownloadItem* download_item) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!ShouldExport(*download_item))
return;
DispatchEvent(events::DOWNLOADS_ON_ERASED, downloads::OnErased::kEventName,
true, Event::WillDispatchCallback(),
base::Value(static_cast<int>(download_item->GetId())));
}
void ExtensionDownloadsEventRouter::DispatchEvent(
events::HistogramValue histogram_value,
const std::string& event_name,
bool include_incognito,
Event::WillDispatchCallback will_dispatch_callback,
base::Value arg) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!EventRouter::Get(profile_))
return;
base::Value::List args;
args.Append(std::move(arg));
// The downloads system wants to share on-record events with off-record
// extension renderers even in incognito_split_mode because that's how
// chrome://downloads works. The "restrict_to_profile" mechanism does not
// anticipate this, so it does not automatically prevent sharing off-record
// events with on-record extension renderers.
// TODO(lazyboy): When |restrict_to_browser_context| is nullptr, this will
// broadcast events to unrelated profiles, not just incognito. Fix this
// by introducing "include incognito" option to Event constructor.
// https://crbug.com/726022.
Profile* restrict_to_browser_context =
(include_incognito && !profile_->IsOffTheRecord()) ? nullptr
: profile_.get();
auto event =
std::make_unique<Event>(histogram_value, event_name, std::move(args),
restrict_to_browser_context);
event->will_dispatch_callback = std::move(will_dispatch_callback);
EventRouter::Get(profile_)->BroadcastEvent(std::move(event));
}
void ExtensionDownloadsEventRouter::OnExtensionUnloaded(
content::BrowserContext* browser_context,
const Extension* extension,
UnloadedExtensionReason reason) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
auto iter = ui_disabling_extensions_.find(extension);
if (iter != ui_disabling_extensions_.end())
ui_disabling_extensions_.erase(iter);
}
void ExtensionDownloadsEventRouter::CheckForHistoryFilesRemoval() {
static const int kFileExistenceRateLimitSeconds = 10;
DownloadManager* manager = notifier_.GetManager();
if (!manager)
return;
base::Time now(base::Time::Now());
int delta = now.ToTimeT() - last_checked_removal_.ToTimeT();
if (delta <= kFileExistenceRateLimitSeconds)
return;
last_checked_removal_ = now;
manager->CheckForHistoryFilesRemoval();
}
} // namespace extensions