1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include <gdk/gdkx.h>
6 #include <gtk/gtk.h>
7 #include <stddef.h>
8 
9 #include <memory>
10 #include <set>
11 
12 #include "base/bind.h"
13 #include "base/bind_helpers.h"
14 #include "base/command_line.h"
15 #include "base/logging.h"
16 #include "base/macros.h"
17 #include "base/nix/mime_util_xdg.h"
18 #include "base/nix/xdg_util.h"
19 #include "base/process/launch.h"
20 #include "base/strings/string_number_conversions.h"
21 #include "base/strings/string_split.h"
22 #include "base/strings/string_util.h"
23 #include "base/strings/utf_string_conversions.h"
24 #include "base/task/post_task.h"
25 #include "base/task/task_traits.h"
26 #include "base/task/thread_pool.h"
27 #include "base/threading/thread_restrictions.h"
28 #include "ui/aura/window_tree_host.h"
29 #include "ui/base/l10n/l10n_util.h"
30 #include "ui/gfx/x/x11.h"
31 #include "ui/gtk/select_file_dialog_impl.h"
32 #include "ui/strings/grit/ui_strings.h"
33 
34 namespace {
35 
GetTitle(const std::string & title,int message_id)36 std::string GetTitle(const std::string& title, int message_id) {
37   return title.empty() ? l10n_util::GetStringUTF8(message_id) : title;
38 }
39 
40 const char kKdialogBinary[] = "kdialog";
41 
42 }  // namespace
43 
44 namespace gtk {
45 
46 // Implementation of SelectFileDialog that shows a KDE common dialog for
47 // choosing a file or folder. This acts as a modal dialog.
48 class SelectFileDialogImplKDE : public SelectFileDialogImpl {
49  public:
50   SelectFileDialogImplKDE(Listener* listener,
51                           std::unique_ptr<ui::SelectFilePolicy> policy,
52                           base::nix::DesktopEnvironment desktop);
53 
54  protected:
55   ~SelectFileDialogImplKDE() override;
56 
57   // BaseShellDialog implementation:
58   bool IsRunning(gfx::NativeWindow parent_window) const override;
59 
60   // SelectFileDialog implementation.
61   // |params| is user data we pass back via the Listener interface.
62   void SelectFileImpl(Type type,
63                       const base::string16& title,
64                       const base::FilePath& default_path,
65                       const FileTypeInfo* file_types,
66                       int file_type_index,
67                       const base::FilePath::StringType& default_extension,
68                       gfx::NativeWindow owning_window,
69                       void* params) override;
70 
71  private:
72   bool HasMultipleFileTypeChoicesImpl() override;
73 
74   struct KDialogParams {
KDialogParamsgtk::SelectFileDialogImplKDE::KDialogParams75     KDialogParams(const std::string& type,
76                   const std::string& title,
77                   const base::FilePath& default_path,
78                   XID parent,
79                   bool file_operation,
80                   bool multiple_selection)
81         : type(type),
82           title(title),
83           default_path(default_path),
84           parent(parent),
85           file_operation(file_operation),
86           multiple_selection(multiple_selection) {}
87 
88     std::string type;
89     std::string title;
90     base::FilePath default_path;
91     XID parent;
92     bool file_operation;
93     bool multiple_selection;
94   };
95 
96   struct KDialogOutputParams {
97     std::string output;
98     int exit_code;
99   };
100 
101   // Get the filters from |file_types_| and concatenate them into
102   // |filter_string|.
103   std::string GetMimeTypeFilterString();
104 
105   // Get KDialog command line representing the Argv array for KDialog.
106   void GetKDialogCommandLine(const std::string& type,
107                              const std::string& title,
108                              const base::FilePath& default_path,
109                              XID parent,
110                              bool file_operation,
111                              bool multiple_selection,
112                              base::CommandLine* command_line);
113 
114   // Call KDialog on the FILE thread and return the results.
115   std::unique_ptr<KDialogOutputParams> CallKDialogOutput(
116       const KDialogParams& params);
117 
118   // Notifies the listener that a single file was chosen.
119   void FileSelected(const base::FilePath& path, void* params);
120 
121   // Notifies the listener that multiple files were chosen.
122   void MultiFilesSelected(const std::vector<base::FilePath>& files,
123                           void* params);
124 
125   // Notifies the listener that no file was chosen (the action was canceled).
126   // Dialog is passed so we can find that |params| pointer that was passed to
127   // us when we were told to show the dialog.
128   void FileNotSelected(void* params);
129 
130   void CreateSelectFolderDialog(Type type,
131                                 const std::string& title,
132                                 const base::FilePath& default_path,
133                                 XID parent,
134                                 void* params);
135 
136   void CreateFileOpenDialog(const std::string& title,
137                             const base::FilePath& default_path,
138                             XID parent,
139                             void* params);
140 
141   void CreateMultiFileOpenDialog(const std::string& title,
142                                  const base::FilePath& default_path,
143                                  XID parent,
144                                  void* params);
145 
146   void CreateSaveAsDialog(const std::string& title,
147                           const base::FilePath& default_path,
148                           XID parent,
149                           void* params);
150 
151   // Common function for OnSelectSingleFileDialogResponse and
152   // OnSelectSingleFolderDialogResponse.
153   void SelectSingleFileHelper(void* params,
154                               bool allow_folder,
155                               std::unique_ptr<KDialogOutputParams> results);
156 
157   void OnSelectSingleFileDialogResponse(
158       XID parent,
159       void* params,
160       std::unique_ptr<KDialogOutputParams> results);
161   void OnSelectMultiFileDialogResponse(
162       XID parent,
163       void* params,
164       std::unique_ptr<KDialogOutputParams> results);
165   void OnSelectSingleFolderDialogResponse(
166       XID parent,
167       void* params,
168       std::unique_ptr<KDialogOutputParams> results);
169 
170   // Should be either DESKTOP_ENVIRONMENT_KDE3, KDE4, or KDE5.
171   base::nix::DesktopEnvironment desktop_;
172 
173   // The set of all parent windows for which we are currently running
174   // dialogs. This should only be accessed on the UI thread.
175   std::set<XID> parents_;
176 
177   // A task runner for blocking pipe reads.
178   scoped_refptr<base::SequencedTaskRunner> pipe_task_runner_;
179 
180   SEQUENCE_CHECKER(sequence_checker_);
181 
182   DISALLOW_COPY_AND_ASSIGN(SelectFileDialogImplKDE);
183 };
184 
185 // static
CheckKDEDialogWorksOnUIThread()186 bool SelectFileDialogImpl::CheckKDEDialogWorksOnUIThread() {
187   // No choice. UI thread can't continue without an answer here. Fortunately we
188   // only do this once, the first time a file dialog is displayed.
189   base::ThreadRestrictions::ScopedAllowIO allow_io;
190 
191   base::CommandLine::StringVector cmd_vector;
192   cmd_vector.push_back(kKdialogBinary);
193   cmd_vector.push_back("--version");
194   base::CommandLine command_line(cmd_vector);
195   std::string dummy;
196   return base::GetAppOutput(command_line, &dummy);
197 }
198 
199 // static
NewSelectFileDialogImplKDE(Listener * listener,std::unique_ptr<ui::SelectFilePolicy> policy,base::nix::DesktopEnvironment desktop)200 SelectFileDialogImpl* SelectFileDialogImpl::NewSelectFileDialogImplKDE(
201     Listener* listener,
202     std::unique_ptr<ui::SelectFilePolicy> policy,
203     base::nix::DesktopEnvironment desktop) {
204   return new SelectFileDialogImplKDE(listener, std::move(policy), desktop);
205 }
206 
SelectFileDialogImplKDE(Listener * listener,std::unique_ptr<ui::SelectFilePolicy> policy,base::nix::DesktopEnvironment desktop)207 SelectFileDialogImplKDE::SelectFileDialogImplKDE(
208     Listener* listener,
209     std::unique_ptr<ui::SelectFilePolicy> policy,
210     base::nix::DesktopEnvironment desktop)
211     : SelectFileDialogImpl(listener, std::move(policy)),
212       desktop_(desktop),
213       pipe_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
214           {base::MayBlock(), base::TaskPriority::USER_BLOCKING,
215            base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) {
216   DCHECK(desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE3 ||
217          desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE4 ||
218          desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE5);
219 }
220 
~SelectFileDialogImplKDE()221 SelectFileDialogImplKDE::~SelectFileDialogImplKDE() {}
222 
IsRunning(gfx::NativeWindow parent_window) const223 bool SelectFileDialogImplKDE::IsRunning(gfx::NativeWindow parent_window) const {
224   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
225   if (parent_window && parent_window->GetHost()) {
226     XID xid = parent_window->GetHost()->GetAcceleratedWidget();
227     return parents_.find(xid) != parents_.end();
228   }
229 
230   return false;
231 }
232 
233 // We ignore |default_extension|.
SelectFileImpl(Type type,const base::string16 & 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)234 void SelectFileDialogImplKDE::SelectFileImpl(
235     Type type,
236     const base::string16& title,
237     const base::FilePath& default_path,
238     const FileTypeInfo* file_types,
239     int file_type_index,
240     const base::FilePath::StringType& default_extension,
241     gfx::NativeWindow owning_window,
242     void* params) {
243   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
244   type_ = type;
245 
246   XID window_xid = x11::None;
247   if (owning_window && owning_window->GetHost()) {
248     // |owning_window| can be null when user right-clicks on a downloadable item
249     // and chooses 'Open Link in New Tab' when 'Ask where to save each file
250     // before downloading.' preference is turned on. (http://crbug.com/29213)
251     window_xid = owning_window->GetHost()->GetAcceleratedWidget();
252     parents_.insert(window_xid);
253   }
254 
255   std::string title_string = base::UTF16ToUTF8(title);
256 
257   file_type_index_ = file_type_index;
258   if (file_types)
259     file_types_ = *file_types;
260   else
261     file_types_.include_all_files = true;
262 
263   switch (type) {
264     case SELECT_FOLDER:
265     case SELECT_UPLOAD_FOLDER:
266     case SELECT_EXISTING_FOLDER:
267       CreateSelectFolderDialog(type, title_string, default_path, window_xid,
268                                params);
269       return;
270     case SELECT_OPEN_FILE:
271       CreateFileOpenDialog(title_string, default_path, window_xid, params);
272       return;
273     case SELECT_OPEN_MULTI_FILE:
274       CreateMultiFileOpenDialog(title_string, default_path, window_xid, params);
275       return;
276     case SELECT_SAVEAS_FILE:
277       CreateSaveAsDialog(title_string, default_path, window_xid, params);
278       return;
279     case SELECT_NONE:
280       NOTREACHED();
281       return;
282   }
283 }
284 
HasMultipleFileTypeChoicesImpl()285 bool SelectFileDialogImplKDE::HasMultipleFileTypeChoicesImpl() {
286   return file_types_.extensions.size() > 1;
287 }
288 
GetMimeTypeFilterString()289 std::string SelectFileDialogImplKDE::GetMimeTypeFilterString() {
290   DCHECK(pipe_task_runner_->RunsTasksInCurrentSequence());
291   // We need a filter set because the same mime type can appear multiple times.
292   std::set<std::string> filter_set;
293   for (size_t i = 0; i < file_types_.extensions.size(); ++i) {
294     for (size_t j = 0; j < file_types_.extensions[i].size(); ++j) {
295       if (!file_types_.extensions[i][j].empty()) {
296         std::string mime_type =
297             base::nix::GetFileMimeType(base::FilePath("name").ReplaceExtension(
298                 file_types_.extensions[i][j]));
299         filter_set.insert(mime_type);
300       }
301     }
302   }
303   std::vector<std::string> filter_vector(filter_set.cbegin(),
304                                          filter_set.cend());
305   // Add the *.* filter, but only if we have added other filters (otherwise it
306   // is implied). It needs to be added last to avoid being picked as the default
307   // filter.
308   if (file_types_.include_all_files && !file_types_.extensions.empty()) {
309     DCHECK(filter_set.find("application/octet-stream") == filter_set.end());
310     filter_vector.push_back("application/octet-stream");
311   }
312   return base::JoinString(filter_vector, " ");
313 }
314 
315 std::unique_ptr<SelectFileDialogImplKDE::KDialogOutputParams>
CallKDialogOutput(const KDialogParams & params)316 SelectFileDialogImplKDE::CallKDialogOutput(const KDialogParams& params) {
317   DCHECK(pipe_task_runner_->RunsTasksInCurrentSequence());
318   base::CommandLine::StringVector cmd_vector;
319   cmd_vector.push_back(kKdialogBinary);
320   base::CommandLine command_line(cmd_vector);
321   GetKDialogCommandLine(params.type, params.title, params.default_path,
322                         params.parent, params.file_operation,
323                         params.multiple_selection, &command_line);
324 
325   auto results = std::make_unique<KDialogOutputParams>();
326   // Get output from KDialog
327   base::GetAppOutputWithExitCode(command_line, &results->output,
328                                  &results->exit_code);
329   if (!results->output.empty())
330     results->output.erase(results->output.size() - 1);
331   return results;
332 }
333 
GetKDialogCommandLine(const std::string & type,const std::string & title,const base::FilePath & path,XID parent,bool file_operation,bool multiple_selection,base::CommandLine * command_line)334 void SelectFileDialogImplKDE::GetKDialogCommandLine(
335     const std::string& type,
336     const std::string& title,
337     const base::FilePath& path,
338     XID parent,
339     bool file_operation,
340     bool multiple_selection,
341     base::CommandLine* command_line) {
342   CHECK(command_line);
343 
344   // Attach to the current Chrome window.
345   if (parent != x11::None) {
346     command_line->AppendSwitchNative(
347         desktop_ == base::nix::DESKTOP_ENVIRONMENT_KDE3 ? "--embed"
348                                                         : "--attach",
349         base::NumberToString(parent));
350   }
351 
352   // Set the correct title for the dialog.
353   if (!title.empty())
354     command_line->AppendSwitchNative("--title", title);
355   // Enable multiple file selection if we need to.
356   if (multiple_selection) {
357     command_line->AppendSwitch("--multiple");
358     command_line->AppendSwitch("--separate-output");
359   }
360   command_line->AppendSwitch(type);
361   // The path should never be empty. If it is, set it to PWD.
362   if (path.empty())
363     command_line->AppendArgPath(base::FilePath("."));
364   else
365     command_line->AppendArgPath(path);
366   // Depending on the type of the operation we need, get the path to the
367   // file/folder and set up mime type filters.
368   if (file_operation)
369     command_line->AppendArg(GetMimeTypeFilterString());
370   VLOG(1) << "KDialog command line: " << command_line->GetCommandLineString();
371 }
372 
FileSelected(const base::FilePath & path,void * params)373 void SelectFileDialogImplKDE::FileSelected(const base::FilePath& path,
374                                            void* params) {
375   if (type_ == SELECT_SAVEAS_FILE)
376     *last_saved_path_ = path.DirName();
377   else if (type_ == SELECT_OPEN_FILE)
378     *last_opened_path_ = path.DirName();
379   else if (type_ == SELECT_FOLDER || type_ == SELECT_UPLOAD_FOLDER ||
380            type_ == SELECT_EXISTING_FOLDER)
381     *last_opened_path_ = path;
382   else
383     NOTREACHED();
384   if (listener_) {  // What does the filter index actually do?
385     // TODO(dfilimon): Get a reasonable index value from somewhere.
386     listener_->FileSelected(path, 1, params);
387   }
388 }
389 
MultiFilesSelected(const std::vector<base::FilePath> & files,void * params)390 void SelectFileDialogImplKDE::MultiFilesSelected(
391     const std::vector<base::FilePath>& files,
392     void* params) {
393   *last_opened_path_ = files[0].DirName();
394   if (listener_)
395     listener_->MultiFilesSelected(files, params);
396 }
397 
FileNotSelected(void * params)398 void SelectFileDialogImplKDE::FileNotSelected(void* params) {
399   if (listener_)
400     listener_->FileSelectionCanceled(params);
401 }
402 
CreateSelectFolderDialog(Type type,const std::string & title,const base::FilePath & default_path,XID parent,void * params)403 void SelectFileDialogImplKDE::CreateSelectFolderDialog(
404     Type type,
405     const std::string& title,
406     const base::FilePath& default_path,
407     XID parent,
408     void* params) {
409   int title_message_id = (type == SELECT_UPLOAD_FOLDER)
410                              ? IDS_SELECT_UPLOAD_FOLDER_DIALOG_TITLE
411                              : IDS_SELECT_FOLDER_DIALOG_TITLE;
412   pipe_task_runner_->PostTaskAndReplyWithResult(
413       FROM_HERE,
414       base::BindOnce(
415           &SelectFileDialogImplKDE::CallKDialogOutput, this,
416           KDialogParams(
417               "--getexistingdirectory", GetTitle(title, title_message_id),
418               default_path.empty() ? *last_opened_path_ : default_path, parent,
419               false, false)),
420       base::BindOnce(
421           &SelectFileDialogImplKDE::OnSelectSingleFolderDialogResponse, this,
422           parent, params));
423 }
424 
CreateFileOpenDialog(const std::string & title,const base::FilePath & default_path,XID parent,void * params)425 void SelectFileDialogImplKDE::CreateFileOpenDialog(
426     const std::string& title,
427     const base::FilePath& default_path,
428     XID parent,
429     void* params) {
430   pipe_task_runner_->PostTaskAndReplyWithResult(
431       FROM_HERE,
432       base::BindOnce(
433           &SelectFileDialogImplKDE::CallKDialogOutput, this,
434           KDialogParams(
435               "--getopenfilename", GetTitle(title, IDS_OPEN_FILE_DIALOG_TITLE),
436               default_path.empty() ? *last_opened_path_ : default_path, parent,
437               true, false)),
438       base::BindOnce(&SelectFileDialogImplKDE::OnSelectSingleFileDialogResponse,
439                      this, parent, params));
440 }
441 
CreateMultiFileOpenDialog(const std::string & title,const base::FilePath & default_path,XID parent,void * params)442 void SelectFileDialogImplKDE::CreateMultiFileOpenDialog(
443     const std::string& title,
444     const base::FilePath& default_path,
445     XID parent,
446     void* params) {
447   pipe_task_runner_->PostTaskAndReplyWithResult(
448       FROM_HERE,
449       base::BindOnce(
450           &SelectFileDialogImplKDE::CallKDialogOutput, this,
451           KDialogParams(
452               "--getopenfilename", GetTitle(title, IDS_OPEN_FILES_DIALOG_TITLE),
453               default_path.empty() ? *last_opened_path_ : default_path, parent,
454               true, true)),
455       base::BindOnce(&SelectFileDialogImplKDE::OnSelectMultiFileDialogResponse,
456                      this, parent, params));
457 }
458 
CreateSaveAsDialog(const std::string & title,const base::FilePath & default_path,XID parent,void * params)459 void SelectFileDialogImplKDE::CreateSaveAsDialog(
460     const std::string& title,
461     const base::FilePath& default_path,
462     XID parent,
463     void* params) {
464   pipe_task_runner_->PostTaskAndReplyWithResult(
465       FROM_HERE,
466       base::BindOnce(
467           &SelectFileDialogImplKDE::CallKDialogOutput, this,
468           KDialogParams("--getsavefilename",
469                         GetTitle(title, IDS_SAVE_AS_DIALOG_TITLE),
470                         default_path.empty() ? *last_saved_path_ : default_path,
471                         parent, true, false)),
472       base::BindOnce(&SelectFileDialogImplKDE::OnSelectSingleFileDialogResponse,
473                      this, parent, params));
474 }
475 
SelectSingleFileHelper(void * params,bool allow_folder,std::unique_ptr<KDialogOutputParams> results)476 void SelectFileDialogImplKDE::SelectSingleFileHelper(
477     void* params,
478     bool allow_folder,
479     std::unique_ptr<KDialogOutputParams> results) {
480   VLOG(1) << "[kdialog] SingleFileResponse: " << results->output;
481   if (results->exit_code || results->output.empty()) {
482     FileNotSelected(params);
483     return;
484   }
485 
486   base::FilePath path(results->output);
487   if (allow_folder) {
488     FileSelected(path, params);
489     return;
490   }
491 
492   if (CallDirectoryExistsOnUIThread(path))
493     FileNotSelected(params);
494   else
495     FileSelected(path, params);
496 }
497 
OnSelectSingleFileDialogResponse(XID parent,void * params,std::unique_ptr<KDialogOutputParams> results)498 void SelectFileDialogImplKDE::OnSelectSingleFileDialogResponse(
499     XID parent,
500     void* params,
501     std::unique_ptr<KDialogOutputParams> results) {
502   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
503   parents_.erase(parent);
504   SelectSingleFileHelper(params, false, std::move(results));
505 }
506 
OnSelectSingleFolderDialogResponse(XID parent,void * params,std::unique_ptr<KDialogOutputParams> results)507 void SelectFileDialogImplKDE::OnSelectSingleFolderDialogResponse(
508     XID parent,
509     void* params,
510     std::unique_ptr<KDialogOutputParams> results) {
511   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
512   parents_.erase(parent);
513   SelectSingleFileHelper(params, true, std::move(results));
514 }
515 
OnSelectMultiFileDialogResponse(XID parent,void * params,std::unique_ptr<KDialogOutputParams> results)516 void SelectFileDialogImplKDE::OnSelectMultiFileDialogResponse(
517     XID parent,
518     void* params,
519     std::unique_ptr<KDialogOutputParams> results) {
520   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
521   VLOG(1) << "[kdialog] MultiFileResponse: " << results->output;
522 
523   parents_.erase(parent);
524 
525   if (results->exit_code || results->output.empty()) {
526     FileNotSelected(params);
527     return;
528   }
529 
530   std::vector<base::FilePath> filenames_fp;
531   for (const base::StringPiece& line :
532        base::SplitStringPiece(results->output, "\n", base::KEEP_WHITESPACE,
533                               base::SPLIT_WANT_NONEMPTY)) {
534     base::FilePath path(line);
535     if (CallDirectoryExistsOnUIThread(path))
536       continue;
537     filenames_fp.push_back(path);
538   }
539 
540   if (filenames_fp.empty()) {
541     FileNotSelected(params);
542     return;
543   }
544   MultiFilesSelected(filenames_fp, params);
545 }
546 
547 }  // namespace gtk
548