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 "chrome/browser/file_select_helper.h"
6
7#include <string>
8#include <utility>
9
10#include "base/bind.h"
11#include "base/files/file_enumerator.h"
12#include "base/files/file_util.h"
13#include "base/strings/string_split.h"
14#include "base/strings/string_util.h"
15#include "base/strings/utf_string_conversions.h"
16#include "chrome/browser/platform_util.h"
17#include "chrome/browser/profiles/profile.h"
18#include "chrome/browser/ui/browser.h"
19#include "chrome/browser/ui/browser_list.h"
20#include "chrome/browser/ui/chrome_select_file_policy.h"
21#include "chrome/grit/generated_resources.h"
22#include "content/public/browser/browser_thread.h"
23#include "content/public/browser/notification_details.h"
24#include "content/public/browser/notification_source.h"
25#include "content/public/browser/notification_types.h"
26#include "content/public/browser/render_view_host.h"
27#include "content/public/browser/render_widget_host_view.h"
28#include "content/public/browser/web_contents.h"
29#include "content/public/common/file_chooser_params.h"
30#include "net/base/mime_util.h"
31#include "ui/base/l10n/l10n_util.h"
32#include "ui/shell_dialogs/selected_file_info.h"
33
34using content::BrowserThread;
35using content::FileChooserParams;
36using content::RenderViewHost;
37using content::RenderWidgetHost;
38using content::WebContents;
39
40namespace {
41
42// There is only one file-selection happening at any given time,
43// so we allocate an enumeration ID for that purpose.  All IDs from
44// the renderer must start at 0 and increase.
45const int kFileSelectEnumerationId = -1;
46
47void NotifyRenderViewHost(RenderViewHost* render_view_host,
48                          const std::vector<ui::SelectedFileInfo>& files,
49                          FileChooserParams::Mode dialog_mode) {
50  render_view_host->FilesSelectedInChooser(files, dialog_mode);
51}
52
53// Converts a list of FilePaths to a list of ui::SelectedFileInfo.
54std::vector<ui::SelectedFileInfo> FilePathListToSelectedFileInfoList(
55    const std::vector<base::FilePath>& paths) {
56  std::vector<ui::SelectedFileInfo> selected_files;
57  for (size_t i = 0; i < paths.size(); ++i) {
58    selected_files.push_back(
59        ui::SelectedFileInfo(paths[i], paths[i]));
60  }
61  return selected_files;
62}
63
64}  // namespace
65
66struct FileSelectHelper::ActiveDirectoryEnumeration {
67  ActiveDirectoryEnumeration() : rvh_(NULL) {}
68
69  scoped_ptr<DirectoryListerDispatchDelegate> delegate_;
70  scoped_ptr<net::DirectoryLister> lister_;
71  RenderViewHost* rvh_;
72  std::vector<base::FilePath> results_;
73};
74
75FileSelectHelper::FileSelectHelper(Profile* profile)
76    : profile_(profile),
77      render_view_host_(NULL),
78      web_contents_(NULL),
79      select_file_dialog_(),
80      select_file_types_(),
81      dialog_type_(ui::SelectFileDialog::SELECT_OPEN_FILE),
82      dialog_mode_(FileChooserParams::Open) {
83}
84
85FileSelectHelper::~FileSelectHelper() {
86  // There may be pending file dialogs, we need to tell them that we've gone
87  // away so they don't try and call back to us.
88  if (select_file_dialog_.get())
89    select_file_dialog_->ListenerDestroyed();
90
91  // Stop any pending directory enumeration, prevent a callback, and free
92  // allocated memory.
93  std::map<int, ActiveDirectoryEnumeration*>::iterator iter;
94  for (iter = directory_enumerations_.begin();
95       iter != directory_enumerations_.end();
96       ++iter) {
97    iter->second->lister_.reset();
98    delete iter->second;
99  }
100}
101
102void FileSelectHelper::DirectoryListerDispatchDelegate::OnListFile(
103    const net::DirectoryLister::DirectoryListerData& data) {
104  parent_->OnListFile(id_, data);
105}
106
107void FileSelectHelper::DirectoryListerDispatchDelegate::OnListDone(int error) {
108  parent_->OnListDone(id_, error);
109}
110
111void FileSelectHelper::FileSelected(const base::FilePath& path,
112                                    int index, void* params) {
113  FileSelectedWithExtraInfo(ui::SelectedFileInfo(path, path), index, params);
114}
115
116void FileSelectHelper::FileSelectedWithExtraInfo(
117    const ui::SelectedFileInfo& file,
118    int index,
119    void* params) {
120  if (!render_view_host_)
121    return;
122
123  profile_->set_last_selected_directory(file.file_path.DirName());
124
125  const base::FilePath& path = file.local_path;
126  if (dialog_type_ == ui::SelectFileDialog::SELECT_UPLOAD_FOLDER) {
127    StartNewEnumeration(path, kFileSelectEnumerationId, render_view_host_);
128    return;
129  }
130
131  std::vector<ui::SelectedFileInfo> files;
132  files.push_back(file);
133  NotifyRenderViewHost(render_view_host_, files, dialog_mode_);
134
135  // No members should be accessed from here on.
136  RunFileChooserEnd();
137}
138
139void FileSelectHelper::MultiFilesSelected(
140    const std::vector<base::FilePath>& files,
141    void* params) {
142  std::vector<ui::SelectedFileInfo> selected_files =
143      FilePathListToSelectedFileInfoList(files);
144
145  MultiFilesSelectedWithExtraInfo(selected_files, params);
146}
147
148void FileSelectHelper::MultiFilesSelectedWithExtraInfo(
149    const std::vector<ui::SelectedFileInfo>& files,
150    void* params) {
151  if (!files.empty())
152    profile_->set_last_selected_directory(files[0].file_path.DirName());
153  if (!render_view_host_)
154    return;
155
156  NotifyRenderViewHost(render_view_host_, files, dialog_mode_);
157
158  // No members should be accessed from here on.
159  RunFileChooserEnd();
160}
161
162void FileSelectHelper::FileSelectionCanceled(void* params) {
163  if (!render_view_host_)
164    return;
165
166  // If the user cancels choosing a file to upload we pass back an
167  // empty vector.
168  NotifyRenderViewHost(
169      render_view_host_, std::vector<ui::SelectedFileInfo>(),
170      dialog_mode_);
171
172  // No members should be accessed from here on.
173  RunFileChooserEnd();
174}
175
176void FileSelectHelper::StartNewEnumeration(const base::FilePath& path,
177                                           int request_id,
178                                           RenderViewHost* render_view_host) {
179  scoped_ptr<ActiveDirectoryEnumeration> entry(new ActiveDirectoryEnumeration);
180  entry->rvh_ = render_view_host;
181  entry->delegate_.reset(new DirectoryListerDispatchDelegate(this, request_id));
182  entry->lister_.reset(new net::DirectoryLister(path,
183                                                true,
184                                                net::DirectoryLister::NO_SORT,
185                                                entry->delegate_.get()));
186  if (!entry->lister_->Start()) {
187    if (request_id == kFileSelectEnumerationId)
188      FileSelectionCanceled(NULL);
189    else
190      render_view_host->DirectoryEnumerationFinished(request_id,
191                                                     entry->results_);
192  } else {
193    directory_enumerations_[request_id] = entry.release();
194  }
195}
196
197void FileSelectHelper::OnListFile(
198    int id,
199    const net::DirectoryLister::DirectoryListerData& data) {
200  ActiveDirectoryEnumeration* entry = directory_enumerations_[id];
201
202  // Directory upload only cares about files.
203  if (data.info.IsDirectory())
204    return;
205
206  entry->results_.push_back(data.path);
207}
208
209void FileSelectHelper::OnListDone(int id, int error) {
210  // This entry needs to be cleaned up when this function is done.
211  scoped_ptr<ActiveDirectoryEnumeration> entry(directory_enumerations_[id]);
212  directory_enumerations_.erase(id);
213  if (!entry->rvh_)
214    return;
215  if (error) {
216    FileSelectionCanceled(NULL);
217    return;
218  }
219
220  std::vector<ui::SelectedFileInfo> selected_files =
221      FilePathListToSelectedFileInfoList(entry->results_);
222
223  if (id == kFileSelectEnumerationId)
224    NotifyRenderViewHost(entry->rvh_, selected_files, dialog_mode_);
225  else
226    entry->rvh_->DirectoryEnumerationFinished(id, entry->results_);
227
228  EnumerateDirectoryEnd();
229}
230
231scoped_ptr<ui::SelectFileDialog::FileTypeInfo>
232FileSelectHelper::GetFileTypesFromAcceptType(
233    const std::vector<base::string16>& accept_types) {
234  scoped_ptr<ui::SelectFileDialog::FileTypeInfo> base_file_type(
235      new ui::SelectFileDialog::FileTypeInfo());
236  if (accept_types.empty())
237    return base_file_type.Pass();
238
239  // Create FileTypeInfo and pre-allocate for the first extension list.
240  scoped_ptr<ui::SelectFileDialog::FileTypeInfo> file_type(
241      new ui::SelectFileDialog::FileTypeInfo(*base_file_type));
242  file_type->include_all_files = true;
243  file_type->extensions.resize(1);
244  std::vector<base::FilePath::StringType>* extensions =
245      &file_type->extensions.back();
246
247  // Find the corresponding extensions.
248  int valid_type_count = 0;
249  int description_id = 0;
250  for (size_t i = 0; i < accept_types.size(); ++i) {
251    std::string ascii_type = base::UTF16ToASCII(accept_types[i]);
252    if (!IsAcceptTypeValid(ascii_type))
253      continue;
254
255    size_t old_extension_size = extensions->size();
256    if (ascii_type[0] == '.') {
257      // If the type starts with a period it is assumed to be a file extension
258      // so we just have to add it to the list.
259      base::FilePath::StringType ext(ascii_type.begin(), ascii_type.end());
260      extensions->push_back(ext.substr(1));
261    } else {
262      if (ascii_type == "image/*")
263        description_id = IDS_IMAGE_FILES;
264      else if (ascii_type == "audio/*")
265        description_id = IDS_AUDIO_FILES;
266      else if (ascii_type == "video/*")
267        description_id = IDS_VIDEO_FILES;
268
269      net::GetExtensionsForMimeType(ascii_type, extensions);
270    }
271
272    if (extensions->size() > old_extension_size)
273      valid_type_count++;
274  }
275
276  // If no valid extension is added, bail out.
277  if (valid_type_count == 0)
278    return base_file_type.Pass();
279
280  // Use a generic description "Custom Files" if either of the following is
281  // true:
282  // 1) There're multiple types specified, like "audio/*,video/*"
283  // 2) There're multiple extensions for a MIME type without parameter, like
284  //    "ehtml,shtml,htm,html" for "text/html". On Windows, the select file
285  //    dialog uses the first extension in the list to form the description,
286  //    like "EHTML Files". This is not what we want.
287  if (valid_type_count > 1 ||
288      (valid_type_count == 1 && description_id == 0 && extensions->size() > 1))
289    description_id = IDS_CUSTOM_FILES;
290
291  if (description_id) {
292    file_type->extension_description_overrides.push_back(
293        l10n_util::GetStringUTF16(description_id));
294  }
295
296  return file_type.Pass();
297}
298
299// static
300void FileSelectHelper::RunFileChooser(content::WebContents* tab,
301                                      const FileChooserParams& params) {
302  Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext());
303  // FileSelectHelper will keep itself alive until it sends the result message.
304  scoped_refptr<FileSelectHelper> file_select_helper(
305      new FileSelectHelper(profile));
306  file_select_helper->RunFileChooser(tab->GetRenderViewHost(), tab, params);
307}
308
309// static
310void FileSelectHelper::EnumerateDirectory(content::WebContents* tab,
311                                          int request_id,
312                                          const base::FilePath& path) {
313  Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext());
314  // FileSelectHelper will keep itself alive until it sends the result message.
315  scoped_refptr<FileSelectHelper> file_select_helper(
316      new FileSelectHelper(profile));
317  file_select_helper->EnumerateDirectory(
318      request_id, tab->GetRenderViewHost(), path);
319}
320
321void FileSelectHelper::RunFileChooser(RenderViewHost* render_view_host,
322                                      content::WebContents* web_contents,
323                                      const FileChooserParams& params) {
324  DCHECK(!render_view_host_);
325  DCHECK(!web_contents_);
326  render_view_host_ = render_view_host;
327  web_contents_ = web_contents;
328  notification_registrar_.RemoveAll();
329  notification_registrar_.Add(
330      this, content::NOTIFICATION_RENDER_WIDGET_HOST_DESTROYED,
331      content::Source<RenderWidgetHost>(render_view_host_));
332  notification_registrar_.Add(
333      this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED,
334      content::Source<WebContents>(web_contents_));
335
336  BrowserThread::PostTask(
337      BrowserThread::FILE, FROM_HERE,
338      base::Bind(&FileSelectHelper::RunFileChooserOnFileThread, this, params));
339
340  // Because this class returns notifications to the RenderViewHost, it is
341  // difficult for callers to know how long to keep a reference to this
342  // instance. We AddRef() here to keep the instance alive after we return
343  // to the caller, until the last callback is received from the file dialog.
344  // At that point, we must call RunFileChooserEnd().
345  AddRef();
346}
347
348void FileSelectHelper::RunFileChooserOnFileThread(
349    const FileChooserParams& params) {
350  select_file_types_ = GetFileTypesFromAcceptType(params.accept_types);
351
352  BrowserThread::PostTask(
353      BrowserThread::UI, FROM_HERE,
354      base::Bind(&FileSelectHelper::RunFileChooserOnUIThread, this, params));
355}
356
357void FileSelectHelper::RunFileChooserOnUIThread(
358    const FileChooserParams& params) {
359  if (!render_view_host_ || !web_contents_) {
360    // If the renderer was destroyed before we started, just cancel the
361    // operation.
362    RunFileChooserEnd();
363    return;
364  }
365
366  select_file_dialog_ = ui::SelectFileDialog::Create(
367      this, new ChromeSelectFilePolicy(web_contents_));
368  if (!select_file_dialog_.get())
369    return;
370
371  dialog_mode_ = params.mode;
372  switch (params.mode) {
373    case FileChooserParams::Open:
374      dialog_type_ = ui::SelectFileDialog::SELECT_OPEN_FILE;
375      break;
376    case FileChooserParams::OpenMultiple:
377      dialog_type_ = ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE;
378      break;
379    case FileChooserParams::UploadFolder:
380      dialog_type_ = ui::SelectFileDialog::SELECT_UPLOAD_FOLDER;
381      break;
382    case FileChooserParams::Save:
383      dialog_type_ = ui::SelectFileDialog::SELECT_SAVEAS_FILE;
384      break;
385    default:
386      // Prevent warning.
387      dialog_type_ = ui::SelectFileDialog::SELECT_OPEN_FILE;
388      NOTREACHED();
389  }
390
391  base::FilePath default_file_name = params.default_file_name.IsAbsolute() ?
392      params.default_file_name :
393      profile_->last_selected_directory().Append(params.default_file_name);
394
395  gfx::NativeWindow owning_window =
396      platform_util::GetTopLevel(render_view_host_->GetView()->GetNativeView());
397
398#if defined(OS_ANDROID)
399  // Android needs the original MIME types and an additional capture value.
400  std::pair<std::vector<base::string16>, bool> accept_types =
401      std::make_pair(params.accept_types, params.capture);
402#endif
403
404  select_file_dialog_->SelectFile(
405      dialog_type_,
406      params.title,
407      default_file_name,
408      select_file_types_.get(),
409      select_file_types_.get() && !select_file_types_->extensions.empty()
410          ? 1
411          : 0,  // 1-based index of default extension to show.
412      base::FilePath::StringType(),
413      owning_window,
414#if defined(OS_ANDROID)
415      &accept_types);
416#else
417      NULL);
418#endif
419
420  select_file_types_.reset();
421}
422
423// This method is called when we receive the last callback from the file
424// chooser dialog. Perform any cleanup and release the reference we added
425// in RunFileChooser().
426void FileSelectHelper::RunFileChooserEnd() {
427  render_view_host_ = NULL;
428  web_contents_ = NULL;
429  Release();
430}
431
432void FileSelectHelper::EnumerateDirectory(int request_id,
433                                          RenderViewHost* render_view_host,
434                                          const base::FilePath& path) {
435
436  // Because this class returns notifications to the RenderViewHost, it is
437  // difficult for callers to know how long to keep a reference to this
438  // instance. We AddRef() here to keep the instance alive after we return
439  // to the caller, until the last callback is received from the enumeration
440  // code. At that point, we must call EnumerateDirectoryEnd().
441  AddRef();
442  StartNewEnumeration(path, request_id, render_view_host);
443}
444
445// This method is called when we receive the last callback from the enumeration
446// code. Perform any cleanup and release the reference we added in
447// EnumerateDirectory().
448void FileSelectHelper::EnumerateDirectoryEnd() {
449  Release();
450}
451
452void FileSelectHelper::Observe(int type,
453                               const content::NotificationSource& source,
454                               const content::NotificationDetails& details) {
455  switch (type) {
456    case content::NOTIFICATION_RENDER_WIDGET_HOST_DESTROYED: {
457      DCHECK(content::Source<RenderWidgetHost>(source).ptr() ==
458             render_view_host_);
459      render_view_host_ = NULL;
460      break;
461    }
462
463    case content::NOTIFICATION_WEB_CONTENTS_DESTROYED: {
464      DCHECK(content::Source<WebContents>(source).ptr() == web_contents_);
465      web_contents_ = NULL;
466      break;
467    }
468
469    default:
470      NOTREACHED();
471  }
472}
473
474// static
475bool FileSelectHelper::IsAcceptTypeValid(const std::string& accept_type) {
476  // TODO(raymes): This only does some basic checks, extend to test more cases.
477  // A 1 character accept type will always be invalid (either a "." in the case
478  // of an extension or a "/" in the case of a MIME type).
479  std::string unused;
480  if (accept_type.length() <= 1 ||
481      base::StringToLowerASCII(accept_type) != accept_type ||
482      base::TrimWhitespaceASCII(accept_type, base::TRIM_ALL, &unused) !=
483          base::TRIM_NONE) {
484    return false;
485  }
486  return true;
487}
488