| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <cstddef> |
| #include <memory> |
| #include <set> |
| |
| #include "base/command_line.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/logging.h" |
| #include "base/nix/mime_util_xdg.h" |
| #include "base/nix/xdg_util.h" |
| #include "base/process/launch.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/version.h" |
| #include "ui/aura/window_tree_host.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/shell_dialogs/select_file_dialog_linux.h" |
| #include "ui/shell_dialogs/selected_file_info.h" |
| #include "ui/strings/grit/ui_strings.h" |
| #include "url/gurl.h" |
| |
| namespace { |
| |
| std::string GetTitle(const std::string& title, int message_id) { |
| return title.empty() ? l10n_util::GetStringUTF8(message_id) : title; |
| } |
| |
| const char kKdialogBinary[] = "kdialog"; |
| |
| } // namespace |
| |
| namespace ui { |
| |
| // Implementation of SelectFileDialog that shows a KDE common dialog for |
| // choosing a file or folder. This acts as a modal dialog. |
| class SelectFileDialogLinuxKde : public SelectFileDialogLinux { |
| public: |
| SelectFileDialogLinuxKde(Listener* listener, |
| std::unique_ptr<ui::SelectFilePolicy> policy, |
| base::nix::DesktopEnvironment desktop, |
| const std::string& kdialog_version); |
| |
| SelectFileDialogLinuxKde(const SelectFileDialogLinuxKde&) = delete; |
| SelectFileDialogLinuxKde& operator=(const SelectFileDialogLinuxKde&) = delete; |
| |
| protected: |
| ~SelectFileDialogLinuxKde() override; |
| |
| // BaseShellDialog implementation: |
| bool IsRunning(gfx::NativeWindow parent_window) const override; |
| |
| // SelectFileDialog implementation. |
| // |params| is user data we pass back via the Listener interface. |
| void 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, |
| void* params, |
| const GURL* caller) override; |
| |
| private: |
| bool HasMultipleFileTypeChoicesImpl() override; |
| |
| struct KDialogParams { |
| KDialogParams(const std::string& type, |
| const std::string& title, |
| const base::FilePath& default_path, |
| gfx::AcceleratedWidget parent, |
| bool file_operation, |
| bool multiple_selection) |
| : type(type), |
| title(title), |
| default_path(default_path), |
| parent(parent), |
| file_operation(file_operation), |
| multiple_selection(multiple_selection) {} |
| |
| std::string type; |
| std::string title; |
| base::FilePath default_path; |
| gfx::AcceleratedWidget parent; |
| bool file_operation; |
| bool multiple_selection; |
| }; |
| |
| struct KDialogOutputParams { |
| std::string output; |
| int exit_code; |
| }; |
| |
| // Get the filters from |file_types_| and concatenate them into |
| // |filter_string|. |
| std::string GetMimeTypeFilterString(); |
| |
| // Get KDialog command line representing the Argv array for KDialog. |
| void GetKDialogCommandLine(const std::string& type, |
| const std::string& title, |
| const base::FilePath& default_path, |
| gfx::AcceleratedWidget parent, |
| bool file_operation, |
| bool multiple_selection, |
| base::CommandLine* command_line); |
| |
| // Call KDialog on the FILE thread and return the results. |
| std::unique_ptr<KDialogOutputParams> CallKDialogOutput( |
| const KDialogParams& params); |
| |
| // Notifies the listener that a single file was chosen. |
| void FileSelected(const base::FilePath& path, void* params); |
| |
| // Notifies the listener that multiple files were chosen. |
| void MultiFilesSelected(const std::vector<base::FilePath>& files, |
| void* params); |
| |
| // Notifies the listener that no file was chosen (the action was canceled). |
| // Dialog is passed so we can find that |params| pointer that was passed to |
| // us when we were told to show the dialog. |
| void FileNotSelected(void* params); |
| |
| void CreateSelectFolderDialog(Type type, |
| const std::string& title, |
| const base::FilePath& default_path, |
| gfx::AcceleratedWidget parent, |
| void* params); |
| |
| void CreateFileOpenDialog(const std::string& title, |
| const base::FilePath& default_path, |
| gfx::AcceleratedWidget parent, |
| void* params); |
| |
| void CreateMultiFileOpenDialog(const std::string& title, |
| const base::FilePath& default_path, |
| gfx::AcceleratedWidget parent, |
| void* params); |
| |
| void CreateSaveAsDialog(const std::string& title, |
| const base::FilePath& default_path, |
| gfx::AcceleratedWidget parent, |
| void* params); |
| |
| // Common function for OnSelectSingleFileDialogResponse and |
| // OnSelectSingleFolderDialogResponse. |
| void SelectSingleFileHelper(void* params, |
| bool allow_folder, |
| std::unique_ptr<KDialogOutputParams> results); |
| |
| void OnSelectSingleFileDialogResponse( |
| gfx::AcceleratedWidget parent, |
| void* params, |
| std::unique_ptr<KDialogOutputParams> results); |
| void OnSelectMultiFileDialogResponse( |
| gfx::AcceleratedWidget parent, |
| void* params, |
| std::unique_ptr<KDialogOutputParams> results); |
| void OnSelectSingleFolderDialogResponse( |
| gfx::AcceleratedWidget parent, |
| void* params, |
| std::unique_ptr<KDialogOutputParams> results); |
| |
| // Should be either DESKTOP_ENVIRONMENT_KDE3, KDE4, KDE5, or KDE6. |
| base::nix::DesktopEnvironment desktop_; |
| |
| // The set of all parent windows for which we are currently running |
| // dialogs. This should only be accessed on the UI thread. |
| std::set<gfx::AcceleratedWidget> parents_; |
| |
| // Set to true if the kdialog version is new enough to support passing |
| // multiple extensions with descriptions, eliminating the need for the lossy |
| // conversion of extensions to mime-types. |
| bool kdialog_supports_multiple_extensions_ = false; |
| |
| // A task runner for blocking pipe reads. |
| scoped_refptr<base::SequencedTaskRunner> pipe_task_runner_; |
| |
| SEQUENCE_CHECKER(sequence_checker_); |
| }; |
| |
| // static |
| bool SelectFileDialogLinux::CheckKDEDialogWorksOnUIThread( |
| std::string& kdialog_version) { |
| // No choice. UI thread can't continue without an answer here. Fortunately we |
| // only do this once, the first time a file dialog is displayed. |
| base::ScopedAllowBlocking scoped_allow_blocking; |
| |
| base::CommandLine::StringVector cmd_vector; |
| cmd_vector.push_back(kKdialogBinary); |
| cmd_vector.push_back("--version"); |
| base::CommandLine command_line(cmd_vector); |
| return base::GetAppOutput(command_line, &kdialog_version); |
| } |
| |
| SelectFileDialog* NewSelectFileDialogLinuxKde( |
| SelectFileDialog::Listener* listener, |
| std::unique_ptr<ui::SelectFilePolicy> policy, |
| base::nix::DesktopEnvironment desktop, |
| const std::string& kdialog_version) { |
| return new SelectFileDialogLinuxKde(listener, std::move(policy), desktop, |
| kdialog_version); |
| } |
| |
| SelectFileDialogLinuxKde::SelectFileDialogLinuxKde( |
| Listener* listener, |
| std::unique_ptr<ui::SelectFilePolicy> policy, |
| base::nix::DesktopEnvironment desktop, |
| const std::string& kdialog_version) |
| : SelectFileDialogLinux(listener, std::move(policy)), |
| desktop_(desktop), |
| pipe_task_runner_(base::ThreadPool::CreateSequencedTaskRunner( |
| {base::MayBlock(), base::TaskPriority::USER_BLOCKING, |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) { |
| DCHECK(desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE3 || |
| desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE4 || |
| desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE5 || |
| desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE6); |
| // |kdialog_version| should be of the form "kdialog 1.2.3", so split on |
| // whitespace and then try to parse a version from the second piece. If |
| // parsing fails for whatever reason, we fall back to the behavior that works |
| // with all currently known versions of kdialog. |
| std::vector<base::StringPiece> version_pieces = base::SplitStringPiece( |
| kdialog_version, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| if (version_pieces.size() >= 2) { |
| base::Version parsed_version(version_pieces[1]); |
| if (parsed_version.IsValid()) { |
| kdialog_supports_multiple_extensions_ = |
| parsed_version >= base::Version("19.12"); |
| } |
| } |
| } |
| |
| SelectFileDialogLinuxKde::~SelectFileDialogLinuxKde() = default; |
| |
| bool SelectFileDialogLinuxKde::IsRunning( |
| gfx::NativeWindow parent_window) const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (parent_window && parent_window->GetHost()) { |
| auto window = parent_window->GetHost()->GetAcceleratedWidget(); |
| return parents_.find(window) != parents_.end(); |
| } |
| |
| return false; |
| } |
| |
| // We ignore |default_extension|. |
| void SelectFileDialogLinuxKde::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, |
| void* params, |
| const GURL* caller) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| set_type(type); |
| |
| gfx::AcceleratedWidget window = gfx::kNullAcceleratedWidget; |
| if (owning_window && owning_window->GetHost()) { |
| // |owning_window| can be null when user right-clicks on a downloadable item |
| // and chooses 'Open Link in New Tab' when 'Ask where to save each file |
| // before downloading.' preference is turned on. (http://crbug.com/29213) |
| window = owning_window->GetHost()->GetAcceleratedWidget(); |
| parents_.insert(window); |
| } |
| |
| std::string title_string = base::UTF16ToUTF8(title); |
| |
| set_file_type_index(file_type_index); |
| if (file_types) { |
| set_file_types(*file_types); |
| } else { |
| auto file_types_copy = SelectFileDialogLinux::file_types(); |
| file_types_copy.include_all_files = true; |
| set_file_types(file_types_copy); |
| } |
| |
| switch (type) { |
| case SELECT_FOLDER: |
| case SELECT_UPLOAD_FOLDER: |
| case SELECT_EXISTING_FOLDER: |
| CreateSelectFolderDialog(type, title_string, default_path, window, |
| params); |
| return; |
| case SELECT_OPEN_FILE: |
| CreateFileOpenDialog(title_string, default_path, window, params); |
| return; |
| case SELECT_OPEN_MULTI_FILE: |
| CreateMultiFileOpenDialog(title_string, default_path, window, params); |
| return; |
| case SELECT_SAVEAS_FILE: |
| CreateSaveAsDialog(title_string, default_path, window, params); |
| return; |
| case SELECT_NONE: |
| NOTREACHED(); |
| return; |
| } |
| } |
| |
| bool SelectFileDialogLinuxKde::HasMultipleFileTypeChoicesImpl() { |
| return file_types().extensions.size() > 1; |
| } |
| |
| std::string SelectFileDialogLinuxKde::GetMimeTypeFilterString() { |
| DCHECK(pipe_task_runner_->RunsTasksInCurrentSequence()); |
| |
| if (!kdialog_supports_multiple_extensions_) { |
| // We need a filter set because the same mime type can appear multiple |
| // times. |
| std::set<std::string> filter_set; |
| for (auto& extensions : file_types().extensions) { |
| for (auto& extension : extensions) { |
| if (!extension.empty()) { |
| std::string mime_type = base::nix::GetFileMimeType( |
| base::FilePath("name").ReplaceExtension(extension)); |
| filter_set.insert(mime_type); |
| } |
| } |
| } |
| std::vector<std::string> filter_vector(filter_set.cbegin(), |
| filter_set.cend()); |
| // Add the *.* filter, but only if we have added other filters (otherwise it |
| // is implied). It needs to be added last to avoid being picked as the |
| // default filter. |
| if (file_types().include_all_files && !file_types().extensions.empty()) { |
| DCHECK(filter_set.find("application/octet-stream") == filter_set.end()); |
| filter_vector.push_back("application/octet-stream"); |
| } |
| return base::JoinString(filter_vector, " "); |
| } |
| |
| std::vector<std::string> filters; |
| for (size_t i = 0; i < file_types().extensions.size(); ++i) { |
| std::set<std::string> extension_filters; |
| for (const auto& extension : file_types().extensions[i]) { |
| if (extension.empty()) |
| continue; |
| extension_filters.insert(std::string("*.") + extension); |
| } |
| |
| // We didn't find any non-empty extensions to filter on. |
| if (extension_filters.empty()) |
| continue; |
| |
| std::vector<std::string> extension_filters_vector(extension_filters.begin(), |
| extension_filters.end()); |
| |
| std::string description; |
| // The description vector may be blank, in which case we are supposed to |
| // use some sort of default description based on the filter. |
| if (i < file_types().extension_description_overrides.size()) { |
| description = |
| base::UTF16ToUTF8(file_types().extension_description_overrides[i]); |
| // Filter out any characters that would mess up kdialog's parsing. |
| base::ReplaceChars(description, "|()", "", &description); |
| } else { |
| // There is no system default filter description so we use |
| // the extensions themselves if the description is blank. |
| description = base::JoinString(extension_filters_vector, ","); |
| } |
| |
| filters.push_back(description + " (" + |
| base::JoinString(extension_filters_vector, " ") + ")"); |
| } |
| |
| if (file_types().include_all_files && !file_types().extensions.empty()) |
| filters.push_back(l10n_util::GetStringUTF8(IDS_SAVEAS_ALL_FILES) + " (*)"); |
| |
| return base::JoinString(filters, "|"); |
| } |
| |
| std::unique_ptr<SelectFileDialogLinuxKde::KDialogOutputParams> |
| SelectFileDialogLinuxKde::CallKDialogOutput(const KDialogParams& params) { |
| DCHECK(pipe_task_runner_->RunsTasksInCurrentSequence()); |
| base::CommandLine::StringVector cmd_vector; |
| cmd_vector.push_back(kKdialogBinary); |
| base::CommandLine command_line(cmd_vector); |
| GetKDialogCommandLine(params.type, params.title, params.default_path, |
| params.parent, params.file_operation, |
| params.multiple_selection, &command_line); |
| |
| auto results = std::make_unique<KDialogOutputParams>(); |
| // Get output from KDialog |
| base::GetAppOutputWithExitCode(command_line, &results->output, |
| &results->exit_code); |
| if (!results->output.empty()) |
| results->output.erase(results->output.size() - 1); |
| return results; |
| } |
| |
| void SelectFileDialogLinuxKde::GetKDialogCommandLine( |
| const std::string& type, |
| const std::string& title, |
| const base::FilePath& path, |
| gfx::AcceleratedWidget parent, |
| bool file_operation, |
| bool multiple_selection, |
| base::CommandLine* command_line) { |
| CHECK(command_line); |
| |
| // Attach to the current Chrome window. |
| if (parent != gfx::kNullAcceleratedWidget) { |
| command_line->AppendSwitchNative( |
| desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE3 ? "--embed" |
| : "--attach", |
| base::NumberToString(static_cast<uint32_t>(parent))); |
| } |
| |
| // Set the correct title for the dialog. |
| if (!title.empty()) |
| command_line->AppendSwitchNative("--title", title); |
| // Enable multiple file selection if we need to. |
| if (multiple_selection) { |
| command_line->AppendSwitch("--multiple"); |
| command_line->AppendSwitch("--separate-output"); |
| } |
| command_line->AppendSwitch(type); |
| // The path should never be empty. If it is, set it to PWD. |
| if (path.empty()) |
| command_line->AppendArgPath(base::FilePath(".")); |
| else |
| command_line->AppendArgPath(path); |
| // Depending on the type of the operation we need, get the path to the |
| // file/folder and set up mime type filters. |
| if (file_operation) |
| command_line->AppendArg(GetMimeTypeFilterString()); |
| VLOG(1) << "KDialog command line: " << command_line->GetCommandLineString(); |
| } |
| |
| void SelectFileDialogLinuxKde::FileSelected(const base::FilePath& path, |
| void* params) { |
| if (type() == SELECT_SAVEAS_FILE) |
| set_last_saved_path(path.DirName()); |
| else if (type() == SELECT_OPEN_FILE) |
| set_last_opened_path(path.DirName()); |
| else if (type() == SELECT_FOLDER || type() == SELECT_UPLOAD_FOLDER || |
| type() == SELECT_EXISTING_FOLDER) |
| set_last_opened_path(path); |
| else |
| NOTREACHED(); |
| if (listener_) { // What does the filter index actually do? |
| // TODO(dfilimon): Get a reasonable index value from somewhere. |
| listener_->FileSelected(SelectedFileInfo(path), 1, params); |
| } |
| } |
| |
| void SelectFileDialogLinuxKde::MultiFilesSelected( |
| const std::vector<base::FilePath>& files, |
| void* params) { |
| set_last_opened_path(files[0].DirName()); |
| if (listener_) |
| listener_->MultiFilesSelected(FilePathListToSelectedFileInfoList(files), |
| params); |
| } |
| |
| void SelectFileDialogLinuxKde::FileNotSelected(void* params) { |
| if (listener_) |
| listener_->FileSelectionCanceled(params); |
| } |
| |
| void SelectFileDialogLinuxKde::CreateSelectFolderDialog( |
| Type type, |
| const std::string& title, |
| const base::FilePath& default_path, |
| gfx::AcceleratedWidget parent, |
| void* params) { |
| int title_message_id = (type == SELECT_UPLOAD_FOLDER) |
| ? IDS_SELECT_UPLOAD_FOLDER_DIALOG_TITLE |
| : IDS_SELECT_FOLDER_DIALOG_TITLE; |
| pipe_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce( |
| &SelectFileDialogLinuxKde::CallKDialogOutput, this, |
| KDialogParams( |
| "--getexistingdirectory", GetTitle(title, title_message_id), |
| default_path.empty() ? *last_opened_path() : default_path, parent, |
| false, false)), |
| base::BindOnce( |
| &SelectFileDialogLinuxKde::OnSelectSingleFolderDialogResponse, this, |
| parent, params)); |
| } |
| |
| void SelectFileDialogLinuxKde::CreateFileOpenDialog( |
| const std::string& title, |
| const base::FilePath& default_path, |
| gfx::AcceleratedWidget parent, |
| void* params) { |
| pipe_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce( |
| &SelectFileDialogLinuxKde::CallKDialogOutput, this, |
| KDialogParams( |
| "--getopenfilename", GetTitle(title, IDS_OPEN_FILE_DIALOG_TITLE), |
| default_path.empty() ? *last_opened_path() : default_path, parent, |
| true, false)), |
| base::BindOnce( |
| &SelectFileDialogLinuxKde::OnSelectSingleFileDialogResponse, this, |
| parent, params)); |
| } |
| |
| void SelectFileDialogLinuxKde::CreateMultiFileOpenDialog( |
| const std::string& title, |
| const base::FilePath& default_path, |
| gfx::AcceleratedWidget parent, |
| void* params) { |
| pipe_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce( |
| &SelectFileDialogLinuxKde::CallKDialogOutput, this, |
| KDialogParams( |
| "--getopenfilename", GetTitle(title, IDS_OPEN_FILES_DIALOG_TITLE), |
| default_path.empty() ? *last_opened_path() : default_path, parent, |
| true, true)), |
| base::BindOnce(&SelectFileDialogLinuxKde::OnSelectMultiFileDialogResponse, |
| this, parent, params)); |
| } |
| |
| void SelectFileDialogLinuxKde::CreateSaveAsDialog( |
| const std::string& title, |
| const base::FilePath& default_path, |
| gfx::AcceleratedWidget parent, |
| void* params) { |
| pipe_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce( |
| &SelectFileDialogLinuxKde::CallKDialogOutput, this, |
| KDialogParams( |
| "--getsavefilename", GetTitle(title, IDS_SAVE_AS_DIALOG_TITLE), |
| default_path.empty() ? *last_saved_path() : default_path, parent, |
| true, false)), |
| base::BindOnce( |
| &SelectFileDialogLinuxKde::OnSelectSingleFileDialogResponse, this, |
| parent, params)); |
| } |
| |
| void SelectFileDialogLinuxKde::SelectSingleFileHelper( |
| void* params, |
| bool allow_folder, |
| std::unique_ptr<KDialogOutputParams> results) { |
| VLOG(1) << "[kdialog] SingleFileResponse: " << results->output; |
| if (results->exit_code || results->output.empty()) { |
| FileNotSelected(params); |
| return; |
| } |
| |
| base::FilePath path(results->output); |
| if (allow_folder) { |
| FileSelected(path, params); |
| return; |
| } |
| |
| if (CallDirectoryExistsOnUIThread(path)) |
| FileNotSelected(params); |
| else |
| FileSelected(path, params); |
| } |
| |
| void SelectFileDialogLinuxKde::OnSelectSingleFileDialogResponse( |
| gfx::AcceleratedWidget parent, |
| void* params, |
| std::unique_ptr<KDialogOutputParams> results) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| parents_.erase(parent); |
| SelectSingleFileHelper(params, false, std::move(results)); |
| } |
| |
| void SelectFileDialogLinuxKde::OnSelectSingleFolderDialogResponse( |
| gfx::AcceleratedWidget parent, |
| void* params, |
| std::unique_ptr<KDialogOutputParams> results) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| parents_.erase(parent); |
| SelectSingleFileHelper(params, true, std::move(results)); |
| } |
| |
| void SelectFileDialogLinuxKde::OnSelectMultiFileDialogResponse( |
| gfx::AcceleratedWidget parent, |
| void* params, |
| std::unique_ptr<KDialogOutputParams> results) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| VLOG(1) << "[kdialog] MultiFileResponse: " << results->output; |
| |
| parents_.erase(parent); |
| |
| if (results->exit_code || results->output.empty()) { |
| FileNotSelected(params); |
| return; |
| } |
| |
| std::vector<base::FilePath> filenames_fp; |
| for (const base::StringPiece& line : |
| base::SplitStringPiece(results->output, "\n", base::KEEP_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY)) { |
| base::FilePath path(line); |
| if (CallDirectoryExistsOnUIThread(path)) |
| continue; |
| filenames_fp.push_back(path); |
| } |
| |
| if (filenames_fp.empty()) { |
| FileNotSelected(params); |
| return; |
| } |
| MultiFilesSelected(filenames_fp, params); |
| } |
| |
| } // namespace ui |