file_select_helper.cc 19 KB

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