/* -*- 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 #include #include #include #include #include #include #include "ContentAnalysis.h" #include "mozilla/Assertions.h" #include "mozilla/BackgroundHangMonitor.h" #include "mozilla/Components.h" #include "mozilla/dom/BrowsingContext.h" #include "mozilla/dom/CanonicalBrowsingContext.h" #include "mozilla/dom/Directory.h" #include "mozilla/dom/WindowGlobalParent.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 "nsArrayEnumerator.h" #include "nsCRT.h" #include "nsEnumeratorUtils.h" #include "nsHashPropertyBag.h" #include "nsIContentAnalysis.h" #include "nsIFile.h" #include "nsISimpleEnumerator.h" #include "nsCExternalHandlerService.h" #include "nsIExternalHelperAppService.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/widget/filedialog/WinFileDialogCommands.h" #include "mozilla/widget/filedialog/WinFileDialogParent.h" using mozilla::LogLevel; using mozilla::UniquePtr; using namespace mozilla::widget; template using FDPromise = filedialog::Promise; MOZ_RUNINIT UniquePtr nsFilePicker::sLastUsedUnicodeDirectory; #define MAX_EXTENSION_LENGTH 10 /////////////////////////////////////////////////////////////////////////////// // Helper classes // Manages matching PickerOpen/PickerClosed calls on the parent widget. class AutoWidgetPickerState { static RefPtr 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 mWindow; }; /////////////////////////////////////////////////////////////////////////////// // nsIFilePicker nsFilePicker::nsFilePicker() = default; NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker) NS_IMETHODIMP nsFilePicker::Init( mozilla::dom::BrowsingContext* aBrowsingContext, const nsAString& aTitle, nsIFilePicker::Mode aMode) { // Don't attempt to open a real file-picker in headless mode. if (gfxPlatform::IsHeadless()) { return nsresult::NS_ERROR_NOT_AVAILABLE; } return nsBaseFilePicker::Init(aBrowsingContext, aTitle, aMode); } namespace mozilla::detail { using Error = mozilla::widget::filedialog::Error; // Boilerplate for remotely showing a file dialog. template ()( nullptr))::element_type::ResolveValueType> static auto ShowRemote(ActionType&& action) -> RefPtr> { using RetPromise = FDPromise; // "function-local" #define #define FAIL(where_, why_) \ return RetPromise::CreateAndReject(MOZ_FD_LOCAL_ERROR(where_, why_), \ __PRETTY_FUNCTION__) auto mgr = mozilla::ipc::UtilityProcessManager::GetSingleton(); if (!mgr) { MOZ_ASSERT(false); FAIL("ShowRemote: UtilityProcessManager::GetSingleton", E_POINTER); } auto wfda = mgr->CreateWinFileDialogActor(); if (!wfda) { FAIL("ShowRemote: invocation of CreateWinFileDialogActor", E_POINTER); } using mozilla::widget::filedialog::sLogFileDialog; return wfda->Then( mozilla::GetMainThreadSerialEventTarget(), "nsFilePicker ShowRemote acquire", [action = std::forward(action)]( filedialog::ProcessProxy p) -> RefPtr { MOZ_LOG(sLogFileDialog, LogLevel::Info, ("nsFilePicker ShowRemote first callback: p = [%p]", p.get())); // false positive: not actually redundant // NOLINTNEXTLINE(readability-redundant-smartptr-get) auto promise = action(p.get()); return promise->Map( mozilla::GetMainThreadSerialEventTarget(), __func__, [p = std::move(p)](typename RetPromise::ResolveValueType&& val) { // explicitly retain the ProcessProxy until at least this point return std::move(val); }); }, [](mozilla::ipc::LaunchError const& error) { MOZ_LOG(sLogFileDialog, LogLevel::Error, ("could not acquire WinFileDialog: %s:%zu", error.FunctionName().get(), size_t(error.ErrorCode()))); return RetPromise::CreateAndReject(Error::From(error), "nsFilePicker::ShowRemote"); }); #undef FAIL } namespace { static RefPtr>> ShowFilePickerRemote( HWND parent, filedialog::FileDialogType type, nsTArray 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->ShowFileDialogImpl(parent, type, commands); }); } static RefPtr>> ShowFolderPickerRemote( HWND parent, nsTArray 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->ShowFolderDialogImpl(parent, commands); }); } static RefPtr>> ShowFilePickerLocal( HWND parent, filedialog::FileDialogType type, nsTArray const& commands) { return filedialog::SpawnFilePicker(parent, type, commands.Clone()); } static RefPtr>> ShowFolderPickerLocal( HWND parent, nsTArray const& commands) { return filedialog::SpawnFolderPicker(parent, commands.Clone()); } } // namespace // 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 static T Copy(T const& val) { return val; } template static nsTArray Copy(nsTArray const& arr) { return arr.Clone(); } // The possible execution strategies of AsyncExecute. enum Strategy { // Always and only open the dialog in-process. This is effectively the // only behavior in older versions of Gecko. LocalOnly, // Always and only open the dialog out-of-process. RemoteOnly, // Open the dialog out-of-process. If that fails in any way, try to recover by // opening it in-process. RemoteWithFallback, // Try to open the dialog out-of-process. If and only if the process can't // even be created, fall back to in-process. // // This heuristic is crafted to avoid the out-of-process file-dialog causing // user-experience regressions compared to the previous "LocalOnly" behavior: // * If the file-dialog actually crashes, then it would have brought down the // entire browser. In this case just surfacing an error is a strict // improvement. // * If the utility process simply fails to start, there's usually nothing // preventing the dialog from being opened in-process instead. Producing an // error would be a degradation. FallbackUnlessCrash, }; // 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 LocalOnly; case 3: return FallbackUnlessCrash; case 2: return RemoteOnly; case 1: return RemoteWithFallback; default: // by default, fall back to local only on non-crash failures return FallbackUnlessCrash; } }; template class AsyncAllIterator final { public: NS_INLINE_DECL_REFCOUNTING(AsyncAllIterator) AsyncAllIterator( nsTArray aItems, std::function< RefPtr>(const T& item)> aPredicate, RefPtr::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 mItems; uint32_t mNextIndex; std::function>( const T& item)> mPredicate; RefPtr::Private> mPromise; }; /* N.B.: L and R stand for Local and Remote, not just Left and Right */ template struct AsyncExecuteInfo { template using DestructurePromise = widget::filedialog::detail::DestructurePromise; using Unit = ::mozilla::Ok; using RetL = std::invoke_result_t; using RetR = std::invoke_result_t; using InfoL = DestructurePromise; using InfoR = DestructurePromise; MOZ_ASSERT_SAME_TYPE( typename InfoL::ResolveT, typename InfoR::ResolveT, "local and remote promises must have identical resolve-types"); // At present, the local and remote promises have the same type, but this // isn't logically necessary. (In particular, a future refactor may remove the // redundant `.kind` from the local promises' return types.) MOZ_ASSERT_SAME_TYPE(typename InfoL::RejectT, filedialog::Error, "local promise must reject with a filedialog::Error"); MOZ_ASSERT_SAME_TYPE(typename InfoR::RejectT, filedialog::Error, "remote promise must reject with a filedialog::Error"); using ResolveT = typename InfoL::ResolveT; using PromiseT = MozPromise; using RetT = RefPtr; }; } // namespace details // Invoke either or both of a promise-returning "do locally" and "do remotely" // function with the provided arguments, depending on the relevant preference's // value and on whether or not the remote version fails (returns a rejection- // promise). // // Both provided functions must return a `RefPtr>`. As // `AsyncExecute` reports failures itself, its rejection-type is `()`. template static auto AsyncExecute(Fn1 local, Fn2 remote, Args const&... args) -> typename details::AsyncExecuteInfo::RetT { using namespace details; using Info = AsyncExecuteInfo; using ResolveT = typename Info::ResolveT; using PromiseT = typename Info::PromiseT; using LPromiseT = typename Info::InfoL::Promise; using RPromiseT = typename Info::InfoR::Promise; constexpr static char kFunctionName[] = "LocalAndOrRemote::AsyncExecute"; bool (*useLocalFallback)(Error const& err) = [](Error const& err) { MOZ_ASSERT_UNREACHABLE("useLocalFallback not set?!"); return true; }; switch (GetStrategy()) { case LocalOnly: { return local(args...)->MapErr( NS_GetCurrentThread(), __func__, [](Error const& err) { MOZ_ASSERT(err.kind == Error::LocalError); MOZ_LOG(filedialog::sLogFileDialog, LogLevel::Info, ("local file-dialog failed: where=%s, why=%08" PRIX32, err.where.c_str(), err.why)); return err; }); } case RemoteOnly: useLocalFallback = [](Error const&) { return false; }; break; case RemoteWithFallback: useLocalFallback = [](Error const&) { return true; }; break; case FallbackUnlessCrash: useLocalFallback = [](Error const& err) { // All remote crashes are reported as IPCError. The converse isn't // necessarily true in theory, but (per telemetry) appears to be true in // practice. return err.kind != Error::IPCError; }; break; } return remote(args...)->Then( NS_GetCurrentThread(), kFunctionName, [](typename RPromiseT::ResolveValueType result) -> RefPtr { // success; stop here return PromiseT::CreateAndResolve(std::move(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 RPromiseT::RejectValueType err) mutable -> RefPtr { // failure; record time // should we fall back to a local implementation? if (!useLocalFallback(err)) { // if not, log this failure immediately... MOZ_LOG(filedialog::sLogFileDialog, LogLevel::Info, ("remote file-dialog failed: kind=%s, where=%s, " "why=%08" PRIX32, Error::KindName(err.kind), err.where.c_str(), err.why)); // ... and stop here return PromiseT::CreateAndReject(err, kFunctionName); } // otherwise, retry locally auto p0 = std::apply(local, std::move(tuple)); return p0->Then( NS_GetCurrentThread(), kFunctionName, [](typename LPromiseT::ResolveOrRejectValue&& val) -> RefPtr { using V = typename PromiseT::ResolveOrRejectValue; return PromiseT::CreateAndResolveOrReject( val.IsResolve() ? V::MakeResolve(std::move(val).ResolveValue()) : V::MakeReject(val.RejectValue()), kFunctionName); }); }); } } // namespace fd_async using fd_async::AsyncExecute; } // namespace mozilla::detail /* * 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> nsFilePicker::ShowFolderPicker(const nsString& aInitialDir) { namespace fd = ::mozilla::widget::filedialog; nsTArray 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(&mozilla::detail::ShowFolderPickerLocal, &mozilla::detail::ShowFolderPickerRemote, shim.get(), commands) ->Map(NS_GetCurrentThread(), __PRETTY_FUNCTION__, [self = RefPtr(this), shim = std::move(shim), awps = std::move(awps)](Maybe val) { if (val) { self->mUnicodeFile = val.extract(); return true; } return false; }); } /* * 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> nsFilePicker::ShowFilePicker(const nsString& aInitialDir) { AUTO_PROFILER_LABEL("nsFilePicker::ShowFilePicker", OTHER); using Promise = mozilla::MozPromise; constexpr static auto NotOk = [](Error error) -> RefPtr { return Promise::CreateAndReject(std::move(error), "nsFilePicker::ShowFilePicker"); }; namespace fd = ::mozilla::widget::filedialog; nsTArray commands; // options { FILEOPENDIALOGOPTIONS fos = 0; // FOS_OVERWRITEPROMPT: always confirm on overwrite in Save dialogs // FOS_FORCEFILESYSTEM: provide only filesystem-objects, not more exotic // entities like libraries fos |= FOS_OVERWRITEPROMPT | FOS_FORCEFILESYSTEM; // Handle add to recent docs settings if (IsPrivacyModeEnabled() || !mAddToRecentDocs) { fos |= FOS_DONTADDTORECENT; } // mode specification switch (mMode) { case modeOpenMultiple: fos |= FOS_ALLOWMULTISELECT; [[fallthrough]]; case modeOpen: fos |= FOS_FILEMUSTEXIST; switch (mozilla::StaticPrefs:: widget_windows_follow_shortcuts_on_file_open()) { case 1: break; default: fos |= FOS_NODEREFERENCELINKS; } break; case modeSave: fos |= FOS_NOREADONLYRETURN; // Don't follow shortcuts when saving a shortcut; this can be used to // trick users (bug 271732). _Do_ follow shortcuts when not saving a // shortcut (bug 283730). if (IsDefaultPathLink()) { fos |= FOS_NODEREFERENCELINKS; } break; case modeGetFolder: MOZ_ASSERT(false, "file-picker opened in directory-picker mode"); return NotOk(MOZ_FD_LOCAL_ERROR( "file-picker opened in directory-picker mode", E_INVALIDARG)); } 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. // // Note that we do _not_ need to preserve this sanitization for the fallback // case where the file dialog fails. Variable-expansion only occurs in the // file dialog specifically, and not when creating a file directly via other // means. 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 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( &mozilla::detail::ShowFilePickerLocal, &mozilla::detail::ShowFilePickerRemote, shim.get(), type, commands); return promise->Map( mozilla::GetMainThreadSerialEventTarget(), __PRETTY_FUNCTION__, [self = RefPtr(this), mode = mMode, shim = std::move(shim), awps = std::move(awps)](Maybe res_opt) { if (!res_opt) { return false; // operation cancelled by user } 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 true; } return false; } // multiple selection for (auto const& str : paths) { nsCOMPtr file; if (NS_SUCCEEDED(NS_NewLocalFile(str, getter_AddRefs(file)))) { self->mFiles.AppendObject(file); } } return true; }); } void nsFilePicker::ClearFiles() { mUnicodeFile.Truncate(); mFiles.Clear(); } RefPtr nsFilePicker::CheckContentAnalysisService() { nsresult rv; nsCOMPtr 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 || !mozilla::StaticPrefs:: browser_contentanalysis_interception_point_file_upload_enabled()) { nsCOMArray files; if (mMode == modeGetFolder || !mUnicodeFile.IsEmpty()) { RefPtr folderOrFile; nsresult rv = GetFile(getter_AddRefs(folderOrFile)); if (NS_WARN_IF(NS_FAILED(rv) || !folderOrFile)) { return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv, __func__); } files.AppendElement(folderOrFile); } else { // multiple selections files.AppendElements(mFiles); } return nsFilePicker::ContentAnalysisResponse::CreateAndResolve( std::move(files), __func__); } // Entries may be files or folders. Folder contents will be recursively // checked. nsCOMArray files; if (mMode == modeGetFolder || !mUnicodeFile.IsEmpty()) { nsCOMPtr folderOrFile; nsresult rv = GetFile(getter_AddRefs(folderOrFile)); if (NS_WARN_IF(NS_FAILED(rv) || !folderOrFile)) { return nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv, __func__); } files.AppendElement(folderOrFile); } else { // multiple selections files.AppendElements(mFiles); } MOZ_ASSERT(!files.IsEmpty()); auto* windowGlobal = mBrowsingContext->Canonical()->GetCurrentWindowGlobal(); NS_ENSURE_TRUE( windowGlobal, nsFilePicker::ContentAnalysisResponse::CreateAndReject(rv, __func__)); // This will check all of the files even if BLOCK responses are received for // some of them, and the promise will resolve with the files that are ALLOWed. return mozilla::contentanalysis::ContentAnalysis::CheckFilesInBatchMode( std::move(files), /* aAutoAcknowledge */ true, windowGlobal, nsIContentAnalysisRequest::Reason::eFilePickerDialog); }; /////////////////////////////////////////////////////////////////////////////// // 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); 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 file; nsresult rv = NS_NewLocalFile(self->mUnicodeFile, 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}](nsCOMArray aAllowedFiles) { if (aAllowedFiles.IsEmpty()) { self->ClearFiles(); callback->Done(ResultCode::returnCancel); } if (self->mMode == modeGetFolder || !self->mUnicodeFile.IsEmpty()) { // Callers will use mUnicodeFile, so just call the callback callback->Done(retValue); return; } // Only the files in aAllowedFiles should be returned, so // swap these into mFiles. self->ClearFiles(); aAllowedFiles.SwapElements(self->mFiles); callback->Done(retValue); }, [callback, self = RefPtr{self}](nsresult aError) { self->ClearFiles(); callback->Done(ResultCode::returnCancel); }); return; } callback->Done(retValue); }, [callback = RefPtr(aCallback), self = RefPtr{this}](Error const& err) { // The file-dialog process (probably) crashed. Report this fact to the // user, and try to recover with a fallback rather than discarding the // file. // // (Note that at this point, logging of the crash -- and possibly also a // telemetry ping -- has already occurred.) ResultCode resultCode = ResultCode::returnCancel; // This does not describe the original error, just the error when trying // to select a fallback location -- no such attempt means no such error. FallbackResult fallback{nullptr}; if (self->mMode == Mode::modeSave) { fallback = self->ComputeFallbackSavePath(); // don't set sLastUsedUnicodeDirectory here: the user didn't // actually select anything } self->SendFailureNotification(resultCode, err, std::move(fallback)); callback->Done(resultCode); }); return NS_OK; } NS_IMETHODIMP nsFilePicker::GetFile(nsIFile** aFile) { NS_ENSURE_ARG_POINTER(aFile); *aFile = nullptr; if (mUnicodeFile.IsEmpty()) { return NS_OK; } nsCOMPtr file; nsresult rv = NS_NewLocalFile(mUnicodeFile, getter_AddRefs(file)); if (NS_FAILED(rv)) { return rv; } file.forget(aFile); return NS_OK; } NS_IMETHODIMP nsFilePicker::GetFileURL(nsIURI** aFileURL) { *aFileURL = nullptr; nsCOMPtr 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 file; if (NS_FAILED(NS_NewLocalFile(mUnicodeFile, getter_AddRefs(file)))) { NS_WARNING("RememberLastUsedDirectory failed to init file path."); return; } nsCOMPtr 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 mBrowsingContext && mBrowsingContext->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; } auto nsFilePicker::ComputeFallbackSavePath() const -> FallbackResult { using mozilla::Err; // we shouldn't even be here if we're not trying to save if (mMode != Mode::modeSave) { return Err(NS_ERROR_FAILURE); } // get a fallback download-location RefPtr location; { // try to query the helper service for the preferred downloads directory nsresult rv; nsCOMPtr svc = do_GetService(NS_EXTERNALHELPERAPPSERVICE_CONTRACTID, &rv); MOZ_TRY(rv); MOZ_TRY(svc->GetPreferredDownloadsDirectory(getter_AddRefs(location))); } MOZ_ASSERT(location); constexpr static const auto EndsWithExtension = [](nsAString const& path, nsAString const& extension) -> bool { size_t const len = path.Length(); size_t const extLen = extension.Length(); if (extLen + 2 > len) { // `path` is too short and can't possibly end with `extension`. (Note that // we consider, _e.g._, ".jpg" not to end with the extension "jpg".) return false; } if (path[len - extLen - 1] == L'.' && StringTail(path, extLen) == extension) { return true; } return false; }; nsString filename(mDefaultFilename); if (!mDefaultExtension.IsEmpty() && !EndsWithExtension(filename, mDefaultExtension)) { filename.AppendLiteral("."); filename.Append(mDefaultExtension); } MOZ_TRY(location->Append(filename)); MOZ_TRY(location->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600)); return location; } void nsFilePicker::SendFailureNotification(nsFilePicker::ResultCode aResult, Error error, FallbackResult aFallback) const { if (MOZ_LOG_TEST(filedialog::sLogFileDialog, LogLevel::Info)) { nsString msg; if (aFallback.isOk()) { nsString path; aFallback.inspect()->GetPath(path); msg = u"path: "_ns; msg.Append(path); } else { msg.AppendPrintf("err: 0x%08" PRIX32, (uint32_t)aFallback.inspectErr()); } MOZ_LOG(filedialog::sLogFileDialog, LogLevel::Info, ("SendCrashNotification: %" PRIX16 ", %ls", aResult, static_cast(msg.get()))); } nsCOMPtr obsSvc = mozilla::services::GetObserverService(); if (!obsSvc) { return; // normal during XPCOM shutdown } RefPtr props = new nsHashPropertyBag(); props->SetPropertyAsInterface(u"ctx"_ns, mBrowsingContext); props->SetPropertyAsUint32(u"mode"_ns, mMode); if (aFallback.isOk()) { props->SetPropertyAsInterface(u"file"_ns, aFallback.unwrap().get()); } else { props->SetPropertyAsUint32(u"file-error"_ns, (uint32_t)aFallback.unwrapErr()); } props->SetPropertyAsBool(u"crash"_ns, error.kind == Error::IPCError); nsIPropertyBag2* const iface = props; obsSvc->NotifyObservers(iface, "file-picker-crashed", nullptr); }