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/Assertions.h"
14 #include "mozilla/BackgroundHangMonitor.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 #include "mozilla/widget/filedialog/WinFileDialogCommands.h"
30 using mozilla::UniquePtr
;
32 using namespace mozilla::widget
;
34 UniquePtr
<char16_t
[], nsFilePicker::FreeDeleter
>
35 nsFilePicker::sLastUsedUnicodeDirectory
;
37 #define MAX_EXTENSION_LENGTH 10
39 ///////////////////////////////////////////////////////////////////////////////
42 // Manages matching PickerOpen/PickerClosed calls on the parent widget.
43 class AutoWidgetPickerState
{
45 explicit AutoWidgetPickerState(nsIWidget
* aWidget
)
46 : mWindow(static_cast<nsWindow
*>(aWidget
)) {
50 ~AutoWidgetPickerState() { PickerState(false); }
53 void PickerState(bool aFlag
) {
56 mWindow
->PickerOpen();
58 mWindow
->PickerClosed();
61 RefPtr
<nsWindow
> mWindow
;
64 ///////////////////////////////////////////////////////////////////////////////
67 nsFilePicker::nsFilePicker() : mSelectedType(1) {}
69 NS_IMPL_ISUPPORTS(nsFilePicker
, nsIFilePicker
)
71 NS_IMETHODIMP
nsFilePicker::Init(mozIDOMWindowProxy
* aParent
,
72 const nsAString
& aTitle
,
73 nsIFilePicker::Mode aMode
) {
74 nsCOMPtr
<nsPIDOMWindowOuter
> window
= do_QueryInterface(aParent
);
75 nsIDocShell
* docShell
= window
? window
->GetDocShell() : nullptr;
76 mLoadContext
= do_QueryInterface(docShell
);
78 return nsBaseFilePicker::Init(aParent
, aTitle
, aMode
);
82 * Folder picker invocation
86 * Show a folder picker.
88 * @param aInitialDir The initial directory, the last used directory will be
90 * @return true if a file was selected successfully.
92 bool nsFilePicker::ShowFolderPicker(const nsString
& aInitialDir
) {
93 RefPtr
<IFileOpenDialog
> dialog
;
94 if (FAILED(CoCreateInstance(CLSID_FileOpenDialog
, nullptr,
95 CLSCTX_INPROC_SERVER
, IID_IFileOpenDialog
,
96 getter_AddRefs(dialog
)))) {
100 namespace fd
= ::mozilla::widget::filedialog
;
101 nsTArray
<fd::Command
> commands
= {
102 fd::SetOptions(FOS_PICKFOLDERS
),
103 fd::SetTitle(mTitle
),
106 if (!mOkButtonLabel
.IsEmpty()) {
107 commands
.AppendElement(fd::SetOkButtonLabel(mOkButtonLabel
));
110 if (!aInitialDir
.IsEmpty()) {
111 commands
.AppendElement(fd::SetFolder(aInitialDir
));
115 if (NS_FAILED(fd::ApplyCommands(dialog
, commands
))) {
119 ScopedRtlShimWindow
shim(mParentWidget
.get());
120 mozilla::BackgroundHangMonitor().NotifyWait();
122 if (FAILED(dialog
->Show(shim
.get()))) {
127 auto result
= fd::GetFolderResults(dialog
.get());
128 if (result
.isErr()) {
132 mUnicodeFile
= result
.unwrap();
137 * File open and save picker invocation
141 * Show a file picker.
143 * @param aInitialDir The initial directory, the last used directory will be
144 * used if left blank.
145 * @return true if a file was selected successfully.
147 bool nsFilePicker::ShowFilePicker(const nsString
& aInitialDir
) {
148 AUTO_PROFILER_LABEL("nsFilePicker::ShowFilePicker", OTHER
);
150 RefPtr
<IFileDialog
> dialog
;
151 if (mMode
!= modeSave
) {
152 if (FAILED(CoCreateInstance(CLSID_FileOpenDialog
, nullptr,
153 CLSCTX_INPROC_SERVER
, IID_IFileOpenDialog
,
154 getter_AddRefs(dialog
)))) {
158 if (FAILED(CoCreateInstance(CLSID_FileSaveDialog
, nullptr,
159 CLSCTX_INPROC_SERVER
, IID_IFileSaveDialog
,
160 getter_AddRefs(dialog
)))) {
165 namespace fd
= ::mozilla::widget::filedialog
;
166 nsTArray
<fd::Command
> commands
;
169 FILEOPENDIALOGOPTIONS fos
= 0;
170 fos
|= FOS_SHAREAWARE
| FOS_OVERWRITEPROMPT
| FOS_FORCEFILESYSTEM
;
172 // Handle add to recent docs settings
173 if (IsPrivacyModeEnabled() || !mAddToRecentDocs
) {
174 fos
|= FOS_DONTADDTORECENT
;
180 fos
|= FOS_FILEMUSTEXIST
;
183 case modeOpenMultiple
:
184 fos
|= FOS_FILEMUSTEXIST
| FOS_ALLOWMULTISELECT
;
188 fos
|= FOS_NOREADONLYRETURN
;
189 // Don't follow shortcuts when saving a shortcut, this can be used
190 // to trick users (bug 271732)
191 if (IsDefaultPathLink()) fos
|= FOS_NODEREFERENCELINKS
;
195 MOZ_ASSERT(false, "file-picker opened in directory-picker mode");
199 commands
.AppendElement(fd::SetOptions(fos
));
205 commands
.AppendElement(fd::SetTitle(mTitle
));
208 if (!mDefaultFilename
.IsEmpty()) {
209 // Prevent the shell from expanding environment variables by removing
210 // the % characters that are used to delimit them.
211 nsAutoString
sanitizedFilename(mDefaultFilename
);
212 sanitizedFilename
.ReplaceChar('%', '_');
214 commands
.AppendElement(fd::SetFileName(sanitizedFilename
));
217 // default extension to append to new files
218 if (!mDefaultExtension
.IsEmpty()) {
219 // We don't want environment variables expanded in the extension either.
220 nsAutoString
sanitizedExtension(mDefaultExtension
);
221 sanitizedExtension
.ReplaceChar('%', '_');
223 commands
.AppendElement(fd::SetDefaultExtension(sanitizedExtension
));
224 } else if (IsDefaultPathHtml()) {
225 commands
.AppendElement(fd::SetDefaultExtension(u
"html"_ns
));
229 if (!aInitialDir
.IsEmpty()) {
230 commands
.AppendElement(fd::SetFolder(aInitialDir
));
233 // filter types and the default index
234 if (!mFilterList
.IsEmpty()) {
235 nsTArray
<fd::ComDlgFilterSpec
> fileTypes
;
236 for (auto const& filter
: mFilterList
) {
237 fileTypes
.EmplaceBack(filter
.title
, filter
.filter
);
239 commands
.AppendElement(fd::SetFileTypes(std::move(fileTypes
)));
240 commands
.AppendElement(fd::SetFileTypeIndex(mSelectedType
));
245 if (NS_FAILED(fd::ApplyCommands(dialog
, commands
))) {
249 ScopedRtlShimWindow
shim(mParentWidget
.get());
250 AutoWidgetPickerState
awps(mParentWidget
);
252 mozilla::BackgroundHangMonitor().NotifyWait();
253 if (FAILED(dialog
->Show(shim
.get()))) {
259 auto result_
= fd::GetFileResults(dialog
.get());
260 if (result_
.isErr()) {
263 auto result
= result_
.unwrap();
265 // Remember what filter type the user selected
266 mSelectedType
= result
.selectedFileTypeIndex();
268 auto const& paths
= result
.paths();
271 if (mMode
!= modeOpenMultiple
) {
272 if (!paths
.IsEmpty()) {
273 MOZ_ASSERT(paths
.Length() == 1);
274 mUnicodeFile
= paths
[0];
280 // multiple selection
281 for (auto const& str
: paths
) {
282 nsCOMPtr
<nsIFile
> file
;
283 if (NS_SUCCEEDED(NS_NewLocalFile(str
, false, getter_AddRefs(file
)))) {
284 mFiles
.AppendObject(file
);
290 ///////////////////////////////////////////////////////////////////////////////
291 // nsIFilePicker impl.
293 nsresult
nsFilePicker::ShowW(nsIFilePicker::ResultCode
* aReturnVal
) {
294 NS_ENSURE_ARG_POINTER(aReturnVal
);
296 *aReturnVal
= returnCancel
;
298 nsAutoString initialDir
;
299 if (mDisplayDirectory
) mDisplayDirectory
->GetPath(initialDir
);
301 // If no display directory, re-use the last one.
302 if (initialDir
.IsEmpty()) {
303 // Allocate copy of last used dir.
304 initialDir
= sLastUsedUnicodeDirectory
.get();
307 // Clear previous file selections
308 mUnicodeFile
.Truncate();
311 // On Win10, the picker doesn't support per-monitor DPI, so we open it
312 // with our context set temporarily to system-dpi-aware
313 WinUtils::AutoSystemDpiAware dpiAwareness
;
316 if (mMode
== modeGetFolder
) {
317 result
= ShowFolderPicker(initialDir
);
319 result
= ShowFilePicker(initialDir
);
322 // exit, and return returnCancel in aReturnVal
323 if (!result
) return NS_OK
;
325 RememberLastUsedDirectory();
327 nsIFilePicker::ResultCode retValue
= returnOK
;
328 if (mMode
== modeSave
) {
329 // Windows does not return resultReplace, we must check if file
331 nsCOMPtr
<nsIFile
> file
;
332 nsresult rv
= NS_NewLocalFile(mUnicodeFile
, false, getter_AddRefs(file
));
335 if (NS_SUCCEEDED(rv
) && NS_SUCCEEDED(file
->Exists(&flag
)) && flag
) {
336 retValue
= returnReplace
;
340 *aReturnVal
= retValue
;
344 nsresult
nsFilePicker::Show(nsIFilePicker::ResultCode
* aReturnVal
) {
345 return ShowW(aReturnVal
);
349 nsFilePicker::GetFile(nsIFile
** aFile
) {
350 NS_ENSURE_ARG_POINTER(aFile
);
353 if (mUnicodeFile
.IsEmpty()) return NS_OK
;
355 nsCOMPtr
<nsIFile
> file
;
356 nsresult rv
= NS_NewLocalFile(mUnicodeFile
, false, getter_AddRefs(file
));
366 nsFilePicker::GetFileURL(nsIURI
** aFileURL
) {
368 nsCOMPtr
<nsIFile
> file
;
369 nsresult rv
= GetFile(getter_AddRefs(file
));
370 if (!file
) return rv
;
372 return NS_NewFileURI(aFileURL
, file
);
376 nsFilePicker::GetFiles(nsISimpleEnumerator
** aFiles
) {
377 NS_ENSURE_ARG_POINTER(aFiles
);
378 return NS_NewArrayEnumerator(aFiles
, mFiles
, NS_GET_IID(nsIFile
));
381 // Get the file + path
383 nsBaseWinFilePicker::SetDefaultString(const nsAString
& aString
) {
384 mDefaultFilePath
= aString
;
386 // First, make sure the file name is not too long.
388 int32_t nameIndex
= mDefaultFilePath
.RFind(u
"\\");
389 if (nameIndex
== kNotFound
)
393 nameLength
= mDefaultFilePath
.Length() - nameIndex
;
394 mDefaultFilename
.Assign(Substring(mDefaultFilePath
, nameIndex
));
396 if (nameLength
> MAX_PATH
) {
397 int32_t extIndex
= mDefaultFilePath
.RFind(u
".");
398 if (extIndex
== kNotFound
) extIndex
= mDefaultFilePath
.Length();
400 // Let's try to shave the needed characters from the name part.
401 int32_t charsToRemove
= nameLength
- MAX_PATH
;
402 if (extIndex
- nameIndex
>= charsToRemove
) {
403 mDefaultFilePath
.Cut(extIndex
- charsToRemove
, charsToRemove
);
407 // Then, we need to replace illegal characters. At this stage, we cannot
408 // replace the backslash as the string might represent a file path.
409 mDefaultFilePath
.ReplaceChar(u
"" FILE_ILLEGAL_CHARACTERS
, u
'-');
410 mDefaultFilename
.ReplaceChar(u
"" FILE_ILLEGAL_CHARACTERS
, u
'-');
416 nsBaseWinFilePicker::GetDefaultString(nsAString
& aString
) {
417 return NS_ERROR_FAILURE
;
420 // The default extension to use for files
422 nsBaseWinFilePicker::GetDefaultExtension(nsAString
& aExtension
) {
423 aExtension
= mDefaultExtension
;
428 nsBaseWinFilePicker::SetDefaultExtension(const nsAString
& aExtension
) {
429 mDefaultExtension
= aExtension
;
433 // Set the filter index
435 nsFilePicker::GetFilterIndex(int32_t* aFilterIndex
) {
436 // Windows' filter index is 1-based, we use a 0-based system.
437 *aFilterIndex
= mSelectedType
- 1;
442 nsFilePicker::SetFilterIndex(int32_t aFilterIndex
) {
443 // Windows' filter index is 1-based, we use a 0-based system.
444 mSelectedType
= aFilterIndex
+ 1;
448 void nsFilePicker::InitNative(nsIWidget
* aParent
, const nsAString
& aTitle
) {
449 mParentWidget
= aParent
;
450 mTitle
.Assign(aTitle
);
454 nsFilePicker::AppendFilter(const nsAString
& aTitle
, const nsAString
& aFilter
) {
455 nsString
sanitizedFilter(aFilter
);
456 sanitizedFilter
.ReplaceChar('%', '_');
458 if (sanitizedFilter
== u
"..apps"_ns
) {
459 sanitizedFilter
= u
"*.exe;*.com"_ns
;
461 sanitizedFilter
.StripWhitespace();
462 if (sanitizedFilter
== u
"*"_ns
) {
463 sanitizedFilter
= u
"*.*"_ns
;
466 mFilterList
.AppendElement(
467 Filter
{.title
= nsString(aTitle
), .filter
= std::move(sanitizedFilter
)});
471 void nsFilePicker::RememberLastUsedDirectory() {
472 if (IsPrivacyModeEnabled()) {
473 // Don't remember the directory if private browsing was in effect
477 nsCOMPtr
<nsIFile
> file
;
478 if (NS_FAILED(NS_NewLocalFile(mUnicodeFile
, false, getter_AddRefs(file
)))) {
479 NS_WARNING("RememberLastUsedDirectory failed to init file path.");
483 nsCOMPtr
<nsIFile
> dir
;
485 if (NS_FAILED(file
->GetParent(getter_AddRefs(dir
))) ||
486 !(mDisplayDirectory
= dir
) ||
487 NS_FAILED(mDisplayDirectory
->GetPath(newDir
)) || newDir
.IsEmpty()) {
488 NS_WARNING("RememberLastUsedDirectory failed to get parent directory.");
492 sLastUsedUnicodeDirectory
.reset(ToNewUnicode(newDir
));
495 bool nsFilePicker::IsPrivacyModeEnabled() {
496 return mLoadContext
&& mLoadContext
->UsePrivateBrowsing();
499 bool nsFilePicker::IsDefaultPathLink() {
500 NS_ConvertUTF16toUTF8
ext(mDefaultFilePath
);
501 ext
.Trim(" .", false, true); // watch out for trailing space and dots
503 return StringEndsWith(ext
, ".lnk"_ns
) || StringEndsWith(ext
, ".pif"_ns
) ||
504 StringEndsWith(ext
, ".url"_ns
);
507 bool nsFilePicker::IsDefaultPathHtml() {
508 int32_t extIndex
= mDefaultFilePath
.RFind(u
".");
511 mDefaultFilePath
.Right(ext
, mDefaultFilePath
.Length() - extIndex
);
512 if (ext
.LowerCaseEqualsLiteral(".htm") ||
513 ext
.LowerCaseEqualsLiteral(".html") ||
514 ext
.LowerCaseEqualsLiteral(".shtml"))