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"
12 #include <sysinfoapi.h>
17 #include "ContentAnalysis.h"
18 #include "mozilla/Assertions.h"
19 #include "mozilla/BackgroundHangMonitor.h"
20 #include "mozilla/Components.h"
21 #include "mozilla/dom/BrowsingContext.h"
22 #include "mozilla/dom/CanonicalBrowsingContext.h"
23 #include "mozilla/dom/Directory.h"
24 #include "mozilla/dom/WindowGlobalParent.h"
25 #include "mozilla/Logging.h"
26 #include "mozilla/ipc/UtilityProcessManager.h"
27 #include "mozilla/ProfilerLabels.h"
28 #include "mozilla/StaticPrefs_widget.h"
29 #include "mozilla/UniquePtr.h"
30 #include "mozilla/WindowsVersion.h"
31 #include "nsArrayEnumerator.h"
33 #include "nsEnumeratorUtils.h"
34 #include "nsHashPropertyBag.h"
35 #include "nsIContentAnalysis.h"
37 #include "nsISimpleEnumerator.h"
38 #include "nsCExternalHandlerService.h"
39 #include "nsIExternalHelperAppService.h"
40 #include "nsNetUtil.h"
41 #include "nsPIDOMWindow.h"
42 #include "nsPrintfCString.h"
43 #include "nsReadableUtils.h"
45 #include "nsToolkit.h"
49 #include "mozilla/widget/filedialog/WinFileDialogCommands.h"
50 #include "mozilla/widget/filedialog/WinFileDialogParent.h"
52 using mozilla::LogLevel
;
53 using mozilla::UniquePtr
;
55 using namespace mozilla::widget
;
57 template <typename Res
>
58 using FDPromise
= filedialog::Promise
<Res
>;
60 MOZ_RUNINIT UniquePtr
<char16_t
[], nsFilePicker::FreeDeleter
>
61 nsFilePicker::sLastUsedUnicodeDirectory
;
63 #define MAX_EXTENSION_LENGTH 10
65 ///////////////////////////////////////////////////////////////////////////////
68 // Manages matching PickerOpen/PickerClosed calls on the parent widget.
69 class AutoWidgetPickerState
{
70 static RefPtr
<nsWindow
> GetWindowForWidget(nsIWidget
* aWidget
) {
71 MOZ_ASSERT(NS_IsMainThread());
75 HWND hwnd
= (HWND
)aWidget
->GetNativeData(NS_NATIVE_WINDOW
);
76 return RefPtr(WinUtils::GetNSWindowPtr(hwnd
));
80 explicit AutoWidgetPickerState(nsIWidget
* aWidget
)
81 : mWindow(GetWindowForWidget(aWidget
)) {
84 mWindow
->PickerOpen();
87 ~AutoWidgetPickerState() {
88 // may be null if moved-from
90 mWindow
->PickerClosed();
94 AutoWidgetPickerState(AutoWidgetPickerState
const&) = delete;
95 AutoWidgetPickerState(AutoWidgetPickerState
&& that
) noexcept
= default;
98 RefPtr
<nsWindow
> mWindow
;
101 ///////////////////////////////////////////////////////////////////////////////
104 nsFilePicker::nsFilePicker() = default;
106 NS_IMPL_ISUPPORTS(nsFilePicker
, nsIFilePicker
)
108 NS_IMETHODIMP
nsFilePicker::Init(
109 mozilla::dom::BrowsingContext
* aBrowsingContext
, const nsAString
& aTitle
,
110 nsIFilePicker::Mode aMode
) {
111 // Don't attempt to open a real file-picker in headless mode.
112 if (gfxPlatform::IsHeadless()) {
113 return nsresult::NS_ERROR_NOT_AVAILABLE
;
116 return nsBaseFilePicker::Init(aBrowsingContext
, aTitle
, aMode
);
119 namespace mozilla::detail
{
120 using Error
= mozilla::widget::filedialog::Error
;
122 // Boilerplate for remotely showing a file dialog.
123 template <typename ActionType
,
124 typename ReturnType
= typename
decltype(std::declval
<ActionType
>()(
125 nullptr))::element_type::ResolveValueType
>
126 static auto ShowRemote(ActionType
&& action
) -> RefPtr
<FDPromise
<ReturnType
>> {
127 using RetPromise
= FDPromise
<ReturnType
>;
129 // "function-local" #define
130 #define FAIL(where_, why_) \
131 return RetPromise::CreateAndReject(MOZ_FD_LOCAL_ERROR(where_, why_), \
134 auto mgr
= mozilla::ipc::UtilityProcessManager::GetSingleton();
137 FAIL("ShowRemote: UtilityProcessManager::GetSingleton", E_POINTER
);
140 auto wfda
= mgr
->CreateWinFileDialogActor();
142 FAIL("ShowRemote: invocation of CreateWinFileDialogActor", E_POINTER
);
145 using mozilla::widget::filedialog::sLogFileDialog
;
148 mozilla::GetMainThreadSerialEventTarget(),
149 "nsFilePicker ShowRemote acquire",
150 [action
= std::forward
<ActionType
>(action
)](
151 filedialog::ProcessProxy p
) -> RefPtr
<RetPromise
> {
152 MOZ_LOG(sLogFileDialog
, LogLevel::Info
,
153 ("nsFilePicker ShowRemote first callback: p = [%p]", p
.get()));
155 // false positive: not actually redundant
156 // NOLINTNEXTLINE(readability-redundant-smartptr-get)
157 auto promise
= action(p
.get());
159 mozilla::GetMainThreadSerialEventTarget(), __func__
,
160 [p
= std::move(p
)](typename
RetPromise::ResolveValueType
&& val
) {
161 // explicitly retain the ProcessProxy until at least this point
162 return std::move(val
);
165 [](mozilla::ipc::LaunchError
const& error
) {
166 MOZ_LOG(sLogFileDialog
, LogLevel::Error
,
167 ("could not acquire WinFileDialog: %s:%zu",
168 error
.FunctionName().get(), size_t(error
.ErrorCode())));
169 return RetPromise::CreateAndReject(Error::From(error
),
170 "nsFilePicker::ShowRemote");
178 static RefPtr
<FDPromise
<Maybe
<filedialog::Results
>>> ShowFilePickerRemote(
179 HWND parent
, filedialog::FileDialogType type
,
180 nsTArray
<filedialog::Command
> const& commands
) {
181 using mozilla::widget::filedialog::sLogFileDialog
;
182 return mozilla::detail::ShowRemote(
184 commands
= commands
.Clone()](filedialog::WinFileDialogParent
* p
) {
185 MOZ_LOG(sLogFileDialog
, LogLevel::Info
,
186 ("%s: p = [%p]", __PRETTY_FUNCTION__
, p
));
187 return p
->ShowFileDialogImpl(parent
, type
, commands
);
191 static RefPtr
<FDPromise
<Maybe
<nsString
>>> ShowFolderPickerRemote(
192 HWND parent
, nsTArray
<filedialog::Command
> const& commands
) {
193 using mozilla::widget::filedialog::sLogFileDialog
;
194 return mozilla::detail::ShowRemote([parent
, commands
= commands
.Clone()](
195 filedialog::WinFileDialogParent
* p
) {
196 MOZ_LOG(sLogFileDialog
, LogLevel::Info
,
197 ("%s: p = [%p]", __PRETTY_FUNCTION__
, p
));
198 return p
->ShowFolderDialogImpl(parent
, commands
);
202 static RefPtr
<FDPromise
<Maybe
<filedialog::Results
>>> ShowFilePickerLocal(
203 HWND parent
, filedialog::FileDialogType type
,
204 nsTArray
<filedialog::Command
> const& commands
) {
205 return filedialog::SpawnFilePicker(parent
, type
, commands
.Clone());
208 static RefPtr
<FDPromise
<Maybe
<nsString
>>> ShowFolderPickerLocal(
209 HWND parent
, nsTArray
<filedialog::Command
> const& commands
) {
210 return filedialog::SpawnFolderPicker(parent
, commands
.Clone());
217 // Wrapper-namespace for the AsyncExecute() and AsyncAll() functions.
220 // Implementation details of, specifically, the AsyncExecute() and AsyncAll()
223 // Helper for generically copying ordinary types and nsTArray (which lacks a
224 // copy constructor) in the same breath.
225 template <typename T
>
226 static T
Copy(T
const& val
) {
229 template <typename T
>
230 static nsTArray
<T
> Copy(nsTArray
<T
> const& arr
) {
234 // The possible execution strategies of AsyncExecute.
236 // Always and only open the dialog in-process. This is effectively the
237 // only behavior in older versions of Gecko.
240 // Always and only open the dialog out-of-process.
243 // Open the dialog out-of-process. If that fails in any way, try to recover by
244 // opening it in-process.
247 // Try to open the dialog out-of-process. If and only if the process can't
248 // even be created, fall back to in-process.
250 // This heuristic is crafted to avoid the out-of-process file-dialog causing
251 // user-experience regressions compared to the previous "LocalOnly" behavior:
252 // * If the file-dialog actually crashes, then it would have brought down the
253 // entire browser. In this case just surfacing an error is a strict
255 // * If the utility process simply fails to start, there's usually nothing
256 // preventing the dialog from being opened in-process instead. Producing an
257 // error would be a degradation.
261 // Decode the relevant preference to determine the desired execution-
263 static Strategy
GetStrategy() {
265 mozilla::StaticPrefs::widget_windows_utility_process_file_picker();
270 return FallbackUnlessCrash
;
274 return RemoteWithFallback
;
277 // by default, fall back to local only on non-crash failures
278 return FallbackUnlessCrash
;
282 template <typename T
>
283 class AsyncAllIterator final
{
285 NS_INLINE_DECL_REFCOUNTING(AsyncAllIterator
)
289 RefPtr
<mozilla::MozPromise
<bool, nsresult
, true>>(const T
& item
)>
291 RefPtr
<mozilla::MozPromise
<bool, nsresult
, true>::Private
> aPromise
)
292 : mItems(std::move(aItems
)),
294 mPredicate(std::move(aPredicate
)),
295 mPromise(std::move(aPromise
)) {}
297 void StartIterating() { ContinueIterating(); }
300 ~AsyncAllIterator() = default;
301 void ContinueIterating() {
302 if (mNextIndex
>= mItems
.Length()) {
303 mPromise
->Resolve(true, __func__
);
306 mPredicate(mItems
.ElementAt(mNextIndex
))
308 mozilla::GetMainThreadSerialEventTarget(), __func__
,
309 [self
= RefPtr
{this}](bool aResult
) {
311 self
->mPromise
->Resolve(false, __func__
);
315 self
->ContinueIterating();
317 [self
= RefPtr
{this}](nsresult aError
) {
318 self
->mPromise
->Reject(aError
, __func__
);
323 std::function
<RefPtr
<mozilla::MozPromise
<bool, nsresult
, true>>(
326 RefPtr
<mozilla::MozPromise
<bool, nsresult
, true>::Private
> mPromise
;
329 /* N.B.: L and R stand for Local and Remote, not just Left and Right */
330 template <typename FnL
, typename FnR
, typename
... Args
>
331 struct AsyncExecuteInfo
{
332 template <typename T
>
333 using DestructurePromise
= widget::filedialog::detail::DestructurePromise
<T
>;
335 using Unit
= ::mozilla::Ok
;
337 using RetL
= std::invoke_result_t
<FnL
, Args
...>;
338 using RetR
= std::invoke_result_t
<FnR
, Args
...>;
340 using InfoL
= DestructurePromise
<RetL
>;
341 using InfoR
= DestructurePromise
<RetR
>;
343 MOZ_ASSERT_SAME_TYPE(
344 typename
InfoL::ResolveT
, typename
InfoR::ResolveT
,
345 "local and remote promises must have identical resolve-types");
347 // At present, the local and remote promises have the same type, but this
348 // isn't logically necessary. (In particular, a future refactor may remove the
349 // redundant `.kind` from the local promises' return types.)
350 MOZ_ASSERT_SAME_TYPE(typename
InfoL::RejectT
, filedialog::Error
,
351 "local promise must reject with a filedialog::Error");
353 MOZ_ASSERT_SAME_TYPE(typename
InfoR::RejectT
, filedialog::Error
,
354 "remote promise must reject with a filedialog::Error");
356 using ResolveT
= typename
InfoL::ResolveT
;
357 using PromiseT
= MozPromise
<ResolveT
, filedialog::Error
, true>;
359 using RetT
= RefPtr
<PromiseT
>;
362 } // namespace details
364 // Invoke either or both of a promise-returning "do locally" and "do remotely"
365 // function with the provided arguments, depending on the relevant preference's
366 // value and on whether or not the remote version fails (returns a rejection-
369 // Both provided functions must return a `RefPtr<filedialog::MozPromise<T>>`. As
370 // `AsyncExecute` reports failures itself, its rejection-type is `()`.
371 template <typename Fn1
, typename Fn2
, typename
... Args
>
372 static auto AsyncExecute(Fn1 local
, Fn2 remote
, Args
const&... args
) ->
373 typename
details::AsyncExecuteInfo
<Fn1
, Fn2
, Args
...>::RetT
{
374 using namespace details
;
375 using Info
= AsyncExecuteInfo
<Fn1
, Fn2
, Args
...>;
377 using ResolveT
= typename
Info::ResolveT
;
378 using PromiseT
= typename
Info::PromiseT
;
379 using LPromiseT
= typename
Info::InfoL::Promise
;
380 using RPromiseT
= typename
Info::InfoR::Promise
;
382 constexpr static char kFunctionName
[] = "LocalAndOrRemote::AsyncExecute";
384 bool (*useLocalFallback
)(Error
const& err
) = [](Error
const& err
) {
385 MOZ_ASSERT_UNREACHABLE("useLocalFallback not set?!");
389 switch (GetStrategy()) {
391 return local(args
...)->MapErr(
392 NS_GetCurrentThread(), __func__
, [](Error
const& err
) {
393 MOZ_ASSERT(err
.kind
== Error::LocalError
);
394 MOZ_LOG(filedialog::sLogFileDialog
, LogLevel::Info
,
395 ("local file-dialog failed: where=%s, why=%08" PRIX32
,
396 err
.where
.c_str(), err
.why
));
402 useLocalFallback
= [](Error
const&) { return false; };
405 case RemoteWithFallback
:
406 useLocalFallback
= [](Error
const&) { return true; };
409 case FallbackUnlessCrash
:
410 useLocalFallback
= [](Error
const& err
) {
411 // All remote crashes are reported as IPCError. The converse isn't
412 // necessarily true in theory, but (per telemetry) appears to be true in
414 return err
.kind
!= Error::IPCError
;
419 return remote(args
...)->Then(
420 NS_GetCurrentThread(), kFunctionName
,
421 [](typename
RPromiseT::ResolveValueType result
) -> RefPtr
<PromiseT
> {
422 // success; stop here
423 return PromiseT::CreateAndResolve(std::move(result
), kFunctionName
);
425 // initialized lambda pack captures are C++20 (clang 9, gcc 9);
426 // `make_tuple` is just a C++17 workaround
427 [=, tuple
= std::make_tuple(Copy(args
)...)](
428 typename
RPromiseT::RejectValueType err
) mutable -> RefPtr
<PromiseT
> {
429 // failure; record time
431 // should we fall back to a local implementation?
432 if (!useLocalFallback(err
)) {
433 // if not, log this failure immediately...
434 MOZ_LOG(filedialog::sLogFileDialog
, LogLevel::Info
,
435 ("remote file-dialog failed: kind=%s, where=%s, "
437 Error::KindName(err
.kind
), err
.where
.c_str(), err
.why
));
439 return PromiseT::CreateAndReject(err
, kFunctionName
);
442 // otherwise, retry locally
443 auto p0
= std::apply(local
, std::move(tuple
));
445 NS_GetCurrentThread(), kFunctionName
,
446 [](typename
LPromiseT::ResolveOrRejectValue
&& val
)
447 -> RefPtr
<PromiseT
> {
448 using V
= typename
PromiseT::ResolveOrRejectValue
;
449 return PromiseT::CreateAndResolveOrReject(
451 ? V::MakeResolve(std::move(val
).ResolveValue())
452 : V::MakeReject(val
.RejectValue()),
458 // Asynchronously invokes `aPredicate` on each member of `aItems`.
459 // Yields `false` (and stops immediately) if any invocation of
460 // `predicate` yielded `false`; otherwise yields `true`.
461 template <typename T
>
462 static RefPtr
<mozilla::MozPromise
<bool, nsresult
, true>> AsyncAll(
465 RefPtr
<mozilla::MozPromise
<bool, nsresult
, true>>(const T
& item
)>
468 mozilla::MakeRefPtr
<mozilla::MozPromise
<bool, nsresult
, true>::Private
>(
470 auto iterator
= mozilla::MakeRefPtr
<details::AsyncAllIterator
<T
>>(
471 std::move(aItems
), aPredicate
, promise
);
472 iterator
->StartIterating();
475 } // namespace fd_async
477 using fd_async::AsyncAll
;
478 using fd_async::AsyncExecute
;
480 } // namespace mozilla::detail
483 * Folder picker invocation
487 * Show a folder picker.
489 * @param aInitialDir The initial directory. The last-used directory will be
490 * used if left blank.
491 * @return A promise which:
492 * - resolves to true if a file was selected successfully (in which
493 * case mUnicodeFile will be updated);
494 * - resolves to false if the dialog was cancelled by the user;
495 * - is rejected with the associated HRESULT if some error occurred.
497 RefPtr
<mozilla::MozPromise
<bool, nsFilePicker::Error
, true>>
498 nsFilePicker::ShowFolderPicker(const nsString
& aInitialDir
) {
499 namespace fd
= ::mozilla::widget::filedialog
;
500 nsTArray
<fd::Command
> commands
= {
501 fd::SetOptions(FOS_PICKFOLDERS
),
502 fd::SetTitle(mTitle
),
505 if (!mOkButtonLabel
.IsEmpty()) {
506 commands
.AppendElement(fd::SetOkButtonLabel(mOkButtonLabel
));
509 if (!aInitialDir
.IsEmpty()) {
510 commands
.AppendElement(fd::SetFolder(aInitialDir
));
513 ScopedRtlShimWindow
shim(mParentWidget
.get());
514 AutoWidgetPickerState
awps(mParentWidget
);
516 return mozilla::detail::AsyncExecute(&mozilla::detail::ShowFolderPickerLocal
,
517 &mozilla::detail::ShowFolderPickerRemote
,
518 shim
.get(), commands
)
519 ->Map(NS_GetCurrentThread(), __PRETTY_FUNCTION__
,
520 [self
= RefPtr(this), shim
= std::move(shim
),
521 awps
= std::move(awps
)](Maybe
<nsString
> val
) {
523 self
->mUnicodeFile
= val
.extract();
531 * File open and save picker invocation
535 * Show a file picker.
537 * @param aInitialDir The initial directory. The last-used directory will be
538 * used if left blank.
539 * @return A promise which:
540 * - resolves to true if one or more files were selected successfully
541 * (in which case mUnicodeFile and/or mFiles will be updated);
542 * - resolves to false if the dialog was cancelled by the user;
543 * - is rejected with the associated HRESULT if some error occurred.
545 RefPtr
<mozilla::MozPromise
<bool, nsFilePicker::Error
, true>>
546 nsFilePicker::ShowFilePicker(const nsString
& aInitialDir
) {
547 AUTO_PROFILER_LABEL("nsFilePicker::ShowFilePicker", OTHER
);
549 using Promise
= mozilla::MozPromise
<bool, Error
, true>;
550 constexpr static auto NotOk
= [](Error error
) -> RefPtr
<Promise
> {
551 return Promise::CreateAndReject(std::move(error
),
552 "nsFilePicker::ShowFilePicker");
555 namespace fd
= ::mozilla::widget::filedialog
;
556 nsTArray
<fd::Command
> commands
;
559 FILEOPENDIALOGOPTIONS fos
= 0;
561 // FOS_OVERWRITEPROMPT: always confirm on overwrite in Save dialogs
562 // FOS_FORCEFILESYSTEM: provide only filesystem-objects, not more exotic
563 // entities like libraries
564 fos
|= FOS_OVERWRITEPROMPT
| FOS_FORCEFILESYSTEM
;
566 // Handle add to recent docs settings
567 if (IsPrivacyModeEnabled() || !mAddToRecentDocs
) {
568 fos
|= FOS_DONTADDTORECENT
;
571 // mode specification
574 fos
|= FOS_FILEMUSTEXIST
;
577 case modeOpenMultiple
:
578 fos
|= FOS_FILEMUSTEXIST
| FOS_ALLOWMULTISELECT
;
582 fos
|= FOS_NOREADONLYRETURN
;
583 // Don't follow shortcuts when saving a shortcut, this can be used
584 // to trick users (bug 271732)
585 if (IsDefaultPathLink()) {
586 fos
|= FOS_NODEREFERENCELINKS
;
591 MOZ_ASSERT(false, "file-picker opened in directory-picker mode");
592 return NotOk(MOZ_FD_LOCAL_ERROR(
593 "file-picker opened in directory-picker mode", E_INVALIDARG
));
596 commands
.AppendElement(fd::SetOptions(fos
));
602 commands
.AppendElement(fd::SetTitle(mTitle
));
605 if (!mDefaultFilename
.IsEmpty()) {
606 // Prevent the shell from expanding environment variables by removing the %
607 // characters that are used to delimit them.
609 // Note that we do _not_ need to preserve this sanitization for the fallback
610 // case where the file dialog fails. Variable-expansion only occurs in the
611 // file dialog specifically, and not when creating a file directly via other
613 nsAutoString
sanitizedFilename(mDefaultFilename
);
614 sanitizedFilename
.ReplaceChar('%', '_');
616 commands
.AppendElement(fd::SetFileName(sanitizedFilename
));
619 // default extension to append to new files
620 if (!mDefaultExtension
.IsEmpty()) {
621 // We don't want environment variables expanded in the extension either.
622 nsAutoString
sanitizedExtension(mDefaultExtension
);
623 sanitizedExtension
.ReplaceChar('%', '_');
625 commands
.AppendElement(fd::SetDefaultExtension(sanitizedExtension
));
626 } else if (IsDefaultPathHtml()) {
627 commands
.AppendElement(fd::SetDefaultExtension(u
"html"_ns
));
631 if (!aInitialDir
.IsEmpty()) {
632 commands
.AppendElement(fd::SetFolder(aInitialDir
));
635 // filter types and the default index
636 if (!mFilterList
.IsEmpty()) {
637 nsTArray
<fd::ComDlgFilterSpec
> fileTypes
;
638 for (auto const& filter
: mFilterList
) {
639 fileTypes
.EmplaceBack(filter
.title
, filter
.filter
);
641 commands
.AppendElement(fd::SetFileTypes(std::move(fileTypes
)));
642 commands
.AppendElement(fd::SetFileTypeIndex(mSelectedType
));
645 ScopedRtlShimWindow
shim(mParentWidget
.get());
646 AutoWidgetPickerState
awps(mParentWidget
);
648 mozilla::BackgroundHangMonitor().NotifyWait();
649 auto type
= mMode
== modeSave
? FileDialogType::Save
: FileDialogType::Open
;
651 auto promise
= mozilla::detail::AsyncExecute(
652 &mozilla::detail::ShowFilePickerLocal
,
653 &mozilla::detail::ShowFilePickerRemote
, shim
.get(), type
, commands
);
656 mozilla::GetMainThreadSerialEventTarget(), __PRETTY_FUNCTION__
,
657 [self
= RefPtr(this), mode
= mMode
, shim
= std::move(shim
),
658 awps
= std::move(awps
)](Maybe
<Results
> res_opt
) {
660 return false; // operation cancelled by user
662 auto result
= res_opt
.extract();
664 // Remember what filter type the user selected
665 self
->mSelectedType
= int32_t(result
.selectedFileTypeIndex());
667 auto const& paths
= result
.paths();
670 if (mode
!= modeOpenMultiple
) {
671 if (!paths
.IsEmpty()) {
672 MOZ_ASSERT(paths
.Length() == 1);
673 self
->mUnicodeFile
= paths
[0];
679 // multiple selection
680 for (auto const& str
: paths
) {
681 nsCOMPtr
<nsIFile
> file
;
682 if (NS_SUCCEEDED(NS_NewLocalFile(str
, getter_AddRefs(file
)))) {
683 self
->mFiles
.AppendObject(file
);
691 void nsFilePicker::ClearFiles() {
692 mUnicodeFile
.Truncate();
696 RefPtr
<nsFilePicker::ContentAnalysisResponse
>
697 nsFilePicker::CheckContentAnalysisService() {
699 nsCOMPtr
<nsIContentAnalysis
> contentAnalysis
=
700 mozilla::components::nsIContentAnalysis::Service(&rv
);
701 if (NS_WARN_IF(NS_FAILED(rv
))) {
702 return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv
, __func__
);
704 bool contentAnalysisIsActive
= false;
705 rv
= contentAnalysis
->GetIsActive(&contentAnalysisIsActive
);
706 if (NS_WARN_IF(NS_FAILED(rv
))) {
707 return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv
, __func__
);
709 if (!contentAnalysisIsActive
||
710 !mozilla::StaticPrefs::
711 browser_contentanalysis_interception_point_file_upload_enabled()) {
712 return nsFilePicker::ContentAnalysisResponse::CreateAndResolve(true,
716 nsCOMPtr
<nsIURI
> uri
=
717 mozilla::contentanalysis::ContentAnalysis::GetURIForBrowsingContext(
718 mBrowsingContext
->Canonical());
720 return nsFilePicker::ContentAnalysisResponse::CreateAndReject(
721 NS_ERROR_FAILURE
, __func__
);
724 // Entries may be files or folders. Folder contents will be recursively
726 nsTArray
<mozilla::PathString
> filePaths
;
727 if (mMode
== modeGetFolder
|| !mUnicodeFile
.IsEmpty()) {
728 RefPtr
<nsIFile
> folderOrFile
;
729 nsresult rv
= GetFile(getter_AddRefs(folderOrFile
));
730 if (NS_WARN_IF(NS_FAILED(rv
) || !folderOrFile
)) {
731 return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv
,
734 filePaths
.AppendElement(folderOrFile
->NativePath());
736 // multiple selections
737 std::transform(mFiles
.begin(), mFiles
.end(), MakeBackInserter(filePaths
),
738 [](auto* entry
) { return entry
->NativePath(); });
741 auto processOneItem
= [self
= RefPtr
{this},
742 contentAnalysis
= std::move(contentAnalysis
),
744 std::move(uri
)](const mozilla::PathString
& aItem
) {
745 nsCString emptyDigestString
;
747 self
->mBrowsingContext
->Canonical()->GetCurrentWindowGlobal();
748 nsCOMPtr
<nsIContentAnalysisRequest
> contentAnalysisRequest(
749 new mozilla::contentanalysis::ContentAnalysisRequest(
750 nsIContentAnalysisRequest::AnalysisType::eFileAttached
,
751 nsIContentAnalysisRequest::Reason::eFilePickerDialog
, aItem
, true,
752 std::move(emptyDigestString
), uri
,
753 nsIContentAnalysisRequest::OperationType::eCustomDisplayString
,
757 mozilla::MakeRefPtr
<nsFilePicker::ContentAnalysisResponse::Private
>(
759 auto contentAnalysisCallback
=
760 mozilla::MakeRefPtr
<mozilla::contentanalysis::ContentAnalysisCallback
>(
761 [promise
](nsIContentAnalysisResponse
* aResponse
) {
762 bool shouldAllow
= false;
763 mozilla::DebugOnly
<nsresult
> rv
=
764 aResponse
->GetShouldAllowContent(&shouldAllow
);
765 MOZ_ASSERT(NS_SUCCEEDED(rv
));
766 promise
->Resolve(shouldAllow
, __func__
);
768 [promise
](nsresult aError
) { promise
->Reject(aError
, __func__
); });
770 nsresult rv
= contentAnalysis
->AnalyzeContentRequestCallback(
771 contentAnalysisRequest
, /* aAutoAcknowledge */ true,
772 contentAnalysisCallback
);
773 if (NS_WARN_IF(NS_FAILED(rv
))) {
774 promise
->Reject(rv
, __func__
);
779 return mozilla::detail::AsyncAll
<mozilla::PathString
>(std::move(filePaths
),
783 ///////////////////////////////////////////////////////////////////////////////
784 // nsIFilePicker impl.
786 nsresult
nsFilePicker::Open(nsIFilePickerShownCallback
* aCallback
) {
787 NS_ENSURE_ARG_POINTER(aCallback
);
789 if (MaybeBlockFilePicker(aCallback
)) {
793 // Don't attempt to open a real file-picker in headless mode.
794 if (gfxPlatform::IsHeadless()) {
795 return nsresult::NS_ERROR_NOT_AVAILABLE
;
798 nsAutoString initialDir
;
799 if (mDisplayDirectory
) {
800 mDisplayDirectory
->GetPath(initialDir
);
803 // If no display directory, re-use the last one.
804 if (initialDir
.IsEmpty()) {
805 // Allocate copy of last used dir.
806 initialDir
= sLastUsedUnicodeDirectory
.get();
809 // Clear previous file selections
812 auto promise
= mMode
== modeGetFolder
? ShowFolderPicker(initialDir
)
813 : ShowFilePicker(initialDir
);
816 mozilla::GetMainThreadSerialEventTarget(), __PRETTY_FUNCTION__
,
817 [self
= RefPtr(this),
818 callback
= RefPtr(aCallback
)](bool selectionMade
) -> void {
819 if (!selectionMade
) {
820 callback
->Done(ResultCode::returnCancel
);
824 self
->RememberLastUsedDirectory();
826 nsIFilePicker::ResultCode retValue
= ResultCode::returnOK
;
828 if (self
->mMode
== modeSave
) {
829 // Windows does not return resultReplace; we must check whether the
830 // file already exists.
831 nsCOMPtr
<nsIFile
> file
;
833 NS_NewLocalFile(self
->mUnicodeFile
, getter_AddRefs(file
));
836 if (NS_SUCCEEDED(rv
) && NS_SUCCEEDED(file
->Exists(&flag
)) && flag
) {
837 retValue
= ResultCode::returnReplace
;
841 if (self
->mBrowsingContext
&& !self
->mBrowsingContext
->IsChrome() &&
842 self
->mMode
!= modeSave
&& retValue
!= ResultCode::returnCancel
) {
843 self
->CheckContentAnalysisService()->Then(
844 mozilla::GetMainThreadSerialEventTarget(), __func__
,
845 [retValue
, callback
, self
= RefPtr
{self
}](bool aAllowContent
) {
847 callback
->Done(retValue
);
850 callback
->Done(ResultCode::returnCancel
);
853 [callback
, self
= RefPtr
{self
}](nsresult aError
) {
855 callback
->Done(ResultCode::returnCancel
);
860 callback
->Done(retValue
);
862 [callback
= RefPtr(aCallback
), self
= RefPtr
{this}](Error
const& err
) {
863 // The file-dialog process (probably) crashed. Report this fact to the
864 // user, and try to recover with a fallback rather than discarding the
867 // (Note that at this point, logging of the crash -- and possibly also a
868 // telemetry ping -- has already occurred.)
869 ResultCode resultCode
= ResultCode::returnCancel
;
871 // This does not describe the original error, just the error when trying
872 // to select a fallback location -- no such attempt means no such error.
873 FallbackResult fallback
{nullptr};
875 if (self
->mMode
== Mode::modeSave
) {
876 fallback
= self
->ComputeFallbackSavePath();
877 // don't set sLastUsedUnicodeDirectory here: the user didn't
878 // actually select anything
881 self
->SendFailureNotification(resultCode
, err
, std::move(fallback
));
882 callback
->Done(resultCode
);
889 nsFilePicker::GetFile(nsIFile
** aFile
) {
890 NS_ENSURE_ARG_POINTER(aFile
);
893 if (mUnicodeFile
.IsEmpty()) {
897 nsCOMPtr
<nsIFile
> file
;
898 nsresult rv
= NS_NewLocalFile(mUnicodeFile
, getter_AddRefs(file
));
908 nsFilePicker::GetFileURL(nsIURI
** aFileURL
) {
910 nsCOMPtr
<nsIFile
> file
;
911 nsresult rv
= GetFile(getter_AddRefs(file
));
916 return NS_NewFileURI(aFileURL
, file
);
920 nsFilePicker::GetFiles(nsISimpleEnumerator
** aFiles
) {
921 NS_ENSURE_ARG_POINTER(aFiles
);
922 return NS_NewArrayEnumerator(aFiles
, mFiles
, NS_GET_IID(nsIFile
));
925 // Get the file + path
927 nsBaseWinFilePicker::SetDefaultString(const nsAString
& aString
) {
928 mDefaultFilePath
= aString
;
930 // First, make sure the file name is not too long.
932 int32_t nameIndex
= mDefaultFilePath
.RFind(u
"\\");
933 if (nameIndex
== kNotFound
) {
938 nameLength
= mDefaultFilePath
.Length() - nameIndex
;
939 mDefaultFilename
.Assign(Substring(mDefaultFilePath
, nameIndex
));
941 if (nameLength
> MAX_PATH
) {
942 int32_t extIndex
= mDefaultFilePath
.RFind(u
".");
943 if (extIndex
== kNotFound
) {
944 extIndex
= mDefaultFilePath
.Length();
947 // Let's try to shave the needed characters from the name part.
948 int32_t charsToRemove
= nameLength
- MAX_PATH
;
949 if (extIndex
- nameIndex
>= charsToRemove
) {
950 mDefaultFilePath
.Cut(extIndex
- charsToRemove
, charsToRemove
);
954 // Then, we need to replace illegal characters. At this stage, we cannot
955 // replace the backslash as the string might represent a file path.
956 mDefaultFilePath
.ReplaceChar(u
"" FILE_ILLEGAL_CHARACTERS
, u
'-');
957 mDefaultFilename
.ReplaceChar(u
"" FILE_ILLEGAL_CHARACTERS
, u
'-');
963 nsBaseWinFilePicker::GetDefaultString(nsAString
& aString
) {
964 return NS_ERROR_FAILURE
;
967 // The default extension to use for files
969 nsBaseWinFilePicker::GetDefaultExtension(nsAString
& aExtension
) {
970 aExtension
= mDefaultExtension
;
975 nsBaseWinFilePicker::SetDefaultExtension(const nsAString
& aExtension
) {
976 mDefaultExtension
= aExtension
;
980 // Set the filter index
982 nsFilePicker::GetFilterIndex(int32_t* aFilterIndex
) {
983 // Windows' filter index is 1-based, we use a 0-based system.
984 *aFilterIndex
= mSelectedType
- 1;
989 nsFilePicker::SetFilterIndex(int32_t aFilterIndex
) {
990 // Windows' filter index is 1-based, we use a 0-based system.
991 mSelectedType
= aFilterIndex
+ 1;
995 void nsFilePicker::InitNative(nsIWidget
* aParent
, const nsAString
& aTitle
) {
996 mParentWidget
= aParent
;
997 mTitle
.Assign(aTitle
);
1001 nsFilePicker::AppendFilter(const nsAString
& aTitle
, const nsAString
& aFilter
) {
1002 nsString
sanitizedFilter(aFilter
);
1003 sanitizedFilter
.ReplaceChar('%', '_');
1005 if (sanitizedFilter
== u
"..apps"_ns
) {
1006 sanitizedFilter
= u
"*.exe;*.com"_ns
;
1008 sanitizedFilter
.StripWhitespace();
1009 if (sanitizedFilter
== u
"*"_ns
) {
1010 sanitizedFilter
= u
"*.*"_ns
;
1013 mFilterList
.AppendElement(
1014 Filter
{.title
= nsString(aTitle
), .filter
= std::move(sanitizedFilter
)});
1018 void nsFilePicker::RememberLastUsedDirectory() {
1019 if (IsPrivacyModeEnabled()) {
1020 // Don't remember the directory if private browsing was in effect
1024 nsCOMPtr
<nsIFile
> file
;
1025 if (NS_FAILED(NS_NewLocalFile(mUnicodeFile
, getter_AddRefs(file
)))) {
1026 NS_WARNING("RememberLastUsedDirectory failed to init file path.");
1030 nsCOMPtr
<nsIFile
> dir
;
1031 nsAutoString newDir
;
1032 if (NS_FAILED(file
->GetParent(getter_AddRefs(dir
))) ||
1033 !(mDisplayDirectory
= dir
) ||
1034 NS_FAILED(mDisplayDirectory
->GetPath(newDir
)) || newDir
.IsEmpty()) {
1035 NS_WARNING("RememberLastUsedDirectory failed to get parent directory.");
1039 sLastUsedUnicodeDirectory
.reset(ToNewUnicode(newDir
));
1042 bool nsFilePicker::IsPrivacyModeEnabled() {
1043 return mBrowsingContext
&& mBrowsingContext
->UsePrivateBrowsing();
1046 bool nsFilePicker::IsDefaultPathLink() {
1047 NS_ConvertUTF16toUTF8
ext(mDefaultFilePath
);
1048 ext
.Trim(" .", false, true); // watch out for trailing space and dots
1050 return StringEndsWith(ext
, ".lnk"_ns
) || StringEndsWith(ext
, ".pif"_ns
) ||
1051 StringEndsWith(ext
, ".url"_ns
);
1054 bool nsFilePicker::IsDefaultPathHtml() {
1055 int32_t extIndex
= mDefaultFilePath
.RFind(u
".");
1056 if (extIndex
>= 0) {
1058 mDefaultFilePath
.Right(ext
, mDefaultFilePath
.Length() - extIndex
);
1059 if (ext
.LowerCaseEqualsLiteral(".htm") ||
1060 ext
.LowerCaseEqualsLiteral(".html") ||
1061 ext
.LowerCaseEqualsLiteral(".shtml")) {
1068 auto nsFilePicker::ComputeFallbackSavePath() const -> FallbackResult
{
1071 // we shouldn't even be here if we're not trying to save
1072 if (mMode
!= Mode::modeSave
) {
1073 return Err(NS_ERROR_FAILURE
);
1076 // get a fallback download-location
1077 RefPtr
<nsIFile
> location
;
1079 // try to query the helper service for the preferred downloads directory
1081 nsCOMPtr
<nsIExternalHelperAppService
> svc
=
1082 do_GetService(NS_EXTERNALHELPERAPPSERVICE_CONTRACTID
, &rv
);
1085 MOZ_TRY(svc
->GetPreferredDownloadsDirectory(getter_AddRefs(location
)));
1087 MOZ_ASSERT(location
);
1089 constexpr static const auto EndsWithExtension
=
1090 [](nsAString
const& path
, nsAString
const& extension
) -> bool {
1091 size_t const len
= path
.Length();
1092 size_t const extLen
= extension
.Length();
1093 if (extLen
+ 2 > len
) {
1094 // `path` is too short and can't possibly end with `extension`. (Note that
1095 // we consider, _e.g._, ".jpg" not to end with the extension "jpg".)
1098 if (path
[len
- extLen
- 1] == L
'.' &&
1099 StringTail(path
, extLen
) == extension
) {
1105 nsString
filename(mDefaultFilename
);
1106 if (!mDefaultExtension
.IsEmpty() &&
1107 !EndsWithExtension(filename
, mDefaultExtension
)) {
1108 filename
.AppendLiteral(".");
1109 filename
.Append(mDefaultExtension
);
1112 MOZ_TRY(location
->Append(filename
));
1113 MOZ_TRY(location
->CreateUnique(nsIFile::NORMAL_FILE_TYPE
, 0600));
1117 void nsFilePicker::SendFailureNotification(nsFilePicker::ResultCode aResult
,
1119 FallbackResult aFallback
) const {
1120 if (MOZ_LOG_TEST(filedialog::sLogFileDialog
, LogLevel::Info
)) {
1122 if (aFallback
.isOk()) {
1124 aFallback
.inspect()->GetPath(path
);
1128 msg
.AppendPrintf("err: 0x%08" PRIX32
, (uint32_t)aFallback
.inspectErr());
1130 MOZ_LOG(filedialog::sLogFileDialog
, LogLevel::Info
,
1131 ("SendCrashNotification: %" PRIX16
", %ls", aResult
,
1132 static_cast<wchar_t const*>(msg
.get())));
1135 nsCOMPtr
<nsIObserverService
> obsSvc
= mozilla::services::GetObserverService();
1137 return; // normal during XPCOM shutdown
1140 RefPtr
<nsHashPropertyBag
> props
= new nsHashPropertyBag();
1141 props
->SetPropertyAsInterface(u
"ctx"_ns
, mBrowsingContext
);
1142 props
->SetPropertyAsUint32(u
"mode"_ns
, mMode
);
1143 if (aFallback
.isOk()) {
1144 props
->SetPropertyAsInterface(u
"file"_ns
, aFallback
.unwrap().get());
1146 props
->SetPropertyAsUint32(u
"file-error"_ns
,
1147 (uint32_t)aFallback
.unwrapErr());
1150 props
->SetPropertyAsBool(u
"crash"_ns
, error
.kind
== Error::IPCError
);
1152 nsIPropertyBag2
* const iface
= props
;
1153 obsSvc
->NotifyObservers(iface
, "file-picker-crashed", nullptr);