blob: 54cefe5b6236b24cd9c4625d603fe9a7205676f5 [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/utils/check_for_service_and_start.h"
#include "components/dbus/xdg/request.h"
#include "components/dbus/xdg/systemd.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_widget_types.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 kFileChooserInterfaceName[] =
"org.freedesktop.portal.FileChooser";
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://";
enum class ServiceAvailability {
kNotStarted,
kInProgress,
kNotAvailable,
kAvailable,
};
ServiceAvailability g_service_availability = ServiceAvailability::kNotStarted;
scoped_refptr<base::SequencedTaskRunner>& GetMainTaskRunner() {
static base::NoDestructor<scoped_refptr<base::SequencedTaskRunner>>
main_task_runner;
return *main_task_runner;
}
void OnGetPropertyReply(dbus::Response* response) {
if (!response) {
g_service_availability = ServiceAvailability::kNotAvailable;
return;
}
uint32_t version = 0;
dbus::MessageReader reader(response);
if (!reader.PopVariantOfUint32(&version)) {
g_service_availability = ServiceAvailability::kNotAvailable;
return;
}
g_service_availability = version >= kXdgPortalRequiredVersion
? ServiceAvailability::kAvailable
: ServiceAvailability::kNotAvailable;
}
void OnServiceStarted(std::optional<bool> service_started) {
if (!service_started.value_or(false)) {
g_service_availability = ServiceAvailability::kNotAvailable;
return;
}
dbus::ObjectProxy* portal =
dbus_thread_linux::GetSharedSessionBus()->GetObjectProxy(
kXdgPortalService, dbus::ObjectPath(kXdgPortalObject));
dbus::MethodCall get_property_call(DBUS_INTERFACE_PROPERTIES, "Get");
dbus::MessageWriter get_property_writer(&get_property_call);
get_property_writer.AppendString(kFileChooserInterfaceName);
get_property_writer.AppendString("version");
portal->CallMethod(&get_property_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
base::BindOnce(&OnGetPropertyReply));
}
void OnSystemdUnitStarted(dbus_xdg::SystemdUnitStatus) {
dbus_utils::CheckForServiceAndStart(dbus_thread_linux::GetSharedSessionBus(),
kXdgPortalService,
base::BindOnce(&OnServiceStarted));
}
DbusByteArray PathToByteArray(const base::FilePath& path) {
return DbusByteArray(base::MakeRefCounted<base::RefCountedBytes>(
base::as_bytes(base::span_with_nul_from_cstring_view(
base::cstring_view(path.value())))));
}
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_));
}
}
// static
void SelectFileDialogLinuxPortal::StartAvailabilityTestInBackground() {
if (g_service_availability != ServiceAvailability::kNotStarted) {
return;
}
g_service_availability = ServiceAvailability::kInProgress;
GetMainTaskRunner() = base::SequencedTaskRunner::GetCurrentDefault();
dbus_xdg::SetSystemdScopeUnitNameForXdgPortal(
dbus_thread_linux::GetSharedSessionBus().get(),
base::BindOnce(&OnSystemdUnitStarted));
}
// static
bool SelectFileDialogLinuxPortal::IsPortalAvailable() {
if (g_service_availability == ServiceAvailability::kInProgress) {
LOG(WARNING) << "Portal availability checked before test was complete";
}
return g_service_availability == ServiceAvailability::kAvailable;
}
bool SelectFileDialogLinuxPortal::IsRunning(
gfx::NativeWindow parent_window) const {
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) {
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);
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;
if (host_) {
auto* delegate = ui::LinuxUiDelegate::GetInstance();
if (delegate &&
delegate->ExportWindowHandle(
host_->GetAcceleratedWidget(),
base::BindOnce(
&SelectFileDialogLinuxPortal::SelectFileImplWithParentHandle,
this, title, default_path, filter_set, default_extension))) {
// Return early to skip the fallback below.
return;
} else {
LOG(WARNING) << "Failed to export window handle for portal select dialog";
}
}
// No parent, so just use a blank parent handle.
SelectFileImplWithParentHandle(title, default_path, filter_set,
default_extension, "");
}
bool SelectFileDialogLinuxPortal::HasMultipleFileTypeChoicesImpl() {
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);
GetMainTaskRunner()->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(GetMainTaskRunner()->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));
}
DbusDictionary SelectFileDialogLinuxPortal::BuildOptionsDictionary(
const base::FilePath& default_path,
bool default_path_exists,
const PortalFilterSet& filter_set) {
DbusDictionary dict;
switch (type_) {
case SelectFileDialog::SELECT_UPLOAD_FOLDER:
dict.PutAs(kFileChooserOptionAcceptLabel,
DbusString(l10n_util::GetStringUTF8(
IDS_SELECT_UPLOAD_FOLDER_DIALOG_UPLOAD_BUTTON)));
[[fallthrough]];
case SelectFileDialog::SELECT_FOLDER:
case SelectFileDialog::Type::SELECT_EXISTING_FOLDER:
dict.PutAs(kFileChooserOptionDirectory, DbusBoolean(true));
break;
case SelectFileDialog::SELECT_OPEN_MULTI_FILE:
dict.PutAs(kFileChooserOptionMultiple, DbusBoolean(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.PutAs(kFileChooserOptionCurrentFolder,
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.PutAs(kFileChooserOptionCurrentFolder,
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.PutAs(kFileChooserOptionCurrentName,
DbusString(default_path.BaseName().value()));
}
}
}
if (!filter_set.filters.empty()) {
DbusFilters filters_array;
for (const auto& filter : filter_set.filters) {
filters_array.value().push_back(MakeFilterStruct(filter));
}
dict.PutAs(kFileChooserOptionFilters, std::move(filters_array));
if (filter_set.default_filter) {
dict.PutAs(kFileChooserOptionCurrentFilter,
MakeFilterStruct(*filter_set.default_filter));
}
}
dict.PutAs(kFileChooserOptionModal, DbusBoolean(true));
return dict;
}
SelectFileDialogLinuxPortal::DbusFilter
SelectFileDialogLinuxPortal::MakeFilterStruct(const PortalFilter& filter) {
DbusFilterPatterns patterns;
for (const std::string& pattern_str : filter.patterns) {
patterns.value().push_back(MakeDbusStruct(
DbusUint32(kFileChooserFilterKindGlob), DbusString(pattern_str)));
}
return MakeDbusStruct(DbusString(filter.name), std::move(patterns));
}
void SelectFileDialogLinuxPortal::MakeFileChooserRequest(
const std::string& method,
const std::string& title,
DbusDictionary options,
std::string parent_handle) {
CHECK(GetMainTaskRunner()->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, kFileChooserInterfaceName, method,
MakeDbusParameters(DbusString(std::move(parent_handle)),
DbusString(title)),
std::move(options), std::move(callback));
invoker_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&SelectFileDialogLinuxPortal::DialogCreatedOnInvoker,
this));
}
void SelectFileDialogLinuxPortal::OnFileChooserResponse(
base::expected<DbusDictionary, dbus_xdg::ResponseError> results) {
CHECK(GetMainTaskRunner()->RunsTasksInCurrentSequence());
file_chooser_request_.reset();
if (!results.has_value()) {
CancelOpen();
return;
}
std::vector<std::string> uris;
auto* wrapped_uris = results->GetAs<DbusArray<DbusString>>("uris");
if (wrapped_uris) {
for (auto& element : wrapped_uris->value()) {
uris.push_back(element.value());
}
}
if (uris.empty()) {
CancelOpen();
return;
}
std::vector<base::FilePath> paths = ConvertUrisToPaths(uris);
if (paths.empty()) {
CancelOpen();
return;
}
std::string current_filter_name;
auto* current_filter = results->GetAs<DbusFilter>("current_filter");
if (current_filter) {
current_filter_name = std::get<0>(current_filter->value()).value();
}
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, this,
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, this));
}
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