file_select_helper.cc 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. // Copyright (c) 2021 Microsoft. 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/file_select_helper.h"
  5. #include <stddef.h>
  6. #include <memory>
  7. #include <string>
  8. #include <utility>
  9. #include "base/files/file_util.h"
  10. #include "base/functional/bind.h"
  11. #include "base/memory/ptr_util.h"
  12. #include "base/strings/string_util.h"
  13. #include "base/strings/utf_string_conversions.h"
  14. #include "base/task/thread_pool.h"
  15. #include "base/threading/hang_watcher.h"
  16. #include "build/build_config.h"
  17. #include "chrome/browser/browser_process.h"
  18. #include "chrome/common/pref_names.h"
  19. #include "chrome/grit/generated_resources.h"
  20. #include "components/prefs/pref_service.h"
  21. #include "content/public/browser/browser_task_traits.h"
  22. #include "content/public/browser/browser_thread.h"
  23. #include "content/public/browser/file_select_listener.h"
  24. #include "content/public/browser/render_frame_host.h"
  25. #include "content/public/browser/render_process_host.h"
  26. #include "content/public/browser/web_contents.h"
  27. #include "net/base/filename_util.h"
  28. #include "net/base/mime_util.h"
  29. #include "shell/browser/api/electron_api_web_contents.h"
  30. #include "shell/browser/electron_browser_context.h"
  31. #include "shell/browser/native_window.h"
  32. #include "ui/base/l10n/l10n_util.h"
  33. #include "ui/shell_dialogs/select_file_policy.h"
  34. #include "ui/shell_dialogs/selected_file_info.h"
  35. using blink::mojom::FileChooserFileInfo;
  36. using blink::mojom::FileChooserFileInfoPtr;
  37. using blink::mojom::FileChooserParams;
  38. using blink::mojom::FileChooserParamsPtr;
  39. using content::BrowserThread;
  40. using content::WebContents;
  41. namespace {
  42. void DeleteFiles(std::vector<base::FilePath> paths) {
  43. for (auto& file_path : paths)
  44. base::DeleteFile(file_path);
  45. }
  46. } // namespace
  47. struct FileSelectHelper::ActiveDirectoryEnumeration {
  48. explicit ActiveDirectoryEnumeration(const base::FilePath& path)
  49. : path_(path) {}
  50. std::unique_ptr<net::DirectoryLister> lister_;
  51. const base::FilePath path_;
  52. std::vector<base::FilePath> results_;
  53. };
  54. FileSelectHelper::FileSelectHelper()
  55. : render_frame_host_(nullptr),
  56. web_contents_(nullptr),
  57. dialog_type_(ui::SelectFileDialog::SELECT_OPEN_FILE),
  58. dialog_mode_(FileChooserParams::Mode::kOpen) {}
  59. FileSelectHelper::~FileSelectHelper() {
  60. // There may be pending file dialogs, we need to tell them that we've gone
  61. // away so they don't try and call back to us.
  62. if (select_file_dialog_)
  63. select_file_dialog_->ListenerDestroyed();
  64. }
  65. void FileSelectHelper::FileSelected(const ui::SelectedFileInfo& file,
  66. int index) {
  67. if (!render_frame_host_) {
  68. RunFileChooserEnd();
  69. return;
  70. }
  71. const base::FilePath& path = file.local_path;
  72. if (dialog_type_ == ui::SelectFileDialog::SELECT_UPLOAD_FOLDER) {
  73. StartNewEnumeration(path);
  74. return;
  75. }
  76. std::vector<ui::SelectedFileInfo> files;
  77. files.push_back(file);
  78. MultiFilesSelected(files);
  79. }
  80. void FileSelectHelper::MultiFilesSelected(
  81. const std::vector<ui::SelectedFileInfo>& files) {
  82. #if BUILDFLAG(IS_MAC)
  83. base::ThreadPool::PostTask(
  84. FROM_HERE,
  85. {base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
  86. base::BindOnce(&FileSelectHelper::ProcessSelectedFilesMac, this, files));
  87. #else
  88. ConvertToFileChooserFileInfoList(files);
  89. #endif // BUILDFLAG(IS_MAC)
  90. }
  91. void FileSelectHelper::FileSelectionCanceled() {
  92. RunFileChooserEnd();
  93. }
  94. void FileSelectHelper::StartNewEnumeration(const base::FilePath& path) {
  95. base_dir_ = path;
  96. auto entry = std::make_unique<ActiveDirectoryEnumeration>(path);
  97. entry->lister_ = base::WrapUnique(new net::DirectoryLister(
  98. path, net::DirectoryLister::NO_SORT_RECURSIVE, this));
  99. entry->lister_->Start();
  100. directory_enumeration_ = std::move(entry);
  101. }
  102. void FileSelectHelper::OnListFile(
  103. const net::DirectoryLister::DirectoryListerData& data) {
  104. // Directory upload only cares about files.
  105. if (data.info.IsDirectory())
  106. return;
  107. directory_enumeration_->results_.push_back(data.path);
  108. }
  109. void FileSelectHelper::LaunchConfirmationDialog(
  110. const base::FilePath& path,
  111. std::vector<ui::SelectedFileInfo> selected_files) {
  112. ConvertToFileChooserFileInfoList(std::move(selected_files));
  113. }
  114. void FileSelectHelper::OnListDone(int error) {
  115. if (!web_contents_) {
  116. // Web contents was destroyed under us (probably by closing the tab). We
  117. // must notify |listener_| and release our reference to
  118. // ourself. RunFileChooserEnd() performs this.
  119. RunFileChooserEnd();
  120. return;
  121. }
  122. // This entry needs to be cleaned up when this function is done.
  123. std::unique_ptr<ActiveDirectoryEnumeration> entry =
  124. std::move(directory_enumeration_);
  125. if (error) {
  126. FileSelectionCanceled();
  127. return;
  128. }
  129. std::vector<ui::SelectedFileInfo> selected_files =
  130. ui::FilePathListToSelectedFileInfoList(entry->results_);
  131. if (dialog_type_ == ui::SelectFileDialog::SELECT_UPLOAD_FOLDER) {
  132. LaunchConfirmationDialog(entry->path_, std::move(selected_files));
  133. } else {
  134. std::vector<FileChooserFileInfoPtr> chooser_files;
  135. for (const auto& file_path : entry->results_) {
  136. chooser_files.push_back(FileChooserFileInfo::NewNativeFile(
  137. blink::mojom::NativeFileInfo::New(file_path, std::u16string())));
  138. }
  139. listener_->FileSelected(std::move(chooser_files), base_dir_,
  140. FileChooserParams::Mode::kUploadFolder);
  141. listener_.reset();
  142. EnumerateDirectoryEnd();
  143. }
  144. }
  145. void FileSelectHelper::ConvertToFileChooserFileInfoList(
  146. const std::vector<ui::SelectedFileInfo>& files) {
  147. if (AbortIfWebContentsDestroyed())
  148. return;
  149. std::vector<FileChooserFileInfoPtr> chooser_files;
  150. for (const auto& file : files) {
  151. chooser_files.push_back(
  152. FileChooserFileInfo::NewNativeFile(blink::mojom::NativeFileInfo::New(
  153. file.local_path,
  154. base::FilePath(file.display_name).AsUTF16Unsafe())));
  155. }
  156. PerformContentAnalysisIfNeeded(std::move(chooser_files));
  157. }
  158. void FileSelectHelper::PerformContentAnalysisIfNeeded(
  159. std::vector<FileChooserFileInfoPtr> list) {
  160. if (AbortIfWebContentsDestroyed())
  161. return;
  162. NotifyListenerAndEnd(std::move(list));
  163. }
  164. void FileSelectHelper::NotifyListenerAndEnd(
  165. std::vector<blink::mojom::FileChooserFileInfoPtr> list) {
  166. listener_->FileSelected(std::move(list), base_dir_, dialog_mode_);
  167. listener_.reset();
  168. // No members should be accessed from here on.
  169. RunFileChooserEnd();
  170. }
  171. void FileSelectHelper::DeleteTemporaryFiles() {
  172. base::ThreadPool::PostTask(
  173. FROM_HERE,
  174. {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
  175. base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
  176. base::BindOnce(&DeleteFiles, std::move(temporary_files_)));
  177. }
  178. void FileSelectHelper::CleanUp() {
  179. if (!temporary_files_.empty()) {
  180. DeleteTemporaryFiles();
  181. // Now that the temporary files have been scheduled for deletion, there
  182. // is no longer any reason to keep this instance around.
  183. Release();
  184. }
  185. }
  186. bool FileSelectHelper::AbortIfWebContentsDestroyed() {
  187. if (render_frame_host_ == nullptr || web_contents_ == nullptr) {
  188. RunFileChooserEnd();
  189. return true;
  190. }
  191. return false;
  192. }
  193. void FileSelectHelper::SetFileSelectListenerForTesting(
  194. scoped_refptr<content::FileSelectListener> listener) {
  195. DCHECK(listener);
  196. DCHECK(!listener_);
  197. listener_ = std::move(listener);
  198. }
  199. std::unique_ptr<ui::SelectFileDialog::FileTypeInfo>
  200. FileSelectHelper::GetFileTypesFromAcceptType(
  201. const std::vector<std::u16string>& accept_types) {
  202. std::unique_ptr<ui::SelectFileDialog::FileTypeInfo> base_file_type(
  203. new ui::SelectFileDialog::FileTypeInfo());
  204. if (accept_types.empty())
  205. return base_file_type;
  206. // Create FileTypeInfo and pre-allocate for the first extension list.
  207. std::unique_ptr<ui::SelectFileDialog::FileTypeInfo> file_type(
  208. new ui::SelectFileDialog::FileTypeInfo(*base_file_type));
  209. file_type->include_all_files = true;
  210. file_type->extensions.resize(1);
  211. std::vector<base::FilePath::StringType>* extensions =
  212. &file_type->extensions.back();
  213. // Find the corresponding extensions.
  214. int valid_type_count = 0;
  215. int description_id = 0;
  216. for (const auto& accept_type : accept_types) {
  217. size_t old_extension_size = extensions->size();
  218. if (accept_type[0] == '.') {
  219. // If the type starts with a period it is assumed to be a file extension
  220. // so we just have to add it to the list.
  221. base::FilePath::StringType ext =
  222. base::FilePath::FromUTF16Unsafe(accept_type).value();
  223. extensions->push_back(ext.substr(1));
  224. } else {
  225. if (!base::IsStringASCII(accept_type))
  226. continue;
  227. std::string ascii_type = base::UTF16ToASCII(accept_type);
  228. if (ascii_type == "image/*")
  229. description_id = IDS_IMAGE_FILES;
  230. else if (ascii_type == "audio/*")
  231. description_id = IDS_AUDIO_FILES;
  232. else if (ascii_type == "video/*")
  233. description_id = IDS_VIDEO_FILES;
  234. net::GetExtensionsForMimeType(ascii_type, extensions);
  235. }
  236. if (extensions->size() > old_extension_size)
  237. valid_type_count++;
  238. }
  239. // If no valid extension is added, bail out.
  240. if (valid_type_count == 0)
  241. return base_file_type;
  242. // Use a generic description "Custom Files" if either of the following is
  243. // true:
  244. // 1) There're multiple types specified, like "audio/*,video/*"
  245. // 2) There're multiple extensions for a MIME type without parameter, like
  246. // "ehtml,shtml,htm,html" for "text/html". On Windows, the select file
  247. // dialog uses the first extension in the list to form the description,
  248. // like "EHTML Files". This is not what we want.
  249. if (valid_type_count > 1 ||
  250. (valid_type_count == 1 && description_id == 0 && extensions->size() > 1))
  251. description_id = IDS_CUSTOM_FILES;
  252. if (description_id) {
  253. file_type->extension_description_overrides.push_back(
  254. l10n_util::GetStringUTF16(description_id));
  255. }
  256. return file_type;
  257. }
  258. // static
  259. void FileSelectHelper::RunFileChooser(
  260. content::RenderFrameHost* render_frame_host,
  261. scoped_refptr<content::FileSelectListener> listener,
  262. const FileChooserParams& params) {
  263. // FileSelectHelper will keep itself alive until it sends the result
  264. // message.
  265. scoped_refptr<FileSelectHelper> file_select_helper(new FileSelectHelper());
  266. file_select_helper->RunFileChooser(render_frame_host, std::move(listener),
  267. params.Clone());
  268. }
  269. // static
  270. void FileSelectHelper::EnumerateDirectory(
  271. content::WebContents* tab,
  272. scoped_refptr<content::FileSelectListener> listener,
  273. const base::FilePath& path) {
  274. // FileSelectHelper will keep itself alive until it sends the result
  275. // message.
  276. scoped_refptr<FileSelectHelper> file_select_helper(new FileSelectHelper());
  277. file_select_helper->EnumerateDirectoryImpl(tab, std::move(listener), path);
  278. }
  279. void FileSelectHelper::RunFileChooser(
  280. content::RenderFrameHost* render_frame_host,
  281. scoped_refptr<content::FileSelectListener> listener,
  282. FileChooserParamsPtr params) {
  283. DCHECK(!render_frame_host_);
  284. DCHECK(!web_contents_);
  285. DCHECK(listener);
  286. DCHECK(!listener_);
  287. DCHECK(params->default_file_name.empty() ||
  288. params->mode == FileChooserParams::Mode::kSave)
  289. << "The default_file_name parameter should only be specified for Save "
  290. "file choosers";
  291. DCHECK(params->default_file_name == params->default_file_name.BaseName())
  292. << "The default_file_name parameter should not contain path separators";
  293. render_frame_host_ = render_frame_host;
  294. web_contents_ = WebContents::FromRenderFrameHost(render_frame_host);
  295. listener_ = std::move(listener);
  296. content::WebContentsObserver::Observe(web_contents_);
  297. base::ThreadPool::PostTask(
  298. FROM_HERE, {base::MayBlock()},
  299. base::BindOnce(&FileSelectHelper::GetFileTypesInThreadPool, this,
  300. std::move(params)));
  301. // Because this class returns notifications to the RenderViewHost, it is
  302. // difficult for callers to know how long to keep a reference to this
  303. // instance. We AddRef() here to keep the instance alive after we return
  304. // to the caller, until the last callback is received from the file dialog.
  305. // At that point, we must call RunFileChooserEnd().
  306. AddRef();
  307. }
  308. void FileSelectHelper::GetFileTypesInThreadPool(FileChooserParamsPtr params) {
  309. select_file_types_ = GetFileTypesFromAcceptType(params->accept_types);
  310. select_file_types_->allowed_paths =
  311. params->need_local_path ? ui::SelectFileDialog::FileTypeInfo::NATIVE_PATH
  312. : ui::SelectFileDialog::FileTypeInfo::ANY_PATH;
  313. content::GetUIThreadTaskRunner({})->PostTask(
  314. FROM_HERE,
  315. base::BindOnce(&FileSelectHelper::GetSanitizedFilenameOnUIThread, this,
  316. std::move(params)));
  317. }
  318. void FileSelectHelper::GetSanitizedFilenameOnUIThread(
  319. FileChooserParamsPtr params) {
  320. if (AbortIfWebContentsDestroyed())
  321. return;
  322. auto* browser_context = static_cast<electron::ElectronBrowserContext*>(
  323. render_frame_host_->GetProcess()->GetBrowserContext());
  324. base::FilePath default_file_path =
  325. browser_context->prefs()
  326. ->GetFilePath(prefs::kSelectFileLastDirectory)
  327. .Append(params->default_file_name);
  328. RunFileChooserOnUIThread(default_file_path, std::move(params));
  329. }
  330. void FileSelectHelper::RunFileChooserOnUIThread(
  331. const base::FilePath& default_file_path,
  332. FileChooserParamsPtr params) {
  333. DCHECK(params);
  334. select_file_dialog_ = ui::SelectFileDialog::Create(this, nullptr);
  335. if (!select_file_dialog_.get())
  336. return;
  337. dialog_mode_ = params->mode;
  338. switch (params->mode) {
  339. case FileChooserParams::Mode::kOpen:
  340. dialog_type_ = ui::SelectFileDialog::SELECT_OPEN_FILE;
  341. break;
  342. case FileChooserParams::Mode::kOpenMultiple:
  343. dialog_type_ = ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE;
  344. break;
  345. case FileChooserParams::Mode::kUploadFolder:
  346. dialog_type_ = ui::SelectFileDialog::SELECT_UPLOAD_FOLDER;
  347. break;
  348. case FileChooserParams::Mode::kSave:
  349. dialog_type_ = ui::SelectFileDialog::SELECT_SAVEAS_FILE;
  350. break;
  351. default:
  352. // Prevent warning.
  353. dialog_type_ = ui::SelectFileDialog::SELECT_OPEN_FILE;
  354. NOTREACHED();
  355. }
  356. auto* web_contents = electron::api::WebContents::From(
  357. content::WebContents::FromRenderFrameHost(render_frame_host_));
  358. if (!web_contents || !web_contents->owner_window())
  359. return;
  360. // Never consider the current scope as hung. The hang watching deadline (if
  361. // any) is not valid since the user can take unbounded time to choose the
  362. // file.
  363. base::HangWatcher::InvalidateActiveExpectations();
  364. select_file_dialog_->SelectFile(
  365. dialog_type_, params->title, default_file_path, select_file_types_.get(),
  366. select_file_types_.get() && !select_file_types_->extensions.empty()
  367. ? 1
  368. : 0, // 1-based index of default extension to show.
  369. base::FilePath::StringType(),
  370. web_contents->owner_window()->GetNativeWindow(), nullptr);
  371. select_file_types_.reset();
  372. }
  373. // This method is called when we receive the last callback from the file chooser
  374. // dialog or if the renderer was destroyed. Perform any cleanup and release the
  375. // reference we added in RunFileChooser().
  376. void FileSelectHelper::RunFileChooserEnd() {
  377. // If there are temporary files, then this instance needs to stick around
  378. // until web_contents_ is destroyed, so that this instance can delete the
  379. // temporary files.
  380. if (!temporary_files_.empty())
  381. return;
  382. if (listener_)
  383. listener_->FileSelectionCanceled();
  384. render_frame_host_ = nullptr;
  385. web_contents_ = nullptr;
  386. // If the dialog was actually opened, dispose of our reference.
  387. if (select_file_dialog_) {
  388. select_file_dialog_->ListenerDestroyed();
  389. select_file_dialog_.reset();
  390. }
  391. Release();
  392. }
  393. void FileSelectHelper::EnumerateDirectoryImpl(
  394. content::WebContents* tab,
  395. scoped_refptr<content::FileSelectListener> listener,
  396. const base::FilePath& path) {
  397. DCHECK(listener);
  398. DCHECK(!listener_);
  399. dialog_type_ = ui::SelectFileDialog::SELECT_NONE;
  400. web_contents_ = tab;
  401. listener_ = std::move(listener);
  402. // Because this class returns notifications to the RenderViewHost, it is
  403. // difficult for callers to know how long to keep a reference to this
  404. // instance. We AddRef() here to keep the instance alive after we return
  405. // to the caller, until the last callback is received from the enumeration
  406. // code. At that point, we must call EnumerateDirectoryEnd().
  407. AddRef();
  408. StartNewEnumeration(path);
  409. }
  410. // This method is called when we receive the last callback from the enumeration
  411. // code. Perform any cleanup and release the reference we added in
  412. // EnumerateDirectoryImpl().
  413. void FileSelectHelper::EnumerateDirectoryEnd() {
  414. Release();
  415. }
  416. void FileSelectHelper::RenderFrameHostChanged(
  417. content::RenderFrameHost* old_host,
  418. content::RenderFrameHost* new_host) {
  419. // The |old_host| and its children are now pending deletion. Do not give them
  420. // file access past this point.
  421. for (content::RenderFrameHost* host = render_frame_host_; host;
  422. host = host->GetParentOrOuterDocument()) {
  423. if (host == old_host) {
  424. render_frame_host_ = nullptr;
  425. return;
  426. }
  427. }
  428. }
  429. void FileSelectHelper::RenderFrameDeleted(
  430. content::RenderFrameHost* render_frame_host) {
  431. if (render_frame_host == render_frame_host_)
  432. render_frame_host_ = nullptr;
  433. }
  434. void FileSelectHelper::WebContentsDestroyed() {
  435. render_frame_host_ = nullptr;
  436. web_contents_ = nullptr;
  437. CleanUp();
  438. }
  439. // static
  440. bool FileSelectHelper::IsAcceptTypeValid(const std::string& accept_type) {
  441. // TODO(raymes): This only does some basic checks, extend to test more cases.
  442. // A 1 character accept type will always be invalid (either a "." in the case
  443. // of an extension or a "/" in the case of a MIME type).
  444. std::string unused;
  445. if (accept_type.length() <= 1 ||
  446. base::ToLowerASCII(accept_type) != accept_type ||
  447. base::TrimWhitespaceASCII(accept_type, base::TRIM_ALL, &unused) !=
  448. base::TRIM_NONE) {
  449. return false;
  450. }
  451. return true;
  452. }
  453. // static
  454. base::FilePath FileSelectHelper::GetSanitizedFileName(
  455. const base::FilePath& suggested_filename) {
  456. if (suggested_filename.empty())
  457. return {};
  458. return net::GenerateFileName(
  459. GURL(), std::string(), std::string(), suggested_filename.AsUTF8Unsafe(),
  460. std::string(), l10n_util::GetStringUTF8(IDS_DEFAULT_DOWNLOAD_FILENAME));
  461. }