Bug 1692971 [wpt PR 27638] - WebKit export of https://bugs.webkit.org/show_bug.cgi...
[gecko.git] / widget / windows / nsFilePicker.cpp
blob9cb90ff261aec8d411242e69127e73a2c9ea9f2d
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "nsFilePicker.h"
9 #include <shlobj.h>
10 #include <shlwapi.h>
11 #include <cderr.h>
13 #include "mozilla/BackgroundHangMonitor.h"
14 #include "mozilla/mscom/EnsureMTA.h"
15 #include "mozilla/ProfilerLabels.h"
16 #include "mozilla/UniquePtr.h"
17 #include "mozilla/WindowsVersion.h"
18 #include "nsReadableUtils.h"
19 #include "nsNetUtil.h"
20 #include "nsWindow.h"
21 #include "nsEnumeratorUtils.h"
22 #include "nsCRT.h"
23 #include "nsString.h"
24 #include "nsToolkit.h"
25 #include "WinUtils.h"
26 #include "nsPIDOMWindow.h"
28 using mozilla::IsWin8OrLater;
29 using mozilla::MakeUnique;
30 using mozilla::UniquePtr;
31 using mozilla::mscom::EnsureMTA;
33 using namespace mozilla::widget;
35 UniquePtr<char16_t[], nsFilePicker::FreeDeleter>
36 nsFilePicker::sLastUsedUnicodeDirectory;
38 #define MAX_EXTENSION_LENGTH 10
39 #define FILE_BUFFER_SIZE 4096
41 typedef DWORD FILEOPENDIALOGOPTIONS;
43 ///////////////////////////////////////////////////////////////////////////////
44 // Helper classes
46 // Manages NS_NATIVE_TMP_WINDOW child windows. NS_NATIVE_TMP_WINDOWs are
47 // temporary child windows of mParentWidget created to address RTL issues
48 // in picker dialogs. We are responsible for destroying these.
49 class AutoDestroyTmpWindow {
50 public:
51 explicit AutoDestroyTmpWindow(HWND aTmpWnd) : mWnd(aTmpWnd) {}
53 ~AutoDestroyTmpWindow() {
54 if (mWnd) DestroyWindow(mWnd);
57 inline HWND get() const { return mWnd; }
59 private:
60 HWND mWnd;
63 // Manages matching PickerOpen/PickerClosed calls on the parent widget.
64 class AutoWidgetPickerState {
65 public:
66 explicit AutoWidgetPickerState(nsIWidget* aWidget)
67 : mWindow(static_cast<nsWindow*>(aWidget)) {
68 PickerState(true);
71 ~AutoWidgetPickerState() { PickerState(false); }
73 private:
74 void PickerState(bool aFlag) {
75 if (mWindow) {
76 if (aFlag)
77 mWindow->PickerOpen();
78 else
79 mWindow->PickerClosed();
82 RefPtr<nsWindow> mWindow;
85 ///////////////////////////////////////////////////////////////////////////////
86 // nsIFilePicker
88 nsFilePicker::nsFilePicker() : mSelectedType(1) {}
90 NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker)
92 NS_IMETHODIMP nsFilePicker::Init(mozIDOMWindowProxy* aParent,
93 const nsAString& aTitle, int16_t aMode) {
94 nsCOMPtr<nsPIDOMWindowOuter> window = do_QueryInterface(aParent);
95 nsIDocShell* docShell = window ? window->GetDocShell() : nullptr;
96 mLoadContext = do_QueryInterface(docShell);
98 return nsBaseFilePicker::Init(aParent, aTitle, aMode);
102 * Folder picker invocation
106 * Show a folder picker.
108 * @param aInitialDir The initial directory, the last used directory will be
109 * used if left blank.
110 * @return true if a file was selected successfully.
112 bool nsFilePicker::ShowFolderPicker(const nsString& aInitialDir) {
113 if (!IsWin8OrLater()) {
114 // Some Windows 7 users are experiencing a race condition when some dlls
115 // that are loaded by the file picker cause a crash while attempting to shut
116 // down the COM multithreaded apartment. By instantiating EnsureMTA, we hold
117 // an additional reference to the MTA that should prevent this race, since
118 // the MTA will remain alive until shutdown.
119 EnsureMTA ensureMTA;
122 RefPtr<IFileOpenDialog> dialog;
123 if (FAILED(CoCreateInstance(CLSID_FileOpenDialog, nullptr,
124 CLSCTX_INPROC_SERVER, IID_IFileOpenDialog,
125 getter_AddRefs(dialog)))) {
126 return false;
129 // options
130 FILEOPENDIALOGOPTIONS fos = FOS_PICKFOLDERS;
131 HRESULT hr = dialog->SetOptions(fos);
132 if (FAILED(hr)) {
133 return false;
136 // initial strings
137 hr = dialog->SetTitle(mTitle.get());
138 if (FAILED(hr)) {
139 return false;
142 if (!mOkButtonLabel.IsEmpty()) {
143 hr = dialog->SetOkButtonLabel(mOkButtonLabel.get());
144 if (FAILED(hr)) {
145 return false;
149 if (!aInitialDir.IsEmpty()) {
150 RefPtr<IShellItem> folder;
151 if (SUCCEEDED(SHCreateItemFromParsingName(aInitialDir.get(), nullptr,
152 IID_IShellItem,
153 getter_AddRefs(folder)))) {
154 hr = dialog->SetFolder(folder);
155 if (FAILED(hr)) {
156 return false;
161 AutoDestroyTmpWindow adtw((HWND)(
162 mParentWidget.get() ? mParentWidget->GetNativeData(NS_NATIVE_TMP_WINDOW)
163 : nullptr));
165 // display
166 mozilla::BackgroundHangMonitor().NotifyWait();
167 RefPtr<IShellItem> item;
168 if (FAILED(dialog->Show(adtw.get())) ||
169 FAILED(dialog->GetResult(getter_AddRefs(item))) || !item) {
170 return false;
173 // results
175 // If the user chose a Win7 Library, resolve to the library's
176 // default save folder.
177 RefPtr<IShellItem> folderPath;
178 RefPtr<IShellLibrary> shellLib;
179 if (FAILED(CoCreateInstance(CLSID_ShellLibrary, nullptr, CLSCTX_INPROC_SERVER,
180 IID_IShellLibrary, getter_AddRefs(shellLib)))) {
181 return false;
184 if (shellLib && SUCCEEDED(shellLib->LoadLibraryFromItem(item, STGM_READ)) &&
185 SUCCEEDED(shellLib->GetDefaultSaveFolder(DSFT_DETECT, IID_IShellItem,
186 getter_AddRefs(folderPath)))) {
187 item.swap(folderPath);
190 // get the folder's file system path
191 return WinUtils::GetShellItemPath(item, mUnicodeFile);
195 * File open and save picker invocation
199 * Show a file picker.
201 * @param aInitialDir The initial directory, the last used directory will be
202 * used if left blank.
203 * @return true if a file was selected successfully.
205 bool nsFilePicker::ShowFilePicker(const nsString& aInitialDir) {
206 AUTO_PROFILER_LABEL("nsFilePicker::ShowFilePicker", OTHER);
208 if (!IsWin8OrLater()) {
209 // Some Windows 7 users are experiencing a race condition when some dlls
210 // that are loaded by the file picker cause a crash while attempting to shut
211 // down the COM multithreaded apartment. By instantiating EnsureMTA, we hold
212 // an additional reference to the MTA that should prevent this race, since
213 // the MTA will remain alive until shutdown.
214 EnsureMTA ensureMTA;
217 RefPtr<IFileDialog> dialog;
218 if (mMode != modeSave) {
219 if (FAILED(CoCreateInstance(CLSID_FileOpenDialog, nullptr,
220 CLSCTX_INPROC_SERVER, IID_IFileOpenDialog,
221 getter_AddRefs(dialog)))) {
222 return false;
224 } else {
225 if (FAILED(CoCreateInstance(CLSID_FileSaveDialog, nullptr,
226 CLSCTX_INPROC_SERVER, IID_IFileSaveDialog,
227 getter_AddRefs(dialog)))) {
228 return false;
232 // options
234 FILEOPENDIALOGOPTIONS fos = 0;
235 fos |= FOS_SHAREAWARE | FOS_OVERWRITEPROMPT | FOS_FORCEFILESYSTEM;
237 // Handle add to recent docs settings
238 if (IsPrivacyModeEnabled() || !mAddToRecentDocs) {
239 fos |= FOS_DONTADDTORECENT;
242 // mode specific
243 switch (mMode) {
244 case modeOpen:
245 fos |= FOS_FILEMUSTEXIST;
246 break;
248 case modeOpenMultiple:
249 fos |= FOS_FILEMUSTEXIST | FOS_ALLOWMULTISELECT;
250 break;
252 case modeSave:
253 fos |= FOS_NOREADONLYRETURN;
254 // Don't follow shortcuts when saving a shortcut, this can be used
255 // to trick users (bug 271732)
256 if (IsDefaultPathLink()) fos |= FOS_NODEREFERENCELINKS;
257 break;
260 HRESULT hr = dialog->SetOptions(fos);
261 if (FAILED(hr)) {
262 return false;
265 // initial strings
267 // title
268 hr = dialog->SetTitle(mTitle.get());
269 if (FAILED(hr)) {
270 return false;
273 // default filename
274 if (!mDefaultFilename.IsEmpty()) {
275 hr = dialog->SetFileName(mDefaultFilename.get());
276 if (FAILED(hr)) {
277 return false;
281 // default extension to append to new files
282 if (!mDefaultExtension.IsEmpty()) {
283 hr = dialog->SetDefaultExtension(mDefaultExtension.get());
284 if (FAILED(hr)) {
285 return false;
287 } else if (IsDefaultPathHtml()) {
288 hr = dialog->SetDefaultExtension(L"html");
289 if (FAILED(hr)) {
290 return false;
294 // initial location
295 if (!aInitialDir.IsEmpty()) {
296 RefPtr<IShellItem> folder;
297 if (SUCCEEDED(SHCreateItemFromParsingName(aInitialDir.get(), nullptr,
298 IID_IShellItem,
299 getter_AddRefs(folder)))) {
300 hr = dialog->SetFolder(folder);
301 if (FAILED(hr)) {
302 return false;
307 // filter types and the default index
308 if (!mComFilterList.IsEmpty()) {
309 hr = dialog->SetFileTypes(mComFilterList.Length(), mComFilterList.get());
310 if (FAILED(hr)) {
311 return false;
314 hr = dialog->SetFileTypeIndex(mSelectedType);
315 if (FAILED(hr)) {
316 return false;
320 // display
323 AutoDestroyTmpWindow adtw((HWND)(
324 mParentWidget.get() ? mParentWidget->GetNativeData(NS_NATIVE_TMP_WINDOW)
325 : nullptr));
326 AutoWidgetPickerState awps(mParentWidget);
328 mozilla::BackgroundHangMonitor().NotifyWait();
329 if (FAILED(dialog->Show(adtw.get()))) {
330 return false;
334 // results
336 // Remember what filter type the user selected
337 UINT filterIdxResult;
338 if (SUCCEEDED(dialog->GetFileTypeIndex(&filterIdxResult))) {
339 mSelectedType = (int16_t)filterIdxResult;
342 // single selection
343 if (mMode != modeOpenMultiple) {
344 RefPtr<IShellItem> item;
345 if (FAILED(dialog->GetResult(getter_AddRefs(item))) || !item) return false;
346 return WinUtils::GetShellItemPath(item, mUnicodeFile);
349 // multiple selection
350 RefPtr<IFileOpenDialog> openDlg;
351 dialog->QueryInterface(IID_IFileOpenDialog, getter_AddRefs(openDlg));
352 if (!openDlg) {
353 // should not happen
354 return false;
357 RefPtr<IShellItemArray> items;
358 if (FAILED(openDlg->GetResults(getter_AddRefs(items))) || !items) {
359 return false;
362 DWORD count = 0;
363 items->GetCount(&count);
364 for (unsigned int idx = 0; idx < count; idx++) {
365 RefPtr<IShellItem> item;
366 nsAutoString str;
367 if (SUCCEEDED(items->GetItemAt(idx, getter_AddRefs(item)))) {
368 if (!WinUtils::GetShellItemPath(item, str)) continue;
369 nsCOMPtr<nsIFile> file;
370 if (NS_SUCCEEDED(NS_NewLocalFile(str, false, getter_AddRefs(file)))) {
371 mFiles.AppendObject(file);
375 return true;
378 ///////////////////////////////////////////////////////////////////////////////
379 // nsIFilePicker impl.
381 nsresult nsFilePicker::ShowW(int16_t* aReturnVal) {
382 NS_ENSURE_ARG_POINTER(aReturnVal);
384 *aReturnVal = returnCancel;
386 nsAutoString initialDir;
387 if (mDisplayDirectory) mDisplayDirectory->GetPath(initialDir);
389 // If no display directory, re-use the last one.
390 if (initialDir.IsEmpty()) {
391 // Allocate copy of last used dir.
392 initialDir = sLastUsedUnicodeDirectory.get();
395 // Clear previous file selections
396 mUnicodeFile.Truncate();
397 mFiles.Clear();
399 // On Win10, the picker doesn't support per-monitor DPI, so we open it
400 // with our context set temporarily to system-dpi-aware
401 WinUtils::AutoSystemDpiAware dpiAwareness;
403 bool result = false;
404 if (mMode == modeGetFolder) {
405 result = ShowFolderPicker(initialDir);
406 } else {
407 result = ShowFilePicker(initialDir);
410 // exit, and return returnCancel in aReturnVal
411 if (!result) return NS_OK;
413 RememberLastUsedDirectory();
415 int16_t retValue = returnOK;
416 if (mMode == modeSave) {
417 // Windows does not return resultReplace, we must check if file
418 // already exists.
419 nsCOMPtr<nsIFile> file;
420 nsresult rv = NS_NewLocalFile(mUnicodeFile, false, getter_AddRefs(file));
422 bool flag = false;
423 if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(file->Exists(&flag)) && flag) {
424 retValue = returnReplace;
428 *aReturnVal = retValue;
429 return NS_OK;
432 nsresult nsFilePicker::Show(int16_t* aReturnVal) { return ShowW(aReturnVal); }
434 NS_IMETHODIMP
435 nsFilePicker::GetFile(nsIFile** aFile) {
436 NS_ENSURE_ARG_POINTER(aFile);
437 *aFile = nullptr;
439 if (mUnicodeFile.IsEmpty()) return NS_OK;
441 nsCOMPtr<nsIFile> file;
442 nsresult rv = NS_NewLocalFile(mUnicodeFile, false, getter_AddRefs(file));
443 if (NS_FAILED(rv)) {
444 return rv;
447 file.forget(aFile);
448 return NS_OK;
451 NS_IMETHODIMP
452 nsFilePicker::GetFileURL(nsIURI** aFileURL) {
453 *aFileURL = nullptr;
454 nsCOMPtr<nsIFile> file;
455 nsresult rv = GetFile(getter_AddRefs(file));
456 if (!file) return rv;
458 return NS_NewFileURI(aFileURL, file);
461 NS_IMETHODIMP
462 nsFilePicker::GetFiles(nsISimpleEnumerator** aFiles) {
463 NS_ENSURE_ARG_POINTER(aFiles);
464 return NS_NewArrayEnumerator(aFiles, mFiles, NS_GET_IID(nsIFile));
467 // Get the file + path
468 NS_IMETHODIMP
469 nsBaseWinFilePicker::SetDefaultString(const nsAString& aString) {
470 mDefaultFilePath = aString;
472 // First, make sure the file name is not too long.
473 int32_t nameLength;
474 int32_t nameIndex = mDefaultFilePath.RFind("\\");
475 if (nameIndex == kNotFound)
476 nameIndex = 0;
477 else
478 nameIndex++;
479 nameLength = mDefaultFilePath.Length() - nameIndex;
480 mDefaultFilename.Assign(Substring(mDefaultFilePath, nameIndex));
482 if (nameLength > MAX_PATH) {
483 int32_t extIndex = mDefaultFilePath.RFind(".");
484 if (extIndex == kNotFound) extIndex = mDefaultFilePath.Length();
486 // Let's try to shave the needed characters from the name part.
487 int32_t charsToRemove = nameLength - MAX_PATH;
488 if (extIndex - nameIndex >= charsToRemove) {
489 mDefaultFilePath.Cut(extIndex - charsToRemove, charsToRemove);
493 // Then, we need to replace illegal characters. At this stage, we cannot
494 // replace the backslash as the string might represent a file path.
495 mDefaultFilePath.ReplaceChar(FILE_ILLEGAL_CHARACTERS, '-');
496 mDefaultFilename.ReplaceChar(FILE_ILLEGAL_CHARACTERS, '-');
498 return NS_OK;
501 NS_IMETHODIMP
502 nsBaseWinFilePicker::GetDefaultString(nsAString& aString) {
503 return NS_ERROR_FAILURE;
506 // The default extension to use for files
507 NS_IMETHODIMP
508 nsBaseWinFilePicker::GetDefaultExtension(nsAString& aExtension) {
509 aExtension = mDefaultExtension;
510 return NS_OK;
513 NS_IMETHODIMP
514 nsBaseWinFilePicker::SetDefaultExtension(const nsAString& aExtension) {
515 mDefaultExtension = aExtension;
516 return NS_OK;
519 // Set the filter index
520 NS_IMETHODIMP
521 nsFilePicker::GetFilterIndex(int32_t* aFilterIndex) {
522 // Windows' filter index is 1-based, we use a 0-based system.
523 *aFilterIndex = mSelectedType - 1;
524 return NS_OK;
527 NS_IMETHODIMP
528 nsFilePicker::SetFilterIndex(int32_t aFilterIndex) {
529 // Windows' filter index is 1-based, we use a 0-based system.
530 mSelectedType = aFilterIndex + 1;
531 return NS_OK;
534 void nsFilePicker::InitNative(nsIWidget* aParent, const nsAString& aTitle) {
535 mParentWidget = aParent;
536 mTitle.Assign(aTitle);
539 NS_IMETHODIMP
540 nsFilePicker::AppendFilter(const nsAString& aTitle, const nsAString& aFilter) {
541 mComFilterList.Append(aTitle, aFilter);
542 return NS_OK;
545 void nsFilePicker::RememberLastUsedDirectory() {
546 if (IsPrivacyModeEnabled()) {
547 // Don't remember the directory if private browsing was in effect
548 return;
551 nsCOMPtr<nsIFile> file;
552 if (NS_FAILED(NS_NewLocalFile(mUnicodeFile, false, getter_AddRefs(file)))) {
553 NS_WARNING("RememberLastUsedDirectory failed to init file path.");
554 return;
557 nsCOMPtr<nsIFile> dir;
558 nsAutoString newDir;
559 if (NS_FAILED(file->GetParent(getter_AddRefs(dir))) ||
560 !(mDisplayDirectory = dir) ||
561 NS_FAILED(mDisplayDirectory->GetPath(newDir)) || newDir.IsEmpty()) {
562 NS_WARNING("RememberLastUsedDirectory failed to get parent directory.");
563 return;
566 sLastUsedUnicodeDirectory.reset(ToNewUnicode(newDir));
569 bool nsFilePicker::IsPrivacyModeEnabled() {
570 return mLoadContext && mLoadContext->UsePrivateBrowsing();
573 bool nsFilePicker::IsDefaultPathLink() {
574 NS_ConvertUTF16toUTF8 ext(mDefaultFilePath);
575 ext.Trim(" .", false, true); // watch out for trailing space and dots
576 ToLowerCase(ext);
577 if (StringEndsWith(ext, ".lnk"_ns) || StringEndsWith(ext, ".pif"_ns) ||
578 StringEndsWith(ext, ".url"_ns))
579 return true;
580 return false;
583 bool nsFilePicker::IsDefaultPathHtml() {
584 int32_t extIndex = mDefaultFilePath.RFind(".");
585 if (extIndex >= 0) {
586 nsAutoString ext;
587 mDefaultFilePath.Right(ext, mDefaultFilePath.Length() - extIndex);
588 if (ext.LowerCaseEqualsLiteral(".htm") ||
589 ext.LowerCaseEqualsLiteral(".html") ||
590 ext.LowerCaseEqualsLiteral(".shtml"))
591 return true;
593 return false;
596 void nsFilePicker::ComDlgFilterSpec::Append(const nsAString& aTitle,
597 const nsAString& aFilter) {
598 COMDLG_FILTERSPEC* pSpecForward = mSpecList.AppendElement();
599 if (!pSpecForward) {
600 NS_WARNING("mSpecList realloc failed.");
601 return;
603 memset(pSpecForward, 0, sizeof(*pSpecForward));
604 nsString* pStr = mStrings.AppendElement(aTitle);
605 if (!pStr) {
606 NS_WARNING("mStrings.AppendElement failed.");
607 return;
609 pSpecForward->pszName = pStr->get();
610 pStr = mStrings.AppendElement(aFilter);
611 if (!pStr) {
612 NS_WARNING("mStrings.AppendElement failed.");
613 return;
615 if (aFilter.EqualsLiteral("..apps"))
616 pStr->AssignLiteral("*.exe;*.com");
617 else {
618 pStr->StripWhitespace();
619 if (pStr->EqualsLiteral("*")) pStr->AppendLiteral(".*");
621 pSpecForward->pszSpec = pStr->get();