Home | History | Annotate | Download | only in browser
      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