/* -*- 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/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 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 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() : mSelectedType(1) {} 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 { // Boilerplate for remotely showing a file dialog. template ()( nullptr))::element_type::ResolveValueType> static auto ShowRemote(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; return wfda->Then( mozilla::GetMainThreadSerialEventTarget(), "nsFilePicker ShowRemote acquire", [action = std::forward(action)]( filedialog::ProcessProxy const& 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) 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 { 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 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 } }; 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; }; 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>`. // "Failure" is defined as the promise being rejected. template static auto AsyncExecute(Fn1 local, Fn2 remote, Args const&... args) -> std::invoke_result_t { using namespace details; 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: // 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 { // 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 { // 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 { 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 static RefPtr> AsyncAll( nsTArray aItems, std::function< RefPtr>(const T& item)> aPredicate) { auto promise = mozilla::MakeRefPtr::Private>( __func__); auto iterator = mozilla::MakeRefPtr>( 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 nsFilePicker::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->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, 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::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::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); }); } void nsFilePicker::ClearFiles() { mUnicodeFile.Truncate(); mFiles.Clear(); } namespace { class GetFilesInDirectoryCallback final : public mozilla::dom::GetFilesCallback { public: explicit GetFilesInDirectoryCallback( RefPtr, nsresult, true>::Private> aPromise) : mPromise(std::move(aPromise)) {} void Callback( nsresult aStatus, const FallibleTArray>& aBlobImpls) { if (NS_FAILED(aStatus)) { mPromise->Reject(aStatus, __func__); return; } nsTArray 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, nsresult, true>::Private> mPromise; }; } // anonymous namespace 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) { return nsFilePicker::ContentAnalysisResponse::CreateAndResolve(true, __func__); } nsCOMPtr 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 contentAnalysisRequest( new mozilla::contentanalysis::ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eFileAttached, aItem, true, std::move(emptyDigestString), uri, nsIContentAnalysisRequest::OperationType::eCustomDisplayString, windowGlobal)); auto promise = mozilla::MakeRefPtr( __func__); auto contentAnalysisCallback = mozilla::MakeRefPtr( [promise](nsIContentAnalysisResponse* aResponse) { bool shouldAllow = false; mozilla::DebugOnly rv = aResponse->GetShouldAllowContent(&shouldAllow); MOZ_ASSERT(NS_SUCCEEDED(rv)); promise->Resolve(shouldAllow, __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, nsresult, true>::Private>(__func__); if (mMode == modeGetFolder) { nsCOMPtr 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(tmp.get()); mozilla::dom::OwningFileOrDirectory owningDirectory; owningDirectory.SetAsDirectory() = directory; nsTArray directoryArray{ std::move(owningDirectory)}; mozilla::ErrorResult error; RefPtr 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( getFilesToAnalyzePromise); helper->AddCallback(getFilesCallback); } else { nsCOMArray files; if (!mUnicodeFile.IsEmpty()) { nsCOMPtr 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 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 aPaths) mutable { return mozilla::detail::AsyncAll(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); 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; } } 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 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 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; }