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"
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"
21 #include "nsEnumeratorUtils.h"
24 #include "nsToolkit.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 ///////////////////////////////////////////////////////////////////////////////
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
{
51 explicit AutoDestroyTmpWindow(HWND aTmpWnd
) : mWnd(aTmpWnd
) {}
53 ~AutoDestroyTmpWindow() {
54 if (mWnd
) DestroyWindow(mWnd
);
57 inline HWND
get() const { return mWnd
; }
63 // Manages matching PickerOpen/PickerClosed calls on the parent widget.
64 class AutoWidgetPickerState
{
66 explicit AutoWidgetPickerState(nsIWidget
* aWidget
)
67 : mWindow(static_cast<nsWindow
*>(aWidget
)) {
71 ~AutoWidgetPickerState() { PickerState(false); }
74 void PickerState(bool aFlag
) {
77 mWindow
->PickerOpen();
79 mWindow
->PickerClosed();
82 RefPtr
<nsWindow
> mWindow
;
85 ///////////////////////////////////////////////////////////////////////////////
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.
122 RefPtr
<IFileOpenDialog
> dialog
;
123 if (FAILED(CoCreateInstance(CLSID_FileOpenDialog
, nullptr,
124 CLSCTX_INPROC_SERVER
, IID_IFileOpenDialog
,
125 getter_AddRefs(dialog
)))) {
130 FILEOPENDIALOGOPTIONS fos
= FOS_PICKFOLDERS
;
131 HRESULT hr
= dialog
->SetOptions(fos
);
137 hr
= dialog
->SetTitle(mTitle
.get());
142 if (!mOkButtonLabel
.IsEmpty()) {
143 hr
= dialog
->SetOkButtonLabel(mOkButtonLabel
.get());
149 if (!aInitialDir
.IsEmpty()) {
150 RefPtr
<IShellItem
> folder
;
151 if (SUCCEEDED(SHCreateItemFromParsingName(aInitialDir
.get(), nullptr,
153 getter_AddRefs(folder
)))) {
154 hr
= dialog
->SetFolder(folder
);
161 AutoDestroyTmpWindow
adtw((HWND
)(
162 mParentWidget
.get() ? mParentWidget
->GetNativeData(NS_NATIVE_TMP_WINDOW
)
166 mozilla::BackgroundHangMonitor().NotifyWait();
167 RefPtr
<IShellItem
> item
;
168 if (FAILED(dialog
->Show(adtw
.get())) ||
169 FAILED(dialog
->GetResult(getter_AddRefs(item
))) || !item
) {
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
)))) {
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.
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
)))) {
225 if (FAILED(CoCreateInstance(CLSID_FileSaveDialog
, nullptr,
226 CLSCTX_INPROC_SERVER
, IID_IFileSaveDialog
,
227 getter_AddRefs(dialog
)))) {
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
;
245 fos
|= FOS_FILEMUSTEXIST
;
248 case modeOpenMultiple
:
249 fos
|= FOS_FILEMUSTEXIST
| FOS_ALLOWMULTISELECT
;
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
;
260 HRESULT hr
= dialog
->SetOptions(fos
);
268 hr
= dialog
->SetTitle(mTitle
.get());
274 if (!mDefaultFilename
.IsEmpty()) {
275 hr
= dialog
->SetFileName(mDefaultFilename
.get());
281 // default extension to append to new files
282 if (!mDefaultExtension
.IsEmpty()) {
283 hr
= dialog
->SetDefaultExtension(mDefaultExtension
.get());
287 } else if (IsDefaultPathHtml()) {
288 hr
= dialog
->SetDefaultExtension(L
"html");
295 if (!aInitialDir
.IsEmpty()) {
296 RefPtr
<IShellItem
> folder
;
297 if (SUCCEEDED(SHCreateItemFromParsingName(aInitialDir
.get(), nullptr,
299 getter_AddRefs(folder
)))) {
300 hr
= dialog
->SetFolder(folder
);
307 // filter types and the default index
308 if (!mComFilterList
.IsEmpty()) {
309 hr
= dialog
->SetFileTypes(mComFilterList
.Length(), mComFilterList
.get());
314 hr
= dialog
->SetFileTypeIndex(mSelectedType
);
323 AutoDestroyTmpWindow
adtw((HWND
)(
324 mParentWidget
.get() ? mParentWidget
->GetNativeData(NS_NATIVE_TMP_WINDOW
)
326 AutoWidgetPickerState
awps(mParentWidget
);
328 mozilla::BackgroundHangMonitor().NotifyWait();
329 if (FAILED(dialog
->Show(adtw
.get()))) {
336 // Remember what filter type the user selected
337 UINT filterIdxResult
;
338 if (SUCCEEDED(dialog
->GetFileTypeIndex(&filterIdxResult
))) {
339 mSelectedType
= (int16_t)filterIdxResult
;
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
));
357 RefPtr
<IShellItemArray
> items
;
358 if (FAILED(openDlg
->GetResults(getter_AddRefs(items
))) || !items
) {
363 items
->GetCount(&count
);
364 for (unsigned int idx
= 0; idx
< count
; idx
++) {
365 RefPtr
<IShellItem
> item
;
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
);
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();
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
;
404 if (mMode
== modeGetFolder
) {
405 result
= ShowFolderPicker(initialDir
);
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
419 nsCOMPtr
<nsIFile
> file
;
420 nsresult rv
= NS_NewLocalFile(mUnicodeFile
, false, getter_AddRefs(file
));
423 if (NS_SUCCEEDED(rv
) && NS_SUCCEEDED(file
->Exists(&flag
)) && flag
) {
424 retValue
= returnReplace
;
428 *aReturnVal
= retValue
;
432 nsresult
nsFilePicker::Show(int16_t* aReturnVal
) { return ShowW(aReturnVal
); }
435 nsFilePicker::GetFile(nsIFile
** aFile
) {
436 NS_ENSURE_ARG_POINTER(aFile
);
439 if (mUnicodeFile
.IsEmpty()) return NS_OK
;
441 nsCOMPtr
<nsIFile
> file
;
442 nsresult rv
= NS_NewLocalFile(mUnicodeFile
, false, getter_AddRefs(file
));
452 nsFilePicker::GetFileURL(nsIURI
** aFileURL
) {
454 nsCOMPtr
<nsIFile
> file
;
455 nsresult rv
= GetFile(getter_AddRefs(file
));
456 if (!file
) return rv
;
458 return NS_NewFileURI(aFileURL
, file
);
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
469 nsBaseWinFilePicker::SetDefaultString(const nsAString
& aString
) {
470 mDefaultFilePath
= aString
;
472 // First, make sure the file name is not too long.
474 int32_t nameIndex
= mDefaultFilePath
.RFind("\\");
475 if (nameIndex
== kNotFound
)
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
, '-');
502 nsBaseWinFilePicker::GetDefaultString(nsAString
& aString
) {
503 return NS_ERROR_FAILURE
;
506 // The default extension to use for files
508 nsBaseWinFilePicker::GetDefaultExtension(nsAString
& aExtension
) {
509 aExtension
= mDefaultExtension
;
514 nsBaseWinFilePicker::SetDefaultExtension(const nsAString
& aExtension
) {
515 mDefaultExtension
= aExtension
;
519 // Set the filter index
521 nsFilePicker::GetFilterIndex(int32_t* aFilterIndex
) {
522 // Windows' filter index is 1-based, we use a 0-based system.
523 *aFilterIndex
= mSelectedType
- 1;
528 nsFilePicker::SetFilterIndex(int32_t aFilterIndex
) {
529 // Windows' filter index is 1-based, we use a 0-based system.
530 mSelectedType
= aFilterIndex
+ 1;
534 void nsFilePicker::InitNative(nsIWidget
* aParent
, const nsAString
& aTitle
) {
535 mParentWidget
= aParent
;
536 mTitle
.Assign(aTitle
);
540 nsFilePicker::AppendFilter(const nsAString
& aTitle
, const nsAString
& aFilter
) {
541 mComFilterList
.Append(aTitle
, aFilter
);
545 void nsFilePicker::RememberLastUsedDirectory() {
546 if (IsPrivacyModeEnabled()) {
547 // Don't remember the directory if private browsing was in effect
551 nsCOMPtr
<nsIFile
> file
;
552 if (NS_FAILED(NS_NewLocalFile(mUnicodeFile
, false, getter_AddRefs(file
)))) {
553 NS_WARNING("RememberLastUsedDirectory failed to init file path.");
557 nsCOMPtr
<nsIFile
> dir
;
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.");
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
577 if (StringEndsWith(ext
, ".lnk"_ns
) || StringEndsWith(ext
, ".pif"_ns
) ||
578 StringEndsWith(ext
, ".url"_ns
))
583 bool nsFilePicker::IsDefaultPathHtml() {
584 int32_t extIndex
= mDefaultFilePath
.RFind(".");
587 mDefaultFilePath
.Right(ext
, mDefaultFilePath
.Length() - extIndex
);
588 if (ext
.LowerCaseEqualsLiteral(".htm") ||
589 ext
.LowerCaseEqualsLiteral(".html") ||
590 ext
.LowerCaseEqualsLiteral(".shtml"))
596 void nsFilePicker::ComDlgFilterSpec::Append(const nsAString
& aTitle
,
597 const nsAString
& aFilter
) {
598 COMDLG_FILTERSPEC
* pSpecForward
= mSpecList
.AppendElement();
600 NS_WARNING("mSpecList realloc failed.");
603 memset(pSpecForward
, 0, sizeof(*pSpecForward
));
604 nsString
* pStr
= mStrings
.AppendElement(aTitle
);
606 NS_WARNING("mStrings.AppendElement failed.");
609 pSpecForward
->pszName
= pStr
->get();
610 pStr
= mStrings
.AppendElement(aFilter
);
612 NS_WARNING("mStrings.AppendElement failed.");
615 if (aFilter
.EqualsLiteral("..apps"))
616 pStr
->AssignLiteral("*.exe;*.com");
618 pStr
->StripWhitespace();
619 if (pStr
->EqualsLiteral("*")) pStr
->AppendLiteral(".*");
621 pSpecForward
->pszSpec
= pStr
->get();