1// Copyright (c) 2011 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
9#include "base/file_util.h"
10#include "base/string_split.h"
11#include "base/string_util.h"
12#include "base/utf_string_conversions.h"
13#include "chrome/browser/platform_util.h"
14#include "chrome/browser/profiles/profile.h"
15#include "content/browser/child_process_security_policy.h"
16#include "content/browser/renderer_host/render_process_host.h"
17#include "content/browser/renderer_host/render_view_host.h"
18#include "content/browser/renderer_host/render_widget_host_view.h"
19#include "content/browser/tab_contents/tab_contents.h"
20#include "chrome/browser/ui/browser.h"
21#include "chrome/browser/ui/browser_list.h"
22#include "content/common/notification_details.h"
23#include "content/common/notification_source.h"
24#include "content/common/view_messages.h"
25#include "grit/generated_resources.h"
26#include "net/base/mime_util.h"
27#include "ui/base/l10n/l10n_util.h"
28
29namespace {
30
31// There is only one file-selection happening at any given time,
32// so we allocate an enumeration ID for that purpose.  All IDs from
33// the renderer must start at 0 and increase.
34static const int kFileSelectEnumerationId = -1;
35}
36
37struct FileSelectHelper::ActiveDirectoryEnumeration {
38  ActiveDirectoryEnumeration() {}
39
40  scoped_ptr<DirectoryListerDispatchDelegate> delegate_;
41  scoped_refptr<net::DirectoryLister> lister_;
42  RenderViewHost* rvh_;
43  std::vector<FilePath> results_;
44};
45
46FileSelectHelper::FileSelectHelper(Profile* profile)
47    : profile_(profile),
48      render_view_host_(NULL),
49      select_file_dialog_(),
50      dialog_type_(SelectFileDialog::SELECT_OPEN_FILE) {
51}
52
53FileSelectHelper::~FileSelectHelper() {
54  // There may be pending file dialogs, we need to tell them that we've gone
55  // away so they don't try and call back to us.
56  if (select_file_dialog_.get())
57    select_file_dialog_->ListenerDestroyed();
58
59  // Stop any pending directory enumeration, prevent a callback, and free
60  // allocated memory.
61  std::map<int, ActiveDirectoryEnumeration*>::iterator iter;
62  for (iter = directory_enumerations_.begin();
63       iter != directory_enumerations_.end();
64       ++iter) {
65    if (iter->second->lister_.get()) {
66      iter->second->lister_->set_delegate(NULL);
67      iter->second->lister_->Cancel();
68    }
69    delete iter->second;
70  }
71}
72
73void FileSelectHelper::FileSelected(const FilePath& path,
74                                    int index, void* params) {
75  if (!render_view_host_)
76    return;
77
78  profile_->set_last_selected_directory(path.DirName());
79
80  if (dialog_type_ == SelectFileDialog::SELECT_FOLDER) {
81    StartNewEnumeration(path, kFileSelectEnumerationId, render_view_host_);
82    return;
83  }
84
85  std::vector<FilePath> files;
86  files.push_back(path);
87  render_view_host_->FilesSelectedInChooser(files);
88  // We are done with this showing of the dialog.
89  render_view_host_ = NULL;
90}
91
92void FileSelectHelper::MultiFilesSelected(const std::vector<FilePath>& files,
93                                          void* params) {
94  if (!files.empty())
95    profile_->set_last_selected_directory(files[0].DirName());
96  if (!render_view_host_)
97    return;
98
99  render_view_host_->FilesSelectedInChooser(files);
100  // We are done with this showing of the dialog.
101  render_view_host_ = NULL;
102}
103
104void FileSelectHelper::FileSelectionCanceled(void* params) {
105  if (!render_view_host_)
106    return;
107
108  // If the user cancels choosing a file to upload we pass back an
109  // empty vector.
110  render_view_host_->FilesSelectedInChooser(std::vector<FilePath>());
111
112  // We are done with this showing of the dialog.
113  render_view_host_ = NULL;
114}
115
116void FileSelectHelper::StartNewEnumeration(const FilePath& path,
117                                           int request_id,
118                                           RenderViewHost* render_view_host) {
119  scoped_ptr<ActiveDirectoryEnumeration> entry(new ActiveDirectoryEnumeration);
120  entry->rvh_ = render_view_host;
121  entry->delegate_.reset(new DirectoryListerDispatchDelegate(this, request_id));
122  entry->lister_ = new net::DirectoryLister(path,
123                                            true,
124                                            net::DirectoryLister::NO_SORT,
125                                            entry->delegate_.get());
126  if (!entry->lister_->Start()) {
127    if (request_id == kFileSelectEnumerationId)
128      FileSelectionCanceled(NULL);
129    else
130      render_view_host->DirectoryEnumerationFinished(request_id,
131                                                     entry->results_);
132  } else {
133    directory_enumerations_[request_id] = entry.release();
134  }
135}
136
137void FileSelectHelper::OnListFile(
138    int id,
139    const net::DirectoryLister::DirectoryListerData& data) {
140  ActiveDirectoryEnumeration* entry = directory_enumerations_[id];
141
142  // Directory upload returns directories via a "." file, so that
143  // empty directories are included.  This util call just checks
144  // the flags in the structure; there's no file I/O going on.
145  if (file_util::FileEnumerator::IsDirectory(data.info))
146    entry->results_.push_back(data.path.Append(FILE_PATH_LITERAL(".")));
147  else
148    entry->results_.push_back(data.path);
149}
150
151void FileSelectHelper::OnListDone(int id, int error) {
152  // This entry needs to be cleaned up when this function is done.
153  scoped_ptr<ActiveDirectoryEnumeration> entry(directory_enumerations_[id]);
154  directory_enumerations_.erase(id);
155  if (!entry->rvh_)
156    return;
157  if (error) {
158    FileSelectionCanceled(NULL);
159    return;
160  }
161  if (id == kFileSelectEnumerationId)
162    entry->rvh_->FilesSelectedInChooser(entry->results_);
163  else
164    entry->rvh_->DirectoryEnumerationFinished(id, entry->results_);
165}
166
167SelectFileDialog::FileTypeInfo* FileSelectHelper::GetFileTypesFromAcceptType(
168    const string16& accept_types) {
169  if (accept_types.empty())
170    return NULL;
171
172  // Split the accept-type string on commas.
173  std::vector<string16> mime_types;
174  base::SplitStringUsingSubstr(accept_types, ASCIIToUTF16(","), &mime_types);
175  if (mime_types.empty())
176    return NULL;
177
178  // Create FileTypeInfo and pre-allocate for the first extension list.
179  scoped_ptr<SelectFileDialog::FileTypeInfo> file_type(
180      new SelectFileDialog::FileTypeInfo());
181  file_type->include_all_files = true;
182  file_type->extensions.resize(1);
183  std::vector<FilePath::StringType>* extensions = &file_type->extensions.back();
184
185  // Find the correspondinge extensions.
186  int valid_type_count = 0;
187  int description_id = 0;
188  for (size_t i = 0; i < mime_types.size(); ++i) {
189    string16 mime_type = mime_types[i];
190    std::string ascii_mime_type = StringToLowerASCII(UTF16ToASCII(mime_type));
191
192    TrimWhitespace(ascii_mime_type, TRIM_ALL, &ascii_mime_type);
193    if (ascii_mime_type.empty())
194      continue;
195
196    size_t old_extension_size = extensions->size();
197    if (ascii_mime_type == "image/*") {
198      description_id = IDS_IMAGE_FILES;
199      net::GetImageExtensions(extensions);
200    } else if (ascii_mime_type == "audio/*") {
201      description_id = IDS_AUDIO_FILES;
202      net::GetAudioExtensions(extensions);
203    } else if (ascii_mime_type == "video/*") {
204      description_id = IDS_VIDEO_FILES;
205      net::GetVideoExtensions(extensions);
206    } else {
207      net::GetExtensionsForMimeType(ascii_mime_type, extensions);
208    }
209
210    if (extensions->size() > old_extension_size)
211      valid_type_count++;
212  }
213
214  // If no valid extension is added, bail out.
215  if (valid_type_count == 0)
216    return NULL;
217
218  // Use a generic description "Custom Files" if either of the following is
219  // true:
220  // 1) There're multiple types specified, like "audio/*,video/*"
221  // 2) There're multiple extensions for a MIME type without parameter, like
222  //    "ehtml,shtml,htm,html" for "text/html". On Windows, the select file
223  //    dialog uses the first extension in the list to form the description,
224  //    like "EHTML Files". This is not what we want.
225  if (valid_type_count > 1 ||
226      (valid_type_count == 1 && description_id == 0 && extensions->size() > 1))
227    description_id = IDS_CUSTOM_FILES;
228
229  if (description_id) {
230    file_type->extension_description_overrides.push_back(
231        l10n_util::GetStringUTF16(description_id));
232  }
233
234  return file_type.release();
235}
236
237void FileSelectHelper::RunFileChooser(
238    RenderViewHost* render_view_host,
239    TabContents* tab_contents,
240    const ViewHostMsg_RunFileChooser_Params& params) {
241  DCHECK(!render_view_host_);
242  render_view_host_ = render_view_host;
243  notification_registrar_.RemoveAll();
244  notification_registrar_.Add(this,
245                              NotificationType::RENDER_WIDGET_HOST_DESTROYED,
246                              Source<RenderViewHost>(render_view_host));
247
248  if (!select_file_dialog_.get())
249    select_file_dialog_ = SelectFileDialog::Create(this);
250
251  switch (params.mode) {
252    case ViewHostMsg_RunFileChooser_Mode::Open:
253      dialog_type_ = SelectFileDialog::SELECT_OPEN_FILE;
254      break;
255    case ViewHostMsg_RunFileChooser_Mode::OpenMultiple:
256      dialog_type_ = SelectFileDialog::SELECT_OPEN_MULTI_FILE;
257      break;
258    case ViewHostMsg_RunFileChooser_Mode::OpenFolder:
259      dialog_type_ = SelectFileDialog::SELECT_FOLDER;
260      break;
261    case ViewHostMsg_RunFileChooser_Mode::Save:
262      dialog_type_ = SelectFileDialog::SELECT_SAVEAS_FILE;
263      break;
264    default:
265      dialog_type_ = SelectFileDialog::SELECT_OPEN_FILE;  // Prevent warning.
266      NOTREACHED();
267  }
268  scoped_ptr<SelectFileDialog::FileTypeInfo> file_types(
269      GetFileTypesFromAcceptType(params.accept_types));
270  FilePath default_file_name = params.default_file_name;
271  if (default_file_name.empty())
272    default_file_name = profile_->last_selected_directory();
273
274  gfx::NativeWindow owning_window =
275      platform_util::GetTopLevel(render_view_host_->view()->GetNativeView());
276
277  select_file_dialog_->SelectFile(dialog_type_,
278                                  params.title,
279                                  default_file_name,
280                                  file_types.get(),
281                                  file_types.get() ? 1 : 0,  // 1-based index.
282                                  FILE_PATH_LITERAL(""),
283                                  tab_contents,
284                                  owning_window,
285                                  NULL);
286}
287
288void FileSelectHelper::EnumerateDirectory(int request_id,
289                                          RenderViewHost* render_view_host,
290                                          const FilePath& path) {
291  DCHECK_NE(kFileSelectEnumerationId, request_id);
292  StartNewEnumeration(path, request_id, render_view_host);
293}
294
295void FileSelectHelper::Observe(NotificationType type,
296                               const NotificationSource& source,
297                               const NotificationDetails& details) {
298  DCHECK(type == NotificationType::RENDER_WIDGET_HOST_DESTROYED);
299  DCHECK(Details<RenderViewHost>(details).ptr() == render_view_host_);
300  render_view_host_ = NULL;
301}
302
303FileSelectObserver::FileSelectObserver(TabContents* tab_contents)
304    : TabContentsObserver(tab_contents) {
305}
306
307FileSelectObserver::~FileSelectObserver() {
308}
309
310bool FileSelectObserver::OnMessageReceived(const IPC::Message& message) {
311  bool handled = true;
312  IPC_BEGIN_MESSAGE_MAP(FileSelectObserver, message)
313    IPC_MESSAGE_HANDLER(ViewHostMsg_RunFileChooser, OnRunFileChooser)
314    IPC_MESSAGE_HANDLER(ViewHostMsg_EnumerateDirectory, OnEnumerateDirectory)
315    IPC_MESSAGE_UNHANDLED(handled = false)
316  IPC_END_MESSAGE_MAP()
317
318  return handled;
319}
320
321void FileSelectObserver::OnRunFileChooser(
322    const ViewHostMsg_RunFileChooser_Params& params) {
323  if (!file_select_helper_.get())
324    file_select_helper_.reset(new FileSelectHelper(tab_contents()->profile()));
325  file_select_helper_->RunFileChooser(tab_contents()->render_view_host(),
326                                      tab_contents(),
327                                      params);
328}
329
330void FileSelectObserver::OnEnumerateDirectory(int request_id,
331                                              const FilePath& path) {
332  ChildProcessSecurityPolicy* policy =
333      ChildProcessSecurityPolicy::GetInstance();
334  if (!policy->CanReadDirectory(
335          tab_contents()->render_view_host()->process()->id(),
336          path)) {
337    return;
338  }
339
340  if (!file_select_helper_.get())
341    file_select_helper_.reset(new FileSelectHelper(tab_contents()->profile()));
342  file_select_helper_->EnumerateDirectory(request_id,
343                                          tab_contents()->render_view_host(),
344                                          path);
345}
346