blob: c66b8da5d3ce640c1a1bdfdaec66bba3b4878d3e [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/shell_dialogs/select_file_dialog_linux_portal.h"
#include <string_view>
#include "base/check.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/ref_counted_memory.h"
#include "base/nix/xdg_util.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "components/dbus/thread_linux/dbus_thread_linux.h"
#include "components/dbus/xdg/portal.h"
#include "components/dbus/xdg/portal_constants.h"
#include "components/dbus/xdg/request.h"
#include "dbus/message.h"
#include "dbus/object_path.h"
#include "dbus/object_proxy.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/native_ui_types.h"
#include "ui/linux/linux_ui.h"
#include "ui/linux/linux_ui_delegate.h"
#include "ui/shell_dialogs/selected_file_info.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/widget/desktop_aura/desktop_window_tree_host_linux.h"
#include "url/gurl.h"
#include "url/url_util.h"
namespace ui {
namespace {
constexpr char kXdgPortalService[] = "org.freedesktop.portal.Desktop";
constexpr char kXdgPortalObject[] = "/org/freedesktop/portal/desktop";
constexpr int kXdgPortalRequiredVersion = 3;
constexpr char kFileChooserMethodOpenFile[] = "OpenFile";
constexpr char kFileChooserMethodSaveFile[] = "SaveFile";
constexpr char kFileChooserOptionAcceptLabel[] = "accept_label";
constexpr char kFileChooserOptionMultiple[] = "multiple";
constexpr char kFileChooserOptionDirectory[] = "directory";
constexpr char kFileChooserOptionFilters[] = "filters";
constexpr char kFileChooserOptionCurrentFilter[] = "current_filter";
constexpr char kFileChooserOptionCurrentFolder[] = "current_folder";
constexpr char kFileChooserOptionCurrentName[] = "current_name";
constexpr char kFileChooserOptionModal[] = "modal";
constexpr uint32_t kFileChooserFilterKindGlob = 0;
constexpr char kFileUriPrefix[] = "file://";
std::vector<uint8_t> PathToByteArray(const base::FilePath& path) {
std::vector<uint8_t> bytes(path.value().begin(), path.value().end());
// Null-terminate the array.
bytes.push_back(0);
return bytes;
}
std::vector<base::FilePath> ConvertUrisToPaths(
const std::vector<std::string>& uris) {
std::vector<base::FilePath> paths;
for (const std::string& uri : uris) {
if (!base::StartsWith(uri, kFileUriPrefix, base::CompareCase::SENSITIVE)) {
LOG(WARNING) << "Ignoring unknown file chooser URI: " << uri;
continue;
}
std::string_view encoded_path(uri);
encoded_path.remove_prefix(strlen(kFileUriPrefix));
url::RawCanonOutputT<char16_t> decoded_path;
url::DecodeURLEscapeSequences(
encoded_path, url::DecodeURLMode::kUTF8OrIsomorphic, &decoded_path);
paths.emplace_back(base::UTF16ToUTF8(decoded_path.view()));
}
return paths;
}
} // namespace
SelectFileDialogLinuxPortal::SelectFileDialogLinuxPortal(
Listener* listener,
std::unique_ptr<ui::SelectFilePolicy> policy)
: SelectFileDialogLinux(listener, std::move(policy)) {}
SelectFileDialogLinuxPortal::~SelectFileDialogLinuxPortal() {
if (reenable_window_event_handling_) {
invoker_task_runner_->PostTask(FROM_HERE,
std::move(reenable_window_event_handling_));
}
}
void SelectFileDialogLinuxPortal::ListenerDestroyed() {
weak_factory_.InvalidateWeakPtrs();
if (fallback_dialog_) {
fallback_dialog_->ListenerDestroyed();
fallback_dialog_.reset();
}
SelectFileDialogLinux::ListenerDestroyed();
}
bool SelectFileDialogLinuxPortal::IsRunning(
gfx::NativeWindow parent_window) const {
if (fallback_dialog_) {
return fallback_dialog_->IsRunning(parent_window);
}
return parent_window && host_ && host_.get() == parent_window->GetHost();
}
void SelectFileDialogLinuxPortal::SelectFileImpl(
Type type,
const std::u16string& title,
const base::FilePath& default_path,
const FileTypeInfo* file_types,
int file_type_index,
const base::FilePath::StringType& default_extension,
gfx::NativeWindow owning_window,
const GURL* caller) {
set_type(type);
invoker_task_runner_ = base::SequencedTaskRunner::GetCurrentDefault();
if (owning_window) {
if (auto* root = owning_window->GetRootWindow()) {
if (auto* host = root->GetNativeWindowProperty(
views::DesktopWindowTreeHostLinux::kWindowKey)) {
host_ = static_cast<aura::WindowTreeHost*>(host)->GetWeakPtr();
}
}
}
if (file_types) {
set_file_types(*file_types);
}
set_file_type_index(file_type_index);
std::optional<GURL> caller_copy;
if (caller) {
caller_copy = *caller;
}
dbus_xdg::RequestXdgDesktopPortal(
dbus_thread_linux::GetSharedSessionBus().get(),
base::BindOnce(&SelectFileDialogLinuxPortal::OnPortalAvailable,
weak_factory_.GetWeakPtr(), title, default_path,
default_extension, std::move(caller_copy)));
}
void SelectFileDialogLinuxPortal::OnPortalAvailable(
std::u16string title,
base::FilePath default_path,
base::FilePath::StringType default_extension,
std::optional<GURL> caller,
uint32_t version) {
if (version < kXdgPortalRequiredVersion) {
gfx::NativeWindow owning_window = host_ ? host_->window() : nullptr;
if (auto* linux_ui = ui::LinuxUi::instance()) {
fallback_dialog_ = linux_ui->CreateSelectFileDialog(listener_, nullptr);
}
if (fallback_dialog_) {
fallback_dialog_->SelectFile(
type(), title, default_path, &file_types(), file_type_index(),
default_extension, owning_window, caller ? &caller.value() : nullptr);
return;
}
if (listener_) {
listener_->FileSelectionCanceled();
}
return;
}
PortalFilterSet filter_set = BuildFilterSet();
// Keep a copy of the filters so the index of the chosen one can be identified
// and returned to listeners later.
filters_ = filter_set.filters;
auto* delegate = ui::LinuxUiDelegate::GetInstance();
if (host_ && delegate) {
delegate->ExportWindowHandle(
host_->GetAcceleratedWidget(),
base::BindOnce(
&SelectFileDialogLinuxPortal::SelectFileImplWithParentHandle, this,
title, default_path, filter_set, default_extension));
} else {
// No parent or no delegate, so just use a blank parent handle.
SelectFileImplWithParentHandle(title, default_path, filter_set,
default_extension, "");
}
}
bool SelectFileDialogLinuxPortal::HasMultipleFileTypeChoicesImpl() {
if (fallback_dialog_) {
return fallback_dialog_->HasMultipleFileTypeChoices();
}
return file_types().extensions.size() > 1;
}
SelectFileDialogLinuxPortal::PortalFilterSet
SelectFileDialogLinuxPortal::BuildFilterSet() {
PortalFilterSet filter_set;
for (size_t i = 0; i < file_types().extensions.size(); ++i) {
PortalFilter filter;
std::vector<std::string> original_patterns;
for (const std::string& extension : file_types().extensions[i]) {
if (extension.empty()) {
continue;
}
// We want to allow ASCII case-insensitive matches for the extension on
// a per-character basis, since that's what
// https://html.spec.whatwg.org/multipage/input.html#attr-input-accept
// suggests. For example, we should accept file.txt, file.TXT, or
// file.tXt. To do this, we expand characters with ASCII case
// equivalents to be represented by [aA], as documented in
// https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileChooser.html
std::string pattern("*.");
for (char c : extension) {
char lower = base::ToLowerASCII(c);
char upper = base::ToUpperASCII(c);
if (upper != lower) {
pattern.append({'[', lower, upper, ']'});
} else {
pattern.append({c});
}
}
filter.patterns.push_back(pattern);
// Save the original form for use as a fallback description.
original_patterns.push_back("*." + extension);
}
if (filter.patterns.empty()) {
continue;
}
// If there is no matching description, use a default description based on
// the filter.
if (i < file_types().extension_description_overrides.size()) {
filter.name =
base::UTF16ToUTF8(file_types().extension_description_overrides[i]);
}
if (filter.name.empty()) {
filter.name = base::JoinString(original_patterns, ",");
}
// The -1 is required to match against the right filter because
// |file_type_index_| is 1-indexed.
if (i == file_type_index() - 1) {
filter_set.default_filter = filter;
}
filter_set.filters.push_back(std::move(filter));
}
if (file_types().include_all_files && !filter_set.filters.empty()) {
// Add the *.* filter, but only if we have added other filters (otherwise it
// is implied).
PortalFilter filter;
filter.name = l10n_util::GetStringUTF8(IDS_SAVEAS_ALL_FILES);
filter.patterns.push_back("*.*");
filter_set.filters.push_back(std::move(filter));
}
return filter_set;
}
void SelectFileDialogLinuxPortal::SelectFileImplWithParentHandle(
std::u16string title,
base::FilePath default_path,
PortalFilterSet filter_set,
base::FilePath::StringType default_extension,
std::string parent_handle) {
bool default_path_exists = CallDirectoryExistsOnUIThread(default_path);
invoker_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&SelectFileDialogLinuxPortal::SelectFileImplOnMainThread,
this, std::move(title), std::move(default_path),
default_path_exists, std::move(filter_set),
std::move(default_extension), std::move(parent_handle)));
}
void SelectFileDialogLinuxPortal::SelectFileImplOnMainThread(
std::u16string title,
base::FilePath default_path,
const bool default_path_exists,
PortalFilterSet filter_set,
base::FilePath::StringType default_extension,
std::string parent_handle) {
CHECK(invoker_task_runner_->RunsTasksInCurrentSequence());
std::string method;
switch (type()) {
case SELECT_FOLDER:
case SELECT_UPLOAD_FOLDER:
case SELECT_EXISTING_FOLDER:
case SELECT_OPEN_FILE:
case SELECT_OPEN_MULTI_FILE:
method = kFileChooserMethodOpenFile;
break;
case SELECT_SAVEAS_FILE:
method = kFileChooserMethodSaveFile;
break;
case SELECT_NONE:
NOTREACHED();
}
std::string utf8_title;
if (!title.empty()) {
utf8_title = base::UTF16ToUTF8(title);
} else {
int message_id = 0;
if (type() == SELECT_SAVEAS_FILE) {
message_id = IDS_SAVEAS_ALL_FILES;
} else if (type() == SELECT_OPEN_MULTI_FILE) {
message_id = IDS_OPEN_FILES_DIALOG_TITLE;
} else {
message_id = IDS_OPEN_FILE_DIALOG_TITLE;
}
utf8_title = l10n_util::GetStringUTF8(message_id);
}
MakeFileChooserRequest(
method, utf8_title,
BuildOptionsDictionary(default_path, default_path_exists, filter_set),
std::move(parent_handle));
}
dbus_xdg::Dictionary SelectFileDialogLinuxPortal::BuildOptionsDictionary(
const base::FilePath& default_path,
bool default_path_exists,
const PortalFilterSet& filter_set) {
dbus_xdg::Dictionary dict;
switch (type()) {
case SelectFileDialog::SELECT_UPLOAD_FOLDER:
dict[kFileChooserOptionAcceptLabel] =
dbus_utils::Variant::Wrap<"s">(l10n_util::GetStringUTF8(
IDS_SELECT_UPLOAD_FOLDER_DIALOG_UPLOAD_BUTTON));
[[fallthrough]];
case SelectFileDialog::SELECT_FOLDER:
case SelectFileDialog::Type::SELECT_EXISTING_FOLDER:
dict[kFileChooserOptionDirectory] = dbus_utils::Variant::Wrap<"b">(true);
break;
case SelectFileDialog::SELECT_OPEN_MULTI_FILE:
dict[kFileChooserOptionMultiple] = dbus_utils::Variant::Wrap<"b">(true);
break;
default:
break;
}
if (!default_path.empty() && base::IsStringUTF8(default_path.value())) {
if (default_path_exists) {
// If this is an existing directory, navigate to that directory, with no
// filename.
dict[kFileChooserOptionCurrentFolder] =
dbus_utils::Variant::Wrap<"ay">(PathToByteArray(default_path));
} else {
// The default path does not exist, or is an existing file. We use
// current_folder followed by current_name, as per the recommendation of
// the GTK docs and the pattern followed by SelectFileDialogLinuxGtk.
dict[kFileChooserOptionCurrentFolder] = dbus_utils::Variant::Wrap<"ay">(
PathToByteArray(default_path.DirName()));
// current_folder is supported by xdg-desktop-portal but current_name
// is not - only try to set this when invoking a save file dialog.
if (type() == SELECT_SAVEAS_FILE) {
dict[kFileChooserOptionCurrentName] =
dbus_utils::Variant::Wrap<"s">(default_path.BaseName().value());
}
}
}
if (!filter_set.filters.empty()) {
DbusFilters filters_array;
for (const auto& filter : filter_set.filters) {
filters_array.push_back(MakeFilterStruct(filter));
}
dict[kFileChooserOptionFilters] =
dbus_utils::Variant::Wrap<"a(sa(us))">(std::move(filters_array));
if (filter_set.default_filter) {
dict[kFileChooserOptionCurrentFilter] =
dbus_utils::Variant::Wrap<"(sa(us))">(
MakeFilterStruct(*filter_set.default_filter));
}
}
dict[kFileChooserOptionModal] = dbus_utils::Variant::Wrap<"b">(true);
return dict;
}
SelectFileDialogLinuxPortal::DbusFilter
SelectFileDialogLinuxPortal::MakeFilterStruct(const PortalFilter& filter) {
DbusFilterPatterns patterns;
for (const std::string& pattern_str : filter.patterns) {
patterns.emplace_back(kFileChooserFilterKindGlob, pattern_str);
}
return std::make_tuple(filter.name, std::move(patterns));
}
void SelectFileDialogLinuxPortal::MakeFileChooserRequest(
const std::string& method,
const std::string& title,
dbus_xdg::Dictionary options,
std::string parent_handle) {
CHECK(invoker_task_runner_->RunsTasksInCurrentSequence());
scoped_refptr<dbus::Bus> bus = dbus_thread_linux::GetSharedSessionBus();
dbus::ObjectProxy* portal = bus->GetObjectProxy(
kXdgPortalService, dbus::ObjectPath(kXdgPortalObject));
// Unretained is safe since we own the Request object.
auto callback =
base::BindOnce(&SelectFileDialogLinuxPortal::OnFileChooserResponse,
base::Unretained(this));
file_chooser_request_ = std::make_unique<dbus_xdg::Request>(
bus, portal, dbus_xdg::kFileChooserInterfaceName, method,
std::move(options), std::move(callback), std::move(parent_handle), title);
invoker_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&SelectFileDialogLinuxPortal::DialogCreatedOnInvoker,
weak_factory_.GetWeakPtr()));
}
void SelectFileDialogLinuxPortal::OnFileChooserResponse(
base::expected<dbus_xdg::Dictionary, dbus_xdg::ResponseError> results) {
CHECK(invoker_task_runner_->RunsTasksInCurrentSequence());
file_chooser_request_.reset();
if (!results.has_value()) {
CancelOpen();
return;
}
std::vector<std::string> uris;
if (auto it = results->find("uris"); it != results->end()) {
if (auto opt_uris =
std::move(it->second).Take<std::vector<std::string>>()) {
uris = std::move(*opt_uris);
}
}
if (uris.empty()) {
CancelOpen();
return;
}
std::vector<base::FilePath> paths = ConvertUrisToPaths(uris);
if (paths.empty()) {
CancelOpen();
return;
}
std::string current_filter_name;
if (auto it = results->find("current_filter"); it != results->end()) {
if (auto current_filter = std::move(it->second).Take<DbusFilter>()) {
current_filter_name = std::get<0>(*current_filter);
}
}
CompleteOpen(std::move(paths), std::move(current_filter_name));
}
void SelectFileDialogLinuxPortal::CompleteOpen(
std::vector<base::FilePath> paths,
std::string current_filter) {
dbus_thread_linux::GetSharedSessionBus()->AssertOnOriginThread();
invoker_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&SelectFileDialogLinuxPortal::CompleteOpenOnInvoker,
weak_factory_.GetWeakPtr(), std::move(paths),
std::move(current_filter)));
}
void SelectFileDialogLinuxPortal::CancelOpen() {
dbus_thread_linux::GetSharedSessionBus()->AssertOnOriginThread();
invoker_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&SelectFileDialogLinuxPortal::CancelOpenOnInvoker,
weak_factory_.GetWeakPtr()));
}
void SelectFileDialogLinuxPortal::DialogCreatedOnInvoker() {
CHECK(invoker_task_runner_->RunsTasksInCurrentSequence());
if (!host_) {
return;
}
host_->ReleaseCapture();
reenable_window_event_handling_ =
static_cast<views::DesktopWindowTreeHostLinux*>(host_.get())
->DisableEventListening();
}
void SelectFileDialogLinuxPortal::CompleteOpenOnInvoker(
std::vector<base::FilePath> paths,
std::string current_filter) {
CHECK(invoker_task_runner_->RunsTasksInCurrentSequence());
UnparentOnInvoker();
if (listener_) {
if (type() == SELECT_OPEN_MULTI_FILE) {
listener_->MultiFilesSelected(FilePathListToSelectedFileInfoList(paths));
} else if (paths.size() > 1) {
LOG(ERROR) << "Got >1 file URI from a single-file chooser";
} else {
int index = 1;
for (size_t i = 0; i < filters_.size(); ++i) {
if (filters_[i].name == current_filter) {
index = 1 + i;
break;
}
}
listener_->FileSelected(SelectedFileInfo(paths[0]), index);
}
}
}
void SelectFileDialogLinuxPortal::CancelOpenOnInvoker() {
CHECK(invoker_task_runner_->RunsTasksInCurrentSequence());
UnparentOnInvoker();
if (listener_) {
listener_->FileSelectionCanceled();
}
}
void SelectFileDialogLinuxPortal::UnparentOnInvoker() {
if (reenable_window_event_handling_) {
std::move(reenable_window_event_handling_).Run();
}
host_ = nullptr;
}
SelectFileDialogLinuxPortal::PortalFilter::PortalFilter() = default;
SelectFileDialogLinuxPortal::PortalFilter::PortalFilter(
const PortalFilter& other) = default;
SelectFileDialogLinuxPortal::PortalFilter::PortalFilter(PortalFilter&& other) =
default;
SelectFileDialogLinuxPortal::PortalFilter::~PortalFilter() = default;
SelectFileDialogLinuxPortal::PortalFilterSet::PortalFilterSet() = default;
SelectFileDialogLinuxPortal::PortalFilterSet::PortalFilterSet(
const PortalFilterSet& other) = default;
SelectFileDialogLinuxPortal::PortalFilterSet::PortalFilterSet(
PortalFilterSet&& other) = default;
SelectFileDialogLinuxPortal::PortalFilterSet::~PortalFilterSet() = default;
} // namespace ui