/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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 "mozilla/widget/filedialog/WinFileDialogCommands.h" #include #include #include #include #include "WinUtils.h" #include "mozilla/Logging.h" #include "mozilla/RefPtr.h" #include "mozilla/UniquePtrExtensions.h" #include "mozilla/WinHeaderOnlyUtils.h" #include "mozilla/ipc/ProtocolUtils.h" #include "mozilla/ipc/UtilityProcessManager.h" #include "mozilla/mscom/ApartmentRegion.h" #include "nsThreadUtils.h" namespace mozilla::widget::filedialog { const char* Error::KindName(Error::Kind kind) { switch (kind) { case LocalError: return "LocalError"; case RemoteError: return "RemoteError"; case IPCError: return "IPCError"; default: MOZ_ASSERT(false); return ""; } } // Visitor to apply commands to the dialog. struct Applicator { IFileDialog* dialog = nullptr; HRESULT Visit(Command const& c) { switch (c.type()) { default: case Command::T__None: return E_INVALIDARG; case Command::TSetOptions: return Apply(c.get_SetOptions()); case Command::TSetTitle: return Apply(c.get_SetTitle()); case Command::TSetOkButtonLabel: return Apply(c.get_SetOkButtonLabel()); case Command::TSetFolder: return Apply(c.get_SetFolder()); case Command::TSetFileName: return Apply(c.get_SetFileName()); case Command::TSetDefaultExtension: return Apply(c.get_SetDefaultExtension()); case Command::TSetFileTypes: return Apply(c.get_SetFileTypes()); case Command::TSetFileTypeIndex: return Apply(c.get_SetFileTypeIndex()); } } HRESULT Apply(SetOptions const& c) { return dialog->SetOptions(c.options()); } HRESULT Apply(SetTitle const& c) { return dialog->SetTitle(c.title().get()); } HRESULT Apply(SetOkButtonLabel const& c) { return dialog->SetOkButtonLabel(c.label().get()); } HRESULT Apply(SetFolder const& c) { RefPtr folder; if (SUCCEEDED(SHCreateItemFromParsingName( c.path().get(), nullptr, IID_IShellItem, getter_AddRefs(folder)))) { return dialog->SetFolder(folder); } // graciously accept that the provided path may have been nonsense return S_OK; } HRESULT Apply(SetFileName const& c) { return dialog->SetFileName(c.filename().get()); } HRESULT Apply(SetDefaultExtension const& c) { return dialog->SetDefaultExtension(c.extension().get()); } HRESULT Apply(SetFileTypes const& c) { std::vector vec; for (auto const& filter : c.filterList()) { vec.push_back( {.pszName = filter.name().get(), .pszSpec = filter.spec().get()}); } return dialog->SetFileTypes(vec.size(), vec.data()); } HRESULT Apply(SetFileTypeIndex const& c) { return dialog->SetFileTypeIndex(c.index()); } }; namespace { static HRESULT GetShellItemPath(IShellItem* aItem, nsString& aResultString) { NS_ENSURE_TRUE(aItem, E_INVALIDARG); mozilla::UniquePtr str; HRESULT const hr = aItem->GetDisplayName(SIGDN_FILESYSPATH, getter_Transfers(str)); if (SUCCEEDED(hr)) { aResultString.Assign(str.get()); } return hr; } } // namespace #define MOZ_ENSURE_HRESULT_OK(where, call_) \ do { \ HRESULT const _tmp_hr_ = (call_); \ if (FAILED(_tmp_hr_)) { \ return mozilla::Err(MOZ_FD_LOCAL_ERROR(where, _tmp_hr_)); \ } \ } while (0) mozilla::Result, Error> MakeFileDialog( FileDialogType type) { RefPtr dialog; CLSID const clsid = type == FileDialogType::Open ? CLSID_FileOpenDialog : CLSID_FileSaveDialog; HRESULT const hr = CoCreateInstance(clsid, nullptr, CLSCTX_INPROC_SERVER, IID_IFileDialog, getter_AddRefs(dialog)); // more properly: "CoCreateInstance(CLSID_...)", but this suffices MOZ_ENSURE_HRESULT_OK("MakeFileDialog", hr); return std::move(dialog); } mozilla::Result ApplyCommands(::IFileDialog* dialog, nsTArray const& commands) { Applicator applicator{.dialog = dialog}; for (auto const& cmd : commands) { HRESULT const hr = applicator.Visit(cmd); MOZ_ENSURE_HRESULT_OK("ApplyCommands", hr); } return Ok{}; } mozilla::Result GetFileResults(::IFileDialog* dialog) { FILEOPENDIALOGOPTIONS fos; MOZ_ENSURE_HRESULT_OK("IFileDialog::GetOptions", dialog->GetOptions(&fos)); using widget::WinUtils; // Extract which filter type the user selected UINT index; MOZ_ENSURE_HRESULT_OK("IFileDialog::GetFileTypeIndex", dialog->GetFileTypeIndex(&index)); // single selection if ((fos & FOS_ALLOWMULTISELECT) == 0) { RefPtr item; MOZ_ENSURE_HRESULT_OK("IFileDialog::GetResult", dialog->GetResult(getter_AddRefs(item))); if (!item) { return Err(MOZ_FD_LOCAL_ERROR("IFileDialog::GetResult: item", E_POINTER)); } nsAutoString path; MOZ_ENSURE_HRESULT_OK("GetFileResults: GetShellItemPath (1)", GetShellItemPath(item, path)); return Results({path}, index); } // multiple selection RefPtr openDlg; dialog->QueryInterface(IID_IFileOpenDialog, getter_AddRefs(openDlg)); if (!openDlg) { MOZ_ASSERT(false, "a file-save dialog was given FOS_ALLOWMULTISELECT?"); return Err(MOZ_FD_LOCAL_ERROR("Save + FOS_ALLOWMULTISELECT", E_UNEXPECTED)); } RefPtr items; MOZ_ENSURE_HRESULT_OK("IFileOpenDialog::GetResults", openDlg->GetResults(getter_AddRefs(items))); if (!items) { return Err( MOZ_FD_LOCAL_ERROR("IFileOpenDialog::GetResults: items", E_POINTER)); } nsTArray paths; DWORD count = 0; MOZ_ENSURE_HRESULT_OK("IShellItemArray::GetCount", items->GetCount(&count)); for (DWORD idx = 0; idx < count; idx++) { RefPtr item; MOZ_ENSURE_HRESULT_OK("IShellItemArray::GetItemAt", items->GetItemAt(idx, getter_AddRefs(item))); nsAutoString str; MOZ_ENSURE_HRESULT_OK("GetFileResults: GetShellItemPath (2)", GetShellItemPath(item, str)); paths.EmplaceBack(str); } return Results(std::move(paths), std::move(index)); } mozilla::Result GetFolderResults(::IFileDialog* dialog) { RefPtr item; MOZ_ENSURE_HRESULT_OK("IFileDialog::GetResult", dialog->GetResult(getter_AddRefs(item))); if (!item) { // shouldn't happen -- probably a precondition failure on our part, but // might be due to misbehaving shell extensions? MOZ_ASSERT(false, "unexpected lack of item: was `Show`'s return value checked?"); return Err(MOZ_FD_LOCAL_ERROR("IFileDialog::GetResult: item", E_POINTER)); } // If the user chose a Win7 Library, resolve to the library's // default save folder. RefPtr shellLib; RefPtr folderPath; MOZ_ENSURE_HRESULT_OK( "CoCreateInstance(CLSID_ShellLibrary)", CoCreateInstance(CLSID_ShellLibrary, nullptr, CLSCTX_INPROC_SERVER, IID_IShellLibrary, getter_AddRefs(shellLib))); if (shellLib && SUCCEEDED(shellLib->LoadLibraryFromItem(item, STGM_READ)) && SUCCEEDED(shellLib->GetDefaultSaveFolder(DSFT_DETECT, IID_IShellItem, getter_AddRefs(folderPath)))) { item.swap(folderPath); } // get the folder's file system path nsAutoString str; MOZ_ENSURE_HRESULT_OK("GetShellItemPath", GetShellItemPath(item, str)); return str; } #undef MOZ_ENSURE_HRESULT_OK namespace detail { void LogProcessingError(LogModule* aModule, ipc::IProtocol* aCaller, ipc::HasResultCodes::Result aCode, const char* aReason) { LogLevel const level = [&]() { switch (aCode) { case ipc::HasResultCodes::MsgProcessed: // Normal operation. (We probably never actually get this code.) return LogLevel::Verbose; case ipc::HasResultCodes::MsgDropped: return LogLevel::Verbose; default: return LogLevel::Error; } }(); // Processing errors are sometimes unhelpfully formatted. We can't fix that // directly because the unhelpful formatting has made its way to telemetry // (table `telemetry.socorro_crash`, column `ipc_channel_error`) and is being // aggregated on. :( nsCString reason(aReason); if (reason.Last() == '\n') { reason.Truncate(reason.Length() - 1); } if (MOZ_LOG_TEST(aModule, level)) { const char* const side = [&]() { switch (aCaller->GetSide()) { case ipc::ParentSide: return "parent"; case ipc::ChildSide: return "child"; case ipc::UnknownSide: return "unknown side"; default: return ""; } }(); const char* const errorStr = [&]() { switch (aCode) { case ipc::HasResultCodes::MsgProcessed: return "Processed"; case ipc::HasResultCodes::MsgDropped: return "Dropped"; case ipc::HasResultCodes::MsgNotKnown: return "NotKnown"; case ipc::HasResultCodes::MsgNotAllowed: return "NotAllowed"; case ipc::HasResultCodes::MsgPayloadError: return "PayloadError"; case ipc::HasResultCodes::MsgProcessingError: return "ProcessingError"; case ipc::HasResultCodes::MsgRouteError: return "RouteError"; case ipc::HasResultCodes::MsgValueError: return "ValueError"; default: return ""; } }(); MOZ_LOG(aModule, level, ("%s [%s]: IPC error (%s): %s", aCaller->GetProtocolName(), side, errorStr, reason.get())); } if (level == LogLevel::Error) { // kill the child process... if (aCaller->GetSide() == ipc::ParentSide) { // ... which isn't us ipc::UtilityProcessManager::GetSingleton()->CleanShutdown( ipc::SandboxingKind::WINDOWS_FILE_DIALOG); } else { // ... which (presumably) is us CrashReporter::AutoRecordAnnotation( CrashReporter::Annotation::ipc_channel_error, reason); MOZ_CRASH("IPC error"); } } } // Given a (synchronous) Action returning a Result, perform that // action on a new single-purpose "File Dialog" thread, with COM initialized as // STA. (The thread will be destroyed afterwards.) // // Returns a Promise which will resolve to T (if the action returns Ok) or // reject with an HRESULT (if the action either returns Err or couldn't be // performed). template RefPtr> SpawnFileDialogThread(const char (&where)[N], Action action) { { using ActionRetT = std::invoke_result_t; using Info = detail::DestructureResult; MOZ_ASSERT_SAME_TYPE( typename Info::ErrorT, Error, "supplied Action must return Result"); } RefPtr thread; { nsresult rv = NS_NewNamedThread("File Dialog", getter_AddRefs(thread), nullptr, {.isUiThread = true}); if (NS_FAILED(rv)) { return Promise::CreateAndReject( MOZ_FD_LOCAL_ERROR("NS_NewNamedThread", (HRESULT)rv), where); } } // `thread` is single-purpose, and should not perform any additional work // after `action`. Shut it down after we've dispatched that. auto close_thread_ = MakeScopeExit([&]() { auto const res = thread->AsyncShutdown(); static_assert( std::is_same_v>); if (NS_FAILED(res)) { MOZ_LOG(sLogFileDialog, LogLevel::Warning, ("thread->AsyncShutdown() failed: res=0x%08" PRIX32, static_cast(res))); } }); // our eventual return value RefPtr promise = MakeRefPtr::Private>(where); // alias to reduce indentation depth auto const dispatch = [&](auto closure) { return thread->DispatchToQueue( NS_NewRunnableFunction(where, std::move(closure)), mozilla::EventQueuePriority::Normal); }; dispatch([thread, promise, where, action = std::move(action)]() { // Like essentially all COM UI components, the file dialog is STA: it must // be associated with a specific thread to create its HWNDs and receive // messages for them. If it's launched from a thread in the multithreaded // apartment (including via implicit MTA), COM will proxy out to the // process's main STA thread, and the file-dialog's modal loop will run // there. // // This of course would completely negate any point in using a separate // thread, since behind the scenes the dialog would still be running on the // process's main thread. In particular, under that arrangement, file // dialogs (and other nested modal loops, like those performed by // `SpinEventLoopUntil`) will resolve in strictly LIFO order, effectively // remaining suspended until all later modal loops resolve. // // To avoid this, we initialize COM as STA, so that it (rather than the main // STA thread) is the file dialog's "home" thread and the IFileDialog's home // apartment. mozilla::mscom::STARegion staRegion; if (!staRegion) { MOZ_LOG(sLogFileDialog, LogLevel::Error, ("COM init failed on file dialog thread: hr = %08lx", staRegion.GetHResult())); APTTYPE at; APTTYPEQUALIFIER atq; HRESULT const hr = ::CoGetApartmentType(&at, &atq); MOZ_LOG(sLogFileDialog, LogLevel::Error, (" current COM apartment state: hr = %08lX, APTTYPE = " "%08X, APTTYPEQUALIFIER = %08X", hr, at, atq)); // If this happens in the utility process, crash so we learn about it. // (TODO: replace this with a telemetry ping.) if (!XRE_IsParentProcess()) { // Preserve relevant data on the stack for later analysis. std::tuple volatile info{staRegion.GetHResult(), hr, at, atq}; MOZ_CRASH("Could not initialize COM STA in utility process"); } // If this happens in the parent process, don't crash; just fall back to a // nested modal loop. This isn't ideal, but it will probably still work // well enough for the common case, wherein no other modal loops are // active. // // (TODO: replace this with a telemetry ping, too.) } // Actually invoke the action and report the result. Result val = action(); if (val.isErr()) { promise->Reject(val.unwrapErr(), where); } else { promise->Resolve(val.unwrap(), where); } }); return promise; } // For F returning `Result`, yields the type `T`. template using inner_result_of = typename detail::DestructureResult>::OkT; template > auto SpawnPickerT(HWND parent, FileDialogType type, ExtractorF&& extractor, nsTArray commands) -> RefPtr>> { using ActionRetT = Result, Error>; return detail::SpawnFileDialogThread>( __PRETTY_FUNCTION__, [=, commands = std::move(commands)]() -> ActionRetT { // On Win10, the picker doesn't support per-monitor DPI, so we create it // with our context set temporarily to system-dpi-aware. WinUtils::AutoSystemDpiAware dpiAwareness; RefPtr dialog; MOZ_TRY_VAR(dialog, MakeFileDialog(type)); MOZ_TRY(ApplyCommands(dialog, commands)); if (HRESULT const rv = dialog->Show(parent); FAILED(rv)) { if (rv == HRESULT_FROM_WIN32(ERROR_CANCELLED)) { return ActionRetT{Nothing()}; } return mozilla::Err(MOZ_FD_LOCAL_ERROR("IFileDialog::Show", rv)); } RetT res; MOZ_TRY_VAR(res, extractor(dialog.get())); return Some(res); }); } } // namespace detail RefPtr>> SpawnFilePicker(HWND parent, FileDialogType type, nsTArray commands) { return detail::SpawnPickerT(parent, type, GetFileResults, std::move(commands)); } RefPtr>> SpawnFolderPicker(HWND parent, nsTArray commands) { return detail::SpawnPickerT(parent, FileDialogType::Open, GetFolderResults, std::move(commands)); } } // namespace mozilla::widget::filedialog