| // 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 |