• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (c) 2012 The Chromium Embedded Framework Authors.
2 // Portions copyright (c) 2012 The Chromium Authors. All rights reserved.
3 // Use of this source code is governed by a BSD-style license that can be
4 // found in the LICENSE file.
5 
6 #include "libcef/browser/native/file_dialog_runner_win.h"
7 
8 #include <windows.h>
9 
10 #include <commdlg.h>
11 #include <shlobj.h>
12 #include <wrl/client.h>
13 
14 #include "libcef/browser/alloy/alloy_browser_host_impl.h"
15 
16 #include "base/files/file_util.h"
17 #include "base/stl_util.h"
18 #include "base/strings/string_split.h"
19 #include "base/strings/string_util.h"
20 #include "base/strings/utf_string_conversions.h"
21 #include "base/win/registry.h"
22 #include "cef/grit/cef_strings.h"
23 #include "chrome/grit/generated_resources.h"
24 #include "net/base/mime_util.h"
25 #include "ui/base/l10n/l10n_util.h"
26 #include "ui/base/win/shell.h"
27 #include "ui/strings/grit/ui_strings.h"
28 
29 namespace {
30 
31 // From ui/base/dialogs/select_file_dialog_win.cc.
32 
33 // Get the file type description from the registry. This will be "Text Document"
34 // for .txt files, "JPEG Image" for .jpg files, etc. If the registry doesn't
35 // have an entry for the file type, we return false, true if the description was
36 // found. 'file_ext' must be in form ".txt".
GetRegistryDescriptionFromExtension(const std::wstring & file_ext,std::wstring * reg_description)37 static bool GetRegistryDescriptionFromExtension(const std::wstring& file_ext,
38                                                 std::wstring* reg_description) {
39   DCHECK(reg_description);
40   base::win::RegKey reg_ext(HKEY_CLASSES_ROOT, file_ext.c_str(), KEY_READ);
41   std::wstring reg_app;
42   if (reg_ext.ReadValue(NULL, &reg_app) == ERROR_SUCCESS && !reg_app.empty()) {
43     base::win::RegKey reg_link(HKEY_CLASSES_ROOT, reg_app.c_str(), KEY_READ);
44     if (reg_link.ReadValue(NULL, reg_description) == ERROR_SUCCESS)
45       return true;
46   }
47   return false;
48 }
49 
50 // Set up a filter for a Save/Open dialog, which will consist of |file_ext| file
51 // extensions (internally separated by semicolons), |ext_desc| as the text
52 // descriptions of the |file_ext| types (optional), and (optionally) the default
53 // 'All Files' view. The purpose of the filter is to show only files of a
54 // particular type in a Windows Save/Open dialog box. The resulting filter is
55 // returned. The filters created here are:
56 //   1. only files that have 'file_ext' as their extension
57 //   2. all files (only added if 'include_all_files' is true)
58 // Example:
59 //   file_ext: { "*.txt", "*.htm;*.html" }
60 //   ext_desc: { "Text Document" }
61 //   returned: "Text Document\0*.txt\0HTML Document\0*.htm;*.html\0"
62 //             "All Files\0*.*\0\0" (in one big string)
63 // If a description is not provided for a file extension, it will be retrieved
64 // from the registry. If the file extension does not exist in the registry, it
65 // will be omitted from the filter, as it is likely a bogus extension.
FormatFilterForExtensions(const std::vector<std::wstring> & file_ext,const std::vector<std::wstring> & ext_desc,bool include_all_files)66 std::wstring FormatFilterForExtensions(
67     const std::vector<std::wstring>& file_ext,
68     const std::vector<std::wstring>& ext_desc,
69     bool include_all_files) {
70   const std::wstring all_ext = L"*.*";
71   const std::wstring all_desc =
72       base::UTF16ToWide(l10n_util::GetStringUTF16(IDS_APP_SAVEAS_ALL_FILES)) +
73       L" (" + all_ext + L")";
74 
75   DCHECK(file_ext.size() >= ext_desc.size());
76 
77   if (file_ext.empty())
78     include_all_files = true;
79 
80   std::wstring result;
81 
82   // Create all supported .ext filter if more than one filter.
83   if (file_ext.size() > 1) {
84     std::set<base::WStringPiece> unique_exts;
85     for (const auto& exts : file_ext) {
86       for (const auto& ext : base::SplitStringPiece(
87                exts, L";", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY)) {
88         unique_exts.insert(ext);
89       }
90     }
91 
92     if (unique_exts.size() > 1) {
93       std::wstring ext;
94       auto it = unique_exts.cbegin();
95       ext = std::wstring(*it);
96       for (++it; it != unique_exts.cend(); ++it) {
97         ext += L";" + std::wstring(*it);
98       }
99       std::wstring desc =
100           base::UTF16ToWide(l10n_util::GetStringUTF16(IDS_CUSTOM_FILES)) +
101           L" (" + ext + L")";
102 
103       result.append(desc.c_str(), desc.size() + 1);  // Append NULL too.
104       result.append(ext.c_str(), ext.size() + 1);
105     }
106   }
107 
108   for (size_t i = 0; i < file_ext.size(); ++i) {
109     std::wstring ext = file_ext[i];
110     std::wstring desc;
111     if (i < ext_desc.size())
112       desc = ext_desc[i];
113 
114     if (ext.empty()) {
115       // Force something reasonable to appear in the dialog box if there is no
116       // extension provided.
117       include_all_files = true;
118       continue;
119     }
120 
121     if (desc.empty()) {
122       DCHECK(ext.find(L'.') != std::wstring::npos);
123       std::wstring first_extension = ext.substr(ext.find(L'.'));
124       size_t first_separator_index = first_extension.find(L';');
125       if (first_separator_index != std::wstring::npos)
126         first_extension = first_extension.substr(0, first_separator_index);
127 
128       // Find the extension name without the preceeding '.' character.
129       std::wstring ext_name = first_extension;
130       size_t ext_index = ext_name.find_first_not_of(L'.');
131       if (ext_index != std::wstring::npos)
132         ext_name = ext_name.substr(ext_index);
133 
134       if (!GetRegistryDescriptionFromExtension(first_extension, &desc)) {
135         // The extension doesn't exist in the registry.
136         include_all_files = true;
137       }
138     }
139 
140     if (!desc.empty())
141       desc += L" (" + ext + L")";
142     else
143       desc = ext;
144 
145     result.append(desc.c_str(), desc.size() + 1);  // Append NULL too.
146     result.append(ext.c_str(), ext.size() + 1);
147   }
148 
149   if (include_all_files) {
150     result.append(all_desc.c_str(), all_desc.size() + 1);
151     result.append(all_ext.c_str(), all_ext.size() + 1);
152   }
153 
154   result.append(1, '\0');  // Double NULL required.
155   return result;
156 }
157 
GetDescriptionFromMimeType(const std::string & mime_type)158 std::wstring GetDescriptionFromMimeType(const std::string& mime_type) {
159   // Check for wild card mime types and return an appropriate description.
160   static const struct {
161     const char* mime_type;
162     int string_id;
163   } kWildCardMimeTypes[] = {
164       {"audio", IDS_AUDIO_FILES},
165       {"image", IDS_IMAGE_FILES},
166       {"text", IDS_TEXT_FILES},
167       {"video", IDS_VIDEO_FILES},
168   };
169 
170   for (size_t i = 0; i < base::size(kWildCardMimeTypes); ++i) {
171     if (mime_type == std::string(kWildCardMimeTypes[i].mime_type) + "/*") {
172       return base::UTF16ToWide(
173           l10n_util::GetStringUTF16(kWildCardMimeTypes[i].string_id));
174     }
175   }
176 
177   return std::wstring();
178 }
179 
GetFilterString(const std::vector<std::u16string> & accept_filters)180 std::wstring GetFilterString(
181     const std::vector<std::u16string>& accept_filters) {
182   std::vector<std::wstring> extensions;
183   std::vector<std::wstring> descriptions;
184 
185   for (size_t i = 0; i < accept_filters.size(); ++i) {
186     const std::wstring& filter = base::UTF16ToWide(accept_filters[i]);
187     if (filter.empty())
188       continue;
189 
190     size_t sep_index = filter.find(L'|');
191     if (sep_index != std::wstring::npos) {
192       // Treat as a filter of the form "Filter Name|.ext1;.ext2;.ext3".
193       const std::wstring& desc = filter.substr(0, sep_index);
194       const std::vector<std::u16string>& ext = base::SplitString(
195           base::WideToUTF16(filter.substr(sep_index + 1)), u";",
196           base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
197       std::wstring ext_str;
198       for (size_t x = 0; x < ext.size(); ++x) {
199         const std::wstring& file_ext = base::UTF16ToWide(ext[x]);
200         if (!file_ext.empty() && file_ext[0] == L'.') {
201           if (!ext_str.empty())
202             ext_str += L";";
203           ext_str += L"*" + file_ext;
204         }
205       }
206       if (!ext_str.empty()) {
207         extensions.push_back(ext_str);
208         descriptions.push_back(desc);
209       }
210     } else if (filter[0] == L'.') {
211       // Treat as an extension beginning with the '.' character.
212       extensions.push_back(L"*" + filter);
213       descriptions.push_back(std::wstring());
214     } else {
215       // Otherwise convert mime type to one or more extensions.
216       const std::string& ascii = base::WideToASCII(filter);
217       std::vector<base::FilePath::StringType> ext;
218       std::wstring ext_str;
219       net::GetExtensionsForMimeType(ascii, &ext);
220       if (!ext.empty()) {
221         for (size_t x = 0; x < ext.size(); ++x) {
222           if (x != 0)
223             ext_str += L";";
224           ext_str += L"*." + ext[x];
225         }
226         extensions.push_back(ext_str);
227         descriptions.push_back(GetDescriptionFromMimeType(ascii));
228       }
229     }
230   }
231 
232   return FormatFilterForExtensions(extensions, descriptions, true);
233 }
234 
235 // From chrome/browser/views/shell_dialogs_win.cc
236 
RunOpenFileDialog(const CefFileDialogRunner::FileChooserParams & params,HWND owner,int * filter_index,base::FilePath * path)237 bool RunOpenFileDialog(const CefFileDialogRunner::FileChooserParams& params,
238                        HWND owner,
239                        int* filter_index,
240                        base::FilePath* path) {
241   OPENFILENAME ofn;
242 
243   // We must do this otherwise the ofn's FlagsEx may be initialized to random
244   // junk in release builds which can cause the Places Bar not to show up!
245   ZeroMemory(&ofn, sizeof(ofn));
246   ofn.lStructSize = sizeof(ofn);
247   ofn.hwndOwner = owner;
248 
249   wchar_t filename[MAX_PATH] = {0};
250 
251   ofn.lpstrFile = filename;
252   ofn.nMaxFile = MAX_PATH;
253 
254   std::wstring directory;
255   if (!params.default_file_name.empty()) {
256     if (params.default_file_name.EndsWithSeparator()) {
257       // The value is only a directory.
258       directory = params.default_file_name.value();
259     } else {
260       // The value is a file name and possibly a directory.
261       base::wcslcpy(filename, params.default_file_name.value().c_str(),
262                     base::size(filename));
263       directory = params.default_file_name.DirName().value();
264     }
265   }
266   if (!directory.empty())
267     ofn.lpstrInitialDir = directory.c_str();
268 
269   std::wstring title;
270   if (!params.title.empty()) {
271     title = base::UTF16ToWide(params.title);
272   } else {
273     title = base::UTF16ToWide(
274         l10n_util::GetStringUTF16(IDS_OPEN_FILE_DIALOG_TITLE));
275   }
276   if (!title.empty())
277     ofn.lpstrTitle = title.c_str();
278 
279   // We use OFN_NOCHANGEDIR so that the user can rename or delete the directory
280   // without having to close Chrome first.
281   ofn.Flags =
282       OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR | OFN_EXPLORER | OFN_ENABLESIZING;
283   if (params.hidereadonly)
284     ofn.Flags |= OFN_HIDEREADONLY;
285 
286   const std::wstring& filter = GetFilterString(params.accept_types);
287   if (!filter.empty()) {
288     ofn.lpstrFilter = filter.c_str();
289     // Indices into |lpstrFilter| start at 1.
290     ofn.nFilterIndex = *filter_index + 1;
291   }
292 
293   bool success = !!GetOpenFileName(&ofn);
294   if (success) {
295     *filter_index = ofn.nFilterIndex == 0 ? 0 : ofn.nFilterIndex - 1;
296     *path = base::FilePath(filename);
297   }
298   return success;
299 }
300 
RunOpenMultiFileDialog(const CefFileDialogRunner::FileChooserParams & params,HWND owner,int * filter_index,std::vector<base::FilePath> * paths)301 bool RunOpenMultiFileDialog(
302     const CefFileDialogRunner::FileChooserParams& params,
303     HWND owner,
304     int* filter_index,
305     std::vector<base::FilePath>* paths) {
306   OPENFILENAME ofn;
307 
308   // We must do this otherwise the ofn's FlagsEx may be initialized to random
309   // junk in release builds which can cause the Places Bar not to show up!
310   ZeroMemory(&ofn, sizeof(ofn));
311   ofn.lStructSize = sizeof(ofn);
312   ofn.hwndOwner = owner;
313 
314   std::unique_ptr<wchar_t[]> filename(new wchar_t[UNICODE_STRING_MAX_CHARS]);
315   filename[0] = 0;
316 
317   ofn.lpstrFile = filename.get();
318   ofn.nMaxFile = UNICODE_STRING_MAX_CHARS;
319 
320   std::wstring directory;
321   if (!params.default_file_name.empty()) {
322     if (params.default_file_name.EndsWithSeparator()) {
323       // The value is only a directory.
324       directory = params.default_file_name.value();
325     } else {
326       // The value is a file name and possibly a directory.
327       directory = params.default_file_name.DirName().value();
328     }
329   }
330   if (!directory.empty())
331     ofn.lpstrInitialDir = directory.c_str();
332 
333   std::wstring title;
334   if (!params.title.empty()) {
335     title = base::UTF16ToWide(params.title);
336   } else {
337     title = base::UTF16ToWide(
338         l10n_util::GetStringUTF16(IDS_OPEN_FILES_DIALOG_TITLE));
339   }
340   if (!title.empty())
341     ofn.lpstrTitle = title.c_str();
342 
343   // We use OFN_NOCHANGEDIR so that the user can rename or delete the directory
344   // without having to close Chrome first.
345   ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER |
346               OFN_ALLOWMULTISELECT | OFN_ENABLESIZING;
347   if (params.hidereadonly)
348     ofn.Flags |= OFN_HIDEREADONLY;
349 
350   const std::wstring& filter = GetFilterString(params.accept_types);
351   if (!filter.empty()) {
352     ofn.lpstrFilter = filter.c_str();
353     // Indices into |lpstrFilter| start at 1.
354     ofn.nFilterIndex = *filter_index + 1;
355   }
356 
357   bool success = !!GetOpenFileName(&ofn);
358 
359   if (success) {
360     std::vector<base::FilePath> files;
361     const wchar_t* selection = ofn.lpstrFile;
362     while (*selection) {  // Empty string indicates end of list.
363       files.push_back(base::FilePath(selection));
364       // Skip over filename and null-terminator.
365       selection += files.back().value().length() + 1;
366     }
367     if (files.empty()) {
368       success = false;
369     } else if (files.size() == 1) {
370       // When there is one file, it contains the path and filename.
371       paths->swap(files);
372     } else {
373       // Otherwise, the first string is the path, and the remainder are
374       // filenames.
375       std::vector<base::FilePath>::iterator path = files.begin();
376       for (std::vector<base::FilePath>::iterator file = path + 1;
377            file != files.end(); ++file) {
378         paths->push_back(path->Append(*file));
379       }
380     }
381   }
382 
383   if (success)
384     *filter_index = ofn.nFilterIndex == 0 ? 0 : ofn.nFilterIndex - 1;
385 
386   return success;
387 }
388 
389 // The callback function for when the select folder dialog is opened.
BrowseCallbackProc(HWND window,UINT message,LPARAM parameter,LPARAM data)390 int CALLBACK BrowseCallbackProc(HWND window,
391                                 UINT message,
392                                 LPARAM parameter,
393                                 LPARAM data) {
394   if (message == BFFM_INITIALIZED) {
395     // WParam is TRUE since passing a path.
396     // data lParam member of the BROWSEINFO structure.
397     SendMessage(window, BFFM_SETSELECTION, TRUE, (LPARAM)data);
398   }
399   return 0;
400 }
401 
RunOpenFolderDialog(const CefFileDialogRunner::FileChooserParams & params,HWND owner,base::FilePath * path)402 bool RunOpenFolderDialog(const CefFileDialogRunner::FileChooserParams& params,
403                          HWND owner,
404                          base::FilePath* path) {
405   wchar_t dir_buffer[MAX_PATH + 1] = {0};
406 
407   bool result = false;
408   BROWSEINFO browse_info = {0};
409   browse_info.hwndOwner = owner;
410   browse_info.pszDisplayName = dir_buffer;
411   browse_info.ulFlags = BIF_USENEWUI | BIF_RETURNONLYFSDIRS;
412 
413   std::wstring title;
414   if (!params.title.empty()) {
415     title = base::UTF16ToWide(params.title);
416   } else {
417     title = base::UTF16ToWide(
418         l10n_util::GetStringUTF16(IDS_SELECT_FOLDER_DIALOG_TITLE));
419   }
420   if (!title.empty())
421     browse_info.lpszTitle = title.c_str();
422 
423   const std::wstring& file_path = params.default_file_name.value();
424   if (!file_path.empty()) {
425     // Highlight the current value.
426     browse_info.lParam = (LPARAM)file_path.c_str();
427     browse_info.lpfn = &BrowseCallbackProc;
428   }
429 
430   LPITEMIDLIST list = SHBrowseForFolder(&browse_info);
431   if (list) {
432     STRRET out_dir_buffer;
433     ZeroMemory(&out_dir_buffer, sizeof(out_dir_buffer));
434     out_dir_buffer.uType = STRRET_WSTR;
435     Microsoft::WRL::ComPtr<IShellFolder> shell_folder;
436     if (SHGetDesktopFolder(shell_folder.GetAddressOf()) == NOERROR) {
437       HRESULT hr = shell_folder->GetDisplayNameOf(list, SHGDN_FORPARSING,
438                                                   &out_dir_buffer);
439       if (SUCCEEDED(hr) && out_dir_buffer.uType == STRRET_WSTR) {
440         *path = base::FilePath(out_dir_buffer.pOleStr);
441         CoTaskMemFree(out_dir_buffer.pOleStr);
442         result = true;
443       } else {
444         // Use old way if we don't get what we want.
445         wchar_t old_out_dir_buffer[MAX_PATH + 1];
446         if (SHGetPathFromIDList(list, old_out_dir_buffer)) {
447           *path = base::FilePath(old_out_dir_buffer);
448           result = true;
449         }
450       }
451     }
452     CoTaskMemFree(list);
453   }
454 
455   return result;
456 }
457 
RunSaveFileDialog(const CefFileDialogRunner::FileChooserParams & params,HWND owner,int * filter_index,base::FilePath * path)458 bool RunSaveFileDialog(const CefFileDialogRunner::FileChooserParams& params,
459                        HWND owner,
460                        int* filter_index,
461                        base::FilePath* path) {
462   OPENFILENAME ofn;
463 
464   // We must do this otherwise the ofn's FlagsEx may be initialized to random
465   // junk in release builds which can cause the Places Bar not to show up!
466   ZeroMemory(&ofn, sizeof(ofn));
467   ofn.lStructSize = sizeof(ofn);
468   ofn.hwndOwner = owner;
469 
470   wchar_t filename[MAX_PATH] = {0};
471 
472   ofn.lpstrFile = filename;
473   ofn.nMaxFile = MAX_PATH;
474 
475   std::wstring directory;
476   if (!params.default_file_name.empty()) {
477     if (params.default_file_name.EndsWithSeparator()) {
478       // The value is only a directory.
479       directory = params.default_file_name.value();
480     } else {
481       // The value is a file name and possibly a directory.
482       base::wcslcpy(filename, params.default_file_name.value().c_str(),
483                     base::size(filename));
484       directory = params.default_file_name.DirName().value();
485     }
486   }
487   if (!directory.empty())
488     ofn.lpstrInitialDir = directory.c_str();
489 
490   std::wstring title;
491   if (!params.title.empty()) {
492     title = base::UTF16ToWide(params.title);
493   } else {
494     title =
495         base::UTF16ToWide(l10n_util::GetStringUTF16(IDS_SAVE_AS_DIALOG_TITLE));
496   }
497   if (!title.empty())
498     ofn.lpstrTitle = title.c_str();
499 
500   // We use OFN_NOCHANGEDIR so that the user can rename or delete the directory
501   // without having to close Chrome first.
502   ofn.Flags =
503       OFN_EXPLORER | OFN_ENABLESIZING | OFN_NOCHANGEDIR | OFN_PATHMUSTEXIST;
504   if (params.hidereadonly)
505     ofn.Flags |= OFN_HIDEREADONLY;
506   if (params.overwriteprompt)
507     ofn.Flags |= OFN_OVERWRITEPROMPT;
508 
509   const std::wstring& filter = GetFilterString(params.accept_types);
510   if (!filter.empty()) {
511     ofn.lpstrFilter = filter.c_str();
512     // Indices into |lpstrFilter| start at 1.
513     ofn.nFilterIndex = *filter_index + 1;
514     // If a filter is specified and the default file name is changed then append
515     // a file extension to the new name.
516     ofn.lpstrDefExt = L"";
517   }
518 
519   bool success = !!GetSaveFileName(&ofn);
520   if (success) {
521     *filter_index = ofn.nFilterIndex == 0 ? 0 : ofn.nFilterIndex - 1;
522     *path = base::FilePath(filename);
523   }
524   return success;
525 }
526 
527 }  // namespace
528 
CefFileDialogRunnerWin()529 CefFileDialogRunnerWin::CefFileDialogRunnerWin() {}
530 
Run(AlloyBrowserHostImpl * browser,const FileChooserParams & params,RunFileChooserCallback callback)531 void CefFileDialogRunnerWin::Run(AlloyBrowserHostImpl* browser,
532                                  const FileChooserParams& params,
533                                  RunFileChooserCallback callback) {
534   int filter_index = params.selected_accept_filter;
535   std::vector<base::FilePath> files;
536 
537   HWND owner = browser->GetWindowHandle();
538 
539   if (params.mode == blink::mojom::FileChooserParams::Mode::kOpen) {
540     base::FilePath file;
541     if (RunOpenFileDialog(params, owner, &filter_index, &file))
542       files.push_back(file);
543   } else if (params.mode ==
544              blink::mojom::FileChooserParams::Mode::kOpenMultiple) {
545     RunOpenMultiFileDialog(params, owner, &filter_index, &files);
546   } else if (params.mode ==
547              blink::mojom::FileChooserParams::Mode::kUploadFolder) {
548     base::FilePath file;
549     if (RunOpenFolderDialog(params, owner, &file))
550       files.push_back(file);
551   } else if (params.mode == blink::mojom::FileChooserParams::Mode::kSave) {
552     base::FilePath file;
553     if (RunSaveFileDialog(params, owner, &filter_index, &file))
554       files.push_back(file);
555   } else {
556     NOTIMPLEMENTED();
557   }
558 
559   std::move(callback).Run(filter_index, files);
560 }
561