// Copyright 2020 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/webshare/share_service_impl.h"
#include <algorithm>
#include <memory>
#include <string_view>
#include "base/feature_list.h"
#include "base/files/safe_base_name.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_util.h"
#include "build/build_config.h"
#include "chrome/browser/bad_message.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/safe_browsing/safe_browsing_service.h"
#include "chrome/common/chrome_features.h"
#include "components/safe_browsing/content/common/file_type_policies.h"
#include "components/safe_browsing/core/browser/db/database_manager.h"
#include "content/public/browser/web_contents.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "third_party/blink/public/mojom/permissions_policy/permissions_policy_feature.mojom.h"
#if BUILDFLAG(IS_MAC)
#include "chrome/browser/webshare/mac/sharing_service_operation.h"
#endif
#if BUILDFLAG(IS_WIN)
#include "chrome/browser/webshare/win/share_operation.h"
#endif
// IsDangerousFilename() and IsDangerousMimeType() should be kept in sync with
// //third_party/blink/renderer/modules/webshare/FILE_TYPES.md
// //components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/ShareServiceImpl.java
ShareServiceImpl::ShareServiceImpl(
content::RenderFrameHost& render_frame_host,
mojo::PendingReceiver<blink::mojom::ShareService> receiver)
: content::DocumentService<blink::mojom::ShareService>(render_frame_host,
std::move(receiver))
#if BUILDFLAG(IS_CHROMEOS)
,
sharesheet_client_(
content::WebContents::FromRenderFrameHost(&render_frame_host))
#endif
{
DCHECK(base::FeatureList::IsEnabled(features::kWebShare));
}
ShareServiceImpl::~ShareServiceImpl() = default;
// static
void ShareServiceImpl::Create(
content::RenderFrameHost* render_frame_host,
mojo::PendingReceiver<blink::mojom::ShareService> receiver) {
CHECK(render_frame_host);
if (render_frame_host->IsNestedWithinFencedFrame()) {
// The renderer should have checked and disallowed the request for fenced
// frames in NavigatorShare and thrown a DOMException. Ignore the request
// and mark it as bad if it didn't happen for some reason.
bad_message::ReceivedBadMessage(render_frame_host->GetProcess(),
bad_message::SSI_CREATE_FENCED_FRAME);
return;
}
new ShareServiceImpl(*render_frame_host, std::move(receiver));
}
// static
bool ShareServiceImpl::IsDangerousFilename(const base::FilePath& path) {
constexpr const base::FilePath::CharType* kPermitted[] = {
FILE_PATH_LITERAL(".avif"), // image/avif
FILE_PATH_LITERAL(".bmp"), // image/bmp / image/x-ms-bmp
FILE_PATH_LITERAL(".css"), // text/css
FILE_PATH_LITERAL(".csv"), // text/csv / text/comma-separated-values
FILE_PATH_LITERAL(".ehtml"), // text/html
FILE_PATH_LITERAL(".flac"), // audio/flac
FILE_PATH_LITERAL(".gif"), // image/gif
FILE_PATH_LITERAL(".htm"), // text/html
FILE_PATH_LITERAL(".html"), // text/html
FILE_PATH_LITERAL(".ico"), // image/x-icon
FILE_PATH_LITERAL(".jfif"), // image/jpeg
FILE_PATH_LITERAL(".jpeg"), // image/jpeg
FILE_PATH_LITERAL(".jpg"), // image/jpeg
FILE_PATH_LITERAL(".m4a"), // audio/x-m4a
FILE_PATH_LITERAL(".m4v"), // video/mp4
FILE_PATH_LITERAL(".mp3"), // audio/mpeg audio/mp3
FILE_PATH_LITERAL(".mp4"), // video/mp4
FILE_PATH_LITERAL(".mpeg"), // video/mpeg
FILE_PATH_LITERAL(".mpg"), // video/mpeg
FILE_PATH_LITERAL(".oga"), // audio/ogg
FILE_PATH_LITERAL(".ogg"), // audio/ogg
FILE_PATH_LITERAL(".ogm"), // video/ogg
FILE_PATH_LITERAL(".ogv"), // video/ogg
FILE_PATH_LITERAL(".opus"), // audio/ogg
FILE_PATH_LITERAL(".pdf"), // application/pdf
FILE_PATH_LITERAL(".pjp"), // image/jpeg
FILE_PATH_LITERAL(".pjpeg"), // image/jpeg
FILE_PATH_LITERAL(".png"), // image/png
FILE_PATH_LITERAL(".shtm"), // text/html
FILE_PATH_LITERAL(".shtml"), // text/html
FILE_PATH_LITERAL(".svg"), // image/svg+xml
FILE_PATH_LITERAL(".svgz"), // image/svg+xml
FILE_PATH_LITERAL(".text"), // text/plain
FILE_PATH_LITERAL(".tif"), // image/tiff
FILE_PATH_LITERAL(".tiff"), // image/tiff
FILE_PATH_LITERAL(".txt"), // text/plain
FILE_PATH_LITERAL(".wav"), // audio/wav
FILE_PATH_LITERAL(".weba"), // audio/webm
FILE_PATH_LITERAL(".webm"), // video/webm
FILE_PATH_LITERAL(".webp"), // image/webp
FILE_PATH_LITERAL(".xbm"), // image/x-xbitmap
};
for (const base::FilePath::CharType* permitted : kPermitted) {
if (base::EndsWith(path.value(), permitted,
base::CompareCase::INSENSITIVE_ASCII))
return false;
}
return true;
}
// static
bool ShareServiceImpl::IsDangerousMimeType(std::string_view content_type) {
constexpr std::array<const char*, 28> kPermitted = {
"application/pdf",
"audio/flac",
"audio/mp3",
"audio/mpeg",
"audio/ogg",
"audio/wav",
"audio/webm",
"audio/x-m4a",
"image/avif",
"image/bmp",
"image/gif",
"image/jpeg",
"image/png",
"image/svg+xml",
"image/tiff",
"image/webp",
"image/x-icon",
"image/x-ms-bmp",
"image/x-xbitmap",
"text/comma-separated-values",
"text/css",
"text/csv",
"text/html",
"text/plain",
"video/mp4",
"video/mpeg",
"video/ogg",
"video/webm",
};
for (const char* permitted : kPermitted) {
if (content_type == permitted)
return false;
}
return true;
}
void ShareServiceImpl::Share(const std::string& title,
const std::string& text,
const GURL& share_url,
std::vector<blink::mojom::SharedFilePtr> files,
ShareCallback callback) {
UMA_HISTOGRAM_ENUMERATION(kWebShareApiCountMetric, WebShareMethod::kShare);
if (!render_frame_host().IsFeatureEnabled(
blink::mojom::PermissionsPolicyFeature::kWebShare)) {
std::move(callback).Run(blink::mojom::ShareError::PERMISSION_DENIED);
ReportBadMessageAndDeleteThis("Feature policy blocks Web Share");
return;
}
content::WebContents* const web_contents =
content::WebContents::FromRenderFrameHost(&render_frame_host());
if (!web_contents) {
VLOG(1) << "Cannot share after navigating away";
std::move(callback).Run(blink::mojom::ShareError::PERMISSION_DENIED);
return;
}
if (files.size() > kMaxSharedFileCount) {
VLOG(1) << "Share too large: " << files.size() << " files";
std::move(callback).Run(blink::mojom::ShareError::PERMISSION_DENIED);
return;
}
bool should_check_url = false;
for (auto& file : files) {
if (!file || !file->blob || !file->blob->blob) {
mojo::ReportBadMessage("Invalid file to share()");
return;
}
const base::FilePath& path = file->name.path();
if (IsDangerousFilename(path) ||
IsDangerousMimeType(file->blob->content_type)) {
VLOG(1) << "File type is not supported: " << path << " has mime type "
<< file->blob->content_type;
std::move(callback).Run(blink::mojom::ShareError::PERMISSION_DENIED);
return;
}
// Check if at least one file is marked by the download protection service
// to send a ping to check this file type.
if (!should_check_url &&
safe_browsing::FileTypePolicies::GetInstance()->IsCheckedBinaryFile(
path)) {
should_check_url = true;
}
// In the case where the original blob handle was to a native file (of
// unknown size), the serialized data does not contain an accurate file
// size. To handle this, the comparison against kMaxSharedFileBytes should
// be done by the platform-specific implementations as part of processing
// the blobs.
}
DCHECK(!safe_browsing_request_);
if (should_check_url && g_browser_process->safe_browsing_service()) {
safe_browsing_request_.emplace(
g_browser_process->safe_browsing_service()->database_manager(),
web_contents->GetLastCommittedURL(),
base::BindOnce(&ShareServiceImpl::OnSafeBrowsingResultReceived,
weak_factory_.GetWeakPtr(), title, text, share_url,
std::move(files), std::move(callback)));
return;
}
OnSafeBrowsingResultReceived(title, text, share_url, std::move(files),
std::move(callback),
/*is_url_safe=*/true);
}
void ShareServiceImpl::OnSafeBrowsingResultReceived(
const std::string& title,
const std::string& text,
const GURL& share_url,
std::vector<blink::mojom::SharedFilePtr> files,
ShareCallback callback,
bool is_url_safe) {
safe_browsing_request_.reset();
content::WebContents* const web_contents =
content::WebContents::FromRenderFrameHost(&render_frame_host());
if (!web_contents) {
VLOG(1) << "Cannot share after navigating away";
std::move(callback).Run(blink::mojom::ShareError::PERMISSION_DENIED);
return;
}
if (!is_url_safe) {
VLOG(1) << "File not safe to share from this website";
std::move(callback).Run(blink::mojom::ShareError::PERMISSION_DENIED);
return;
}
#if BUILDFLAG(IS_CHROMEOS)
sharesheet_client_.Share(title, text, share_url, std::move(files),
std::move(callback));
#elif BUILDFLAG(IS_MAC)
auto sharing_service_operation =
std::make_unique<webshare::SharingServiceOperation>(
title, text, share_url, std::move(files), web_contents);
// grab a safe reference to |sharing_service_operation| before calling move on
// it.
webshare::SharingServiceOperation* sharing_service_operation_ptr =
sharing_service_operation.get();
// Wrap the |callback| in a binding that owns the |sharing_service_operation|
// so its lifetime can be preserved till its done.
sharing_service_operation_ptr->Share(base::BindOnce(
[](std::unique_ptr<webshare::SharingServiceOperation>
sharing_service_operation,
ShareCallback callback,
blink::mojom::ShareError result) { std::move(callback).Run(result); },
std::move(sharing_service_operation), std::move(callback)));
#elif BUILDFLAG(IS_WIN)
auto share_operation = std::make_unique<webshare::ShareOperation>(
title, text, share_url, std::move(files), web_contents);
auto* const share_operation_ptr = share_operation.get();
share_operation_ptr->Run(base::BindOnce(
[](std::unique_ptr<webshare::ShareOperation> share_operation,
ShareCallback callback,
blink::mojom::ShareError result) { std::move(callback).Run(result); },
std::move(share_operation), std::move(callback)));
#else
NOTREACHED_IN_MIGRATION();
std::move(callback).Run(blink::mojom::ShareError::INTERNAL_ERROR);
#endif
}