/* -*- 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 "mozilla/Assertions.h" #include "mozilla/BackgroundHangMonitor.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 "nsNetUtil.h" #include "nsPIDOMWindow.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::Maybe; using mozilla::Result; using mozilla::UniquePtr; using namespace mozilla::widget; UniquePtr nsFilePicker::sLastUsedUnicodeDirectory; using mozilla::LogLevel; #define MAX_EXTENSION_LENGTH 10 /////////////////////////////////////////////////////////////////////////////// // Helper classes // Manages matching PickerOpen/PickerClosed calls on the parent widget. class AutoWidgetPickerState { public: explicit AutoWidgetPickerState(nsIWidget* aWidget) : mWindow(static_cast(aWidget)) { PickerState(true); } AutoWidgetPickerState(AutoWidgetPickerState const&) = delete; AutoWidgetPickerState(AutoWidgetPickerState&& that) noexcept = default; ~AutoWidgetPickerState() { PickerState(false); } private: void PickerState(bool aFlag) { if (mWindow) { if (aFlag) mWindow->PickerOpen(); else mWindow->PickerClosed(); } } RefPtr mWindow; }; /////////////////////////////////////////////////////////////////////////////// // nsIFilePicker nsFilePicker::nsFilePicker() : mSelectedType(1) {} NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker) NS_IMETHODIMP nsFilePicker::Init(mozIDOMWindowProxy* aParent, 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; } nsCOMPtr window = do_QueryInterface(aParent); nsIDocShell* docShell = window ? window->GetDocShell() : nullptr; mLoadContext = do_QueryInterface(docShell); return nsBaseFilePicker::Init(aParent, aTitle, aMode); } namespace mozilla::detail { // Boilerplate for remotely showing a file dialog. template ()( nullptr))::element_type::ResolveValueType> static auto ShowRemote(HWND parent, ActionType&& action) -> RefPtr> { using RetPromise = MozPromise; 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; // WORKAROUND FOR UNDOCUMENTED BEHAVIOR: `IFileDialog::Show` disables the // top-level ancestor of its provided owner-window. If the modal window's // container process crashes, it will never get a chance to undo that, so // we have to do it manually. struct WindowEnabler { HWND hwnd; BOOL enabled; explicit WindowEnabler(HWND hwnd) : hwnd(hwnd), enabled(::IsWindowEnabled(hwnd)) { MOZ_LOG(sLogFileDialog, LogLevel::Info, ("ShowRemote: HWND %08zX enabled=%u", uintptr_t(hwnd), enabled)); } WindowEnabler(WindowEnabler const&) = default; void restore() const { MOZ_LOG(sLogFileDialog, LogLevel::Info, ("ShowRemote: HWND %08zX enabled=%u (restoring to %u)", uintptr_t(hwnd), ::IsWindowEnabled(hwnd), enabled)); ::EnableWindow(hwnd, enabled); } }; HWND const rootWindow = ::GetAncestor(parent, GA_ROOT); return wfda->Then( mozilla::GetMainThreadSerialEventTarget(), "nsFilePicker ShowRemote acquire", [action = std::forward(action), rootWindow](filedialog::ProcessProxy const& p) -> RefPtr { MOZ_LOG(sLogFileDialog, LogLevel::Info, ("nsFilePicker ShowRemote first callback: p = [%p]", p.get())); WindowEnabler enabledState(rootWindow); // 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(); }) // Unconditionally restore the disabled state. ->Then(mozilla::GetMainThreadSerialEventTarget(), "nsFilePicker ShowRemote cleanup", [enabledState]( typename RetPromise::ResolveOrRejectValue const& val) { enabledState.restore(); return RetPromise::CreateAndResolveOrReject( val, "nsFilePicker ShowRemote cleanup fmap"); }); }, [](nsresult error) -> RefPtr { MOZ_LOG(sLogFileDialog, LogLevel::Error, ("could not acquire WinFileDialog: %zu", size_t(error))); return fail(); }); } // LocalAndOrRemote // // Pseudo-namespace for the private implementation details of its AsyncExecute() // function. Not intended to be instantiated. struct LocalAndOrRemote { LocalAndOrRemote() = delete; private: // Helper for generically copying ordinary types and nsTArray (which lacks a // copy constructor) in the same breath. // // N.B.: This function is ultimately the reason for LocalAndOrRemote existing // (rather than AsyncExecute() being a free function at namespace scope). // While this is notionally an implementation detail of AsyncExecute(), it // can't be confined to a local struct therein because local structs can't // have template member functions [temp.mem/2]. 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 { 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 } }; public: // 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>`. // "Failure" is defined as the promise being rejected. template static auto AsyncExecute(Fn1 local, Fn2 remote, Args const&... args) -> std::invoke_result_t { static_assert(std::is_same_v, std::invoke_result_t>); using PromiseT = typename std::invoke_result_t::element_type; constexpr static char kFunctionName[] = "LocalAndOrRemote::AsyncExecute"; switch (GetStrategy()) { case Local: return local(args...); case Remote: return remote(args...); case RemoteWithFallback: return remote(args...)->Then( NS_GetCurrentThread(), kFunctionName, [](typename PromiseT::ResolveValueType result) -> RefPtr { // success; stop here 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 { // failure; retry locally return std::apply(local, std::move(tuple)); }); } } }; } // namespace mozilla::detail /* static */ nsFilePicker::FPPromise nsFilePicker::ShowFilePickerRemote( HWND parent, filedialog::FileDialogType type, nsTArray const& commands) { using mozilla::widget::filedialog::sLogFileDialog; return mozilla::detail::ShowRemote( parent, [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 nsFilePicker::ShowFolderPickerRemote( HWND parent, nsTArray const& commands) { using mozilla::widget::filedialog::sLogFileDialog; return mozilla::detail::ShowRemote( parent, [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 nsFilePicker::ShowFilePickerLocal( HWND parent, filedialog::FileDialogType type, nsTArray const& commands) { return filedialog::SpawnFilePicker(parent, type, commands.Clone()); } /* static */ nsFilePicker::FPPromise nsFilePicker::ShowFolderPickerLocal( HWND parent, nsTArray 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> nsFilePicker::ShowFolderPicker( const nsString& aInitialDir) { using Promise = mozilla::MozPromise; 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 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::LocalAndOrRemote::AsyncExecute( &ShowFolderPickerLocal, &ShowFolderPickerRemote, shim.get(), commands) ->Then( NS_GetCurrentThread(), __PRETTY_FUNCTION__, [self = RefPtr(this), shim = std::move(shim), awps = std::move(awps)](Maybe 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> nsFilePicker::ShowFilePicker( const nsString& aInitialDir) { AUTO_PROFILER_LABEL("nsFilePicker::ShowFilePicker", OTHER); using Promise = mozilla::MozPromise; 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 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 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::LocalAndOrRemote::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 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 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); }); } /////////////////////////////////////////////////////////////////////////////// // nsIFilePicker impl. nsresult nsFilePicker::Open(nsIFilePickerShownCallback* aCallback) { NS_ENSURE_ARG_POINTER(aCallback); // 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 mUnicodeFile.Truncate(); mFiles.Clear(); 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 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; } } 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 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 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, false, 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 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; }