Merge mozilla-central to autoland on a CLOSED TREE
[gecko.git] / widget / windows / nsFilePicker.cpp
blob310c54bb40a08e744c473114e501668186ce0a21
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/Directory.h"
23 #include "mozilla/Logging.h"
24 #include "mozilla/ipc/UtilityProcessManager.h"
25 #include "mozilla/ProfilerLabels.h"
26 #include "mozilla/StaticPrefs_widget.h"
27 #include "mozilla/UniquePtr.h"
28 #include "mozilla/WindowsVersion.h"
29 #include "nsCRT.h"
30 #include "nsEnumeratorUtils.h"
31 #include "nsIContentAnalysis.h"
32 #include "nsNetUtil.h"
33 #include "nsPIDOMWindow.h"
34 #include "nsPrintfCString.h"
35 #include "nsReadableUtils.h"
36 #include "nsString.h"
37 #include "nsToolkit.h"
38 #include "nsWindow.h"
39 #include "WinUtils.h"
41 #include "mozilla/glean/GleanMetrics.h"
43 #include "mozilla/widget/filedialog/WinFileDialogCommands.h"
44 #include "mozilla/widget/filedialog/WinFileDialogParent.h"
46 using mozilla::UniquePtr;
48 using namespace mozilla::widget;
50 UniquePtr<char16_t[], nsFilePicker::FreeDeleter>
51 nsFilePicker::sLastUsedUnicodeDirectory;
53 using mozilla::LogLevel;
55 #define MAX_EXTENSION_LENGTH 10
57 ///////////////////////////////////////////////////////////////////////////////
58 // Helper classes
60 // Manages matching PickerOpen/PickerClosed calls on the parent widget.
61 class AutoWidgetPickerState {
62 static RefPtr<nsWindow> GetWindowForWidget(nsIWidget* aWidget) {
63 MOZ_ASSERT(NS_IsMainThread());
64 if (!aWidget) {
65 return nullptr;
67 HWND hwnd = (HWND)aWidget->GetNativeData(NS_NATIVE_WINDOW);
68 return RefPtr(WinUtils::GetNSWindowPtr(hwnd));
71 public:
72 explicit AutoWidgetPickerState(nsIWidget* aWidget)
73 : mWindow(GetWindowForWidget(aWidget)) {
74 MOZ_ASSERT(mWindow);
75 if (mWindow) mWindow->PickerOpen();
77 ~AutoWidgetPickerState() {
78 // may be null if moved-from
79 if (mWindow) mWindow->PickerClosed();
82 AutoWidgetPickerState(AutoWidgetPickerState const&) = delete;
83 AutoWidgetPickerState(AutoWidgetPickerState&& that) noexcept = default;
85 private:
86 RefPtr<nsWindow> mWindow;
89 ///////////////////////////////////////////////////////////////////////////////
90 // nsIFilePicker
92 nsFilePicker::nsFilePicker() : mSelectedType(1) {}
94 NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker)
96 NS_IMETHODIMP nsFilePicker::Init(
97 mozIDOMWindowProxy* aParent, const nsAString& aTitle,
98 nsIFilePicker::Mode aMode,
99 mozilla::dom::BrowsingContext* aBrowsingContext) {
100 // Don't attempt to open a real file-picker in headless mode.
101 if (gfxPlatform::IsHeadless()) {
102 return nsresult::NS_ERROR_NOT_AVAILABLE;
105 nsCOMPtr<nsPIDOMWindowOuter> window = do_QueryInterface(aParent);
106 nsIDocShell* docShell = window ? window->GetDocShell() : nullptr;
107 mLoadContext = do_QueryInterface(docShell);
109 return nsBaseFilePicker::Init(aParent, aTitle, aMode, aBrowsingContext);
112 namespace mozilla::detail {
113 // Boilerplate for remotely showing a file dialog.
114 template <typename ActionType,
115 typename ReturnType = typename decltype(std::declval<ActionType>()(
116 nullptr))::element_type::ResolveValueType>
117 static auto ShowRemote(ActionType&& action)
118 -> RefPtr<MozPromise<ReturnType, HRESULT, true>> {
119 using RetPromise = MozPromise<ReturnType, HRESULT, true>;
121 constexpr static const auto fail = []() {
122 return RetPromise::CreateAndReject(E_FAIL, __PRETTY_FUNCTION__);
125 auto mgr = mozilla::ipc::UtilityProcessManager::GetSingleton();
126 if (!mgr) {
127 MOZ_ASSERT(false);
128 return fail();
131 auto wfda = mgr->CreateWinFileDialogActor();
132 if (!wfda) {
133 return fail();
136 using mozilla::widget::filedialog::sLogFileDialog;
138 return wfda->Then(
139 mozilla::GetMainThreadSerialEventTarget(),
140 "nsFilePicker ShowRemote acquire",
141 [action = std::forward<ActionType>(action)](
142 filedialog::ProcessProxy const& p) -> RefPtr<RetPromise> {
143 MOZ_LOG(sLogFileDialog, LogLevel::Info,
144 ("nsFilePicker ShowRemote first callback: p = [%p]", p.get()));
146 // false positive: not actually redundant
147 // NOLINTNEXTLINE(readability-redundant-smartptr-get)
148 return action(p.get())->Then(
149 mozilla::GetMainThreadSerialEventTarget(),
150 "nsFilePicker ShowRemote call",
151 [p](ReturnType ret) {
152 return RetPromise::CreateAndResolve(std::move(ret),
153 __PRETTY_FUNCTION__);
155 [](mozilla::ipc::ResponseRejectReason error) {
156 MOZ_LOG(sLogFileDialog, LogLevel::Error,
157 ("IPC call rejected: %zu", size_t(error)));
158 return fail();
161 [](nsresult error) -> RefPtr<RetPromise> {
162 MOZ_LOG(sLogFileDialog, LogLevel::Error,
163 ("could not acquire WinFileDialog: %zu", size_t(error)));
164 return fail();
168 // fd_async
170 // Wrapper-namespace for the AsyncExecute() and AsyncAll() functions.
171 namespace fd_async {
173 // Implementation details of, specifically, the AsyncExecute() and AsyncAll()
174 // functions.
175 namespace details {
176 // Helper for generically copying ordinary types and nsTArray (which lacks a
177 // copy constructor) in the same breath.
178 template <typename T>
179 static T Copy(T const& val) {
180 return val;
182 template <typename T>
183 static nsTArray<T> Copy(nsTArray<T> const& arr) {
184 return arr.Clone();
187 // The possible execution strategies of AsyncExecute.
188 enum Strategy { Local, Remote, RemoteWithFallback };
190 // Decode the relevant preference to determine the desired execution-
191 // strategy.
192 static Strategy GetStrategy() {
193 int32_t const pref =
194 mozilla::StaticPrefs::widget_windows_utility_process_file_picker();
195 switch (pref) {
196 case -1:
197 return Local;
198 case 2:
199 return Remote;
200 case 1:
201 return RemoteWithFallback;
203 default:
204 #ifdef NIGHTLY_BUILD
205 // on Nightly builds, fall back to local on failure
206 return RemoteWithFallback;
207 #else
208 // on release and beta, remain local-only for now
209 return Local;
210 #endif
214 template <typename T>
215 class AsyncAllIterator final {
216 public:
217 NS_INLINE_DECL_REFCOUNTING(AsyncAllIterator)
218 AsyncAllIterator(
219 nsTArray<T> aItems,
220 std::function<
221 RefPtr<mozilla::MozPromise<bool, nsresult, true>>(const T& item)>
222 aPredicate,
223 RefPtr<mozilla::MozPromise<bool, nsresult, true>::Private> aPromise)
224 : mItems(std::move(aItems)),
225 mNextIndex(0),
226 mPredicate(std::move(aPredicate)),
227 mPromise(std::move(aPromise)) {}
229 void StartIterating() { ContinueIterating(); }
231 private:
232 ~AsyncAllIterator() = default;
233 void ContinueIterating() {
234 if (mNextIndex >= mItems.Length()) {
235 mPromise->Resolve(true, __func__);
236 return;
238 mPredicate(mItems.ElementAt(mNextIndex))
239 ->Then(
240 mozilla::GetMainThreadSerialEventTarget(), __func__,
241 [self = RefPtr{this}](bool aResult) {
242 if (!aResult) {
243 self->mPromise->Resolve(false, __func__);
244 return;
246 ++self->mNextIndex;
247 self->ContinueIterating();
249 [self = RefPtr{this}](nsresult aError) {
250 self->mPromise->Reject(aError, __func__);
253 nsTArray<T> mItems;
254 uint32_t mNextIndex;
255 std::function<RefPtr<mozilla::MozPromise<bool, nsresult, true>>(
256 const T& item)>
257 mPredicate;
258 RefPtr<mozilla::MozPromise<bool, nsresult, true>::Private> mPromise;
261 namespace telemetry {
262 static uint32_t Delta(uint64_t tb, uint64_t ta) {
263 // FILETIMEs are 100ns intervals; we reduce that to 1ms.
264 // (`u32::max()` milliseconds is roughly 47.91 days.)
265 return uint32_t((tb - ta) / 10'000);
267 static nsCString HexString(HRESULT val) {
268 return nsPrintfCString("%08lX", val);
271 static void RecordSuccess(uint64_t (&&time)[2]) {
272 auto [t0, t1] = time;
274 namespace glean_fd = mozilla::glean::file_dialog;
275 glean_fd::FallbackExtra extra{
276 .hresultLocal = Nothing(),
277 .hresultRemote = Nothing(),
278 .succeeded = Some(true),
279 .timeLocal = Nothing(),
280 .timeRemote = Some(Delta(t1, t0)),
282 glean_fd::fallback.Record(Some(extra));
285 static void RecordFailure(uint64_t (&&time)[3], HRESULT hrRemote,
286 HRESULT hrLocal) {
287 auto [t0, t1, t2] = time;
290 namespace glean_fd = mozilla::glean::file_dialog;
291 glean_fd::FallbackExtra extra{
292 .hresultLocal = Some(HexString(hrLocal)),
293 .hresultRemote = Some(HexString(hrRemote)),
294 .succeeded = Some(false),
295 .timeLocal = Some(Delta(t2, t1)),
296 .timeRemote = Some(Delta(t1, t0)),
298 glean_fd::fallback.Record(Some(extra));
302 } // namespace telemetry
303 } // namespace details
305 // Invoke either or both of a "do locally" and "do remotely" function with the
306 // provided arguments, depending on the relevant preference-value and whether
307 // or not the remote version fails.
309 // Both functions must be asynchronous, returning a `RefPtr<MozPromise<...>>`.
310 // "Failure" is defined as the promise being rejected.
311 template <typename Fn1, typename Fn2, typename... Args>
312 static auto AsyncExecute(Fn1 local, Fn2 remote, Args const&... args)
313 -> std::invoke_result_t<Fn1, Args...> {
314 using namespace details;
316 static_assert(std::is_same_v<std::invoke_result_t<Fn1, Args...>,
317 std::invoke_result_t<Fn2, Args...>>);
318 using PromiseT = typename std::invoke_result_t<Fn1, Args...>::element_type;
320 constexpr static char kFunctionName[] = "LocalAndOrRemote::AsyncExecute";
322 switch (GetStrategy()) {
323 case Local:
324 return local(args...);
326 case Remote:
327 return remote(args...);
329 case RemoteWithFallback:
330 // more complicated; continue below
331 break;
334 // capture time for telemetry
335 constexpr static const auto GetTime = []() -> uint64_t {
336 FILETIME t;
337 ::GetSystemTimeAsFileTime(&t);
338 return (uint64_t(t.dwHighDateTime) << 32) | t.dwLowDateTime;
340 uint64_t const t0 = GetTime();
342 return remote(args...)->Then(
343 NS_GetCurrentThread(), kFunctionName,
344 [t0](typename PromiseT::ResolveValueType result) -> RefPtr<PromiseT> {
345 // success; stop here
346 auto const t1 = GetTime();
347 // record success
348 telemetry::RecordSuccess({t0, t1});
349 return PromiseT::CreateAndResolve(result, kFunctionName);
351 // initialized lambda pack captures are C++20 (clang 9, gcc 9);
352 // `make_tuple` is just a C++17 workaround
353 [=, tuple = std::make_tuple(Copy(args)...)](
354 typename PromiseT::RejectValueType err) mutable -> RefPtr<PromiseT> {
355 // failure; record time
356 auto const t1 = GetTime();
357 HRESULT const hrRemote = err;
359 // retry locally...
360 auto p0 = std::apply(local, std::move(tuple));
361 // ...then record the telemetry event
362 return p0->Then(
363 NS_GetCurrentThread(), kFunctionName,
364 [t0, t1,
365 hrRemote](typename PromiseT::ResolveOrRejectValue const& val)
366 -> RefPtr<PromiseT> {
367 auto const t2 = GetTime();
368 HRESULT const hrLocal = val.IsReject() ? val.RejectValue() : S_OK;
369 telemetry::RecordFailure({t0, t1, t2}, hrRemote, hrLocal);
371 return PromiseT::CreateAndResolveOrReject(val, kFunctionName);
376 // Asynchronously invokes `aPredicate` on each member of `aItems`.
377 // Yields `false` (and stops immediately) if any invocation of
378 // `predicate` yielded `false`; otherwise yields `true`.
379 template <typename T>
380 static RefPtr<mozilla::MozPromise<bool, nsresult, true>> AsyncAll(
381 nsTArray<T> aItems,
382 std::function<
383 RefPtr<mozilla::MozPromise<bool, nsresult, true>>(const T& item)>
384 aPredicate) {
385 auto promise =
386 mozilla::MakeRefPtr<mozilla::MozPromise<bool, nsresult, true>::Private>(
387 __func__);
388 auto iterator = mozilla::MakeRefPtr<details::AsyncAllIterator<T>>(
389 std::move(aItems), aPredicate, promise);
390 iterator->StartIterating();
391 return promise;
393 } // namespace fd_async
395 using fd_async::AsyncAll;
396 using fd_async::AsyncExecute;
398 } // namespace mozilla::detail
400 /* static */
401 nsFilePicker::FPPromise<filedialog::Results> nsFilePicker::ShowFilePickerRemote(
402 HWND parent, filedialog::FileDialogType type,
403 nsTArray<filedialog::Command> const& commands) {
404 using mozilla::widget::filedialog::sLogFileDialog;
405 return mozilla::detail::ShowRemote(
406 [parent, type,
407 commands = commands.Clone()](filedialog::WinFileDialogParent* p) {
408 MOZ_LOG(sLogFileDialog, LogLevel::Info,
409 ("%s: p = [%p]", __PRETTY_FUNCTION__, p));
410 return p->SendShowFileDialog((uintptr_t)parent, type, commands);
414 /* static */
415 nsFilePicker::FPPromise<nsString> nsFilePicker::ShowFolderPickerRemote(
416 HWND parent, nsTArray<filedialog::Command> const& commands) {
417 using mozilla::widget::filedialog::sLogFileDialog;
418 return mozilla::detail::ShowRemote([parent, commands = commands.Clone()](
419 filedialog::WinFileDialogParent* p) {
420 MOZ_LOG(sLogFileDialog, LogLevel::Info,
421 ("%s: p = [%p]", __PRETTY_FUNCTION__, p));
422 return p->SendShowFolderDialog((uintptr_t)parent, commands);
426 /* static */
427 nsFilePicker::FPPromise<filedialog::Results> nsFilePicker::ShowFilePickerLocal(
428 HWND parent, filedialog::FileDialogType type,
429 nsTArray<filedialog::Command> const& commands) {
430 return filedialog::SpawnFilePicker(parent, type, commands.Clone());
433 /* static */
434 nsFilePicker::FPPromise<nsString> nsFilePicker::ShowFolderPickerLocal(
435 HWND parent, nsTArray<filedialog::Command> const& commands) {
436 return filedialog::SpawnFolderPicker(parent, commands.Clone());
440 * Folder picker invocation
444 * Show a folder picker.
446 * @param aInitialDir The initial directory. The last-used directory will be
447 * used if left blank.
448 * @return A promise which:
449 * - resolves to true if a file was selected successfully (in which
450 * case mUnicodeFile will be updated);
451 * - resolves to false if the dialog was cancelled by the user;
452 * - is rejected with the associated HRESULT if some error occurred.
454 RefPtr<mozilla::MozPromise<bool, HRESULT, true>> nsFilePicker::ShowFolderPicker(
455 const nsString& aInitialDir) {
456 using Promise = mozilla::MozPromise<bool, HRESULT, true>;
457 constexpr static auto Ok = [](bool val) {
458 return Promise::CreateAndResolve(val, "nsFilePicker::ShowFolderPicker");
460 constexpr static auto NotOk = [](HRESULT val = E_FAIL) {
461 return Promise::CreateAndReject(val, "nsFilePicker::ShowFolderPicker");
464 namespace fd = ::mozilla::widget::filedialog;
465 nsTArray<fd::Command> commands = {
466 fd::SetOptions(FOS_PICKFOLDERS),
467 fd::SetTitle(mTitle),
470 if (!mOkButtonLabel.IsEmpty()) {
471 commands.AppendElement(fd::SetOkButtonLabel(mOkButtonLabel));
474 if (!aInitialDir.IsEmpty()) {
475 commands.AppendElement(fd::SetFolder(aInitialDir));
478 ScopedRtlShimWindow shim(mParentWidget.get());
479 AutoWidgetPickerState awps(mParentWidget);
481 return mozilla::detail::AsyncExecute(&ShowFolderPickerLocal,
482 &ShowFolderPickerRemote, shim.get(),
483 commands)
484 ->Then(
485 NS_GetCurrentThread(), __PRETTY_FUNCTION__,
486 [self = RefPtr(this), shim = std::move(shim),
487 awps = std::move(awps)](Maybe<nsString> val) {
488 if (val) {
489 self->mUnicodeFile = val.extract();
490 return Ok(true);
492 return Ok(false);
494 [](HRESULT err) {
495 NS_WARNING("ShowFolderPicker failed");
496 return NotOk(err);
501 * File open and save picker invocation
505 * Show a file picker.
507 * @param aInitialDir The initial directory. The last-used directory will be
508 * used if left blank.
509 * @return A promise which:
510 * - resolves to true if one or more files were selected successfully
511 * (in which case mUnicodeFile and/or mFiles will be updated);
512 * - resolves to false if the dialog was cancelled by the user;
513 * - is rejected with the associated HRESULT if some error occurred.
515 RefPtr<mozilla::MozPromise<bool, HRESULT, true>> nsFilePicker::ShowFilePicker(
516 const nsString& aInitialDir) {
517 AUTO_PROFILER_LABEL("nsFilePicker::ShowFilePicker", OTHER);
519 using Promise = mozilla::MozPromise<bool, HRESULT, true>;
520 constexpr static auto Ok = [](bool val) {
521 return Promise::CreateAndResolve(val, "nsFilePicker::ShowFilePicker");
523 constexpr static auto NotOk = [](HRESULT val = E_FAIL) {
524 return Promise::CreateAndReject(val, "nsFilePicker::ShowFilePicker");
527 namespace fd = ::mozilla::widget::filedialog;
528 nsTArray<fd::Command> commands;
529 // options
531 FILEOPENDIALOGOPTIONS fos = 0;
532 fos |= FOS_SHAREAWARE | FOS_OVERWRITEPROMPT | FOS_FORCEFILESYSTEM;
534 // Handle add to recent docs settings
535 if (IsPrivacyModeEnabled() || !mAddToRecentDocs) {
536 fos |= FOS_DONTADDTORECENT;
539 // mode specific
540 switch (mMode) {
541 case modeOpen:
542 fos |= FOS_FILEMUSTEXIST;
543 break;
545 case modeOpenMultiple:
546 fos |= FOS_FILEMUSTEXIST | FOS_ALLOWMULTISELECT;
547 break;
549 case modeSave:
550 fos |= FOS_NOREADONLYRETURN;
551 // Don't follow shortcuts when saving a shortcut, this can be used
552 // to trick users (bug 271732)
553 if (IsDefaultPathLink()) fos |= FOS_NODEREFERENCELINKS;
554 break;
556 case modeGetFolder:
557 MOZ_ASSERT(false, "file-picker opened in directory-picker mode");
558 return NotOk(E_FAIL);
561 commands.AppendElement(fd::SetOptions(fos));
564 // initial strings
566 // title
567 commands.AppendElement(fd::SetTitle(mTitle));
569 // default filename
570 if (!mDefaultFilename.IsEmpty()) {
571 // Prevent the shell from expanding environment variables by removing
572 // the % characters that are used to delimit them.
573 nsAutoString sanitizedFilename(mDefaultFilename);
574 sanitizedFilename.ReplaceChar('%', '_');
576 commands.AppendElement(fd::SetFileName(sanitizedFilename));
579 // default extension to append to new files
580 if (!mDefaultExtension.IsEmpty()) {
581 // We don't want environment variables expanded in the extension either.
582 nsAutoString sanitizedExtension(mDefaultExtension);
583 sanitizedExtension.ReplaceChar('%', '_');
585 commands.AppendElement(fd::SetDefaultExtension(sanitizedExtension));
586 } else if (IsDefaultPathHtml()) {
587 commands.AppendElement(fd::SetDefaultExtension(u"html"_ns));
590 // initial location
591 if (!aInitialDir.IsEmpty()) {
592 commands.AppendElement(fd::SetFolder(aInitialDir));
595 // filter types and the default index
596 if (!mFilterList.IsEmpty()) {
597 nsTArray<fd::ComDlgFilterSpec> fileTypes;
598 for (auto const& filter : mFilterList) {
599 fileTypes.EmplaceBack(filter.title, filter.filter);
601 commands.AppendElement(fd::SetFileTypes(std::move(fileTypes)));
602 commands.AppendElement(fd::SetFileTypeIndex(mSelectedType));
605 ScopedRtlShimWindow shim(mParentWidget.get());
606 AutoWidgetPickerState awps(mParentWidget);
608 mozilla::BackgroundHangMonitor().NotifyWait();
609 auto type = mMode == modeSave ? FileDialogType::Save : FileDialogType::Open;
611 auto promise = mozilla::detail::AsyncExecute(
612 &ShowFilePickerLocal, &ShowFilePickerRemote, shim.get(), type, commands);
614 return promise->Then(
615 mozilla::GetMainThreadSerialEventTarget(), __PRETTY_FUNCTION__,
616 [self = RefPtr(this), mode = mMode, shim = std::move(shim),
617 awps = std::move(awps)](Maybe<Results> res_opt) {
618 if (!res_opt) {
619 return Ok(false);
621 auto result = res_opt.extract();
623 // Remember what filter type the user selected
624 self->mSelectedType = int32_t(result.selectedFileTypeIndex());
626 auto const& paths = result.paths();
628 // single selection
629 if (mode != modeOpenMultiple) {
630 if (!paths.IsEmpty()) {
631 MOZ_ASSERT(paths.Length() == 1);
632 self->mUnicodeFile = paths[0];
633 return Ok(true);
635 return Ok(false);
638 // multiple selection
639 for (auto const& str : paths) {
640 nsCOMPtr<nsIFile> file;
641 if (NS_SUCCEEDED(NS_NewLocalFile(str, false, getter_AddRefs(file)))) {
642 self->mFiles.AppendObject(file);
646 return Ok(true);
648 [](HRESULT err) {
649 NS_WARNING("ShowFilePicker failed");
650 return NotOk(err);
654 void nsFilePicker::ClearFiles() {
655 mUnicodeFile.Truncate();
656 mFiles.Clear();
659 namespace {
660 class GetFilesInDirectoryCallback final
661 : public mozilla::dom::GetFilesCallback {
662 public:
663 explicit GetFilesInDirectoryCallback(
664 RefPtr<mozilla::MozPromise<nsTArray<mozilla::PathString>, nsresult,
665 true>::Private>
666 aPromise)
667 : mPromise(std::move(aPromise)) {}
668 void Callback(
669 nsresult aStatus,
670 const FallibleTArray<RefPtr<mozilla::dom::BlobImpl>>& aBlobImpls) {
671 if (NS_FAILED(aStatus)) {
672 mPromise->Reject(aStatus, __func__);
673 return;
675 nsTArray<mozilla::PathString> filePaths;
676 filePaths.SetCapacity(aBlobImpls.Length());
677 for (const auto& blob : aBlobImpls) {
678 if (blob->IsFile()) {
679 mozilla::PathString pathString;
680 mozilla::ErrorResult error;
681 blob->GetMozFullPathInternal(pathString, error);
682 nsresult rv = error.StealNSResult();
683 if (NS_WARN_IF(NS_FAILED(rv))) {
684 mPromise->Reject(rv, __func__);
685 return;
687 filePaths.AppendElement(pathString);
688 } else {
689 NS_WARNING("Got a non-file blob, can't do content analysis on it");
692 mPromise->Resolve(std::move(filePaths), __func__);
695 private:
696 RefPtr<mozilla::MozPromise<nsTArray<mozilla::PathString>, nsresult,
697 true>::Private>
698 mPromise;
700 } // anonymous namespace
702 RefPtr<nsFilePicker::ContentAnalysisResponse>
703 nsFilePicker::CheckContentAnalysisService() {
704 nsresult rv;
705 nsCOMPtr<nsIContentAnalysis> contentAnalysis =
706 mozilla::components::nsIContentAnalysis::Service(&rv);
707 if (NS_WARN_IF(NS_FAILED(rv))) {
708 return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv, __func__);
710 bool contentAnalysisIsActive = false;
711 rv = contentAnalysis->GetIsActive(&contentAnalysisIsActive);
712 if (NS_WARN_IF(NS_FAILED(rv))) {
713 return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv, __func__);
715 if (!contentAnalysisIsActive) {
716 return nsFilePicker::ContentAnalysisResponse::CreateAndResolve(true,
717 __func__);
720 nsCOMPtr<nsIURI> uri = mBrowsingContext->Canonical()->GetCurrentURI();
722 auto processOneItem = [self = RefPtr{this},
723 contentAnalysis = std::move(contentAnalysis),
724 uri =
725 std::move(uri)](const mozilla::PathString& aItem) {
726 nsCString emptyDigestString;
727 auto* windowGlobal =
728 self->mBrowsingContext->Canonical()->GetCurrentWindowGlobal();
729 nsCOMPtr<nsIContentAnalysisRequest> contentAnalysisRequest(
730 new mozilla::contentanalysis::ContentAnalysisRequest(
731 nsIContentAnalysisRequest::AnalysisType::eFileAttached, aItem, true,
732 std::move(emptyDigestString), uri,
733 nsIContentAnalysisRequest::OperationType::eCustomDisplayString,
734 windowGlobal));
736 auto promise =
737 mozilla::MakeRefPtr<nsFilePicker::ContentAnalysisResponse::Private>(
738 __func__);
739 auto contentAnalysisCallback =
740 mozilla::MakeRefPtr<mozilla::contentanalysis::ContentAnalysisCallback>(
741 [promise](nsIContentAnalysisResponse* aResponse) {
742 promise->Resolve(aResponse->GetShouldAllowContent(), __func__);
744 [promise](nsresult aError) { promise->Reject(aError, __func__); });
746 nsresult rv = contentAnalysis->AnalyzeContentRequestCallback(
747 contentAnalysisRequest, /* aAutoAcknowledge */ true,
748 contentAnalysisCallback);
749 if (NS_WARN_IF(NS_FAILED(rv))) {
750 promise->Reject(rv, __func__);
752 return promise;
755 // Since getting the files to analyze might be asynchronous, use a MozPromise
756 // to unify the logic below.
757 auto getFilesToAnalyzePromise = mozilla::MakeRefPtr<mozilla::MozPromise<
758 nsTArray<mozilla::PathString>, nsresult, true>::Private>(__func__);
759 if (mMode == modeGetFolder) {
760 nsCOMPtr<nsISupports> tmp;
761 nsresult rv = GetDomFileOrDirectory(getter_AddRefs(tmp));
762 if (NS_WARN_IF(NS_FAILED(rv))) {
763 getFilesToAnalyzePromise->Reject(rv, __func__);
764 return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv,
765 __func__);
767 auto* directory = static_cast<mozilla::dom::Directory*>(tmp.get());
768 mozilla::dom::OwningFileOrDirectory owningDirectory;
769 owningDirectory.SetAsDirectory() = directory;
770 nsTArray<mozilla::dom::OwningFileOrDirectory> directoryArray{
771 std::move(owningDirectory)};
773 mozilla::ErrorResult error;
774 RefPtr<mozilla::dom::GetFilesHelper> helper =
775 mozilla::dom::GetFilesHelper::Create(directoryArray, true, error);
776 rv = error.StealNSResult();
777 if (NS_WARN_IF(NS_FAILED(rv))) {
778 getFilesToAnalyzePromise->Reject(rv, __func__);
779 return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv,
780 __func__);
782 auto getFilesCallback = mozilla::MakeRefPtr<GetFilesInDirectoryCallback>(
783 getFilesToAnalyzePromise);
784 helper->AddCallback(getFilesCallback);
785 } else {
786 nsCOMArray<nsIFile> files;
787 if (!mUnicodeFile.IsEmpty()) {
788 nsCOMPtr<nsIFile> file;
789 rv = GetFile(getter_AddRefs(file));
790 if (NS_WARN_IF(NS_FAILED(rv))) {
791 getFilesToAnalyzePromise->Reject(rv, __func__);
792 return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv,
793 __func__);
795 files.AppendElement(file);
796 } else {
797 files.AppendElements(mFiles);
799 nsTArray<mozilla::PathString> paths(files.Length());
800 std::transform(files.begin(), files.end(), MakeBackInserter(paths),
801 [](auto* entry) { return entry->NativePath(); });
802 getFilesToAnalyzePromise->Resolve(std::move(paths), __func__);
805 return getFilesToAnalyzePromise->Then(
806 mozilla::GetMainThreadSerialEventTarget(), __func__,
807 [processOneItem](nsTArray<mozilla::PathString> aPaths) mutable {
808 return mozilla::detail::AsyncAll<mozilla::PathString>(std::move(aPaths),
809 processOneItem);
811 [](nsresult aError) {
812 return nsFilePicker::ContentAnalysisResponse::CreateAndReject(aError,
813 __func__);
817 ///////////////////////////////////////////////////////////////////////////////
818 // nsIFilePicker impl.
820 nsresult nsFilePicker::Open(nsIFilePickerShownCallback* aCallback) {
821 NS_ENSURE_ARG_POINTER(aCallback);
823 if (MaybeBlockFilePicker(aCallback)) {
824 return NS_OK;
827 // Don't attempt to open a real file-picker in headless mode.
828 if (gfxPlatform::IsHeadless()) {
829 return nsresult::NS_ERROR_NOT_AVAILABLE;
832 nsAutoString initialDir;
833 if (mDisplayDirectory) mDisplayDirectory->GetPath(initialDir);
835 // If no display directory, re-use the last one.
836 if (initialDir.IsEmpty()) {
837 // Allocate copy of last used dir.
838 initialDir = sLastUsedUnicodeDirectory.get();
841 // Clear previous file selections
842 ClearFiles();
844 auto promise = mMode == modeGetFolder ? ShowFolderPicker(initialDir)
845 : ShowFilePicker(initialDir);
847 auto p2 = promise->Then(
848 mozilla::GetMainThreadSerialEventTarget(), __PRETTY_FUNCTION__,
849 [self = RefPtr(this),
850 callback = RefPtr(aCallback)](bool selectionMade) -> void {
851 if (!selectionMade) {
852 callback->Done(ResultCode::returnCancel);
853 return;
856 self->RememberLastUsedDirectory();
858 nsIFilePicker::ResultCode retValue = ResultCode::returnOK;
860 if (self->mMode == modeSave) {
861 // Windows does not return resultReplace; we must check whether the
862 // file already exists.
863 nsCOMPtr<nsIFile> file;
864 nsresult rv =
865 NS_NewLocalFile(self->mUnicodeFile, false, getter_AddRefs(file));
867 bool flag = false;
868 if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(file->Exists(&flag)) && flag) {
869 retValue = ResultCode::returnReplace;
873 if (self->mBrowsingContext && !self->mBrowsingContext->IsChrome() &&
874 self->mMode != modeSave && retValue != ResultCode::returnCancel) {
875 self->CheckContentAnalysisService()->Then(
876 mozilla::GetMainThreadSerialEventTarget(), __func__,
877 [retValue, callback, self = RefPtr{self}](bool aAllowContent) {
878 if (aAllowContent) {
879 callback->Done(retValue);
880 } else {
881 self->ClearFiles();
882 callback->Done(ResultCode::returnCancel);
885 [callback, self = RefPtr{self}](nsresult aError) {
886 self->ClearFiles();
887 callback->Done(ResultCode::returnCancel);
889 return;
892 callback->Done(retValue);
894 [callback = RefPtr(aCallback)](HRESULT err) {
895 using mozilla::widget::filedialog::sLogFileDialog;
896 MOZ_LOG(sLogFileDialog, LogLevel::Error,
897 ("nsFilePicker: Show failed with hr=0x%08lX", err));
898 callback->Done(ResultCode::returnCancel);
901 return NS_OK;
904 nsresult nsFilePicker::Show(nsIFilePicker::ResultCode* aReturnVal) {
905 return NS_ERROR_NOT_IMPLEMENTED;
908 NS_IMETHODIMP
909 nsFilePicker::GetFile(nsIFile** aFile) {
910 NS_ENSURE_ARG_POINTER(aFile);
911 *aFile = nullptr;
913 if (mUnicodeFile.IsEmpty()) return NS_OK;
915 nsCOMPtr<nsIFile> file;
916 nsresult rv = NS_NewLocalFile(mUnicodeFile, false, getter_AddRefs(file));
917 if (NS_FAILED(rv)) {
918 return rv;
921 file.forget(aFile);
922 return NS_OK;
925 NS_IMETHODIMP
926 nsFilePicker::GetFileURL(nsIURI** aFileURL) {
927 *aFileURL = nullptr;
928 nsCOMPtr<nsIFile> file;
929 nsresult rv = GetFile(getter_AddRefs(file));
930 if (!file) return rv;
932 return NS_NewFileURI(aFileURL, file);
935 NS_IMETHODIMP
936 nsFilePicker::GetFiles(nsISimpleEnumerator** aFiles) {
937 NS_ENSURE_ARG_POINTER(aFiles);
938 return NS_NewArrayEnumerator(aFiles, mFiles, NS_GET_IID(nsIFile));
941 // Get the file + path
942 NS_IMETHODIMP
943 nsBaseWinFilePicker::SetDefaultString(const nsAString& aString) {
944 mDefaultFilePath = aString;
946 // First, make sure the file name is not too long.
947 int32_t nameLength;
948 int32_t nameIndex = mDefaultFilePath.RFind(u"\\");
949 if (nameIndex == kNotFound)
950 nameIndex = 0;
951 else
952 nameIndex++;
953 nameLength = mDefaultFilePath.Length() - nameIndex;
954 mDefaultFilename.Assign(Substring(mDefaultFilePath, nameIndex));
956 if (nameLength > MAX_PATH) {
957 int32_t extIndex = mDefaultFilePath.RFind(u".");
958 if (extIndex == kNotFound) extIndex = mDefaultFilePath.Length();
960 // Let's try to shave the needed characters from the name part.
961 int32_t charsToRemove = nameLength - MAX_PATH;
962 if (extIndex - nameIndex >= charsToRemove) {
963 mDefaultFilePath.Cut(extIndex - charsToRemove, charsToRemove);
967 // Then, we need to replace illegal characters. At this stage, we cannot
968 // replace the backslash as the string might represent a file path.
969 mDefaultFilePath.ReplaceChar(u"" FILE_ILLEGAL_CHARACTERS, u'-');
970 mDefaultFilename.ReplaceChar(u"" FILE_ILLEGAL_CHARACTERS, u'-');
972 return NS_OK;
975 NS_IMETHODIMP
976 nsBaseWinFilePicker::GetDefaultString(nsAString& aString) {
977 return NS_ERROR_FAILURE;
980 // The default extension to use for files
981 NS_IMETHODIMP
982 nsBaseWinFilePicker::GetDefaultExtension(nsAString& aExtension) {
983 aExtension = mDefaultExtension;
984 return NS_OK;
987 NS_IMETHODIMP
988 nsBaseWinFilePicker::SetDefaultExtension(const nsAString& aExtension) {
989 mDefaultExtension = aExtension;
990 return NS_OK;
993 // Set the filter index
994 NS_IMETHODIMP
995 nsFilePicker::GetFilterIndex(int32_t* aFilterIndex) {
996 // Windows' filter index is 1-based, we use a 0-based system.
997 *aFilterIndex = mSelectedType - 1;
998 return NS_OK;
1001 NS_IMETHODIMP
1002 nsFilePicker::SetFilterIndex(int32_t aFilterIndex) {
1003 // Windows' filter index is 1-based, we use a 0-based system.
1004 mSelectedType = aFilterIndex + 1;
1005 return NS_OK;
1008 void nsFilePicker::InitNative(nsIWidget* aParent, const nsAString& aTitle) {
1009 mParentWidget = aParent;
1010 mTitle.Assign(aTitle);
1013 NS_IMETHODIMP
1014 nsFilePicker::AppendFilter(const nsAString& aTitle, const nsAString& aFilter) {
1015 nsString sanitizedFilter(aFilter);
1016 sanitizedFilter.ReplaceChar('%', '_');
1018 if (sanitizedFilter == u"..apps"_ns) {
1019 sanitizedFilter = u"*.exe;*.com"_ns;
1020 } else {
1021 sanitizedFilter.StripWhitespace();
1022 if (sanitizedFilter == u"*"_ns) {
1023 sanitizedFilter = u"*.*"_ns;
1026 mFilterList.AppendElement(
1027 Filter{.title = nsString(aTitle), .filter = std::move(sanitizedFilter)});
1028 return NS_OK;
1031 void nsFilePicker::RememberLastUsedDirectory() {
1032 if (IsPrivacyModeEnabled()) {
1033 // Don't remember the directory if private browsing was in effect
1034 return;
1037 nsCOMPtr<nsIFile> file;
1038 if (NS_FAILED(NS_NewLocalFile(mUnicodeFile, false, getter_AddRefs(file)))) {
1039 NS_WARNING("RememberLastUsedDirectory failed to init file path.");
1040 return;
1043 nsCOMPtr<nsIFile> dir;
1044 nsAutoString newDir;
1045 if (NS_FAILED(file->GetParent(getter_AddRefs(dir))) ||
1046 !(mDisplayDirectory = dir) ||
1047 NS_FAILED(mDisplayDirectory->GetPath(newDir)) || newDir.IsEmpty()) {
1048 NS_WARNING("RememberLastUsedDirectory failed to get parent directory.");
1049 return;
1052 sLastUsedUnicodeDirectory.reset(ToNewUnicode(newDir));
1055 bool nsFilePicker::IsPrivacyModeEnabled() {
1056 return mLoadContext && mLoadContext->UsePrivateBrowsing();
1059 bool nsFilePicker::IsDefaultPathLink() {
1060 NS_ConvertUTF16toUTF8 ext(mDefaultFilePath);
1061 ext.Trim(" .", false, true); // watch out for trailing space and dots
1062 ToLowerCase(ext);
1063 return StringEndsWith(ext, ".lnk"_ns) || StringEndsWith(ext, ".pif"_ns) ||
1064 StringEndsWith(ext, ".url"_ns);
1067 bool nsFilePicker::IsDefaultPathHtml() {
1068 int32_t extIndex = mDefaultFilePath.RFind(u".");
1069 if (extIndex >= 0) {
1070 nsAutoString ext;
1071 mDefaultFilePath.Right(ext, mDefaultFilePath.Length() - extIndex);
1072 if (ext.LowerCaseEqualsLiteral(".htm") ||
1073 ext.LowerCaseEqualsLiteral(".html") ||
1074 ext.LowerCaseEqualsLiteral(".shtml"))
1075 return true;
1077 return false;