web_dialog_helper.cc 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. // Copyright (c) 2014 GitHub, Inc. All rights reserved.
  2. // Use of this source code is governed by the MIT license that can be
  3. // found in the LICENSE file.
  4. #include "shell/browser/web_dialog_helper.h"
  5. #include <string>
  6. #include <utility>
  7. #include <vector>
  8. #include "base/bind.h"
  9. #include "base/files/file_enumerator.h"
  10. #include "base/files/file_path.h"
  11. #include "base/strings/utf_string_conversions.h"
  12. #include "chrome/common/pref_names.h"
  13. #include "components/prefs/pref_service.h"
  14. #include "content/public/browser/file_select_listener.h"
  15. #include "content/public/browser/render_frame_host.h"
  16. #include "content/public/browser/render_process_host.h"
  17. #include "content/public/browser/render_view_host.h"
  18. #include "content/public/browser/web_contents.h"
  19. #include "content/public/browser/web_contents_observer.h"
  20. #include "native_mate/dictionary.h"
  21. #include "net/base/directory_lister.h"
  22. #include "net/base/mime_util.h"
  23. #include "shell/browser/atom_browser_context.h"
  24. #include "shell/browser/native_window.h"
  25. #include "shell/browser/ui/file_dialog.h"
  26. #include "shell/common/native_mate_converters/once_callback.h"
  27. #include "ui/shell_dialogs/selected_file_info.h"
  28. using blink::mojom::FileChooserFileInfo;
  29. using blink::mojom::FileChooserFileInfoPtr;
  30. using blink::mojom::FileChooserParams;
  31. namespace {
  32. class FileSelectHelper : public base::RefCounted<FileSelectHelper>,
  33. public content::WebContentsObserver,
  34. public net::DirectoryLister::DirectoryListerDelegate {
  35. public:
  36. REQUIRE_ADOPTION_FOR_REFCOUNTED_TYPE();
  37. FileSelectHelper(content::RenderFrameHost* render_frame_host,
  38. std::unique_ptr<content::FileSelectListener> listener,
  39. blink::mojom::FileChooserParams::Mode mode)
  40. : render_frame_host_(render_frame_host),
  41. listener_(std::move(listener)),
  42. mode_(mode) {
  43. auto* web_contents =
  44. content::WebContents::FromRenderFrameHost(render_frame_host);
  45. content::WebContentsObserver::Observe(web_contents);
  46. }
  47. void ShowOpenDialog(const file_dialog::DialogSettings& settings) {
  48. v8::Isolate* isolate = v8::Isolate::GetCurrent();
  49. electron::util::Promise promise(isolate);
  50. auto callback = base::BindOnce(&FileSelectHelper::OnOpenDialogDone, this);
  51. ignore_result(promise.Then(std::move(callback)));
  52. file_dialog::ShowOpenDialog(settings, std::move(promise));
  53. }
  54. void ShowSaveDialog(const file_dialog::DialogSettings& settings) {
  55. v8::Isolate* isolate = v8::Isolate::GetCurrent();
  56. v8::Local<v8::Context> context = isolate->GetCurrentContext();
  57. electron::util::Promise promise(isolate);
  58. v8::Local<v8::Promise> handle = promise.GetHandle();
  59. file_dialog::ShowSaveDialog(settings, std::move(promise));
  60. ignore_result(handle->Then(
  61. context,
  62. v8::Local<v8::Function>::Cast(mate::ConvertToV8(
  63. isolate,
  64. base::BindOnce(&FileSelectHelper::OnSaveDialogDone, this)))));
  65. }
  66. private:
  67. friend class base::RefCounted<FileSelectHelper>;
  68. ~FileSelectHelper() override {}
  69. // net::DirectoryLister::DirectoryListerDelegate
  70. void OnListFile(
  71. const net::DirectoryLister::DirectoryListerData& data) override {
  72. // We don't want to return directory paths, only file paths
  73. if (data.info.IsDirectory())
  74. return;
  75. lister_paths_.push_back(data.path);
  76. }
  77. // net::DirectoryLister::DirectoryListerDelegate
  78. void OnListDone(int error) override {
  79. std::vector<FileChooserFileInfoPtr> file_info;
  80. for (const auto& path : lister_paths_)
  81. file_info.push_back(FileChooserFileInfo::NewNativeFile(
  82. blink::mojom::NativeFileInfo::New(path, base::string16())));
  83. OnFilesSelected(std::move(file_info), lister_base_dir_);
  84. Release();
  85. }
  86. void EnumerateDirectory() {
  87. // Ensure that this fn is only called once
  88. DCHECK(!lister_);
  89. DCHECK(!lister_base_dir_.empty());
  90. DCHECK(lister_paths_.empty());
  91. lister_.reset(new net::DirectoryLister(
  92. lister_base_dir_, net::DirectoryLister::NO_SORT_RECURSIVE, this));
  93. lister_->Start();
  94. // It is difficult for callers to know how long to keep a reference to
  95. // this instance. We AddRef() here to keep the instance alive after we
  96. // return to the caller. Once the directory lister is complete we
  97. // Release() & at that point we run OnFilesSelected() which will
  98. // deref the last reference held by the listener.
  99. AddRef();
  100. }
  101. void OnOpenDialogDone(mate::Dictionary result) {
  102. std::vector<FileChooserFileInfoPtr> file_info;
  103. bool canceled = true;
  104. result.Get("canceled", &canceled);
  105. // For certain file chooser modes (kUploadFolder) we need to do some async
  106. // work before calling back to the listener. In that particular case the
  107. // listener is called from the directory enumerator.
  108. bool ready_to_call_listener = false;
  109. if (canceled) {
  110. OnSelectionCancelled();
  111. } else {
  112. std::vector<base::FilePath> paths;
  113. if (result.Get("filePaths", &paths)) {
  114. // If we are uploading a folder we need to enumerate its contents
  115. if (mode_ == FileChooserParams::Mode::kUploadFolder &&
  116. paths.size() >= 1) {
  117. lister_base_dir_ = paths[0];
  118. EnumerateDirectory();
  119. } else {
  120. for (auto& path : paths) {
  121. file_info.push_back(FileChooserFileInfo::NewNativeFile(
  122. blink::mojom::NativeFileInfo::New(
  123. path, path.BaseName().AsUTF16Unsafe())));
  124. }
  125. ready_to_call_listener = true;
  126. }
  127. if (render_frame_host_ && !paths.empty()) {
  128. auto* browser_context = static_cast<electron::AtomBrowserContext*>(
  129. render_frame_host_->GetProcess()->GetBrowserContext());
  130. browser_context->prefs()->SetFilePath(prefs::kSelectFileLastDirectory,
  131. paths[0].DirName());
  132. }
  133. }
  134. // We should only call this if we have not cancelled the dialog
  135. if (ready_to_call_listener)
  136. OnFilesSelected(std::move(file_info), lister_base_dir_);
  137. }
  138. }
  139. void OnSaveDialogDone(mate::Dictionary result) {
  140. std::vector<FileChooserFileInfoPtr> file_info;
  141. bool canceled = true;
  142. result.Get("canceled", &canceled);
  143. if (canceled) {
  144. OnSelectionCancelled();
  145. } else {
  146. base::FilePath path;
  147. if (result.Get("filePath", &path)) {
  148. file_info.push_back(FileChooserFileInfo::NewNativeFile(
  149. blink::mojom::NativeFileInfo::New(
  150. path, path.BaseName().AsUTF16Unsafe())));
  151. }
  152. // We should only call this if we have not cancelled the dialog
  153. OnFilesSelected(std::move(file_info), base::FilePath());
  154. }
  155. }
  156. void OnFilesSelected(std::vector<FileChooserFileInfoPtr> file_info,
  157. base::FilePath base_dir) {
  158. if (listener_) {
  159. listener_->FileSelected(std::move(file_info), base_dir, mode_);
  160. listener_.reset();
  161. }
  162. render_frame_host_ = nullptr;
  163. }
  164. void OnSelectionCancelled() {
  165. if (listener_) {
  166. listener_->FileSelectionCanceled();
  167. listener_.reset();
  168. }
  169. render_frame_host_ = nullptr;
  170. }
  171. // content::WebContentsObserver:
  172. void RenderFrameHostChanged(content::RenderFrameHost* old_host,
  173. content::RenderFrameHost* new_host) override {
  174. if (old_host == render_frame_host_)
  175. render_frame_host_ = nullptr;
  176. }
  177. // content::WebContentsObserver:
  178. void RenderFrameDeleted(content::RenderFrameHost* deleted_host) override {
  179. if (deleted_host == render_frame_host_)
  180. render_frame_host_ = nullptr;
  181. }
  182. // content::WebContentsObserver:
  183. void WebContentsDestroyed() override { render_frame_host_ = nullptr; }
  184. content::RenderFrameHost* render_frame_host_;
  185. std::unique_ptr<content::FileSelectListener> listener_;
  186. blink::mojom::FileChooserParams::Mode mode_;
  187. // DirectoryLister-specific members
  188. std::unique_ptr<net::DirectoryLister> lister_;
  189. base::FilePath lister_base_dir_;
  190. std::vector<base::FilePath> lister_paths_;
  191. };
  192. file_dialog::Filters GetFileTypesFromAcceptType(
  193. const std::vector<base::string16>& accept_types) {
  194. file_dialog::Filters filters;
  195. if (accept_types.empty())
  196. return filters;
  197. std::vector<base::FilePath::StringType> extensions;
  198. int valid_type_count = 0;
  199. std::string description;
  200. for (const auto& accept_type : accept_types) {
  201. std::string ascii_type = base::UTF16ToASCII(accept_type);
  202. auto old_extension_size = extensions.size();
  203. if (ascii_type[0] == '.') {
  204. // If the type starts with a period it is assumed to be a file extension,
  205. // like `.txt`, // so we just have to add it to the list.
  206. base::FilePath::StringType extension(ascii_type.begin(),
  207. ascii_type.end());
  208. // Skip the first character.
  209. extensions.push_back(extension.substr(1));
  210. } else {
  211. if (ascii_type == "image/*")
  212. description = "Image Files";
  213. else if (ascii_type == "audio/*")
  214. description = "Audio Files";
  215. else if (ascii_type == "video/*")
  216. description = "Video Files";
  217. // For MIME Type, `audio/*, video/*, image/*
  218. net::GetExtensionsForMimeType(ascii_type, &extensions);
  219. }
  220. if (extensions.size() > old_extension_size)
  221. valid_type_count++;
  222. }
  223. // If no valid extension is added, return empty filters.
  224. if (extensions.empty())
  225. return filters;
  226. filters.push_back(file_dialog::Filter());
  227. if (valid_type_count > 1 || (valid_type_count == 1 && description.empty()))
  228. description = "Custom Files";
  229. DCHECK(!description.empty());
  230. filters[0].first = description;
  231. for (const auto& extension : extensions) {
  232. #if defined(OS_WIN)
  233. filters[0].second.push_back(base::UTF16ToASCII(extension));
  234. #else
  235. filters[0].second.push_back(extension);
  236. #endif
  237. }
  238. // Allow all files when extension is specified.
  239. filters.push_back(file_dialog::Filter());
  240. filters.back().first = "All Files";
  241. filters.back().second.push_back("*");
  242. return filters;
  243. }
  244. } // namespace
  245. namespace electron {
  246. WebDialogHelper::WebDialogHelper(NativeWindow* window, bool offscreen)
  247. : window_(window), offscreen_(offscreen), weak_factory_(this) {}
  248. WebDialogHelper::~WebDialogHelper() {}
  249. void WebDialogHelper::RunFileChooser(
  250. content::RenderFrameHost* render_frame_host,
  251. std::unique_ptr<content::FileSelectListener> listener,
  252. const blink::mojom::FileChooserParams& params) {
  253. file_dialog::DialogSettings settings;
  254. settings.force_detached = offscreen_;
  255. settings.filters = GetFileTypesFromAcceptType(params.accept_types);
  256. settings.parent_window = window_;
  257. settings.title = base::UTF16ToUTF8(params.title);
  258. auto file_select_helper = base::MakeRefCounted<FileSelectHelper>(
  259. render_frame_host, std::move(listener), params.mode);
  260. if (params.mode == FileChooserParams::Mode::kSave) {
  261. settings.default_path = params.default_file_name;
  262. file_select_helper->ShowSaveDialog(settings);
  263. } else {
  264. int flags = file_dialog::FILE_DIALOG_CREATE_DIRECTORY;
  265. switch (params.mode) {
  266. case FileChooserParams::Mode::kOpenMultiple:
  267. flags |= file_dialog::FILE_DIALOG_MULTI_SELECTIONS;
  268. FALLTHROUGH;
  269. case FileChooserParams::Mode::kOpen:
  270. flags |= file_dialog::FILE_DIALOG_OPEN_FILE;
  271. flags |= file_dialog::FILE_DIALOG_TREAT_PACKAGE_APP_AS_DIRECTORY;
  272. break;
  273. case FileChooserParams::Mode::kUploadFolder:
  274. flags |= file_dialog::FILE_DIALOG_OPEN_DIRECTORY;
  275. break;
  276. default:
  277. NOTREACHED();
  278. }
  279. auto* browser_context = static_cast<electron::AtomBrowserContext*>(
  280. render_frame_host->GetProcess()->GetBrowserContext());
  281. settings.default_path = browser_context->prefs()
  282. ->GetFilePath(prefs::kSelectFileLastDirectory)
  283. .Append(params.default_file_name);
  284. settings.properties = flags;
  285. file_select_helper->ShowOpenDialog(settings);
  286. }
  287. }
  288. void WebDialogHelper::EnumerateDirectory(
  289. content::WebContents* web_contents,
  290. std::unique_ptr<content::FileSelectListener> listener,
  291. const base::FilePath& dir) {
  292. int types = base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES |
  293. base::FileEnumerator::INCLUDE_DOT_DOT;
  294. base::FileEnumerator file_enum(dir, false, types);
  295. base::FilePath path;
  296. std::vector<FileChooserFileInfoPtr> file_info;
  297. while (!(path = file_enum.Next()).empty()) {
  298. file_info.push_back(FileChooserFileInfo::NewNativeFile(
  299. blink::mojom::NativeFileInfo::New(path, base::string16())));
  300. }
  301. listener->FileSelected(std::move(file_info), dir,
  302. FileChooserParams::Mode::kUploadFolder);
  303. }
  304. } // namespace electron