// 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/win/share_operation.h"
#include <shlobj.h>
#include <windows.applicationmodel.datatransfer.h>
#include <windows.foundation.collections.h>
#include <windows.foundation.h>
#include <windows.storage.h>
#include <windows.storage.streams.h>
#include <wininet.h>
#include <wrl/client.h>
#include <wrl/event.h>
#include <utility>
#include "base/functional/bind.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/core_winrt_util.h"
#include "base/win/post_async_results.h"
#include "base/win/scoped_hstring.h"
#include "base/win/vector.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/webshare/share_service_impl.h"
#include "chrome/browser/webshare/win/show_share_ui_for_window_operation.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "net/base/net_errors.h"
#include "storage/browser/blob/blob_data_handle.h"
#include "storage/browser/blob/blob_storage_context.h"
#include "storage/browser/file_system/file_stream_writer.h"
#include "storage/browser/file_system/file_writer_delegate.h"
#include "storage/common/file_system/file_system_mount_option.h"
#include "ui/base/win/internal_constants.h"
#include "ui/views/win/hwnd_util.h"
#include "url/gurl.h"
using ABI::Windows::ApplicationModel::DataTransfer::IDataPackage;
using ABI::Windows::ApplicationModel::DataTransfer::IDataPackage2;
using ABI::Windows::ApplicationModel::DataTransfer::IDataPackagePropertySet;
using ABI::Windows::ApplicationModel::DataTransfer::IDataRequest;
using ABI::Windows::ApplicationModel::DataTransfer::IDataRequestDeferral;
using ABI::Windows::ApplicationModel::DataTransfer::IDataRequestedEventArgs;
using ABI::Windows::Foundation::AsyncStatus;
using ABI::Windows::Foundation::IAsyncOperation;
using ABI::Windows::Foundation::IAsyncOperationCompletedHandler;
using ABI::Windows::Foundation::IClosable;
using ABI::Windows::Foundation::IUriRuntimeClass;
using ABI::Windows::Foundation::IUriRuntimeClassFactory;
using ABI::Windows::Storage::IStorageFile;
using ABI::Windows::Storage::IStorageFileStatics;
using ABI::Windows::Storage::IStorageItem;
using ABI::Windows::Storage::IStreamedFileDataRequestedHandler;
using ABI::Windows::Storage::StorageFile;
using ABI::Windows::Storage::Streams::IDataWriter;
using ABI::Windows::Storage::Streams::IDataWriterFactory;
using ABI::Windows::Storage::Streams::IOutputStream;
using Microsoft::WRL::Callback;
using Microsoft::WRL::ComPtr;
using Microsoft::WRL::Make;
namespace ABI::Windows::Foundation::Collections {
// Define template specializations for the types used. These uuids were randomly
// generated.
template <>
struct __declspec(uuid("CBE31E85-DEC8-4227-987F-9C63D6AA1A2E"))
IObservableVector<IStorageItem*> : IObservableVector_impl<IStorageItem*> {};
template <>
struct __declspec(uuid("30BE4864-5EE5-4111-916E-15126649F3C9"))
VectorChangedEventHandler<IStorageItem*>
: VectorChangedEventHandler_impl<IStorageItem*> {};
} // namespace ABI::Windows::Foundation::Collections
namespace webshare {
namespace {
uint64_t g_max_file_bytes = kMaxSharedFileBytes;
decltype(
&base::win::RoGetActivationFactory) g_ro_get_activation_factory_function =
&base::win::RoGetActivationFactory;
template <typename InterfaceType, wchar_t const* runtime_class_id>
HRESULT GetActivationFactory(InterfaceType** factory) {
auto class_id_hstring = base::win::ScopedHString::Create(runtime_class_id);
if (!class_id_hstring.is_valid())
return E_FAIL;
return g_ro_get_activation_factory_function(class_id_hstring.get(),
IID_PPV_ARGS(factory));
}
// Implements FileStreamWriter for an IDataWriter.
class DataWriterFileStreamWriter final : public storage::FileStreamWriter {
public:
explicit DataWriterFileStreamWriter(
ComPtr<IDataWriter> data_writer,
scoped_refptr<base::RefCountedData<uint64_t>> file_bytes_shared)
: data_writer_(data_writer), file_bytes_shared_(file_bytes_shared) {}
int Cancel(net::CompletionOnceCallback callback) final {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
// If there is no async operation in progress, Cancel() should
// return net::ERR_UNEXPECTED per file_stream_header.h
if (!flush_operation_ && !write_operation_)
return net::ERR_UNEXPECTED;
if (flush_operation_) {
flush_callback_.Reset();
ComPtr<IAsyncInfo> async_info;
auto hr = flush_operation_.As(&async_info);
if (FAILED(hr))
return net::ERR_UNEXPECTED;
hr = async_info->Cancel();
if (FAILED(hr))
return net::ERR_UNEXPECTED;
flush_operation_.Reset();
}
if (write_operation_) {
write_callback_.Reset();
ComPtr<IAsyncInfo> async_info;
auto hr = write_operation_.As(&async_info);
if (FAILED(hr))
return net::ERR_UNEXPECTED;
hr = async_info->Cancel();
if (FAILED(hr))
return net::ERR_UNEXPECTED;
write_operation_.Reset();
}
return net::OK;
}
int Flush(storage::FlushMode /*flush_mode*/,
net::CompletionOnceCallback callback) final {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
DCHECK(flush_callback_.is_null());
DCHECK_EQ(flush_operation_, nullptr);
DCHECK(write_callback_.is_null());
DCHECK_EQ(write_operation_, nullptr);
auto hr = data_writer_->FlushAsync(&flush_operation_);
if (FAILED(hr))
return net::ERR_UNEXPECTED;
flush_callback_ = std::move(callback);
base::win::PostAsyncHandlers(
flush_operation_.Get(),
base::BindOnce(&DataWriterFileStreamWriter::OnFlushCompleted,
weak_factory_.GetWeakPtr()));
return net::ERR_IO_PENDING;
}
int Write(net::IOBuffer* buf,
int buf_len,
net::CompletionOnceCallback callback) final {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
DCHECK(flush_callback_.is_null());
DCHECK_EQ(flush_operation_, nullptr);
DCHECK(write_callback_.is_null());
DCHECK_EQ(write_operation_, nullptr);
// Before processing the Write request, increment the total number of file
// bytes shared as part of the overall Share operation this belongs to, and
// if it has exceeded the maximum allowed, abort writing to the streamed
// file.
file_bytes_shared_->data += buf_len;
if (file_bytes_shared_->data > g_max_file_bytes)
return net::ERR_UNEXPECTED;
auto hr =
data_writer_->WriteBytes(buf_len, reinterpret_cast<BYTE*>(buf->data()));
if (FAILED(hr))
return net::ERR_UNEXPECTED;
hr = data_writer_->StoreAsync(&write_operation_);
if (FAILED(hr))
return net::ERR_UNEXPECTED;
write_callback_ = std::move(callback);
base::win::PostAsyncHandlers(
write_operation_.Get(),
base::BindOnce(&DataWriterFileStreamWriter::OnWriteCompleted,
weak_factory_.GetWeakPtr()));
return net::ERR_IO_PENDING;
}
private:
void OnFlushCompleted(boolean operation_result) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
DCHECK(!flush_callback_.is_null());
DCHECK_NE(flush_operation_, nullptr);
DCHECK(write_callback_.is_null());
DCHECK_EQ(write_operation_, nullptr);
flush_operation_.Reset();
int result = operation_result == TRUE ? net::OK : net::ERR_UNEXPECTED;
std::move(flush_callback_).Run(result);
}
void OnWriteCompleted(UINT32 operation_result) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
DCHECK(flush_callback_.is_null());
DCHECK_EQ(flush_operation_, nullptr);
DCHECK(!write_callback_.is_null());
DCHECK_NE(write_operation_, nullptr);
write_operation_.Reset();
std::move(write_callback_).Run(operation_result);
}
ComPtr<IDataWriter> data_writer_;
scoped_refptr<base::RefCountedData<uint64_t>> file_bytes_shared_;
net::CompletionOnceCallback flush_callback_;
ComPtr<IAsyncOperation<bool>> flush_operation_;
net::CompletionOnceCallback write_callback_;
ComPtr<IAsyncOperation<UINT32>> write_operation_;
base::WeakPtrFactory<DataWriterFileStreamWriter> weak_factory_{this};
};
// Represents an ongoing operation of writing to an IOutputStream.
class OutputStreamWriteOperation
: public base::RefCounted<OutputStreamWriteOperation> {
public:
OutputStreamWriteOperation(
content::BrowserContext::BlobContextGetter blob_context_getter,
scoped_refptr<base::RefCountedData<uint64_t>> file_bytes_shared,
std::string uuid)
: blob_context_getter_(blob_context_getter),
file_bytes_shared_(file_bytes_shared),
uuid_(uuid) {}
// Begins the write operation on the |stream|, maintaining a reference to the
// |stream| until the operation is completed, at which point it will be closed
// (if possible) and the |on_complete| callback will be invoked. The caller
// is still responsible for the lifetime of this object, but not of the
// |stream|.
void WriteStream(IOutputStream* stream,
base::OnceCallback<void()> on_complete) {
stream_ = ComPtr<IOutputStream>(stream);
on_complete_ = std::move(on_complete);
if (!content::GetIOThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&OutputStreamWriteOperation::WriteStreamOnIOThread,
weak_factory_.GetWeakPtr())))
Complete();
}
private:
friend class base::RefCounted<OutputStreamWriteOperation>;
~OutputStreamWriteOperation() = default;
void WriteStreamOnIOThread() {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
storage::BlobStorageContext* blob_storage_context =
blob_context_getter_.Run().get();
if (!blob_storage_context) {
Complete();
return;
}
blob_handle_ = blob_storage_context->GetBlobDataFromUUID(uuid_);
ComPtr<IDataWriterFactory> data_writer_factory;
auto hr =
GetActivationFactory<IDataWriterFactory,
RuntimeClass_Windows_Storage_Streams_DataWriter>(
&data_writer_factory);
if (FAILED(hr)) {
Complete();
return;
}
ComPtr<IDataWriter> data_writer;
hr = data_writer_factory->CreateDataWriter(stream_.Get(), &data_writer);
if (FAILED(hr)) {
Complete();
return;
}
writer_delegate_ = std::make_unique<storage::FileWriterDelegate>(
std::make_unique<DataWriterFileStreamWriter>(std::move(data_writer),
file_bytes_shared_),
storage::FlushPolicy::FLUSH_ON_COMPLETION);
writer_delegate_->Start(
blob_handle_->CreateReader(),
base::BindRepeating(&OutputStreamWriteOperation::OnFileWritten,
weak_factory_.GetWeakPtr()));
}
void OnFileWritten(
base::File::Error error,
int64_t bytes_wrriten,
storage::FileWriterDelegate::WriteProgressStatus write_status) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
// Any status other than SUCCESS_IO_PENDING indicates completion.
if (write_status !=
storage::FileWriterDelegate::WriteProgressStatus::SUCCESS_IO_PENDING) {
Complete();
}
}
void Complete() {
// If the IOutputStream implements IClosable (e.g. the OutputStream class),
// close the stream whenever we are done with this operation, regardless of
// the outcome.
if (stream_) {
ComPtr<IClosable> closable;
if (SUCCEEDED(stream_.As(&closable)))
closable->Close();
}
std::move(on_complete_).Run();
}
content::BrowserContext::BlobContextGetter blob_context_getter_;
std::unique_ptr<storage::BlobDataHandle> blob_handle_;
scoped_refptr<base::RefCountedData<uint64_t>> file_bytes_shared_;
base::OnceCallback<void()> on_complete_;
ComPtr<IOutputStream> stream_;
const std::string uuid_;
std::unique_ptr<storage::FileWriterDelegate> writer_delegate_;
base::WeakPtrFactory<OutputStreamWriteOperation> weak_factory_{this};
};
} // namespace
// static
void ShareOperation::SetMaxFileBytesForTesting(uint64_t max_file_bytes) {
g_max_file_bytes = max_file_bytes;
}
// static
void ShareOperation::SetRoGetActivationFactoryFunctionForTesting(
decltype(&base::win::RoGetActivationFactory) value) {
g_ro_get_activation_factory_function = value;
}
ShareOperation::ShareOperation(const std::string& title,
const std::string& text,
const GURL& url,
std::vector<blink::mojom::SharedFilePtr> files,
content::WebContents* web_contents)
: web_contents_(web_contents->GetWeakPtr()),
title_(std::move(title)),
text_(std::move(text)),
url_(std::move(url)),
files_(std::move(files)) {}
ShareOperation::~ShareOperation() {
if (callback_)
Complete(blink::mojom::ShareError::CANCELED);
}
base::WeakPtr<ShareOperation> ShareOperation::AsWeakPtr() {
return weak_factory_.GetWeakPtr();
}
void ShareOperation::Run(blink::mojom::ShareService::ShareCallback callback) {
DCHECK(!callback_);
callback_ = std::move(callback);
// If the corresponding web_contents have already been cleaned up, cancel
// the operation.
if (!web_contents_) {
Complete(blink::mojom::ShareError::CANCELED);
return;
}
if (files_.size() > 0) {
// Determine the source for use with the OS IAttachmentExecute.
// If the source cannot be determined, does not appear to be valid,
// or is longer than the max length supported by the IAttachmentExecute
// service, use a generic value that reliably maps to the Internet zone.
GURL source_url = web_contents_->GetLastCommittedURL();
std::wstring source = (source_url.is_valid() &&
source_url.spec().size() <= INTERNET_MAX_URL_LENGTH)
? base::UTF8ToWide(source_url.spec())
: L"about:internet";
// For each "file", check against the OS that it is allowed
// The same instance cannot be used to check multiple files, so this
// makes a new one per-file. For more details on this functionality, see
// https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-iattachmentexecute-checkpolicy
for (auto& file : files_) {
ComPtr<IAttachmentExecute> attachment_services;
if (FAILED(CoCreateInstance(CLSID_AttachmentServices, nullptr, CLSCTX_ALL,
IID_PPV_ARGS(&attachment_services)))) {
Complete(blink::mojom::ShareError::INTERNAL_ERROR);
return;
}
if (FAILED(attachment_services->SetSource(source.c_str()))) {
Complete(blink::mojom::ShareError::INTERNAL_ERROR);
return;
}
if (FAILED(attachment_services->SetFileName(
file->name.path().value().c_str()))) {
Complete(blink::mojom::ShareError::INTERNAL_ERROR);
return;
}
if (FAILED(attachment_services->CheckPolicy())) {
Complete(blink::mojom::ShareError::PERMISSION_DENIED);
return;
}
}
}
HWND hwnd =
views::HWNDForNativeWindow(web_contents_->GetTopLevelNativeWindow());
// Attempt to fetch the special HWND maintained for the primary WebContents of
// this window. For the sake of better communication with screen readers this
// HWND is (virtually) scoped to the same space as the WebContents (rather
// than the entire actual window), so allows the resulting Share dialog to
// better position/associate itself with the WebContents.
//
// Note: Though this is exposed to accessibility tools via standardized routes
// we could expect to leverage here, the browser may choose to not set up all
// these routes until an accessibility tool has been detected. Instead we look
// for this specific class directly so we can find it even if accessibility
// has not been configured yet.
if (hwnd) {
HWND accessible_hwnd =
::FindWindowExW(/*hWndParent*/ hwnd, /*hWndChildAfter*/ NULL,
/*lpszClass*/ ui::kLegacyRenderWidgetHostHwnd,
/*lpszWindow*/ NULL);
if (accessible_hwnd) {
hwnd = accessible_hwnd;
}
}
show_share_ui_for_window_operation_ =
std::make_unique<ShowShareUIForWindowOperation>(hwnd);
show_share_ui_for_window_operation_->Run(base::BindOnce(
&ShareOperation::OnDataRequested, weak_factory_.GetWeakPtr()));
}
void ShareOperation::OnDataRequested(IDataRequestedEventArgs* event_args) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
blink::mojom::ShareError share_result;
if (!web_contents_) {
share_result = blink::mojom::ShareError::CANCELED;
} else if (PutShareContentInEventArgs(event_args)) {
share_result = blink::mojom::ShareError::OK;
} else {
share_result = blink::mojom::ShareError::INTERNAL_ERROR;
}
// If the share operation failed or is not being deferred, mark it as complete
if (share_result != blink::mojom::ShareError::OK || !data_request_deferral_)
Complete(share_result);
}
bool ShareOperation::PutShareContentInEventArgs(
IDataRequestedEventArgs* event_args) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!event_args)
return false;
ComPtr<IDataRequest> data_request;
if (FAILED(event_args->get_Request(&data_request)))
return false;
if (FAILED(data_request->get_Data(&data_package_)))
return false;
ComPtr<IDataPackagePropertySet> data_prop_sets;
if (FAILED(data_package_->get_Properties(&data_prop_sets)))
return false;
// Title is a required property for the UWP Share contract, so
// if the provided title is empty we instead use a blank value.
// https://docs.microsoft.com/en-us/windows/uwp/app-to-app/share-data
base::win::ScopedHString title_h =
base::win::ScopedHString::Create(title_.empty() ? " " : title_.c_str());
if (FAILED(data_prop_sets->put_Title(title_h.get())))
return false;
return PutShareContentInDataPackage(data_request.Get());
}
bool ShareOperation::PutShareContentInDataPackage(IDataRequest* data_request) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!text_.empty()) {
auto text_h = base::win::ScopedHString::Create(text_);
if (FAILED(data_package_->SetText(text_h.get())))
return false;
}
if (!url_.spec().empty()) {
ComPtr<IUriRuntimeClassFactory> uri_factory;
auto hr =
GetActivationFactory<IUriRuntimeClassFactory,
RuntimeClass_Windows_Foundation_Uri>(&uri_factory);
if (FAILED(hr))
return hr;
auto url_h = base::win::ScopedHString::Create(url_.spec().c_str());
ComPtr<IUriRuntimeClass> uri;
if (FAILED(uri_factory->CreateUri(url_h.get(), &uri)))
return false;
ComPtr<IDataPackage2> data_package_2;
if (FAILED(data_package_.As(&data_package_2)))
return false;
if (FAILED(data_package_2->SetWebLink(uri.Get())))
return false;
}
if (!files_.empty()) {
// Fetch a deferral to allow for async operations
if (FAILED(data_request->GetDeferral(&data_request_deferral_)))
return false;
// Initialize the output collection for the async operation(s)
storage_items_ = Make<base::win::Vector<IStorageItem*>>();
// Create a variable to be shared between all the operations processing the
// blobs to streams. This will be used to keep a running count of total file
// bytes shared as part of this Share operation so that if the maximum
// allowed is exceeded the processing can be halted. Currently the
// ShareOperation class is not guaranteed to outlive these operations, but
// if that changes in the future it may be appropriate to make this a member
// of the ShareOperation that is shared only be reference.
auto file_bytes_shared =
base::MakeRefCounted<base::RefCountedData<uint64_t>>(0);
ComPtr<IStorageFileStatics> storage_statics;
auto hr = GetActivationFactory<IStorageFileStatics,
RuntimeClass_Windows_Storage_StorageFile>(
&storage_statics);
if (FAILED(hr))
return false;
for (auto& file : files_) {
// This operation for converting the corresponding blob to a stream is
// maintained as a scoped_refptr because it may out live this
// ShareOperation instance. It is only invoked when the user has chosen a
// Share target and that target decides to start reading the contents of
// the corresponding IStorageFile. See
// https://docs.microsoft.com/en-us/uwp/api/windows.storage.storagefile.createstreamedfileasync
// If in the future the ShareOperation class is changed to live until the
// target app has finished fully processing the shared content this could
// be updated to be owned/maintained by this ShareOperation instance.
auto operation = base::MakeRefCounted<OutputStreamWriteOperation>(
web_contents_->GetBrowserContext()->GetBlobStorageContext(),
file_bytes_shared, file->blob->uuid);
auto name_h = base::win::ScopedHString::Create(file->name.path().value());
auto raw_data_requested_callback =
Callback<IStreamedFileDataRequestedHandler>(
[operation](IOutputStream* stream) -> HRESULT {
// No additional work is needed when the write has been
// completed, but a callback is created to hold a reference
// to the |operation| until the operation has completed.
operation->WriteStream(
stream,
base::BindOnce(
[](scoped_refptr<OutputStreamWriteOperation>) {},
operation));
return S_OK;
});
// The Callback function may return null in the E_OUTOFMEMORY case
if (!raw_data_requested_callback)
return false;
ComPtr<IAsyncOperation<StorageFile*>> async_operation;
if (FAILED(storage_statics->CreateStreamedFileAsync(
name_h.get(), raw_data_requested_callback.Get(),
/*thumbnail*/ nullptr, &async_operation))) {
return false;
}
async_operations_.push_back(async_operation);
if (FAILED(base::win::PostAsyncHandlers(
async_operation.Get(),
base::BindOnce(&ShareOperation::OnStreamedFileCreated,
weak_factory_.GetWeakPtr())))) {
return false;
}
}
}
return true;
}
void ShareOperation::OnStreamedFileCreated(ComPtr<IStorageFile> storage_file) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// If there is no callback this ShareOperation already completed due to an
// error, so work can be halted early.
if (!callback_)
return;
if (!storage_file) {
Complete(blink::mojom::ShareError::INTERNAL_ERROR);
return;
}
ComPtr<IStorageItem> storage_item;
if (FAILED(storage_file.As(&storage_item))) {
Complete(blink::mojom::ShareError::INTERNAL_ERROR);
return;
}
if (FAILED(storage_items_->Append(storage_item.Get()))) {
Complete(blink::mojom::ShareError::INTERNAL_ERROR);
return;
}
unsigned int size;
if (FAILED(storage_items_->get_Size(&size))) {
Complete(blink::mojom::ShareError::INTERNAL_ERROR);
return;
}
// If this is not the final file, no more work to do
if (size != files_.size())
return;
if (FAILED(data_package_->SetStorageItems(storage_items_.Get(),
true /*readonly*/))) {
Complete(blink::mojom::ShareError::INTERNAL_ERROR);
return;
}
data_request_deferral_->Complete();
Complete(blink::mojom::ShareError::OK);
return;
}
void ShareOperation::Complete(const blink::mojom::ShareError share_result) {
std::move(callback_).Run(share_result);
}
} // namespace webshare