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/file_util.h" 12 #include "base/files/file_enumerator.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 "content/public/browser/browser_thread.h" 22 #include "content/public/browser/notification_details.h" 23 #include "content/public/browser/notification_source.h" 24 #include "content/public/browser/notification_types.h" 25 #include "content/public/browser/render_view_host.h" 26 #include "content/public/browser/render_widget_host_view.h" 27 #include "content/public/browser/web_contents.h" 28 #include "content/public/common/file_chooser_params.h" 29 #include "grit/generated_resources.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 34 using content::BrowserThread; 35 using content::FileChooserParams; 36 using content::RenderViewHost; 37 using content::RenderWidgetHost; 38 using content::WebContents; 39 40 namespace { 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. 45 const int kFileSelectEnumerationId = -1; 46 47 void 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. 54 std::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 66 struct 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 75 FileSelectHelper::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 85 FileSelectHelper::~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 102 void FileSelectHelper::DirectoryListerDispatchDelegate::OnListFile( 103 const net::DirectoryLister::DirectoryListerData& data) { 104 parent_->OnListFile(id_, data); 105 } 106 107 void FileSelectHelper::DirectoryListerDispatchDelegate::OnListDone(int error) { 108 parent_->OnListDone(id_, error); 109 } 110 111 void FileSelectHelper::FileSelected(const base::FilePath& path, 112 int index, void* params) { 113 FileSelectedWithExtraInfo(ui::SelectedFileInfo(path, path), index, params); 114 } 115 116 void 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 139 void 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 148 void 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 162 void 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 176 void 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 197 void FileSelectHelper::OnListFile( 198 int id, 199 const net::DirectoryLister::DirectoryListerData& data) { 200 ActiveDirectoryEnumeration* entry = directory_enumerations_[id]; 201 202 // Directory upload returns directories via a "." file, so that 203 // empty directories are included. This util call just checks 204 // the flags in the structure; there's no file I/O going on. 205 if (data.info.IsDirectory()) 206 entry->results_.push_back(data.path.Append(FILE_PATH_LITERAL("."))); 207 else 208 entry->results_.push_back(data.path); 209 } 210 211 void FileSelectHelper::OnListDone(int id, int error) { 212 // This entry needs to be cleaned up when this function is done. 213 scoped_ptr<ActiveDirectoryEnumeration> entry(directory_enumerations_[id]); 214 directory_enumerations_.erase(id); 215 if (!entry->rvh_) 216 return; 217 if (error) { 218 FileSelectionCanceled(NULL); 219 return; 220 } 221 222 std::vector<ui::SelectedFileInfo> selected_files = 223 FilePathListToSelectedFileInfoList(entry->results_); 224 225 if (id == kFileSelectEnumerationId) 226 NotifyRenderViewHost(entry->rvh_, selected_files, dialog_mode_); 227 else 228 entry->rvh_->DirectoryEnumerationFinished(id, entry->results_); 229 230 EnumerateDirectoryEnd(); 231 } 232 233 scoped_ptr<ui::SelectFileDialog::FileTypeInfo> 234 FileSelectHelper::GetFileTypesFromAcceptType( 235 const std::vector<string16>& accept_types) { 236 scoped_ptr<ui::SelectFileDialog::FileTypeInfo> base_file_type( 237 new ui::SelectFileDialog::FileTypeInfo()); 238 if (accept_types.empty()) 239 return base_file_type.Pass(); 240 241 // Create FileTypeInfo and pre-allocate for the first extension list. 242 scoped_ptr<ui::SelectFileDialog::FileTypeInfo> file_type( 243 new ui::SelectFileDialog::FileTypeInfo(*base_file_type)); 244 file_type->include_all_files = true; 245 file_type->extensions.resize(1); 246 std::vector<base::FilePath::StringType>* extensions = 247 &file_type->extensions.back(); 248 249 // Find the corresponding extensions. 250 int valid_type_count = 0; 251 int description_id = 0; 252 for (size_t i = 0; i < accept_types.size(); ++i) { 253 std::string ascii_type = UTF16ToASCII(accept_types[i]); 254 if (!IsAcceptTypeValid(ascii_type)) 255 continue; 256 257 size_t old_extension_size = extensions->size(); 258 if (ascii_type[0] == '.') { 259 // If the type starts with a period it is assumed to be a file extension 260 // so we just have to add it to the list. 261 base::FilePath::StringType ext(ascii_type.begin(), ascii_type.end()); 262 extensions->push_back(ext.substr(1)); 263 } else { 264 if (ascii_type == "image/*") 265 description_id = IDS_IMAGE_FILES; 266 else if (ascii_type == "audio/*") 267 description_id = IDS_AUDIO_FILES; 268 else if (ascii_type == "video/*") 269 description_id = IDS_VIDEO_FILES; 270 271 net::GetExtensionsForMimeType(ascii_type, extensions); 272 } 273 274 if (extensions->size() > old_extension_size) 275 valid_type_count++; 276 } 277 278 // If no valid extension is added, bail out. 279 if (valid_type_count == 0) 280 return base_file_type.Pass(); 281 282 // Use a generic description "Custom Files" if either of the following is 283 // true: 284 // 1) There're multiple types specified, like "audio/*,video/*" 285 // 2) There're multiple extensions for a MIME type without parameter, like 286 // "ehtml,shtml,htm,html" for "text/html". On Windows, the select file 287 // dialog uses the first extension in the list to form the description, 288 // like "EHTML Files". This is not what we want. 289 if (valid_type_count > 1 || 290 (valid_type_count == 1 && description_id == 0 && extensions->size() > 1)) 291 description_id = IDS_CUSTOM_FILES; 292 293 if (description_id) { 294 file_type->extension_description_overrides.push_back( 295 l10n_util::GetStringUTF16(description_id)); 296 } 297 298 return file_type.Pass(); 299 } 300 301 // static 302 void FileSelectHelper::RunFileChooser(content::WebContents* tab, 303 const FileChooserParams& params) { 304 Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext()); 305 // FileSelectHelper will keep itself alive until it sends the result message. 306 scoped_refptr<FileSelectHelper> file_select_helper( 307 new FileSelectHelper(profile)); 308 file_select_helper->RunFileChooser(tab->GetRenderViewHost(), tab, params); 309 } 310 311 // static 312 void FileSelectHelper::EnumerateDirectory(content::WebContents* tab, 313 int request_id, 314 const base::FilePath& path) { 315 Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext()); 316 // FileSelectHelper will keep itself alive until it sends the result message. 317 scoped_refptr<FileSelectHelper> file_select_helper( 318 new FileSelectHelper(profile)); 319 file_select_helper->EnumerateDirectory( 320 request_id, tab->GetRenderViewHost(), path); 321 } 322 323 void FileSelectHelper::RunFileChooser(RenderViewHost* render_view_host, 324 content::WebContents* web_contents, 325 const FileChooserParams& params) { 326 DCHECK(!render_view_host_); 327 DCHECK(!web_contents_); 328 render_view_host_ = render_view_host; 329 web_contents_ = web_contents; 330 notification_registrar_.RemoveAll(); 331 notification_registrar_.Add( 332 this, content::NOTIFICATION_RENDER_WIDGET_HOST_DESTROYED, 333 content::Source<RenderWidgetHost>(render_view_host_)); 334 notification_registrar_.Add( 335 this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED, 336 content::Source<WebContents>(web_contents_)); 337 338 BrowserThread::PostTask( 339 BrowserThread::FILE, FROM_HERE, 340 base::Bind(&FileSelectHelper::RunFileChooserOnFileThread, this, params)); 341 342 // Because this class returns notifications to the RenderViewHost, it is 343 // difficult for callers to know how long to keep a reference to this 344 // instance. We AddRef() here to keep the instance alive after we return 345 // to the caller, until the last callback is received from the file dialog. 346 // At that point, we must call RunFileChooserEnd(). 347 AddRef(); 348 } 349 350 void FileSelectHelper::RunFileChooserOnFileThread( 351 const FileChooserParams& params) { 352 select_file_types_ = GetFileTypesFromAcceptType(params.accept_types); 353 354 BrowserThread::PostTask( 355 BrowserThread::UI, FROM_HERE, 356 base::Bind(&FileSelectHelper::RunFileChooserOnUIThread, this, params)); 357 } 358 359 void FileSelectHelper::RunFileChooserOnUIThread( 360 const FileChooserParams& params) { 361 if (!render_view_host_ || !web_contents_) { 362 // If the renderer was destroyed before we started, just cancel the 363 // operation. 364 RunFileChooserEnd(); 365 return; 366 } 367 368 select_file_dialog_ = ui::SelectFileDialog::Create( 369 this, new ChromeSelectFilePolicy(web_contents_)); 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<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(). 426 void FileSelectHelper::RunFileChooserEnd() { 427 render_view_host_ = NULL; 428 web_contents_ = NULL; 429 Release(); 430 } 431 432 void 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(). 448 void FileSelectHelper::EnumerateDirectoryEnd() { 449 Release(); 450 } 451 452 void 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 475 bool 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 StringToLowerASCII(accept_type) != accept_type || 482 TrimWhitespaceASCII(accept_type, TRIM_ALL, &unused) != TRIM_NONE) { 483 return false; 484 } 485 return true; 486 } 487