Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / widget / windows / nsFilePicker.cpp
blob410ec6c862a3fca8caf33ef2b35f6b31653a3dde
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 <cderr.h>
10 #include <shlobj.h>
11 #include <shlwapi.h>
12 #include <sysinfoapi.h>
13 #include <winerror.h>
14 #include <winuser.h>
15 #include <utility>
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"
32 #include "nsCRT.h"
33 #include "nsEnumeratorUtils.h"
34 #include "nsHashPropertyBag.h"
35 #include "nsIContentAnalysis.h"
36 #include "nsIFile.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"
44 #include "nsString.h"
45 #include "nsToolkit.h"
46 #include "nsWindow.h"
47 #include "WinUtils.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 ///////////////////////////////////////////////////////////////////////////////
66 // Helper classes
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());
72 if (!aWidget) {
73 return nullptr;
75 HWND hwnd = (HWND)aWidget->GetNativeData(NS_NATIVE_WINDOW);
76 return RefPtr(WinUtils::GetNSWindowPtr(hwnd));
79 public:
80 explicit AutoWidgetPickerState(nsIWidget* aWidget)
81 : mWindow(GetWindowForWidget(aWidget)) {
82 MOZ_ASSERT(mWindow);
83 if (mWindow) {
84 mWindow->PickerOpen();
87 ~AutoWidgetPickerState() {
88 // may be null if moved-from
89 if (mWindow) {
90 mWindow->PickerClosed();
94 AutoWidgetPickerState(AutoWidgetPickerState const&) = delete;
95 AutoWidgetPickerState(AutoWidgetPickerState&& that) noexcept = default;
97 private:
98 RefPtr<nsWindow> mWindow;
101 ///////////////////////////////////////////////////////////////////////////////
102 // nsIFilePicker
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_), \
132 __PRETTY_FUNCTION__)
134 auto mgr = mozilla::ipc::UtilityProcessManager::GetSingleton();
135 if (!mgr) {
136 MOZ_ASSERT(false);
137 FAIL("ShowRemote: UtilityProcessManager::GetSingleton", E_POINTER);
140 auto wfda = mgr->CreateWinFileDialogActor();
141 if (!wfda) {
142 FAIL("ShowRemote: invocation of CreateWinFileDialogActor", E_POINTER);
145 using mozilla::widget::filedialog::sLogFileDialog;
147 return wfda->Then(
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());
158 return promise->Map(
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");
173 #undef FAIL
176 namespace {
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(
183 [parent, type,
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());
213 } // namespace
215 // fd_async
217 // Wrapper-namespace for the AsyncExecute() and AsyncAll() functions.
218 namespace fd_async {
220 // Implementation details of, specifically, the AsyncExecute() and AsyncAll()
221 // functions.
222 namespace details {
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) {
227 return val;
229 template <typename T>
230 static nsTArray<T> Copy(nsTArray<T> const& arr) {
231 return arr.Clone();
234 // The possible execution strategies of AsyncExecute.
235 enum Strategy {
236 // Always and only open the dialog in-process. This is effectively the
237 // only behavior in older versions of Gecko.
238 LocalOnly,
240 // Always and only open the dialog out-of-process.
241 RemoteOnly,
243 // Open the dialog out-of-process. If that fails in any way, try to recover by
244 // opening it in-process.
245 RemoteWithFallback,
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
254 // improvement.
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.
258 FallbackUnlessCrash,
261 // Decode the relevant preference to determine the desired execution-
262 // strategy.
263 static Strategy GetStrategy() {
264 int32_t const pref =
265 mozilla::StaticPrefs::widget_windows_utility_process_file_picker();
266 switch (pref) {
267 case -1:
268 return LocalOnly;
269 case 3:
270 return FallbackUnlessCrash;
271 case 2:
272 return RemoteOnly;
273 case 1:
274 return RemoteWithFallback;
276 default:
277 // by default, fall back to local only on non-crash failures
278 return FallbackUnlessCrash;
282 template <typename T>
283 class AsyncAllIterator final {
284 public:
285 NS_INLINE_DECL_REFCOUNTING(AsyncAllIterator)
286 AsyncAllIterator(
287 nsTArray<T> aItems,
288 std::function<
289 RefPtr<mozilla::MozPromise<bool, nsresult, true>>(const T& item)>
290 aPredicate,
291 RefPtr<mozilla::MozPromise<bool, nsresult, true>::Private> aPromise)
292 : mItems(std::move(aItems)),
293 mNextIndex(0),
294 mPredicate(std::move(aPredicate)),
295 mPromise(std::move(aPromise)) {}
297 void StartIterating() { ContinueIterating(); }
299 private:
300 ~AsyncAllIterator() = default;
301 void ContinueIterating() {
302 if (mNextIndex >= mItems.Length()) {
303 mPromise->Resolve(true, __func__);
304 return;
306 mPredicate(mItems.ElementAt(mNextIndex))
307 ->Then(
308 mozilla::GetMainThreadSerialEventTarget(), __func__,
309 [self = RefPtr{this}](bool aResult) {
310 if (!aResult) {
311 self->mPromise->Resolve(false, __func__);
312 return;
314 ++self->mNextIndex;
315 self->ContinueIterating();
317 [self = RefPtr{this}](nsresult aError) {
318 self->mPromise->Reject(aError, __func__);
321 nsTArray<T> mItems;
322 uint32_t mNextIndex;
323 std::function<RefPtr<mozilla::MozPromise<bool, nsresult, true>>(
324 const T& item)>
325 mPredicate;
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-
367 // promise).
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?!");
386 return true;
389 switch (GetStrategy()) {
390 case LocalOnly: {
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));
397 return err;
401 case RemoteOnly:
402 useLocalFallback = [](Error const&) { return false; };
403 break;
405 case RemoteWithFallback:
406 useLocalFallback = [](Error const&) { return true; };
407 break;
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
413 // practice.
414 return err.kind != Error::IPCError;
416 break;
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, "
436 "why=%08" PRIX32,
437 Error::KindName(err.kind), err.where.c_str(), err.why));
438 // ... and stop here
439 return PromiseT::CreateAndReject(err, kFunctionName);
442 // otherwise, retry locally
443 auto p0 = std::apply(local, std::move(tuple));
444 return p0->Then(
445 NS_GetCurrentThread(), kFunctionName,
446 [](typename LPromiseT::ResolveOrRejectValue&& val)
447 -> RefPtr<PromiseT> {
448 using V = typename PromiseT::ResolveOrRejectValue;
449 return PromiseT::CreateAndResolveOrReject(
450 val.IsResolve()
451 ? V::MakeResolve(std::move(val).ResolveValue())
452 : V::MakeReject(val.RejectValue()),
453 kFunctionName);
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(
463 nsTArray<T> aItems,
464 std::function<
465 RefPtr<mozilla::MozPromise<bool, nsresult, true>>(const T& item)>
466 aPredicate) {
467 auto promise =
468 mozilla::MakeRefPtr<mozilla::MozPromise<bool, nsresult, true>::Private>(
469 __func__);
470 auto iterator = mozilla::MakeRefPtr<details::AsyncAllIterator<T>>(
471 std::move(aItems), aPredicate, promise);
472 iterator->StartIterating();
473 return promise;
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) {
522 if (val) {
523 self->mUnicodeFile = val.extract();
524 return true;
526 return false;
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;
557 // options
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
572 switch (mMode) {
573 case modeOpen:
574 fos |= FOS_FILEMUSTEXIST;
575 break;
577 case modeOpenMultiple:
578 fos |= FOS_FILEMUSTEXIST | FOS_ALLOWMULTISELECT;
579 break;
581 case modeSave:
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;
588 break;
590 case modeGetFolder:
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));
599 // initial strings
601 // title
602 commands.AppendElement(fd::SetTitle(mTitle));
604 // default filename
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
612 // means.
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));
630 // initial location
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);
655 return promise->Map(
656 mozilla::GetMainThreadSerialEventTarget(), __PRETTY_FUNCTION__,
657 [self = RefPtr(this), mode = mMode, shim = std::move(shim),
658 awps = std::move(awps)](Maybe<Results> res_opt) {
659 if (!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();
669 // single selection
670 if (mode != modeOpenMultiple) {
671 if (!paths.IsEmpty()) {
672 MOZ_ASSERT(paths.Length() == 1);
673 self->mUnicodeFile = paths[0];
674 return true;
676 return false;
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);
687 return true;
691 void nsFilePicker::ClearFiles() {
692 mUnicodeFile.Truncate();
693 mFiles.Clear();
696 RefPtr<nsFilePicker::ContentAnalysisResponse>
697 nsFilePicker::CheckContentAnalysisService() {
698 nsresult rv;
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,
713 __func__);
716 nsCOMPtr<nsIURI> uri =
717 mozilla::contentanalysis::ContentAnalysis::GetURIForBrowsingContext(
718 mBrowsingContext->Canonical());
719 if (!uri) {
720 return nsFilePicker::ContentAnalysisResponse::CreateAndReject(
721 NS_ERROR_FAILURE, __func__);
724 // Entries may be files or folders. Folder contents will be recursively
725 // checked.
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,
732 __func__);
734 filePaths.AppendElement(folderOrFile->NativePath());
735 } else {
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),
743 uri =
744 std::move(uri)](const mozilla::PathString& aItem) {
745 nsCString emptyDigestString;
746 auto* windowGlobal =
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,
754 windowGlobal));
756 auto promise =
757 mozilla::MakeRefPtr<nsFilePicker::ContentAnalysisResponse::Private>(
758 __func__);
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__);
776 return promise;
779 return mozilla::detail::AsyncAll<mozilla::PathString>(std::move(filePaths),
780 processOneItem);
783 ///////////////////////////////////////////////////////////////////////////////
784 // nsIFilePicker impl.
786 nsresult nsFilePicker::Open(nsIFilePickerShownCallback* aCallback) {
787 NS_ENSURE_ARG_POINTER(aCallback);
789 if (MaybeBlockFilePicker(aCallback)) {
790 return NS_OK;
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
810 ClearFiles();
812 auto promise = mMode == modeGetFolder ? ShowFolderPicker(initialDir)
813 : ShowFilePicker(initialDir);
815 promise->Then(
816 mozilla::GetMainThreadSerialEventTarget(), __PRETTY_FUNCTION__,
817 [self = RefPtr(this),
818 callback = RefPtr(aCallback)](bool selectionMade) -> void {
819 if (!selectionMade) {
820 callback->Done(ResultCode::returnCancel);
821 return;
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;
832 nsresult rv =
833 NS_NewLocalFile(self->mUnicodeFile, getter_AddRefs(file));
835 bool flag = false;
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) {
846 if (aAllowContent) {
847 callback->Done(retValue);
848 } else {
849 self->ClearFiles();
850 callback->Done(ResultCode::returnCancel);
853 [callback, self = RefPtr{self}](nsresult aError) {
854 self->ClearFiles();
855 callback->Done(ResultCode::returnCancel);
857 return;
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
865 // file.
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);
885 return NS_OK;
888 NS_IMETHODIMP
889 nsFilePicker::GetFile(nsIFile** aFile) {
890 NS_ENSURE_ARG_POINTER(aFile);
891 *aFile = nullptr;
893 if (mUnicodeFile.IsEmpty()) {
894 return NS_OK;
897 nsCOMPtr<nsIFile> file;
898 nsresult rv = NS_NewLocalFile(mUnicodeFile, getter_AddRefs(file));
899 if (NS_FAILED(rv)) {
900 return rv;
903 file.forget(aFile);
904 return NS_OK;
907 NS_IMETHODIMP
908 nsFilePicker::GetFileURL(nsIURI** aFileURL) {
909 *aFileURL = nullptr;
910 nsCOMPtr<nsIFile> file;
911 nsresult rv = GetFile(getter_AddRefs(file));
912 if (!file) {
913 return rv;
916 return NS_NewFileURI(aFileURL, file);
919 NS_IMETHODIMP
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
926 NS_IMETHODIMP
927 nsBaseWinFilePicker::SetDefaultString(const nsAString& aString) {
928 mDefaultFilePath = aString;
930 // First, make sure the file name is not too long.
931 int32_t nameLength;
932 int32_t nameIndex = mDefaultFilePath.RFind(u"\\");
933 if (nameIndex == kNotFound) {
934 nameIndex = 0;
935 } else {
936 nameIndex++;
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'-');
959 return NS_OK;
962 NS_IMETHODIMP
963 nsBaseWinFilePicker::GetDefaultString(nsAString& aString) {
964 return NS_ERROR_FAILURE;
967 // The default extension to use for files
968 NS_IMETHODIMP
969 nsBaseWinFilePicker::GetDefaultExtension(nsAString& aExtension) {
970 aExtension = mDefaultExtension;
971 return NS_OK;
974 NS_IMETHODIMP
975 nsBaseWinFilePicker::SetDefaultExtension(const nsAString& aExtension) {
976 mDefaultExtension = aExtension;
977 return NS_OK;
980 // Set the filter index
981 NS_IMETHODIMP
982 nsFilePicker::GetFilterIndex(int32_t* aFilterIndex) {
983 // Windows' filter index is 1-based, we use a 0-based system.
984 *aFilterIndex = mSelectedType - 1;
985 return NS_OK;
988 NS_IMETHODIMP
989 nsFilePicker::SetFilterIndex(int32_t aFilterIndex) {
990 // Windows' filter index is 1-based, we use a 0-based system.
991 mSelectedType = aFilterIndex + 1;
992 return NS_OK;
995 void nsFilePicker::InitNative(nsIWidget* aParent, const nsAString& aTitle) {
996 mParentWidget = aParent;
997 mTitle.Assign(aTitle);
1000 NS_IMETHODIMP
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;
1007 } else {
1008 sanitizedFilter.StripWhitespace();
1009 if (sanitizedFilter == u"*"_ns) {
1010 sanitizedFilter = u"*.*"_ns;
1013 mFilterList.AppendElement(
1014 Filter{.title = nsString(aTitle), .filter = std::move(sanitizedFilter)});
1015 return NS_OK;
1018 void nsFilePicker::RememberLastUsedDirectory() {
1019 if (IsPrivacyModeEnabled()) {
1020 // Don't remember the directory if private browsing was in effect
1021 return;
1024 nsCOMPtr<nsIFile> file;
1025 if (NS_FAILED(NS_NewLocalFile(mUnicodeFile, getter_AddRefs(file)))) {
1026 NS_WARNING("RememberLastUsedDirectory failed to init file path.");
1027 return;
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.");
1036 return;
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
1049 ToLowerCase(ext);
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) {
1057 nsAutoString ext;
1058 mDefaultFilePath.Right(ext, mDefaultFilePath.Length() - extIndex);
1059 if (ext.LowerCaseEqualsLiteral(".htm") ||
1060 ext.LowerCaseEqualsLiteral(".html") ||
1061 ext.LowerCaseEqualsLiteral(".shtml")) {
1062 return true;
1065 return false;
1068 auto nsFilePicker::ComputeFallbackSavePath() const -> FallbackResult {
1069 using mozilla::Err;
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
1080 nsresult rv;
1081 nsCOMPtr<nsIExternalHelperAppService> svc =
1082 do_GetService(NS_EXTERNALHELPERAPPSERVICE_CONTRACTID, &rv);
1083 MOZ_TRY(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".)
1096 return false;
1098 if (path[len - extLen - 1] == L'.' &&
1099 StringTail(path, extLen) == extension) {
1100 return true;
1102 return false;
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));
1114 return location;
1117 void nsFilePicker::SendFailureNotification(nsFilePicker::ResultCode aResult,
1118 Error error,
1119 FallbackResult aFallback) const {
1120 if (MOZ_LOG_TEST(filedialog::sLogFileDialog, LogLevel::Info)) {
1121 nsString msg;
1122 if (aFallback.isOk()) {
1123 nsString path;
1124 aFallback.inspect()->GetPath(path);
1125 msg = u"path: "_ns;
1126 msg.Append(path);
1127 } else {
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();
1136 if (!obsSvc) {
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());
1145 } else {
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);