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.
5 #include "chrome/browser/file_select_helper.h"
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_file_info.h"
30 #include "content/public/common/file_chooser_params.h"
31 #include "net/base/mime_util.h"
32 #include "ui/base/l10n/l10n_util.h"
33 #include "ui/shell_dialogs/selected_file_info.h"
35 using content::BrowserThread
;
36 using content::FileChooserParams
;
37 using content::RenderViewHost
;
38 using content::RenderWidgetHost
;
39 using content::WebContents
;
43 // There is only one file-selection happening at any given time,
44 // so we allocate an enumeration ID for that purpose. All IDs from
45 // the renderer must start at 0 and increase.
46 const int kFileSelectEnumerationId
= -1;
48 void NotifyRenderViewHost(
49 RenderViewHost
* render_view_host
,
50 const std::vector
<ui::SelectedFileInfo
>& files
,
51 FileChooserParams::Mode dialog_mode
) {
52 std::vector
<content::FileChooserFileInfo
> chooser_files
;
53 for (size_t i
= 0; i
< files
.size(); ++i
) {
54 content::FileChooserFileInfo chooser_file
;
55 chooser_file
.file_path
= files
[i
].local_path
;
56 chooser_file
.display_name
= files
[i
].display_name
;
57 chooser_files
.push_back(chooser_file
);
59 render_view_host
->FilesSelectedInChooser(chooser_files
, dialog_mode
);
62 // Converts a list of FilePaths to a list of ui::SelectedFileInfo.
63 std::vector
<ui::SelectedFileInfo
> FilePathListToSelectedFileInfoList(
64 const std::vector
<base::FilePath
>& paths
) {
65 std::vector
<ui::SelectedFileInfo
> selected_files
;
66 for (size_t i
= 0; i
< paths
.size(); ++i
) {
67 selected_files
.push_back(
68 ui::SelectedFileInfo(paths
[i
], paths
[i
]));
70 return selected_files
;
73 void DeleteFiles(const std::vector
<base::FilePath
>& paths
) {
74 for (auto& file_path
: paths
)
75 base::DeleteFile(file_path
, false);
80 struct FileSelectHelper::ActiveDirectoryEnumeration
{
81 ActiveDirectoryEnumeration() : rvh_(NULL
) {}
83 scoped_ptr
<DirectoryListerDispatchDelegate
> delegate_
;
84 scoped_ptr
<net::DirectoryLister
> lister_
;
86 std::vector
<base::FilePath
> results_
;
89 FileSelectHelper::FileSelectHelper(Profile
* profile
)
91 render_view_host_(NULL
),
93 select_file_dialog_(),
95 dialog_type_(ui::SelectFileDialog::SELECT_OPEN_FILE
),
96 dialog_mode_(FileChooserParams::Open
) {
99 FileSelectHelper::~FileSelectHelper() {
100 // There may be pending file dialogs, we need to tell them that we've gone
101 // away so they don't try and call back to us.
102 if (select_file_dialog_
.get())
103 select_file_dialog_
->ListenerDestroyed();
105 // Stop any pending directory enumeration, prevent a callback, and free
107 std::map
<int, ActiveDirectoryEnumeration
*>::iterator iter
;
108 for (iter
= directory_enumerations_
.begin();
109 iter
!= directory_enumerations_
.end();
111 iter
->second
->lister_
.reset();
116 void FileSelectHelper::DirectoryListerDispatchDelegate::OnListFile(
117 const net::DirectoryLister::DirectoryListerData
& data
) {
118 parent_
->OnListFile(id_
, data
);
121 void FileSelectHelper::DirectoryListerDispatchDelegate::OnListDone(int error
) {
122 parent_
->OnListDone(id_
, error
);
125 void FileSelectHelper::FileSelected(const base::FilePath
& path
,
126 int index
, void* params
) {
127 FileSelectedWithExtraInfo(ui::SelectedFileInfo(path
, path
), index
, params
);
130 void FileSelectHelper::FileSelectedWithExtraInfo(
131 const ui::SelectedFileInfo
& file
,
134 profile_
->set_last_selected_directory(file
.file_path
.DirName());
136 if (!render_view_host_
) {
141 const base::FilePath
& path
= file
.local_path
;
142 if (dialog_type_
== ui::SelectFileDialog::SELECT_UPLOAD_FOLDER
) {
143 StartNewEnumeration(path
, kFileSelectEnumerationId
, render_view_host_
);
147 std::vector
<ui::SelectedFileInfo
> files
;
148 files
.push_back(file
);
150 #if defined(OS_MACOSX) && !defined(OS_IOS)
151 content::BrowserThread::PostTask(
152 content::BrowserThread::FILE_USER_BLOCKING
,
154 base::Bind(&FileSelectHelper::ProcessSelectedFilesMac
, this, files
));
156 NotifyRenderViewHostAndEnd(files
);
157 #endif // defined(OS_MACOSX) && !defined(OS_IOS)
160 void FileSelectHelper::MultiFilesSelected(
161 const std::vector
<base::FilePath
>& files
,
163 std::vector
<ui::SelectedFileInfo
> selected_files
=
164 FilePathListToSelectedFileInfoList(files
);
166 MultiFilesSelectedWithExtraInfo(selected_files
, params
);
169 void FileSelectHelper::MultiFilesSelectedWithExtraInfo(
170 const std::vector
<ui::SelectedFileInfo
>& files
,
173 profile_
->set_last_selected_directory(files
[0].file_path
.DirName());
175 #if defined(OS_MACOSX) && !defined(OS_IOS)
176 content::BrowserThread::PostTask(
177 content::BrowserThread::FILE_USER_BLOCKING
,
179 base::Bind(&FileSelectHelper::ProcessSelectedFilesMac
, this, files
));
181 NotifyRenderViewHostAndEnd(files
);
182 #endif // defined(OS_MACOSX) && !defined(OS_IOS)
185 void FileSelectHelper::FileSelectionCanceled(void* params
) {
186 NotifyRenderViewHostAndEnd(std::vector
<ui::SelectedFileInfo
>());
189 void FileSelectHelper::StartNewEnumeration(const base::FilePath
& path
,
191 RenderViewHost
* render_view_host
) {
192 scoped_ptr
<ActiveDirectoryEnumeration
> entry(new ActiveDirectoryEnumeration
);
193 entry
->rvh_
= render_view_host
;
194 entry
->delegate_
.reset(new DirectoryListerDispatchDelegate(this, request_id
));
195 entry
->lister_
.reset(new net::DirectoryLister(path
,
197 net::DirectoryLister::NO_SORT
,
198 entry
->delegate_
.get()));
199 if (!entry
->lister_
->Start()) {
200 if (request_id
== kFileSelectEnumerationId
)
201 FileSelectionCanceled(NULL
);
203 render_view_host
->DirectoryEnumerationFinished(request_id
,
206 directory_enumerations_
[request_id
] = entry
.release();
210 void FileSelectHelper::OnListFile(
212 const net::DirectoryLister::DirectoryListerData
& data
) {
213 ActiveDirectoryEnumeration
* entry
= directory_enumerations_
[id
];
215 // Directory upload only cares about files.
216 if (data
.info
.IsDirectory())
219 entry
->results_
.push_back(data
.path
);
222 void FileSelectHelper::OnListDone(int id
, int error
) {
223 // This entry needs to be cleaned up when this function is done.
224 scoped_ptr
<ActiveDirectoryEnumeration
> entry(directory_enumerations_
[id
]);
225 directory_enumerations_
.erase(id
);
229 FileSelectionCanceled(NULL
);
233 std::vector
<ui::SelectedFileInfo
> selected_files
=
234 FilePathListToSelectedFileInfoList(entry
->results_
);
236 if (id
== kFileSelectEnumerationId
)
237 NotifyRenderViewHost(entry
->rvh_
, selected_files
, dialog_mode_
);
239 entry
->rvh_
->DirectoryEnumerationFinished(id
, entry
->results_
);
241 EnumerateDirectoryEnd();
244 void FileSelectHelper::NotifyRenderViewHostAndEnd(
245 const std::vector
<ui::SelectedFileInfo
>& files
) {
246 if (render_view_host_
)
247 NotifyRenderViewHost(render_view_host_
, files
, dialog_mode_
);
249 // No members should be accessed from here on.
253 void FileSelectHelper::DeleteTemporaryFiles() {
254 BrowserThread::PostTask(BrowserThread::FILE,
256 base::Bind(&DeleteFiles
, temporary_files_
));
257 temporary_files_
.clear();
260 scoped_ptr
<ui::SelectFileDialog::FileTypeInfo
>
261 FileSelectHelper::GetFileTypesFromAcceptType(
262 const std::vector
<base::string16
>& accept_types
) {
263 scoped_ptr
<ui::SelectFileDialog::FileTypeInfo
> base_file_type(
264 new ui::SelectFileDialog::FileTypeInfo());
265 if (accept_types
.empty())
266 return base_file_type
.Pass();
268 // Create FileTypeInfo and pre-allocate for the first extension list.
269 scoped_ptr
<ui::SelectFileDialog::FileTypeInfo
> file_type(
270 new ui::SelectFileDialog::FileTypeInfo(*base_file_type
));
271 file_type
->include_all_files
= true;
272 file_type
->extensions
.resize(1);
273 std::vector
<base::FilePath::StringType
>* extensions
=
274 &file_type
->extensions
.back();
276 // Find the corresponding extensions.
277 int valid_type_count
= 0;
278 int description_id
= 0;
279 for (size_t i
= 0; i
< accept_types
.size(); ++i
) {
280 std::string ascii_type
= base::UTF16ToASCII(accept_types
[i
]);
281 if (!IsAcceptTypeValid(ascii_type
))
284 size_t old_extension_size
= extensions
->size();
285 if (ascii_type
[0] == '.') {
286 // If the type starts with a period it is assumed to be a file extension
287 // so we just have to add it to the list.
288 base::FilePath::StringType
ext(ascii_type
.begin(), ascii_type
.end());
289 extensions
->push_back(ext
.substr(1));
291 if (ascii_type
== "image/*")
292 description_id
= IDS_IMAGE_FILES
;
293 else if (ascii_type
== "audio/*")
294 description_id
= IDS_AUDIO_FILES
;
295 else if (ascii_type
== "video/*")
296 description_id
= IDS_VIDEO_FILES
;
298 net::GetExtensionsForMimeType(ascii_type
, extensions
);
301 if (extensions
->size() > old_extension_size
)
305 // If no valid extension is added, bail out.
306 if (valid_type_count
== 0)
307 return base_file_type
.Pass();
309 // Use a generic description "Custom Files" if either of the following is
311 // 1) There're multiple types specified, like "audio/*,video/*"
312 // 2) There're multiple extensions for a MIME type without parameter, like
313 // "ehtml,shtml,htm,html" for "text/html". On Windows, the select file
314 // dialog uses the first extension in the list to form the description,
315 // like "EHTML Files". This is not what we want.
316 if (valid_type_count
> 1 ||
317 (valid_type_count
== 1 && description_id
== 0 && extensions
->size() > 1))
318 description_id
= IDS_CUSTOM_FILES
;
320 if (description_id
) {
321 file_type
->extension_description_overrides
.push_back(
322 l10n_util::GetStringUTF16(description_id
));
325 return file_type
.Pass();
329 void FileSelectHelper::RunFileChooser(content::WebContents
* tab
,
330 const FileChooserParams
& params
) {
331 Profile
* profile
= Profile::FromBrowserContext(tab
->GetBrowserContext());
332 // FileSelectHelper will keep itself alive until it sends the result message.
333 scoped_refptr
<FileSelectHelper
> file_select_helper(
334 new FileSelectHelper(profile
));
335 file_select_helper
->RunFileChooser(tab
->GetRenderViewHost(), tab
, params
);
339 void FileSelectHelper::EnumerateDirectory(content::WebContents
* tab
,
341 const base::FilePath
& path
) {
342 Profile
* profile
= Profile::FromBrowserContext(tab
->GetBrowserContext());
343 // FileSelectHelper will keep itself alive until it sends the result message.
344 scoped_refptr
<FileSelectHelper
> file_select_helper(
345 new FileSelectHelper(profile
));
346 file_select_helper
->EnumerateDirectory(
347 request_id
, tab
->GetRenderViewHost(), path
);
350 void FileSelectHelper::RunFileChooser(RenderViewHost
* render_view_host
,
351 content::WebContents
* web_contents
,
352 const FileChooserParams
& params
) {
353 DCHECK(!render_view_host_
);
354 DCHECK(!web_contents_
);
355 render_view_host_
= render_view_host
;
356 web_contents_
= web_contents
;
357 notification_registrar_
.RemoveAll();
358 notification_registrar_
.Add(this,
359 content::NOTIFICATION_RENDER_VIEW_HOST_CHANGED
,
360 content::Source
<WebContents
>(web_contents_
));
361 notification_registrar_
.Add(
363 content::NOTIFICATION_RENDER_WIDGET_HOST_DESTROYED
,
364 content::Source
<RenderWidgetHost
>(render_view_host_
));
365 notification_registrar_
.Add(
366 this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED
,
367 content::Source
<WebContents
>(web_contents_
));
369 BrowserThread::PostTask(
370 BrowserThread::FILE, FROM_HERE
,
371 base::Bind(&FileSelectHelper::RunFileChooserOnFileThread
, this, params
));
373 // Because this class returns notifications to the RenderViewHost, it is
374 // difficult for callers to know how long to keep a reference to this
375 // instance. We AddRef() here to keep the instance alive after we return
376 // to the caller, until the last callback is received from the file dialog.
377 // At that point, we must call RunFileChooserEnd().
381 void FileSelectHelper::RunFileChooserOnFileThread(
382 const FileChooserParams
& params
) {
383 select_file_types_
= GetFileTypesFromAcceptType(params
.accept_types
);
385 BrowserThread::PostTask(
386 BrowserThread::UI
, FROM_HERE
,
387 base::Bind(&FileSelectHelper::RunFileChooserOnUIThread
, this, params
));
390 void FileSelectHelper::RunFileChooserOnUIThread(
391 const FileChooserParams
& params
) {
392 if (!render_view_host_
|| !web_contents_
) {
393 // If the renderer was destroyed before we started, just cancel the
399 select_file_dialog_
= ui::SelectFileDialog::Create(
400 this, new ChromeSelectFilePolicy(web_contents_
));
401 if (!select_file_dialog_
.get())
404 dialog_mode_
= params
.mode
;
405 switch (params
.mode
) {
406 case FileChooserParams::Open
:
407 dialog_type_
= ui::SelectFileDialog::SELECT_OPEN_FILE
;
409 case FileChooserParams::OpenMultiple
:
410 dialog_type_
= ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE
;
412 case FileChooserParams::UploadFolder
:
413 dialog_type_
= ui::SelectFileDialog::SELECT_UPLOAD_FOLDER
;
415 case FileChooserParams::Save
:
416 dialog_type_
= ui::SelectFileDialog::SELECT_SAVEAS_FILE
;
420 dialog_type_
= ui::SelectFileDialog::SELECT_OPEN_FILE
;
424 base::FilePath default_file_name
= params
.default_file_name
.IsAbsolute() ?
425 params
.default_file_name
:
426 profile_
->last_selected_directory().Append(params
.default_file_name
);
428 gfx::NativeWindow owning_window
=
429 platform_util::GetTopLevel(render_view_host_
->GetView()->GetNativeView());
431 #if defined(OS_ANDROID)
432 // Android needs the original MIME types and an additional capture value.
433 std::pair
<std::vector
<base::string16
>, bool> accept_types
=
434 std::make_pair(params
.accept_types
, params
.capture
);
437 select_file_dialog_
->SelectFile(
441 select_file_types_
.get(),
442 select_file_types_
.get() && !select_file_types_
->extensions
.empty()
444 : 0, // 1-based index of default extension to show.
445 base::FilePath::StringType(),
447 #if defined(OS_ANDROID)
453 select_file_types_
.reset();
456 // This method is called when we receive the last callback from the file
457 // chooser dialog. Perform any cleanup and release the reference we added
458 // in RunFileChooser().
459 void FileSelectHelper::RunFileChooserEnd() {
460 // If there are temporary files, then this instance needs to stick around
461 // until web_contents_ is destroyed, so that this instance can delete the
463 if (!temporary_files_
.empty())
466 render_view_host_
= NULL
;
467 web_contents_
= NULL
;
471 void FileSelectHelper::EnumerateDirectory(int request_id
,
472 RenderViewHost
* render_view_host
,
473 const base::FilePath
& path
) {
475 // Because this class returns notifications to the RenderViewHost, it is
476 // difficult for callers to know how long to keep a reference to this
477 // instance. We AddRef() here to keep the instance alive after we return
478 // to the caller, until the last callback is received from the enumeration
479 // code. At that point, we must call EnumerateDirectoryEnd().
481 StartNewEnumeration(path
, request_id
, render_view_host
);
484 // This method is called when we receive the last callback from the enumeration
485 // code. Perform any cleanup and release the reference we added in
486 // EnumerateDirectory().
487 void FileSelectHelper::EnumerateDirectoryEnd() {
491 void FileSelectHelper::Observe(int type
,
492 const content::NotificationSource
& source
,
493 const content::NotificationDetails
& details
) {
495 case content::NOTIFICATION_RENDER_WIDGET_HOST_DESTROYED
: {
496 DCHECK(content::Source
<RenderWidgetHost
>(source
).ptr() ==
498 render_view_host_
= NULL
;
502 case content::NOTIFICATION_WEB_CONTENTS_DESTROYED
: {
503 DCHECK(content::Source
<WebContents
>(source
).ptr() == web_contents_
);
504 web_contents_
= NULL
;
507 // Intentional fall through.
508 case content::NOTIFICATION_RENDER_VIEW_HOST_CHANGED
:
509 if (!temporary_files_
.empty()) {
510 DeleteTemporaryFiles();
512 // Now that the temporary files have been scheduled for deletion, there
513 // is no longer any reason to keep this instance around.
525 bool FileSelectHelper::IsAcceptTypeValid(const std::string
& accept_type
) {
526 // TODO(raymes): This only does some basic checks, extend to test more cases.
527 // A 1 character accept type will always be invalid (either a "." in the case
528 // of an extension or a "/" in the case of a MIME type).
530 if (accept_type
.length() <= 1 ||
531 base::StringToLowerASCII(accept_type
) != accept_type
||
532 base::TrimWhitespaceASCII(accept_type
, base::TRIM_ALL
, &unused
) !=