diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /widget/windows/nsFilePicker.cpp | |
parent | Initial commit. (diff) | |
download | firefox-e51783d008170d9ab27d25da98ca3a38b0a41b67.tar.xz firefox-e51783d008170d9ab27d25da98ca3a38b0a41b67.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'widget/windows/nsFilePicker.cpp')
-rw-r--r-- | widget/windows/nsFilePicker.cpp | 1078 |
1 files changed, 1078 insertions, 0 deletions
diff --git a/widget/windows/nsFilePicker.cpp b/widget/windows/nsFilePicker.cpp new file mode 100644 index 0000000000..310c54bb40 --- /dev/null +++ b/widget/windows/nsFilePicker.cpp @@ -0,0 +1,1078 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsFilePicker.h" + +#include <cderr.h> +#include <shlobj.h> +#include <shlwapi.h> +#include <sysinfoapi.h> +#include <winerror.h> +#include <winuser.h> +#include <utility> + +#include "ContentAnalysis.h" +#include "mozilla/Assertions.h" +#include "mozilla/BackgroundHangMonitor.h" +#include "mozilla/Components.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/Directory.h" +#include "mozilla/Logging.h" +#include "mozilla/ipc/UtilityProcessManager.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/StaticPrefs_widget.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WindowsVersion.h" +#include "nsCRT.h" +#include "nsEnumeratorUtils.h" +#include "nsIContentAnalysis.h" +#include "nsNetUtil.h" +#include "nsPIDOMWindow.h" +#include "nsPrintfCString.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include "nsToolkit.h" +#include "nsWindow.h" +#include "WinUtils.h" + +#include "mozilla/glean/GleanMetrics.h" + +#include "mozilla/widget/filedialog/WinFileDialogCommands.h" +#include "mozilla/widget/filedialog/WinFileDialogParent.h" + +using mozilla::UniquePtr; + +using namespace mozilla::widget; + +UniquePtr<char16_t[], nsFilePicker::FreeDeleter> + nsFilePicker::sLastUsedUnicodeDirectory; + +using mozilla::LogLevel; + +#define MAX_EXTENSION_LENGTH 10 + +/////////////////////////////////////////////////////////////////////////////// +// Helper classes + +// Manages matching PickerOpen/PickerClosed calls on the parent widget. +class AutoWidgetPickerState { + static RefPtr<nsWindow> GetWindowForWidget(nsIWidget* aWidget) { + MOZ_ASSERT(NS_IsMainThread()); + if (!aWidget) { + return nullptr; + } + HWND hwnd = (HWND)aWidget->GetNativeData(NS_NATIVE_WINDOW); + return RefPtr(WinUtils::GetNSWindowPtr(hwnd)); + } + + public: + explicit AutoWidgetPickerState(nsIWidget* aWidget) + : mWindow(GetWindowForWidget(aWidget)) { + MOZ_ASSERT(mWindow); + if (mWindow) mWindow->PickerOpen(); + } + ~AutoWidgetPickerState() { + // may be null if moved-from + if (mWindow) mWindow->PickerClosed(); + } + + AutoWidgetPickerState(AutoWidgetPickerState const&) = delete; + AutoWidgetPickerState(AutoWidgetPickerState&& that) noexcept = default; + + private: + RefPtr<nsWindow> mWindow; +}; + +/////////////////////////////////////////////////////////////////////////////// +// nsIFilePicker + +nsFilePicker::nsFilePicker() : mSelectedType(1) {} + +NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker) + +NS_IMETHODIMP nsFilePicker::Init( + mozIDOMWindowProxy* aParent, const nsAString& aTitle, + nsIFilePicker::Mode aMode, + mozilla::dom::BrowsingContext* aBrowsingContext) { + // Don't attempt to open a real file-picker in headless mode. + if (gfxPlatform::IsHeadless()) { + return nsresult::NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr<nsPIDOMWindowOuter> window = do_QueryInterface(aParent); + nsIDocShell* docShell = window ? window->GetDocShell() : nullptr; + mLoadContext = do_QueryInterface(docShell); + + return nsBaseFilePicker::Init(aParent, aTitle, aMode, aBrowsingContext); +} + +namespace mozilla::detail { +// Boilerplate for remotely showing a file dialog. +template <typename ActionType, + typename ReturnType = typename decltype(std::declval<ActionType>()( + nullptr))::element_type::ResolveValueType> +static auto ShowRemote(ActionType&& action) + -> RefPtr<MozPromise<ReturnType, HRESULT, true>> { + using RetPromise = MozPromise<ReturnType, HRESULT, true>; + + constexpr static const auto fail = []() { + return RetPromise::CreateAndReject(E_FAIL, __PRETTY_FUNCTION__); + }; + + auto mgr = mozilla::ipc::UtilityProcessManager::GetSingleton(); + if (!mgr) { + MOZ_ASSERT(false); + return fail(); + } + + auto wfda = mgr->CreateWinFileDialogActor(); + if (!wfda) { + return fail(); + } + + using mozilla::widget::filedialog::sLogFileDialog; + + return wfda->Then( + mozilla::GetMainThreadSerialEventTarget(), + "nsFilePicker ShowRemote acquire", + [action = std::forward<ActionType>(action)]( + filedialog::ProcessProxy const& p) -> RefPtr<RetPromise> { + MOZ_LOG(sLogFileDialog, LogLevel::Info, + ("nsFilePicker ShowRemote first callback: p = [%p]", p.get())); + + // false positive: not actually redundant + // NOLINTNEXTLINE(readability-redundant-smartptr-get) + return action(p.get())->Then( + mozilla::GetMainThreadSerialEventTarget(), + "nsFilePicker ShowRemote call", + [p](ReturnType ret) { + return RetPromise::CreateAndResolve(std::move(ret), + __PRETTY_FUNCTION__); + }, + [](mozilla::ipc::ResponseRejectReason error) { + MOZ_LOG(sLogFileDialog, LogLevel::Error, + ("IPC call rejected: %zu", size_t(error))); + return fail(); + }); + }, + [](nsresult error) -> RefPtr<RetPromise> { + MOZ_LOG(sLogFileDialog, LogLevel::Error, + ("could not acquire WinFileDialog: %zu", size_t(error))); + return fail(); + }); +} + +// fd_async +// +// Wrapper-namespace for the AsyncExecute() and AsyncAll() functions. +namespace fd_async { + +// Implementation details of, specifically, the AsyncExecute() and AsyncAll() +// functions. +namespace details { +// Helper for generically copying ordinary types and nsTArray (which lacks a +// copy constructor) in the same breath. +template <typename T> +static T Copy(T const& val) { + return val; +} +template <typename T> +static nsTArray<T> Copy(nsTArray<T> const& arr) { + return arr.Clone(); +} + +// The possible execution strategies of AsyncExecute. +enum Strategy { Local, Remote, RemoteWithFallback }; + +// Decode the relevant preference to determine the desired execution- +// strategy. +static Strategy GetStrategy() { + int32_t const pref = + mozilla::StaticPrefs::widget_windows_utility_process_file_picker(); + switch (pref) { + case -1: + return Local; + case 2: + return Remote; + case 1: + return RemoteWithFallback; + + default: +#ifdef NIGHTLY_BUILD + // on Nightly builds, fall back to local on failure + return RemoteWithFallback; +#else + // on release and beta, remain local-only for now + return Local; +#endif + } +}; + +template <typename T> +class AsyncAllIterator final { + public: + NS_INLINE_DECL_REFCOUNTING(AsyncAllIterator) + AsyncAllIterator( + nsTArray<T> aItems, + std::function< + RefPtr<mozilla::MozPromise<bool, nsresult, true>>(const T& item)> + aPredicate, + RefPtr<mozilla::MozPromise<bool, nsresult, true>::Private> aPromise) + : mItems(std::move(aItems)), + mNextIndex(0), + mPredicate(std::move(aPredicate)), + mPromise(std::move(aPromise)) {} + + void StartIterating() { ContinueIterating(); } + + private: + ~AsyncAllIterator() = default; + void ContinueIterating() { + if (mNextIndex >= mItems.Length()) { + mPromise->Resolve(true, __func__); + return; + } + mPredicate(mItems.ElementAt(mNextIndex)) + ->Then( + mozilla::GetMainThreadSerialEventTarget(), __func__, + [self = RefPtr{this}](bool aResult) { + if (!aResult) { + self->mPromise->Resolve(false, __func__); + return; + } + ++self->mNextIndex; + self->ContinueIterating(); + }, + [self = RefPtr{this}](nsresult aError) { + self->mPromise->Reject(aError, __func__); + }); + } + nsTArray<T> mItems; + uint32_t mNextIndex; + std::function<RefPtr<mozilla::MozPromise<bool, nsresult, true>>( + const T& item)> + mPredicate; + RefPtr<mozilla::MozPromise<bool, nsresult, true>::Private> mPromise; +}; + +namespace telemetry { +static uint32_t Delta(uint64_t tb, uint64_t ta) { + // FILETIMEs are 100ns intervals; we reduce that to 1ms. + // (`u32::max()` milliseconds is roughly 47.91 days.) + return uint32_t((tb - ta) / 10'000); +}; +static nsCString HexString(HRESULT val) { + return nsPrintfCString("%08lX", val); +}; + +static void RecordSuccess(uint64_t (&&time)[2]) { + auto [t0, t1] = time; + + namespace glean_fd = mozilla::glean::file_dialog; + glean_fd::FallbackExtra extra{ + .hresultLocal = Nothing(), + .hresultRemote = Nothing(), + .succeeded = Some(true), + .timeLocal = Nothing(), + .timeRemote = Some(Delta(t1, t0)), + }; + glean_fd::fallback.Record(Some(extra)); +} + +static void RecordFailure(uint64_t (&&time)[3], HRESULT hrRemote, + HRESULT hrLocal) { + auto [t0, t1, t2] = time; + + { + namespace glean_fd = mozilla::glean::file_dialog; + glean_fd::FallbackExtra extra{ + .hresultLocal = Some(HexString(hrLocal)), + .hresultRemote = Some(HexString(hrRemote)), + .succeeded = Some(false), + .timeLocal = Some(Delta(t2, t1)), + .timeRemote = Some(Delta(t1, t0)), + }; + glean_fd::fallback.Record(Some(extra)); + } +} + +} // namespace telemetry +} // namespace details + +// Invoke either or both of a "do locally" and "do remotely" function with the +// provided arguments, depending on the relevant preference-value and whether +// or not the remote version fails. +// +// Both functions must be asynchronous, returning a `RefPtr<MozPromise<...>>`. +// "Failure" is defined as the promise being rejected. +template <typename Fn1, typename Fn2, typename... Args> +static auto AsyncExecute(Fn1 local, Fn2 remote, Args const&... args) + -> std::invoke_result_t<Fn1, Args...> { + using namespace details; + + static_assert(std::is_same_v<std::invoke_result_t<Fn1, Args...>, + std::invoke_result_t<Fn2, Args...>>); + using PromiseT = typename std::invoke_result_t<Fn1, Args...>::element_type; + + constexpr static char kFunctionName[] = "LocalAndOrRemote::AsyncExecute"; + + switch (GetStrategy()) { + case Local: + return local(args...); + + case Remote: + return remote(args...); + + case RemoteWithFallback: + // more complicated; continue below + break; + } + + // capture time for telemetry + constexpr static const auto GetTime = []() -> uint64_t { + FILETIME t; + ::GetSystemTimeAsFileTime(&t); + return (uint64_t(t.dwHighDateTime) << 32) | t.dwLowDateTime; + }; + uint64_t const t0 = GetTime(); + + return remote(args...)->Then( + NS_GetCurrentThread(), kFunctionName, + [t0](typename PromiseT::ResolveValueType result) -> RefPtr<PromiseT> { + // success; stop here + auto const t1 = GetTime(); + // record success + telemetry::RecordSuccess({t0, t1}); + return PromiseT::CreateAndResolve(result, kFunctionName); + }, + // initialized lambda pack captures are C++20 (clang 9, gcc 9); + // `make_tuple` is just a C++17 workaround + [=, tuple = std::make_tuple(Copy(args)...)]( + typename PromiseT::RejectValueType err) mutable -> RefPtr<PromiseT> { + // failure; record time + auto const t1 = GetTime(); + HRESULT const hrRemote = err; + + // retry locally... + auto p0 = std::apply(local, std::move(tuple)); + // ...then record the telemetry event + return p0->Then( + NS_GetCurrentThread(), kFunctionName, + [t0, t1, + hrRemote](typename PromiseT::ResolveOrRejectValue const& val) + -> RefPtr<PromiseT> { + auto const t2 = GetTime(); + HRESULT const hrLocal = val.IsReject() ? val.RejectValue() : S_OK; + telemetry::RecordFailure({t0, t1, t2}, hrRemote, hrLocal); + + return PromiseT::CreateAndResolveOrReject(val, kFunctionName); + }); + }); +} + +// Asynchronously invokes `aPredicate` on each member of `aItems`. +// Yields `false` (and stops immediately) if any invocation of +// `predicate` yielded `false`; otherwise yields `true`. +template <typename T> +static RefPtr<mozilla::MozPromise<bool, nsresult, true>> AsyncAll( + nsTArray<T> aItems, + std::function< + RefPtr<mozilla::MozPromise<bool, nsresult, true>>(const T& item)> + aPredicate) { + auto promise = + mozilla::MakeRefPtr<mozilla::MozPromise<bool, nsresult, true>::Private>( + __func__); + auto iterator = mozilla::MakeRefPtr<details::AsyncAllIterator<T>>( + std::move(aItems), aPredicate, promise); + iterator->StartIterating(); + return promise; +} +} // namespace fd_async + +using fd_async::AsyncAll; +using fd_async::AsyncExecute; + +} // namespace mozilla::detail + +/* static */ +nsFilePicker::FPPromise<filedialog::Results> nsFilePicker::ShowFilePickerRemote( + HWND parent, filedialog::FileDialogType type, + nsTArray<filedialog::Command> const& commands) { + using mozilla::widget::filedialog::sLogFileDialog; + return mozilla::detail::ShowRemote( + [parent, type, + commands = commands.Clone()](filedialog::WinFileDialogParent* p) { + MOZ_LOG(sLogFileDialog, LogLevel::Info, + ("%s: p = [%p]", __PRETTY_FUNCTION__, p)); + return p->SendShowFileDialog((uintptr_t)parent, type, commands); + }); +} + +/* static */ +nsFilePicker::FPPromise<nsString> nsFilePicker::ShowFolderPickerRemote( + HWND parent, nsTArray<filedialog::Command> const& commands) { + using mozilla::widget::filedialog::sLogFileDialog; + return mozilla::detail::ShowRemote([parent, commands = commands.Clone()]( + filedialog::WinFileDialogParent* p) { + MOZ_LOG(sLogFileDialog, LogLevel::Info, + ("%s: p = [%p]", __PRETTY_FUNCTION__, p)); + return p->SendShowFolderDialog((uintptr_t)parent, commands); + }); +} + +/* static */ +nsFilePicker::FPPromise<filedialog::Results> nsFilePicker::ShowFilePickerLocal( + HWND parent, filedialog::FileDialogType type, + nsTArray<filedialog::Command> const& commands) { + return filedialog::SpawnFilePicker(parent, type, commands.Clone()); +} + +/* static */ +nsFilePicker::FPPromise<nsString> nsFilePicker::ShowFolderPickerLocal( + HWND parent, nsTArray<filedialog::Command> const& commands) { + return filedialog::SpawnFolderPicker(parent, commands.Clone()); +} + +/* + * Folder picker invocation + */ + +/* + * Show a folder picker. + * + * @param aInitialDir The initial directory. The last-used directory will be + * used if left blank. + * @return A promise which: + * - resolves to true if a file was selected successfully (in which + * case mUnicodeFile will be updated); + * - resolves to false if the dialog was cancelled by the user; + * - is rejected with the associated HRESULT if some error occurred. + */ +RefPtr<mozilla::MozPromise<bool, HRESULT, true>> nsFilePicker::ShowFolderPicker( + const nsString& aInitialDir) { + using Promise = mozilla::MozPromise<bool, HRESULT, true>; + constexpr static auto Ok = [](bool val) { + return Promise::CreateAndResolve(val, "nsFilePicker::ShowFolderPicker"); + }; + constexpr static auto NotOk = [](HRESULT val = E_FAIL) { + return Promise::CreateAndReject(val, "nsFilePicker::ShowFolderPicker"); + }; + + namespace fd = ::mozilla::widget::filedialog; + nsTArray<fd::Command> commands = { + fd::SetOptions(FOS_PICKFOLDERS), + fd::SetTitle(mTitle), + }; + + if (!mOkButtonLabel.IsEmpty()) { + commands.AppendElement(fd::SetOkButtonLabel(mOkButtonLabel)); + } + + if (!aInitialDir.IsEmpty()) { + commands.AppendElement(fd::SetFolder(aInitialDir)); + } + + ScopedRtlShimWindow shim(mParentWidget.get()); + AutoWidgetPickerState awps(mParentWidget); + + return mozilla::detail::AsyncExecute(&ShowFolderPickerLocal, + &ShowFolderPickerRemote, shim.get(), + commands) + ->Then( + NS_GetCurrentThread(), __PRETTY_FUNCTION__, + [self = RefPtr(this), shim = std::move(shim), + awps = std::move(awps)](Maybe<nsString> val) { + if (val) { + self->mUnicodeFile = val.extract(); + return Ok(true); + } + return Ok(false); + }, + [](HRESULT err) { + NS_WARNING("ShowFolderPicker failed"); + return NotOk(err); + }); +} + +/* + * File open and save picker invocation + */ + +/* + * Show a file picker. + * + * @param aInitialDir The initial directory. The last-used directory will be + * used if left blank. + * @return A promise which: + * - resolves to true if one or more files were selected successfully + * (in which case mUnicodeFile and/or mFiles will be updated); + * - resolves to false if the dialog was cancelled by the user; + * - is rejected with the associated HRESULT if some error occurred. + */ +RefPtr<mozilla::MozPromise<bool, HRESULT, true>> nsFilePicker::ShowFilePicker( + const nsString& aInitialDir) { + AUTO_PROFILER_LABEL("nsFilePicker::ShowFilePicker", OTHER); + + using Promise = mozilla::MozPromise<bool, HRESULT, true>; + constexpr static auto Ok = [](bool val) { + return Promise::CreateAndResolve(val, "nsFilePicker::ShowFilePicker"); + }; + constexpr static auto NotOk = [](HRESULT val = E_FAIL) { + return Promise::CreateAndReject(val, "nsFilePicker::ShowFilePicker"); + }; + + namespace fd = ::mozilla::widget::filedialog; + nsTArray<fd::Command> commands; + // options + { + FILEOPENDIALOGOPTIONS fos = 0; + fos |= FOS_SHAREAWARE | FOS_OVERWRITEPROMPT | FOS_FORCEFILESYSTEM; + + // Handle add to recent docs settings + if (IsPrivacyModeEnabled() || !mAddToRecentDocs) { + fos |= FOS_DONTADDTORECENT; + } + + // mode specific + switch (mMode) { + case modeOpen: + fos |= FOS_FILEMUSTEXIST; + break; + + case modeOpenMultiple: + fos |= FOS_FILEMUSTEXIST | FOS_ALLOWMULTISELECT; + break; + + case modeSave: + fos |= FOS_NOREADONLYRETURN; + // Don't follow shortcuts when saving a shortcut, this can be used + // to trick users (bug 271732) + if (IsDefaultPathLink()) fos |= FOS_NODEREFERENCELINKS; + break; + + case modeGetFolder: + MOZ_ASSERT(false, "file-picker opened in directory-picker mode"); + return NotOk(E_FAIL); + } + + commands.AppendElement(fd::SetOptions(fos)); + } + + // initial strings + + // title + commands.AppendElement(fd::SetTitle(mTitle)); + + // default filename + if (!mDefaultFilename.IsEmpty()) { + // Prevent the shell from expanding environment variables by removing + // the % characters that are used to delimit them. + nsAutoString sanitizedFilename(mDefaultFilename); + sanitizedFilename.ReplaceChar('%', '_'); + + commands.AppendElement(fd::SetFileName(sanitizedFilename)); + } + + // default extension to append to new files + if (!mDefaultExtension.IsEmpty()) { + // We don't want environment variables expanded in the extension either. + nsAutoString sanitizedExtension(mDefaultExtension); + sanitizedExtension.ReplaceChar('%', '_'); + + commands.AppendElement(fd::SetDefaultExtension(sanitizedExtension)); + } else if (IsDefaultPathHtml()) { + commands.AppendElement(fd::SetDefaultExtension(u"html"_ns)); + } + + // initial location + if (!aInitialDir.IsEmpty()) { + commands.AppendElement(fd::SetFolder(aInitialDir)); + } + + // filter types and the default index + if (!mFilterList.IsEmpty()) { + nsTArray<fd::ComDlgFilterSpec> fileTypes; + for (auto const& filter : mFilterList) { + fileTypes.EmplaceBack(filter.title, filter.filter); + } + commands.AppendElement(fd::SetFileTypes(std::move(fileTypes))); + commands.AppendElement(fd::SetFileTypeIndex(mSelectedType)); + } + + ScopedRtlShimWindow shim(mParentWidget.get()); + AutoWidgetPickerState awps(mParentWidget); + + mozilla::BackgroundHangMonitor().NotifyWait(); + auto type = mMode == modeSave ? FileDialogType::Save : FileDialogType::Open; + + auto promise = mozilla::detail::AsyncExecute( + &ShowFilePickerLocal, &ShowFilePickerRemote, shim.get(), type, commands); + + return promise->Then( + mozilla::GetMainThreadSerialEventTarget(), __PRETTY_FUNCTION__, + [self = RefPtr(this), mode = mMode, shim = std::move(shim), + awps = std::move(awps)](Maybe<Results> res_opt) { + if (!res_opt) { + return Ok(false); + } + auto result = res_opt.extract(); + + // Remember what filter type the user selected + self->mSelectedType = int32_t(result.selectedFileTypeIndex()); + + auto const& paths = result.paths(); + + // single selection + if (mode != modeOpenMultiple) { + if (!paths.IsEmpty()) { + MOZ_ASSERT(paths.Length() == 1); + self->mUnicodeFile = paths[0]; + return Ok(true); + } + return Ok(false); + } + + // multiple selection + for (auto const& str : paths) { + nsCOMPtr<nsIFile> file; + if (NS_SUCCEEDED(NS_NewLocalFile(str, false, getter_AddRefs(file)))) { + self->mFiles.AppendObject(file); + } + } + + return Ok(true); + }, + [](HRESULT err) { + NS_WARNING("ShowFilePicker failed"); + return NotOk(err); + }); +} + +void nsFilePicker::ClearFiles() { + mUnicodeFile.Truncate(); + mFiles.Clear(); +} + +namespace { +class GetFilesInDirectoryCallback final + : public mozilla::dom::GetFilesCallback { + public: + explicit GetFilesInDirectoryCallback( + RefPtr<mozilla::MozPromise<nsTArray<mozilla::PathString>, nsresult, + true>::Private> + aPromise) + : mPromise(std::move(aPromise)) {} + void Callback( + nsresult aStatus, + const FallibleTArray<RefPtr<mozilla::dom::BlobImpl>>& aBlobImpls) { + if (NS_FAILED(aStatus)) { + mPromise->Reject(aStatus, __func__); + return; + } + nsTArray<mozilla::PathString> filePaths; + filePaths.SetCapacity(aBlobImpls.Length()); + for (const auto& blob : aBlobImpls) { + if (blob->IsFile()) { + mozilla::PathString pathString; + mozilla::ErrorResult error; + blob->GetMozFullPathInternal(pathString, error); + nsresult rv = error.StealNSResult(); + if (NS_WARN_IF(NS_FAILED(rv))) { + mPromise->Reject(rv, __func__); + return; + } + filePaths.AppendElement(pathString); + } else { + NS_WARNING("Got a non-file blob, can't do content analysis on it"); + } + } + mPromise->Resolve(std::move(filePaths), __func__); + } + + private: + RefPtr<mozilla::MozPromise<nsTArray<mozilla::PathString>, nsresult, + true>::Private> + mPromise; +}; +} // anonymous namespace + +RefPtr<nsFilePicker::ContentAnalysisResponse> +nsFilePicker::CheckContentAnalysisService() { + nsresult rv; + nsCOMPtr<nsIContentAnalysis> contentAnalysis = + mozilla::components::nsIContentAnalysis::Service(&rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv, __func__); + } + bool contentAnalysisIsActive = false; + rv = contentAnalysis->GetIsActive(&contentAnalysisIsActive); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv, __func__); + } + if (!contentAnalysisIsActive) { + return nsFilePicker::ContentAnalysisResponse::CreateAndResolve(true, + __func__); + } + + nsCOMPtr<nsIURI> uri = mBrowsingContext->Canonical()->GetCurrentURI(); + + auto processOneItem = [self = RefPtr{this}, + contentAnalysis = std::move(contentAnalysis), + uri = + std::move(uri)](const mozilla::PathString& aItem) { + nsCString emptyDigestString; + auto* windowGlobal = + self->mBrowsingContext->Canonical()->GetCurrentWindowGlobal(); + nsCOMPtr<nsIContentAnalysisRequest> contentAnalysisRequest( + new mozilla::contentanalysis::ContentAnalysisRequest( + nsIContentAnalysisRequest::AnalysisType::eFileAttached, aItem, true, + std::move(emptyDigestString), uri, + nsIContentAnalysisRequest::OperationType::eCustomDisplayString, + windowGlobal)); + + auto promise = + mozilla::MakeRefPtr<nsFilePicker::ContentAnalysisResponse::Private>( + __func__); + auto contentAnalysisCallback = + mozilla::MakeRefPtr<mozilla::contentanalysis::ContentAnalysisCallback>( + [promise](nsIContentAnalysisResponse* aResponse) { + promise->Resolve(aResponse->GetShouldAllowContent(), __func__); + }, + [promise](nsresult aError) { promise->Reject(aError, __func__); }); + + nsresult rv = contentAnalysis->AnalyzeContentRequestCallback( + contentAnalysisRequest, /* aAutoAcknowledge */ true, + contentAnalysisCallback); + if (NS_WARN_IF(NS_FAILED(rv))) { + promise->Reject(rv, __func__); + } + return promise; + }; + + // Since getting the files to analyze might be asynchronous, use a MozPromise + // to unify the logic below. + auto getFilesToAnalyzePromise = mozilla::MakeRefPtr<mozilla::MozPromise< + nsTArray<mozilla::PathString>, nsresult, true>::Private>(__func__); + if (mMode == modeGetFolder) { + nsCOMPtr<nsISupports> tmp; + nsresult rv = GetDomFileOrDirectory(getter_AddRefs(tmp)); + if (NS_WARN_IF(NS_FAILED(rv))) { + getFilesToAnalyzePromise->Reject(rv, __func__); + return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv, + __func__); + } + auto* directory = static_cast<mozilla::dom::Directory*>(tmp.get()); + mozilla::dom::OwningFileOrDirectory owningDirectory; + owningDirectory.SetAsDirectory() = directory; + nsTArray<mozilla::dom::OwningFileOrDirectory> directoryArray{ + std::move(owningDirectory)}; + + mozilla::ErrorResult error; + RefPtr<mozilla::dom::GetFilesHelper> helper = + mozilla::dom::GetFilesHelper::Create(directoryArray, true, error); + rv = error.StealNSResult(); + if (NS_WARN_IF(NS_FAILED(rv))) { + getFilesToAnalyzePromise->Reject(rv, __func__); + return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv, + __func__); + } + auto getFilesCallback = mozilla::MakeRefPtr<GetFilesInDirectoryCallback>( + getFilesToAnalyzePromise); + helper->AddCallback(getFilesCallback); + } else { + nsCOMArray<nsIFile> files; + if (!mUnicodeFile.IsEmpty()) { + nsCOMPtr<nsIFile> file; + rv = GetFile(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + getFilesToAnalyzePromise->Reject(rv, __func__); + return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv, + __func__); + } + files.AppendElement(file); + } else { + files.AppendElements(mFiles); + } + nsTArray<mozilla::PathString> paths(files.Length()); + std::transform(files.begin(), files.end(), MakeBackInserter(paths), + [](auto* entry) { return entry->NativePath(); }); + getFilesToAnalyzePromise->Resolve(std::move(paths), __func__); + } + + return getFilesToAnalyzePromise->Then( + mozilla::GetMainThreadSerialEventTarget(), __func__, + [processOneItem](nsTArray<mozilla::PathString> aPaths) mutable { + return mozilla::detail::AsyncAll<mozilla::PathString>(std::move(aPaths), + processOneItem); + }, + [](nsresult aError) { + return nsFilePicker::ContentAnalysisResponse::CreateAndReject(aError, + __func__); + }); +}; + +/////////////////////////////////////////////////////////////////////////////// +// nsIFilePicker impl. + +nsresult nsFilePicker::Open(nsIFilePickerShownCallback* aCallback) { + NS_ENSURE_ARG_POINTER(aCallback); + + if (MaybeBlockFilePicker(aCallback)) { + return NS_OK; + } + + // Don't attempt to open a real file-picker in headless mode. + if (gfxPlatform::IsHeadless()) { + return nsresult::NS_ERROR_NOT_AVAILABLE; + } + + nsAutoString initialDir; + if (mDisplayDirectory) mDisplayDirectory->GetPath(initialDir); + + // If no display directory, re-use the last one. + if (initialDir.IsEmpty()) { + // Allocate copy of last used dir. + initialDir = sLastUsedUnicodeDirectory.get(); + } + + // Clear previous file selections + ClearFiles(); + + auto promise = mMode == modeGetFolder ? ShowFolderPicker(initialDir) + : ShowFilePicker(initialDir); + + auto p2 = promise->Then( + mozilla::GetMainThreadSerialEventTarget(), __PRETTY_FUNCTION__, + [self = RefPtr(this), + callback = RefPtr(aCallback)](bool selectionMade) -> void { + if (!selectionMade) { + callback->Done(ResultCode::returnCancel); + return; + } + + self->RememberLastUsedDirectory(); + + nsIFilePicker::ResultCode retValue = ResultCode::returnOK; + + if (self->mMode == modeSave) { + // Windows does not return resultReplace; we must check whether the + // file already exists. + nsCOMPtr<nsIFile> file; + nsresult rv = + NS_NewLocalFile(self->mUnicodeFile, false, getter_AddRefs(file)); + + bool flag = false; + if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(file->Exists(&flag)) && flag) { + retValue = ResultCode::returnReplace; + } + } + + if (self->mBrowsingContext && !self->mBrowsingContext->IsChrome() && + self->mMode != modeSave && retValue != ResultCode::returnCancel) { + self->CheckContentAnalysisService()->Then( + mozilla::GetMainThreadSerialEventTarget(), __func__, + [retValue, callback, self = RefPtr{self}](bool aAllowContent) { + if (aAllowContent) { + callback->Done(retValue); + } else { + self->ClearFiles(); + callback->Done(ResultCode::returnCancel); + } + }, + [callback, self = RefPtr{self}](nsresult aError) { + self->ClearFiles(); + callback->Done(ResultCode::returnCancel); + }); + return; + } + + callback->Done(retValue); + }, + [callback = RefPtr(aCallback)](HRESULT err) { + using mozilla::widget::filedialog::sLogFileDialog; + MOZ_LOG(sLogFileDialog, LogLevel::Error, + ("nsFilePicker: Show failed with hr=0x%08lX", err)); + callback->Done(ResultCode::returnCancel); + }); + + return NS_OK; +} + +nsresult nsFilePicker::Show(nsIFilePicker::ResultCode* aReturnVal) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsFilePicker::GetFile(nsIFile** aFile) { + NS_ENSURE_ARG_POINTER(aFile); + *aFile = nullptr; + + if (mUnicodeFile.IsEmpty()) return NS_OK; + + nsCOMPtr<nsIFile> file; + nsresult rv = NS_NewLocalFile(mUnicodeFile, false, getter_AddRefs(file)); + if (NS_FAILED(rv)) { + return rv; + } + + file.forget(aFile); + return NS_OK; +} + +NS_IMETHODIMP +nsFilePicker::GetFileURL(nsIURI** aFileURL) { + *aFileURL = nullptr; + nsCOMPtr<nsIFile> file; + nsresult rv = GetFile(getter_AddRefs(file)); + if (!file) return rv; + + return NS_NewFileURI(aFileURL, file); +} + +NS_IMETHODIMP +nsFilePicker::GetFiles(nsISimpleEnumerator** aFiles) { + NS_ENSURE_ARG_POINTER(aFiles); + return NS_NewArrayEnumerator(aFiles, mFiles, NS_GET_IID(nsIFile)); +} + +// Get the file + path +NS_IMETHODIMP +nsBaseWinFilePicker::SetDefaultString(const nsAString& aString) { + mDefaultFilePath = aString; + + // First, make sure the file name is not too long. + int32_t nameLength; + int32_t nameIndex = mDefaultFilePath.RFind(u"\\"); + if (nameIndex == kNotFound) + nameIndex = 0; + else + nameIndex++; + nameLength = mDefaultFilePath.Length() - nameIndex; + mDefaultFilename.Assign(Substring(mDefaultFilePath, nameIndex)); + + if (nameLength > MAX_PATH) { + int32_t extIndex = mDefaultFilePath.RFind(u"."); + if (extIndex == kNotFound) extIndex = mDefaultFilePath.Length(); + + // Let's try to shave the needed characters from the name part. + int32_t charsToRemove = nameLength - MAX_PATH; + if (extIndex - nameIndex >= charsToRemove) { + mDefaultFilePath.Cut(extIndex - charsToRemove, charsToRemove); + } + } + + // Then, we need to replace illegal characters. At this stage, we cannot + // replace the backslash as the string might represent a file path. + mDefaultFilePath.ReplaceChar(u"" FILE_ILLEGAL_CHARACTERS, u'-'); + mDefaultFilename.ReplaceChar(u"" FILE_ILLEGAL_CHARACTERS, u'-'); + + return NS_OK; +} + +NS_IMETHODIMP +nsBaseWinFilePicker::GetDefaultString(nsAString& aString) { + return NS_ERROR_FAILURE; +} + +// The default extension to use for files +NS_IMETHODIMP +nsBaseWinFilePicker::GetDefaultExtension(nsAString& aExtension) { + aExtension = mDefaultExtension; + return NS_OK; +} + +NS_IMETHODIMP +nsBaseWinFilePicker::SetDefaultExtension(const nsAString& aExtension) { + mDefaultExtension = aExtension; + return NS_OK; +} + +// Set the filter index +NS_IMETHODIMP +nsFilePicker::GetFilterIndex(int32_t* aFilterIndex) { + // Windows' filter index is 1-based, we use a 0-based system. + *aFilterIndex = mSelectedType - 1; + return NS_OK; +} + +NS_IMETHODIMP +nsFilePicker::SetFilterIndex(int32_t aFilterIndex) { + // Windows' filter index is 1-based, we use a 0-based system. + mSelectedType = aFilterIndex + 1; + return NS_OK; +} + +void nsFilePicker::InitNative(nsIWidget* aParent, const nsAString& aTitle) { + mParentWidget = aParent; + mTitle.Assign(aTitle); +} + +NS_IMETHODIMP +nsFilePicker::AppendFilter(const nsAString& aTitle, const nsAString& aFilter) { + nsString sanitizedFilter(aFilter); + sanitizedFilter.ReplaceChar('%', '_'); + + if (sanitizedFilter == u"..apps"_ns) { + sanitizedFilter = u"*.exe;*.com"_ns; + } else { + sanitizedFilter.StripWhitespace(); + if (sanitizedFilter == u"*"_ns) { + sanitizedFilter = u"*.*"_ns; + } + } + mFilterList.AppendElement( + Filter{.title = nsString(aTitle), .filter = std::move(sanitizedFilter)}); + return NS_OK; +} + +void nsFilePicker::RememberLastUsedDirectory() { + if (IsPrivacyModeEnabled()) { + // Don't remember the directory if private browsing was in effect + return; + } + + nsCOMPtr<nsIFile> file; + if (NS_FAILED(NS_NewLocalFile(mUnicodeFile, false, getter_AddRefs(file)))) { + NS_WARNING("RememberLastUsedDirectory failed to init file path."); + return; + } + + nsCOMPtr<nsIFile> dir; + nsAutoString newDir; + if (NS_FAILED(file->GetParent(getter_AddRefs(dir))) || + !(mDisplayDirectory = dir) || + NS_FAILED(mDisplayDirectory->GetPath(newDir)) || newDir.IsEmpty()) { + NS_WARNING("RememberLastUsedDirectory failed to get parent directory."); + return; + } + + sLastUsedUnicodeDirectory.reset(ToNewUnicode(newDir)); +} + +bool nsFilePicker::IsPrivacyModeEnabled() { + return mLoadContext && mLoadContext->UsePrivateBrowsing(); +} + +bool nsFilePicker::IsDefaultPathLink() { + NS_ConvertUTF16toUTF8 ext(mDefaultFilePath); + ext.Trim(" .", false, true); // watch out for trailing space and dots + ToLowerCase(ext); + return StringEndsWith(ext, ".lnk"_ns) || StringEndsWith(ext, ".pif"_ns) || + StringEndsWith(ext, ".url"_ns); +} + +bool nsFilePicker::IsDefaultPathHtml() { + int32_t extIndex = mDefaultFilePath.RFind(u"."); + if (extIndex >= 0) { + nsAutoString ext; + mDefaultFilePath.Right(ext, mDefaultFilePath.Length() - extIndex); + if (ext.LowerCaseEqualsLiteral(".htm") || + ext.LowerCaseEqualsLiteral(".html") || + ext.LowerCaseEqualsLiteral(".shtml")) + return true; + } + return false; +} |