diff --git a/chromium_src/BUILD.gn b/chromium_src/BUILD.gn index 9fe0f653de00..acea7708f25f 100644 --- a/chromium_src/BUILD.gn +++ b/chromium_src/BUILD.gn @@ -48,6 +48,8 @@ static_library("chrome") { "//chrome/browser/predictors/resolve_host_client_impl.cc", "//chrome/browser/predictors/resolve_host_client_impl.h", "//chrome/browser/process_singleton.h", + "//chrome/browser/ui/browser_dialogs.cc", + "//chrome/browser/ui/browser_dialogs.h", "//chrome/browser/ui/views/autofill/autofill_popup_view_utils.cc", "//chrome/browser/ui/views/autofill/autofill_popup_view_utils.h", "//chrome/browser/ui/views/eye_dropper/eye_dropper.cc", diff --git a/filenames.gni b/filenames.gni index 3d06281dddef..fe51831f0974 100644 --- a/filenames.gni +++ b/filenames.gni @@ -494,8 +494,6 @@ filenames = { "shell/browser/web_contents_preferences.h", "shell/browser/web_contents_zoom_controller.cc", "shell/browser/web_contents_zoom_controller.h", - "shell/browser/web_dialog_helper.cc", - "shell/browser/web_dialog_helper.h", "shell/browser/web_view_guest_delegate.cc", "shell/browser/web_view_guest_delegate.h", "shell/browser/web_view_manager.cc", diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc index 2bfa4fff2a59..00bcf096ac42 100644 --- a/shell/browser/api/electron_api_web_contents.cc +++ b/shell/browser/api/electron_api_web_contents.cc @@ -86,6 +86,7 @@ #include "shell/browser/electron_browser_main_parts.h" #include "shell/browser/electron_javascript_dialog_manager.h" #include "shell/browser/electron_navigation_throttle.h" +#include "shell/browser/file_select_helper.h" #include "shell/browser/native_window.h" #include "shell/browser/session_preferences.h" #include "shell/browser/ui/drag_util.h" @@ -95,7 +96,6 @@ #include "shell/browser/web_contents_permission_helper.h" #include "shell/browser/web_contents_preferences.h" #include "shell/browser/web_contents_zoom_controller.h" -#include "shell/browser/web_dialog_helper.h" #include "shell/browser/web_view_guest_delegate.h" #include "shell/browser/web_view_manager.h" #include "shell/common/api/electron_api_native_image.h" @@ -3255,21 +3255,15 @@ void WebContents::RunFileChooser( content::RenderFrameHost* render_frame_host, scoped_refptr listener, const blink::mojom::FileChooserParams& params) { - if (!web_dialog_helper_) - web_dialog_helper_ = - std::make_unique(owner_window(), offscreen_); - web_dialog_helper_->RunFileChooser(render_frame_host, std::move(listener), - params); + FileSelectHelper::RunFileChooser(render_frame_host, std::move(listener), + params); } void WebContents::EnumerateDirectory( - content::WebContents* guest, + content::WebContents* web_contents, scoped_refptr listener, const base::FilePath& path) { - if (!web_dialog_helper_) - web_dialog_helper_ = - std::make_unique(owner_window(), offscreen_); - web_dialog_helper_->EnumerateDirectory(guest, std::move(listener), path); + FileSelectHelper::EnumerateDirectory(web_contents, std::move(listener), path); } bool WebContents::IsFullscreenForTabOrPending( diff --git a/shell/browser/api/electron_api_web_contents.h b/shell/browser/api/electron_api_web_contents.h index dcfe9fcc40bb..c30f7fc44276 100644 --- a/shell/browser/api/electron_api_web_contents.h +++ b/shell/browser/api/electron_api_web_contents.h @@ -731,9 +731,6 @@ class WebContents : public gin::Wrappable, // Whether window is fullscreened by window api. bool native_fullscreen_ = false; - // UI related helper classes. - std::unique_ptr web_dialog_helper_; - scoped_refptr devtools_file_system_indexer_; std::unique_ptr eye_dropper_; diff --git a/shell/browser/file_select_helper.cc b/shell/browser/file_select_helper.cc index d3fba02e8d75..01cd90157c8a 100644 --- a/shell/browser/file_select_helper.cc +++ b/shell/browser/file_select_helper.cc @@ -1,133 +1,243 @@ -// Copyright (c) 2020 Microsoft, Inc. All rights reserved. +// Copyright (c) 2021 Microsoft. All rights reserved. // Use of this source code is governed by the MIT license that can be // found in the LICENSE file. -#include - -#include -#include - #include "shell/browser/file_select_helper.h" +#include + +#include +#include +#include + #include "base/bind.h" -#include "base/files/file_enumerator.h" #include "base/files/file_util.h" +#include "base/memory/ptr_util.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "base/task/thread_pool.h" +#include "base/threading/hang_watcher.h" +#include "build/build_config.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/platform_util.h" +#include "chrome/browser/ui/browser_dialogs.h" #include "chrome/common/pref_names.h" +#include "chrome/grit/generated_resources.h" #include "components/prefs/pref_service.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/file_select_listener.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/browser/render_view_host.h" +#include "content/public/browser/render_widget_host_view.h" +#include "content/public/browser/web_contents.h" +#include "net/base/filename_util.h" +#include "net/base/mime_util.h" +#include "shell/browser/api/electron_api_web_contents.h" #include "shell/browser/electron_browser_context.h" -#include "shell/browser/javascript_environment.h" -#include "shell/browser/ui/file_dialog.h" -#include "shell/common/gin_converters/callback_converter.h" -#include "shell/common/gin_converters/file_path_converter.h" +#include "shell/browser/native_window.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/shell_dialogs/select_file_policy.h" +#include "ui/shell_dialogs/selected_file_info.h" using blink::mojom::FileChooserFileInfo; using blink::mojom::FileChooserFileInfoPtr; using blink::mojom::FileChooserParams; -using blink::mojom::NativeFileInfo; +using blink::mojom::FileChooserParamsPtr; +using content::BrowserThread; +using content::RenderViewHost; +using content::RenderWidgetHost; +using content::WebContents; namespace { + void DeleteFiles(std::vector paths) { for (auto& file_path : paths) base::DeleteFile(file_path); } + } // namespace -FileSelectHelper::FileSelectHelper( - content::RenderFrameHost* render_frame_host, - scoped_refptr listener, - FileChooserParams::Mode mode) - : render_frame_host_(render_frame_host), - listener_(std::move(listener)), - mode_(mode) { - DCHECK(render_frame_host_); - DCHECK(listener_); +struct FileSelectHelper::ActiveDirectoryEnumeration { + explicit ActiveDirectoryEnumeration(const base::FilePath& path) + : path_(path) {} - web_contents_ = content::WebContents::FromRenderFrameHost(render_frame_host); - DCHECK(web_contents_); + std::unique_ptr lister_; + const base::FilePath path_; + std::vector results_; +}; - content::WebContentsObserver::Observe(web_contents_); - observation_.Observe(render_frame_host_->GetRenderViewHost()->GetWidget()); +FileSelectHelper::FileSelectHelper() + : render_frame_host_(nullptr), + web_contents_(nullptr), + select_file_dialog_(), + select_file_types_(), + dialog_type_(ui::SelectFileDialog::SELECT_OPEN_FILE), + dialog_mode_(FileChooserParams::Mode::kOpen) {} + +FileSelectHelper::~FileSelectHelper() { + // There may be pending file dialogs, we need to tell them that we've gone + // away so they don't try and call back to us. + if (select_file_dialog_.get()) + select_file_dialog_->ListenerDestroyed(); } -FileSelectHelper::~FileSelectHelper() = default; - -void FileSelectHelper::ShowOpenDialog( - const file_dialog::DialogSettings& settings) { - v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate(); - v8::HandleScope scope(isolate); - gin_helper::Promise promise(isolate); - - auto callback = base::BindOnce(&FileSelectHelper::OnOpenDialogDone, - weak_ptr_factory_.GetWeakPtr()); - ignore_result(promise.Then(std::move(callback))); - - file_dialog::ShowOpenDialog(settings, std::move(promise)); +void FileSelectHelper::FileSelected(const base::FilePath& path, + int index, + void* params) { + FileSelectedWithExtraInfo(ui::SelectedFileInfo(path, path), index, params); } -void FileSelectHelper::ShowSaveDialog( - const file_dialog::DialogSettings& settings) { - v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate(); - v8::HandleScope scope(isolate); - gin_helper::Promise promise(isolate); - - auto callback = base::BindOnce(&FileSelectHelper::OnSaveDialogDone, - weak_ptr_factory_.GetWeakPtr()); - ignore_result(promise.Then(std::move(callback))); - - file_dialog::ShowSaveDialog(settings, std::move(promise)); -} - -// net::DirectoryLister::DirectoryListerDelegate -void FileSelectHelper::OnListFile( - const net::DirectoryLister::DirectoryListerData& data) { - if (!render_frame_host_ || !web_contents_) { - // If the frame or webcontents was destroyed under us. We - // must notify |listener_| and release our reference to - // ourself. RunFileChooserEnd() performs this. +void FileSelectHelper::FileSelectedWithExtraInfo( + const ui::SelectedFileInfo& file, + int index, + void* params) { + if (!render_frame_host_) { RunFileChooserEnd(); return; } - // We don't want to return directory paths, only file paths + + const base::FilePath& path = file.local_path; + if (dialog_type_ == ui::SelectFileDialog::SELECT_UPLOAD_FOLDER) { + StartNewEnumeration(path); + return; + } + + std::vector files; + files.push_back(file); + +#if defined(OS_MAC) + base::ThreadPool::PostTask( + FROM_HERE, + {base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}, + base::BindOnce(&FileSelectHelper::ProcessSelectedFilesMac, this, files)); +#else + ConvertToFileChooserFileInfoList(files); +#endif // defined(OS_MAC) +} + +void FileSelectHelper::MultiFilesSelected( + const std::vector& files, + void* params) { + std::vector selected_files = + ui::FilePathListToSelectedFileInfoList(files); + + MultiFilesSelectedWithExtraInfo(selected_files, params); +} + +void FileSelectHelper::MultiFilesSelectedWithExtraInfo( + const std::vector& files, + void* params) { +#if defined(OS_MAC) + base::ThreadPool::PostTask( + FROM_HERE, + {base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}, + base::BindOnce(&FileSelectHelper::ProcessSelectedFilesMac, this, files)); +#else + ConvertToFileChooserFileInfoList(files); +#endif // defined(OS_MAC) +} + +void FileSelectHelper::FileSelectionCanceled(void* params) { + RunFileChooserEnd(); +} + +void FileSelectHelper::StartNewEnumeration(const base::FilePath& path) { + base_dir_ = path; + auto entry = std::make_unique(path); + entry->lister_ = base::WrapUnique(new net::DirectoryLister( + path, net::DirectoryLister::NO_SORT_RECURSIVE, this)); + entry->lister_->Start(); + directory_enumeration_ = std::move(entry); +} + +void FileSelectHelper::OnListFile( + const net::DirectoryLister::DirectoryListerData& data) { + // Directory upload only cares about files. if (data.info.IsDirectory()) return; - lister_paths_.push_back(data.path); + directory_enumeration_->results_.push_back(data.path); } -void FileSelectHelper::RunFileChooserEnd() { - // If there are temporary files, then this instance needs to stick around - // until web_contents_ is destroyed, so that this instance can delete the - // temporary files. - if (!temporary_files_.empty()) - return; - - if (listener_) - listener_->FileSelectionCanceled(); - - render_frame_host_ = nullptr; - web_contents_ = nullptr; - - delete this; +void FileSelectHelper::LaunchConfirmationDialog( + const base::FilePath& path, + std::vector selected_files) { + ShowFolderUploadConfirmationDialog( + path, + base::BindOnce(&FileSelectHelper::ConvertToFileChooserFileInfoList, this), + std::move(selected_files), web_contents_); } -// net::DirectoryLister::DirectoryListerDelegate void FileSelectHelper::OnListDone(int error) { - if (!render_frame_host_ || !web_contents_) { - // If the frame or webcontents was destroyed under us. We + if (!web_contents_) { + // Web contents was destroyed under us (probably by closing the tab). We // must notify |listener_| and release our reference to // ourself. RunFileChooserEnd() performs this. RunFileChooserEnd(); return; } - std::vector file_info; - for (const auto& path : lister_paths_) - file_info.push_back(FileChooserFileInfo::NewNativeFile( - NativeFileInfo::New(path, std::u16string()))); + // This entry needs to be cleaned up when this function is done. + std::unique_ptr entry = + std::move(directory_enumeration_); + if (error) { + FileSelectionCanceled(NULL); + return; + } - OnFilesSelected(std::move(file_info), lister_base_dir_); + std::vector selected_files = + ui::FilePathListToSelectedFileInfoList(entry->results_); + + if (dialog_type_ == ui::SelectFileDialog::SELECT_UPLOAD_FOLDER) { + LaunchConfirmationDialog(entry->path_, std::move(selected_files)); + } else { + std::vector chooser_files; + for (const auto& file_path : entry->results_) { + chooser_files.push_back(FileChooserFileInfo::NewNativeFile( + blink::mojom::NativeFileInfo::New(file_path, std::u16string()))); + } + + listener_->FileSelected(std::move(chooser_files), base_dir_, + FileChooserParams::Mode::kUploadFolder); + listener_.reset(); + EnumerateDirectoryEnd(); + } +} + +void FileSelectHelper::ConvertToFileChooserFileInfoList( + const std::vector& files) { + if (AbortIfWebContentsDestroyed()) + return; + + std::vector chooser_files; + for (const auto& file : files) { + chooser_files.push_back( + FileChooserFileInfo::NewNativeFile(blink::mojom::NativeFileInfo::New( + file.local_path, + base::FilePath(file.display_name).AsUTF16Unsafe()))); + } + + PerformContentAnalysisIfNeeded(std::move(chooser_files)); +} + +void FileSelectHelper::PerformContentAnalysisIfNeeded( + std::vector list) { + if (AbortIfWebContentsDestroyed()) + return; + + NotifyListenerAndEnd(std::move(list)); +} + +void FileSelectHelper::NotifyListenerAndEnd( + std::vector list) { + listener_->FileSelected(std::move(list), base_dir_, dialog_mode_); + listener_.reset(); + + // No members should be accessed from here on. + RunFileChooserEnd(); } void FileSelectHelper::DeleteTemporaryFiles() { @@ -138,96 +248,275 @@ void FileSelectHelper::DeleteTemporaryFiles() { base::BindOnce(&DeleteFiles, std::move(temporary_files_))); } -void FileSelectHelper::EnumerateDirectory() { - // Ensure that this fn is only called once - DCHECK(!lister_); - DCHECK(!lister_base_dir_.empty()); - DCHECK(lister_paths_.empty()); +void FileSelectHelper::CleanUp() { + if (!temporary_files_.empty()) { + DeleteTemporaryFiles(); - lister_ = std::make_unique( - lister_base_dir_, net::DirectoryLister::NO_SORT_RECURSIVE, this); - lister_->Start(); + // Now that the temporary files have been scheduled for deletion, there + // is no longer any reason to keep this instance around. + Release(); + } } -void FileSelectHelper::OnOpenDialogDone(gin_helper::Dictionary result) { - bool canceled = true; - result.Get("canceled", &canceled); - - if (!render_frame_host_ || canceled) { +bool FileSelectHelper::AbortIfWebContentsDestroyed() { + if (render_frame_host_ == nullptr || web_contents_ == nullptr) { RunFileChooserEnd(); - } else { - std::vector paths; - if (result.Get("filePaths", &paths)) { - std::vector files = - ui::FilePathListToSelectedFileInfoList(paths); - // If we are uploading a folder we need to enumerate its contents - if (mode_ == FileChooserParams::Mode::kUploadFolder && !paths.empty()) { - lister_base_dir_ = paths[0]; - EnumerateDirectory(); - } else { -#if defined(OS_MAC) - base::ThreadPool::PostTask( - FROM_HERE, - {base::MayBlock(), - base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}, - base::BindOnce(&FileSelectHelper::ProcessSelectedFilesMac, - base::Unretained(this), files)); -#else - ConvertToFileChooserFileInfoList(files); -#endif - } + return true; + } - if (render_frame_host_ && !paths.empty()) { - auto* browser_context = static_cast( - render_frame_host_->GetProcess()->GetBrowserContext()); - browser_context->prefs()->SetFilePath(prefs::kSelectFileLastDirectory, - paths[0].DirName()); - } -#if !defined(OS_MAC) - RunFileChooserEnd(); -#endif + return false; +} + +void FileSelectHelper::SetFileSelectListenerForTesting( + scoped_refptr listener) { + DCHECK(listener); + DCHECK(!listener_); + listener_ = std::move(listener); +} + +std::unique_ptr +FileSelectHelper::GetFileTypesFromAcceptType( + const std::vector& accept_types) { + std::unique_ptr base_file_type( + new ui::SelectFileDialog::FileTypeInfo()); + if (accept_types.empty()) + return base_file_type; + + // Create FileTypeInfo and pre-allocate for the first extension list. + std::unique_ptr file_type( + new ui::SelectFileDialog::FileTypeInfo(*base_file_type)); + file_type->include_all_files = true; + file_type->extensions.resize(1); + std::vector* extensions = + &file_type->extensions.back(); + + // Find the corresponding extensions. + int valid_type_count = 0; + int description_id = 0; + for (const auto& accept_type : accept_types) { + size_t old_extension_size = extensions->size(); + if (accept_type[0] == '.') { + // If the type starts with a period it is assumed to be a file extension + // so we just have to add it to the list. + base::FilePath::StringType ext = + base::FilePath::FromUTF16Unsafe(accept_type).value(); + extensions->push_back(ext.substr(1)); + } else { + if (!base::IsStringASCII(accept_type)) + continue; + std::string ascii_type = base::UTF16ToASCII(accept_type); + if (ascii_type == "image/*") + description_id = IDS_IMAGE_FILES; + else if (ascii_type == "audio/*") + description_id = IDS_AUDIO_FILES; + else if (ascii_type == "video/*") + description_id = IDS_VIDEO_FILES; + + net::GetExtensionsForMimeType(ascii_type, extensions); } + + if (extensions->size() > old_extension_size) + valid_type_count++; } + + // If no valid extension is added, bail out. + if (valid_type_count == 0) + return base_file_type; + + // Use a generic description "Custom Files" if either of the following is + // true: + // 1) There're multiple types specified, like "audio/*,video/*" + // 2) There're multiple extensions for a MIME type without parameter, like + // "ehtml,shtml,htm,html" for "text/html". On Windows, the select file + // dialog uses the first extension in the list to form the description, + // like "EHTML Files". This is not what we want. + if (valid_type_count > 1 || + (valid_type_count == 1 && description_id == 0 && extensions->size() > 1)) + description_id = IDS_CUSTOM_FILES; + + if (description_id) { + file_type->extension_description_overrides.push_back( + l10n_util::GetStringUTF16(description_id)); + } + + return file_type; } -void FileSelectHelper::ConvertToFileChooserFileInfoList( - const std::vector& files) { - std::vector file_info; - - for (const auto& file : files) { - file_info.push_back(FileChooserFileInfo::NewNativeFile(NativeFileInfo::New( - file.local_path, base::FilePath(file.display_name).AsUTF16Unsafe()))); - } - - OnFilesSelected(std::move(file_info), lister_base_dir_); +// static +void FileSelectHelper::RunFileChooser( + content::RenderFrameHost* render_frame_host, + scoped_refptr listener, + const FileChooserParams& params) { + // FileSelectHelper will keep itself alive until it sends the result + // message. + scoped_refptr file_select_helper(new FileSelectHelper()); + file_select_helper->RunFileChooser(render_frame_host, std::move(listener), + params.Clone()); } -void FileSelectHelper::OnSaveDialogDone(gin_helper::Dictionary result) { - std::vector file_info; - bool canceled = true; - result.Get("canceled", &canceled); - - if (!render_frame_host_ || canceled) { - RunFileChooserEnd(); - } else { - base::FilePath path; - if (result.Get("filePath", &path)) { - file_info.push_back(FileChooserFileInfo::NewNativeFile( - NativeFileInfo::New(path, path.BaseName().AsUTF16Unsafe()))); - } - // We should only call this if we have not cancelled the dialog. - OnFilesSelected(std::move(file_info), base::FilePath()); - RunFileChooserEnd(); - } +// static +void FileSelectHelper::EnumerateDirectory( + content::WebContents* tab, + scoped_refptr listener, + const base::FilePath& path) { + // FileSelectHelper will keep itself alive until it sends the result + // message. + scoped_refptr file_select_helper(new FileSelectHelper()); + file_select_helper->EnumerateDirectoryImpl(tab, std::move(listener), path); } -void FileSelectHelper::OnFilesSelected( - std::vector file_info, - base::FilePath base_dir) { - if (listener_) { - listener_->FileSelected(std::move(file_info), base_dir, mode_); - listener_.reset(); +void FileSelectHelper::RunFileChooser( + content::RenderFrameHost* render_frame_host, + scoped_refptr listener, + FileChooserParamsPtr params) { + DCHECK(!render_frame_host_); + DCHECK(!web_contents_); + DCHECK(listener); + DCHECK(!listener_); + DCHECK(params->default_file_name.empty() || + params->mode == FileChooserParams::Mode::kSave) + << "The default_file_name parameter should only be specified for Save " + "file choosers"; + DCHECK(params->default_file_name == params->default_file_name.BaseName()) + << "The default_file_name parameter should not contain path separators"; + + render_frame_host_ = render_frame_host; + web_contents_ = WebContents::FromRenderFrameHost(render_frame_host); + listener_ = std::move(listener); + observation_.Reset(); + content::WebContentsObserver::Observe(web_contents_); + observation_.Observe(render_frame_host_->GetRenderViewHost()->GetWidget()); + + base::ThreadPool::PostTask( + FROM_HERE, {base::MayBlock()}, + base::BindOnce(&FileSelectHelper::GetFileTypesInThreadPool, this, + std::move(params))); + + // Because this class returns notifications to the RenderViewHost, it is + // difficult for callers to know how long to keep a reference to this + // instance. We AddRef() here to keep the instance alive after we return + // to the caller, until the last callback is received from the file dialog. + // At that point, we must call RunFileChooserEnd(). + AddRef(); +} + +void FileSelectHelper::GetFileTypesInThreadPool(FileChooserParamsPtr params) { + select_file_types_ = GetFileTypesFromAcceptType(params->accept_types); + select_file_types_->allowed_paths = + params->need_local_path ? ui::SelectFileDialog::FileTypeInfo::NATIVE_PATH + : ui::SelectFileDialog::FileTypeInfo::ANY_PATH; + + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, + base::BindOnce(&FileSelectHelper::GetSanitizedFilenameOnUIThread, this, + std::move(params))); +} + +void FileSelectHelper::GetSanitizedFilenameOnUIThread( + FileChooserParamsPtr params) { + if (AbortIfWebContentsDestroyed()) + return; + + auto* browser_context = static_cast( + render_frame_host_->GetProcess()->GetBrowserContext()); + base::FilePath default_file_path = + browser_context->prefs() + ->GetFilePath(prefs::kSelectFileLastDirectory) + .Append(params->default_file_name); + + RunFileChooserOnUIThread(default_file_path, std::move(params)); +} + +void FileSelectHelper::RunFileChooserOnUIThread( + const base::FilePath& default_file_path, + FileChooserParamsPtr params) { + DCHECK(params); + + select_file_dialog_ = ui::SelectFileDialog::Create(this, nullptr); + if (!select_file_dialog_.get()) + return; + + dialog_mode_ = params->mode; + switch (params->mode) { + case FileChooserParams::Mode::kOpen: + dialog_type_ = ui::SelectFileDialog::SELECT_OPEN_FILE; + break; + case FileChooserParams::Mode::kOpenMultiple: + dialog_type_ = ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE; + break; + case FileChooserParams::Mode::kUploadFolder: + dialog_type_ = ui::SelectFileDialog::SELECT_UPLOAD_FOLDER; + break; + case FileChooserParams::Mode::kSave: + dialog_type_ = ui::SelectFileDialog::SELECT_SAVEAS_FILE; + break; + default: + // Prevent warning. + dialog_type_ = ui::SelectFileDialog::SELECT_OPEN_FILE; + NOTREACHED(); } + + auto* web_contents = electron::api::WebContents::From( + content::WebContents::FromRenderFrameHost(render_frame_host_)); + if (!web_contents || !web_contents->owner_window()) + return; + + // Never consider the current scope as hung. The hang watching deadline (if + // any) is not valid since the user can take unbounded time to choose the + // file. + base::HangWatcher::InvalidateActiveExpectations(); + + select_file_dialog_->SelectFile( + dialog_type_, params->title, default_file_path, select_file_types_.get(), + select_file_types_.get() && !select_file_types_->extensions.empty() + ? 1 + : 0, // 1-based index of default extension to show. + base::FilePath::StringType(), + web_contents->owner_window()->GetNativeWindow(), NULL); + + select_file_types_.reset(); +} + +// This method is called when we receive the last callback from the file chooser +// dialog or if the renderer was destroyed. Perform any cleanup and release the +// reference we added in RunFileChooser(). +void FileSelectHelper::RunFileChooserEnd() { + // If there are temporary files, then this instance needs to stick around + // until web_contents_ is destroyed, so that this instance can delete the + // temporary files. + if (!temporary_files_.empty()) + return; + + if (listener_) + listener_->FileSelectionCanceled(); + render_frame_host_ = nullptr; + web_contents_ = nullptr; + Release(); +} + +void FileSelectHelper::EnumerateDirectoryImpl( + content::WebContents* tab, + scoped_refptr listener, + const base::FilePath& path) { + DCHECK(listener); + DCHECK(!listener_); + dialog_type_ = ui::SelectFileDialog::SELECT_NONE; + web_contents_ = tab; + listener_ = std::move(listener); + // Because this class returns notifications to the RenderViewHost, it is + // difficult for callers to know how long to keep a reference to this + // instance. We AddRef() here to keep the instance alive after we return + // to the caller, until the last callback is received from the enumeration + // code. At that point, we must call EnumerateDirectoryEnd(). + AddRef(); + StartNewEnumeration(path); +} + +// This method is called when we receive the last callback from the enumeration +// code. Perform any cleanup and release the reference we added in +// EnumerateDirectoryImpl(). +void FileSelectHelper::EnumerateDirectoryEnd() { + Release(); } void FileSelectHelper::RenderWidgetHostDestroyed( @@ -237,34 +526,52 @@ void FileSelectHelper::RenderWidgetHostDestroyed( observation_.Reset(); } -// content::WebContentsObserver: void FileSelectHelper::RenderFrameHostChanged( content::RenderFrameHost* old_host, content::RenderFrameHost* new_host) { if (!render_frame_host_) return; - // The |old_host| and its children are now pending deletion. Do not give - // them file access past this point. + // The |old_host| and its children are now pending deletion. Do not give them + // file access past this point. if (render_frame_host_ == old_host || render_frame_host_->IsDescendantOf(old_host)) { render_frame_host_ = nullptr; } } -// content::WebContentsObserver: void FileSelectHelper::RenderFrameDeleted( - content::RenderFrameHost* deleted_host) { - if (deleted_host == render_frame_host_) + content::RenderFrameHost* render_frame_host) { + if (render_frame_host == render_frame_host_) render_frame_host_ = nullptr; } -// content::WebContentsObserver: void FileSelectHelper::WebContentsDestroyed() { render_frame_host_ = nullptr; web_contents_ = nullptr; - - DeleteTemporaryFiles(); - - if (!lister_) - delete this; + CleanUp(); +} + +// static +bool FileSelectHelper::IsAcceptTypeValid(const std::string& accept_type) { + // TODO(raymes): This only does some basic checks, extend to test more cases. + // A 1 character accept type will always be invalid (either a "." in the case + // of an extension or a "/" in the case of a MIME type). + std::string unused; + if (accept_type.length() <= 1 || + base::ToLowerASCII(accept_type) != accept_type || + base::TrimWhitespaceASCII(accept_type, base::TRIM_ALL, &unused) != + base::TRIM_NONE) { + return false; + } + return true; +} + +// static +base::FilePath FileSelectHelper::GetSanitizedFileName( + const base::FilePath& suggested_filename) { + if (suggested_filename.empty()) + return base::FilePath(); + return net::GenerateFileName( + GURL(), std::string(), std::string(), suggested_filename.AsUTF8Unsafe(), + std::string(), l10n_util::GetStringUTF8(IDS_DEFAULT_DOWNLOAD_FILENAME)); } diff --git a/shell/browser/file_select_helper.h b/shell/browser/file_select_helper.h index 16c5c9c8b30f..bba65b317050 100644 --- a/shell/browser/file_select_helper.h +++ b/shell/browser/file_select_helper.h @@ -1,69 +1,130 @@ -// Copyright (c) 2020 Microsoft, Inc. All rights reserved. +// Copyright (c) 2021 Microsoft. All rights reserved. // Use of this source code is governed by the MIT license that can be // found in the LICENSE file. #ifndef SHELL_BROWSER_FILE_SELECT_HELPER_H_ #define SHELL_BROWSER_FILE_SELECT_HELPER_H_ +#include #include +#include #include -#include "base/files/file_path.h" +#include "base/compiler_specific.h" +#include "base/macros.h" #include "base/scoped_observation.h" -#include "chrome/common/pref_names.h" -#include "content/public/browser/file_select_listener.h" -#include "content/public/browser/render_frame_host.h" -#include "content/public/browser/render_process_host.h" -#include "content/public/browser/render_view_host.h" +#include "build/build_config.h" +#include "content/public/browser/browser_thread.h" #include "content/public/browser/render_widget_host.h" #include "content/public/browser/render_widget_host_observer.h" -#include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents_observer.h" -#include "gin/dictionary.h" #include "net/base/directory_lister.h" -#include "shell/browser/electron_browser_context.h" -#include "shell/browser/ui/file_dialog.h" -#include "shell/common/gin_helper/dictionary.h" -#include "ui/shell_dialogs/selected_file_info.h" +#include "third_party/blink/public/mojom/choosers/file_chooser.mojom.h" +#include "ui/shell_dialogs/select_file_dialog.h" -using blink::mojom::FileChooserParams; +namespace content { +class FileSelectListener; +class WebContents; +} // namespace content -class FileSelectHelper : public content::WebContentsObserver, +namespace ui { +struct SelectedFileInfo; +} + +// This class handles file-selection requests coming from renderer processes. +// It implements both the initialisation and listener functions for +// file-selection dialogs. +// +// Since FileSelectHelper listens to observations of a widget, it needs to live +// on and be destroyed on the UI thread. References to FileSelectHelper may be +// passed on to other threads. +class FileSelectHelper : public base::RefCountedThreadSafe< + FileSelectHelper, + content::BrowserThread::DeleteOnUIThread>, + public ui::SelectFileDialog::Listener, + public content::WebContentsObserver, public content::RenderWidgetHostObserver, - public net::DirectoryLister::DirectoryListerDelegate { + private net::DirectoryLister::DirectoryListerDelegate { public: - FileSelectHelper(content::RenderFrameHost* render_frame_host, - scoped_refptr listener, - FileChooserParams::Mode mode); - ~FileSelectHelper() override; + // Show the file chooser dialog. + static void RunFileChooser( + content::RenderFrameHost* render_frame_host, + scoped_refptr listener, + const blink::mojom::FileChooserParams& params); - // WebDialogHelper::RunFileChooser - - void ShowOpenDialog(const file_dialog::DialogSettings& settings); - - void ShowSaveDialog(const file_dialog::DialogSettings& settings); + // Enumerates all the files in directory. + static void EnumerateDirectory( + content::WebContents* tab, + scoped_refptr listener, + const base::FilePath& path); private: + friend class base::RefCountedThreadSafe; + friend class base::DeleteHelper; + friend struct content::BrowserThread::DeleteOnThread< + content::BrowserThread::UI>; + + FileSelectHelper(); + ~FileSelectHelper() override; + + void RunFileChooser(content::RenderFrameHost* render_frame_host, + scoped_refptr listener, + blink::mojom::FileChooserParamsPtr params); + void GetFileTypesInThreadPool(blink::mojom::FileChooserParamsPtr params); + void GetSanitizedFilenameOnUIThread( + blink::mojom::FileChooserParamsPtr params); + + void RunFileChooserOnUIThread(const base::FilePath& default_path, + blink::mojom::FileChooserParamsPtr params); + + // Cleans up and releases this instance. This must be called after the last + // callback is received from the file chooser dialog. + void RunFileChooserEnd(); + + // SelectFileDialog::Listener overrides. + void FileSelected(const base::FilePath& path, + int index, + void* params) override; + void FileSelectedWithExtraInfo(const ui::SelectedFileInfo& file, + int index, + void* params) override; + void MultiFilesSelected(const std::vector& files, + void* params) override; + void MultiFilesSelectedWithExtraInfo( + const std::vector& files, + void* params) override; + void FileSelectionCanceled(void* params) override; + + // content::RenderWidgetHostObserver overrides. + void RenderWidgetHostDestroyed( + content::RenderWidgetHost* widget_host) override; + + // content::WebContentsObserver overrides. + void RenderFrameHostChanged(content::RenderFrameHost* old_host, + content::RenderFrameHost* new_host) override; + void RenderFrameDeleted(content::RenderFrameHost* render_frame_host) override; + void WebContentsDestroyed() override; + + void EnumerateDirectoryImpl( + content::WebContents* tab, + scoped_refptr listener, + const base::FilePath& path); + + // Kicks off a new directory enumeration. + void StartNewEnumeration(const base::FilePath& path); + // net::DirectoryLister::DirectoryListerDelegate overrides. void OnListFile( const net::DirectoryLister::DirectoryListerData& data) override; void OnListDone(int error) override; - void DeleteTemporaryFiles(); + void LaunchConfirmationDialog( + const base::FilePath& path, + std::vector selected_files); - void EnumerateDirectory(); - - void OnOpenDialogDone(gin_helper::Dictionary result); - void OnSaveDialogDone(gin_helper::Dictionary result); - - void OnFilesSelected( - std::vector file_info, - base::FilePath base_dir); - - void RunFileChooserEnd(); - - void ConvertToFileChooserFileInfoList( - const std::vector& files); + // Cleans up and releases this instance. This must be called after the last + // callback is received from the enumeration code. + void EnumerateDirectoryEnd(); #if defined(OS_MAC) // Must be called from a MayBlock() task. Each selected file that is a package @@ -71,11 +132,11 @@ class FileSelectHelper : public content::WebContentsObserver, // of the package. void ProcessSelectedFilesMac(const std::vector& files); - // Saves the paths of |temporary_files| for later deletion. Passes |files| to - // the render view host. + // Saves the paths of |zipped_files| for later deletion. Passes |files| to the + // render view host. void ProcessSelectedFilesMacOnUIThread( const std::vector& files, - const std::vector& temporary_files); + const std::vector& zipped_files); // Zips the package at |path| into a temporary destination. Returns the // temporary destination, if the zip was successful. Otherwise returns an @@ -83,20 +144,78 @@ class FileSelectHelper : public content::WebContentsObserver, static base::FilePath ZipPackage(const base::FilePath& path); #endif // defined(OS_MAC) - // content::RenderWidgetHostObserver: - void RenderWidgetHostDestroyed( - content::RenderWidgetHost* widget_host) override; + void ConvertToFileChooserFileInfoList( + const std::vector& files); - // content::WebContentsObserver: - void RenderFrameHostChanged(content::RenderFrameHost* old_host, - content::RenderFrameHost* new_host) override; - void RenderFrameDeleted(content::RenderFrameHost* deleted_host) override; - void WebContentsDestroyed() override; + // Checks to see if scans are required for the specified files. + void PerformContentAnalysisIfNeeded( + std::vector list); + // Finish the PerformContentAnalysisIfNeeded() handling after the + // deep scanning checks have been performed. Deep scanning may change the + // list of files chosen by the user, so the list of files passed here may be + // a subset of of the files passed to PerformContentAnalysisIfNeeded(). + void NotifyListenerAndEnd( + std::vector list); + + // Schedules the deletion of the files in |temporary_files_| and clears the + // vector. + void DeleteTemporaryFiles(); + + // Cleans up when the initiator of the file chooser is no longer valid. + void CleanUp(); + + // Calls RunFileChooserEnd() if the webcontents was destroyed. Returns true + // if the file chooser operation shouldn't proceed. + bool AbortIfWebContentsDestroyed(); + + void SetFileSelectListenerForTesting( + scoped_refptr listener); + + // Helper method to get allowed extensions for select file dialog from + // the specified accept types as defined in the spec: + // http://whatwg.org/html/number-state.html#attr-input-accept + // |accept_types| contains only valid lowercased MIME types or file extensions + // beginning with a period (.). + static std::unique_ptr + GetFileTypesFromAcceptType(const std::vector& accept_types); + + // Check the accept type is valid. It is expected to be all lower case with + // no whitespace. + static bool IsAcceptTypeValid(const std::string& accept_type); + + // Get a sanitized filename suitable for use as a default filename. + static base::FilePath GetSanitizedFileName( + const base::FilePath& suggested_path); + + // The RenderFrameHost and WebContents for the page showing a file dialog + // (may only be one such dialog). content::RenderFrameHost* render_frame_host_; content::WebContents* web_contents_; + + // |listener_| receives the result of the FileSelectHelper. scoped_refptr listener_; - FileChooserParams::Mode mode_; + + // Dialog box used for choosing files to upload from file form fields. + scoped_refptr select_file_dialog_; + std::unique_ptr select_file_types_; + + // The type of file dialog last shown. This is SELECT_NONE if an + // instance is created through the public EnumerateDirectory(). + ui::SelectFileDialog::Type dialog_type_; + + // The mode of file dialog last shown. + blink::mojom::FileChooserParams::Mode dialog_mode_; + + // The enumeration root directory for EnumerateDirectory() and + // RunFileChooser with kUploadFolder. + base::FilePath base_dir_; + + // Maintain an active directory enumeration. These could come from the file + // select dialog or from drag-and-drop of directories. There could not be + // more than one going on at a time. + struct ActiveDirectoryEnumeration; + std::unique_ptr directory_enumeration_; base::ScopedObservation @@ -106,12 +225,7 @@ class FileSelectHelper : public content::WebContentsObserver, // these files when they are no longer needed. std::vector temporary_files_; - // DirectoryLister-specific members - std::unique_ptr lister_; - base::FilePath lister_base_dir_; - std::vector lister_paths_; - - base::WeakPtrFactory weak_ptr_factory_{this}; + DISALLOW_COPY_AND_ASSIGN(FileSelectHelper); }; #endif // SHELL_BROWSER_FILE_SELECT_HELPER_H_ diff --git a/shell/browser/web_dialog_helper.cc b/shell/browser/web_dialog_helper.cc deleted file mode 100644 index cf94412156ad..000000000000 --- a/shell/browser/web_dialog_helper.cc +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) 2014 GitHub, Inc. All rights reserved. -// Use of this source code is governed by the MIT license that can be -// found in the LICENSE file. - -#include "shell/browser/web_dialog_helper.h" - -#include -#include -#include - -#include "base/bind.h" -#include "base/files/file_enumerator.h" -#include "base/files/file_path.h" -#include "base/strings/utf_string_conversions.h" -#include "chrome/common/pref_names.h" -#include "components/prefs/pref_service.h" -#include "content/public/browser/file_select_listener.h" -#include "content/public/browser/render_frame_host.h" -#include "content/public/browser/render_process_host.h" -#include "content/public/browser/render_view_host.h" -#include "content/public/browser/web_contents.h" -#include "net/base/mime_util.h" -#include "shell/browser/electron_browser_context.h" -#include "shell/browser/file_select_helper.h" -#include "shell/browser/native_window.h" -#include "shell/browser/ui/file_dialog.h" - -using blink::mojom::FileChooserFileInfo; -using blink::mojom::FileChooserFileInfoPtr; -using blink::mojom::FileChooserParams; -using blink::mojom::NativeFileInfo; - -namespace { - -file_dialog::Filters GetFileTypesFromAcceptType( - const std::vector& accept_types) { - file_dialog::Filters filters; - if (accept_types.empty()) - return filters; - - std::vector extensions; - - int valid_type_count = 0; - std::string description; - - for (const auto& accept_type : accept_types) { - std::string ascii_type = base::UTF16ToASCII(accept_type); - auto old_extension_size = extensions.size(); - - if (ascii_type[0] == '.') { - // If the type starts with a period it is assumed to be a file extension, - // like `.txt`, // so we just have to add it to the list. - base::FilePath::StringType extension(ascii_type.begin(), - ascii_type.end()); - // Skip the first character. - extensions.push_back(extension.substr(1)); - } else { - if (ascii_type == "image/*") - description = "Image Files"; - else if (ascii_type == "audio/*") - description = "Audio Files"; - else if (ascii_type == "video/*") - description = "Video Files"; - - // For MIME Type, `audio/*, video/*, image/* - net::GetExtensionsForMimeType(ascii_type, &extensions); - } - - if (extensions.size() > old_extension_size) - valid_type_count++; - } - - // If no valid extension is added, return empty filters. - if (extensions.empty()) - return filters; - - filters.push_back(file_dialog::Filter()); - - if (valid_type_count > 1 || (valid_type_count == 1 && description.empty())) - description = "Custom Files"; - - DCHECK(!description.empty()); - filters[0].first = description; - - for (const auto& extension : extensions) { -#if defined(OS_WIN) - filters[0].second.push_back(base::WideToASCII(extension)); -#else - filters[0].second.push_back(extension); -#endif - } - - // Allow all files when extension is specified. - filters.push_back(file_dialog::Filter()); - filters.back().first = "All Files"; - filters.back().second.emplace_back("*"); - - return filters; -} - -} // namespace - -namespace electron { - -WebDialogHelper::WebDialogHelper(NativeWindow* window, bool offscreen) - : window_(window), offscreen_(offscreen) {} - -WebDialogHelper::~WebDialogHelper() = default; - -void WebDialogHelper::RunFileChooser( - content::RenderFrameHost* render_frame_host, - scoped_refptr listener, - const FileChooserParams& params) { - file_dialog::DialogSettings settings; - settings.force_detached = offscreen_; - settings.filters = GetFileTypesFromAcceptType(params.accept_types); - settings.parent_window = window_; - settings.title = base::UTF16ToUTF8(params.title); - - auto* fsc = - new FileSelectHelper(render_frame_host, std::move(listener), params.mode); - - if (params.mode == FileChooserParams::Mode::kSave) { - settings.default_path = params.default_file_name; - fsc->ShowSaveDialog(settings); - } else { - int flags = file_dialog::OPEN_DIALOG_CREATE_DIRECTORY; - switch (params.mode) { - case FileChooserParams::Mode::kOpenMultiple: - flags |= file_dialog::OPEN_DIALOG_MULTI_SELECTIONS; - FALLTHROUGH; - case FileChooserParams::Mode::kOpen: - flags |= file_dialog::OPEN_DIALOG_OPEN_FILE; - break; - case FileChooserParams::Mode::kUploadFolder: - flags |= file_dialog::OPEN_DIALOG_OPEN_DIRECTORY; - break; - default: - NOTREACHED(); - } - - auto* browser_context = static_cast( - render_frame_host->GetProcess()->GetBrowserContext()); - settings.default_path = browser_context->prefs() - ->GetFilePath(prefs::kSelectFileLastDirectory) - .Append(params.default_file_name); - settings.properties = flags; - fsc->ShowOpenDialog(settings); - } -} - -void WebDialogHelper::EnumerateDirectory( - content::WebContents* web_contents, - scoped_refptr listener, - const base::FilePath& dir) { - int types = base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES | - base::FileEnumerator::INCLUDE_DOT_DOT; - base::FileEnumerator file_enum(dir, false, types); - - base::FilePath path; - std::vector file_info; - while (!(path = file_enum.Next()).empty()) { - file_info.push_back(FileChooserFileInfo::NewNativeFile( - NativeFileInfo::New(path, std::u16string()))); - } - - listener->FileSelected(std::move(file_info), dir, - FileChooserParams::Mode::kUploadFolder); -} - -} // namespace electron diff --git a/shell/browser/web_dialog_helper.h b/shell/browser/web_dialog_helper.h deleted file mode 100644 index 99f300e225eb..000000000000 --- a/shell/browser/web_dialog_helper.h +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2014 GitHub, Inc. All rights reserved. -// Use of this source code is governed by the MIT license that can be -// found in the LICENSE file. - -#ifndef SHELL_BROWSER_WEB_DIALOG_HELPER_H_ -#define SHELL_BROWSER_WEB_DIALOG_HELPER_H_ - -#include "base/memory/weak_ptr.h" -#include "third_party/blink/public/mojom/choosers/file_chooser.mojom.h" - -namespace base { -class FilePath; -} - -namespace content { -class FileSelectListener; -class RenderFrameHost; -class WebContents; -} // namespace content - -namespace electron { - -class NativeWindow; - -class WebDialogHelper { - public: - WebDialogHelper(NativeWindow* window, bool offscreen); - ~WebDialogHelper(); - - void RunFileChooser(content::RenderFrameHost* render_frame_host, - scoped_refptr listener, - const blink::mojom::FileChooserParams& params); - void EnumerateDirectory(content::WebContents* web_contents, - scoped_refptr listener, - const base::FilePath& dir); - - private: - NativeWindow* window_; - bool offscreen_; - - base::WeakPtrFactory weak_factory_{this}; - - DISALLOW_COPY_AND_ASSIGN(WebDialogHelper); -}; - -} // namespace electron - -#endif // SHELL_BROWSER_WEB_DIALOG_HELPER_H_