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, ®_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