file_select_helper.cc 18 KB

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