diff options
Diffstat (limited to 'accessible/windows/msaa')
35 files changed, 5473 insertions, 0 deletions
diff --git a/accessible/windows/msaa/AccessibleWrap.cpp b/accessible/windows/msaa/AccessibleWrap.cpp new file mode 100644 index 0000000000..abab007775 --- /dev/null +++ b/accessible/windows/msaa/AccessibleWrap.cpp @@ -0,0 +1,140 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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 "AccessibleWrap.h" + +#include "mozilla/a11y/DocAccessibleParent.h" +#include "AccEvent.h" +#include "nsAccUtils.h" +#include "nsIAccessibleEvent.h" +#include "nsIWidget.h" +#include "nsWindowsHelpers.h" +#include "mozilla/a11y/HyperTextAccessible.h" +#include "mozilla/a11y/RemoteAccessible.h" +#include "ServiceProvider.h" +#include "sdnAccessible.h" +#include "LocalAccessible-inl.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +/* For documentation of the accessibility architecture, + * see http://lxr.mozilla.org/seamonkey/source/accessible/accessible-docs.html + */ + +//////////////////////////////////////////////////////////////////////////////// +// AccessibleWrap +//////////////////////////////////////////////////////////////////////////////// +AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc) + : LocalAccessible(aContent, aDoc) {} + +NS_IMPL_ISUPPORTS_INHERITED0(AccessibleWrap, LocalAccessible) + +void AccessibleWrap::Shutdown() { + if (mMsaa) { + mMsaa->MsaaShutdown(); + // Don't release mMsaa here because this will cause its id to be released + // immediately, which will result in immediate reuse, causing problems + // for clients. Instead, we release it in the destructor. + } + LocalAccessible::Shutdown(); +} + +//----------------------------------------------------- +// IUnknown interface methods - see iunknown.h for documentation +//----------------------------------------------------- + +MsaaAccessible* AccessibleWrap::GetMsaa() { + if (!mMsaa) { + mMsaa = MsaaAccessible::Create(this); + } + return mMsaa; +} + +void AccessibleWrap::GetNativeInterface(void** aOutAccessible) { + RefPtr<IAccessible> result = GetMsaa(); + return result.forget(aOutAccessible); +} + +//////////////////////////////////////////////////////////////////////////////// +// AccessibleWrap + +//------- Helper methods --------- + +bool AccessibleWrap::IsRootForHWND() { + if (IsRoot()) { + return true; + } + HWND thisHwnd = MsaaAccessible::GetHWNDFor(this); + AccessibleWrap* parent = static_cast<AccessibleWrap*>(LocalParent()); + MOZ_ASSERT(parent); + HWND parentHwnd = MsaaAccessible::GetHWNDFor(parent); + return thisHwnd != parentHwnd; +} + +/* static */ +void AccessibleWrap::UpdateSystemCaretFor( + Accessible* aAccessible, const LayoutDeviceIntRect& aCaretRect) { + if (LocalAccessible* localAcc = aAccessible->AsLocal()) { + // XXX We need the widget for LocalAccessible, so we have to call + // HyperTextAccessible::GetCaretRect. We should find some way of avoiding + // the need for the widget. + UpdateSystemCaretFor(localAcc); + } else { + UpdateSystemCaretFor(aAccessible->AsRemote(), aCaretRect); + } +} + +/* static */ +void AccessibleWrap::UpdateSystemCaretFor(LocalAccessible* aAccessible) { + // Move the system caret so that Windows Tablet Edition and tradional ATs with + // off-screen model can follow the caret + ::DestroyCaret(); + + HyperTextAccessible* text = aAccessible->AsHyperText(); + if (!text) return; + + nsIWidget* widget = nullptr; + LayoutDeviceIntRect caretRect = text->GetCaretRect(&widget); + + if (!widget) { + return; + } + + HWND caretWnd = + reinterpret_cast<HWND>(widget->GetNativeData(NS_NATIVE_WINDOW)); + UpdateSystemCaretFor(caretWnd, caretRect); +} + +/* static */ +void AccessibleWrap::UpdateSystemCaretFor( + RemoteAccessible* aProxy, const LayoutDeviceIntRect& aCaretRect) { + ::DestroyCaret(); + + // The HWND should be the real widget HWND, not an emulated HWND. + // We get the HWND from the proxy's outer doc to bypass window emulation. + LocalAccessible* outerDoc = aProxy->OuterDocOfRemoteBrowser(); + UpdateSystemCaretFor(MsaaAccessible::GetHWNDFor(outerDoc), aCaretRect); +} + +/* static */ +void AccessibleWrap::UpdateSystemCaretFor( + HWND aCaretWnd, const LayoutDeviceIntRect& aCaretRect) { + if (!aCaretWnd || aCaretRect.IsEmpty()) { + return; + } + + // Create invisible bitmap for caret, otherwise its appearance interferes + // with Gecko caret + nsAutoBitmap caretBitMap(CreateBitmap(1, aCaretRect.Height(), 1, 1, nullptr)); + if (::CreateCaret(aCaretWnd, caretBitMap, 1, + aCaretRect.Height())) { // Also destroys the last caret + ::ShowCaret(aCaretWnd); + POINT clientPoint{aCaretRect.X(), aCaretRect.Y()}; + ::ScreenToClient(aCaretWnd, &clientPoint); + ::SetCaretPos(clientPoint.x, clientPoint.y); + } +} diff --git a/accessible/windows/msaa/AccessibleWrap.h b/accessible/windows/msaa/AccessibleWrap.h new file mode 100644 index 0000000000..aeeea87e77 --- /dev/null +++ b/accessible/windows/msaa/AccessibleWrap.h @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef mozilla_a11y_AccessibleWrap_h_ +#define mozilla_a11y_AccessibleWrap_h_ + +#include "nsCOMPtr.h" +#include "LocalAccessible.h" +#include "MsaaAccessible.h" +#include "mozilla/a11y/RemoteAccessible.h" +#include "mozilla/Attributes.h" +#include "mozilla/mscom/Utils.h" +#include "mozilla/StaticPtr.h" +#include "nsXULAppAPI.h" +#include "Units.h" + +namespace mozilla { +namespace a11y { +class DocRemoteAccessibleWrap; + +/** + * Windows specific functionality for an accessibility tree node that originated + * in mDoc's content process. + */ +class AccessibleWrap : public LocalAccessible { + public: // construction, destruction + AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc); + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + public: + // LocalAccessible + virtual void Shutdown() override; + + // Helper methods + /** + * System caret support: update the Windows caret position. + * The system caret works more universally than the MSAA caret + * For example, Window-Eyes, JAWS, ZoomText and Windows Tablet Edition use it + * We will use an invisible system caret. + * Gecko is still responsible for drawing its own caret + */ + static void UpdateSystemCaretFor(Accessible* aAccessible, + const LayoutDeviceIntRect& aCaretRect); + static void UpdateSystemCaretFor(LocalAccessible* aAccessible); + static void UpdateSystemCaretFor(RemoteAccessible* aProxy, + const LayoutDeviceIntRect& aCaretRect); + + private: + static void UpdateSystemCaretFor(HWND aCaretWnd, + const LayoutDeviceIntRect& aCaretRect); + + public: + /** + * Determine whether this is the root accessible for its HWND. + */ + bool IsRootForHWND(); + + MsaaAccessible* GetMsaa(); + virtual void GetNativeInterface(void** aOutAccessible) override; + + protected: + virtual ~AccessibleWrap() = default; + + RefPtr<MsaaAccessible> mMsaa; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/windows/msaa/ApplicationAccessibleWrap.cpp b/accessible/windows/msaa/ApplicationAccessibleWrap.cpp new file mode 100644 index 0000000000..6a10a8bddc --- /dev/null +++ b/accessible/windows/msaa/ApplicationAccessibleWrap.cpp @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=4:tabstop=4: + */ +/* 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 "ApplicationAccessibleWrap.h" + +#include "nsIGfxInfo.h" +#include "AccAttributes.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/Components.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// nsISupports +NS_IMPL_ISUPPORTS_INHERITED0(ApplicationAccessibleWrap, ApplicationAccessible) + +already_AddRefed<AccAttributes> ApplicationAccessibleWrap::NativeAttributes() { + RefPtr<AccAttributes> attributes = new AccAttributes(); + + nsCOMPtr<nsIGfxInfo> gfxInfo = components::GfxInfo::Service(); + if (gfxInfo) { + bool isD2DEnabled = false; + gfxInfo->GetD2DEnabled(&isD2DEnabled); + RefPtr<nsAtom> attrName = NS_Atomize(u"D2D"_ns); + attributes->SetAttribute(attrName, isD2DEnabled); + } + + return attributes.forget(); +} + +void ApplicationAccessibleWrap::Shutdown() { + // ApplicationAccessible::Shutdown doesn't call AccessibleWrap::Shutdown, so + // we have to call MsaaShutdown here. + if (mMsaa) { + mMsaa->MsaaShutdown(); + } + ApplicationAccessible::Shutdown(); +} diff --git a/accessible/windows/msaa/ApplicationAccessibleWrap.h b/accessible/windows/msaa/ApplicationAccessibleWrap.h new file mode 100644 index 0000000000..b40c30fd15 --- /dev/null +++ b/accessible/windows/msaa/ApplicationAccessibleWrap.h @@ -0,0 +1,31 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=4:tabstop=4: + */ +/* 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/. */ + +#ifndef mozilla_a11y_ApplicationAccessibleWrap_h__ +#define mozilla_a11y_ApplicationAccessibleWrap_h__ + +#include "ApplicationAccessible.h" + +namespace mozilla { +namespace a11y { + +class ApplicationAccessibleWrap : public ApplicationAccessible { + ~ApplicationAccessibleWrap() {} + + public: + // nsISupporst + NS_DECL_ISUPPORTS_INHERITED + + // nsAccessible + virtual already_AddRefed<AccAttributes> NativeAttributes() override; + virtual void Shutdown() override; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/windows/msaa/Compatibility.cpp b/accessible/windows/msaa/Compatibility.cpp new file mode 100644 index 0000000000..440f327520 --- /dev/null +++ b/accessible/windows/msaa/Compatibility.cpp @@ -0,0 +1,181 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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 "Compatibility.h" + +#include "mozilla/WindowsVersion.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "mozilla/StaticPrefs_accessibility.h" +#include "nsExceptionHandler.h" +#include "nsIXULRuntime.h" +#include "nsPrintfCString.h" +#include "nsUnicharUtils.h" +#include "nsWinUtils.h" +#include "Statistics.h" +#include "AccessibleWrap.h" + +#include "mozilla/Preferences.h" + +#include <shlobj.h> + +using namespace mozilla; +using namespace mozilla::a11y; + +/** + * String versions of consumer flags. See GetHumanReadableConsumersStr. + */ +static const wchar_t* ConsumerStringMap[CONSUMERS_ENUM_LEN + 1] = { + L"NVDA", L"JAWS", L"OLDJAWS", L"WE", L"DOLPHIN", + L"SEROTEK", L"COBRA", L"ZOOMTEXT", L"KAZAGURU", L"YOUDAO", + L"UNKNOWN", L"UIAUTOMATION", L"VISPEROSHARED", L"\0"}; + +bool Compatibility::IsModuleVersionLessThan(HMODULE aModuleHandle, + unsigned long long aVersion) { + LauncherResult<ModuleVersion> version = GetModuleVersion(aModuleHandle); + if (version.isErr()) { + return true; + } + + return version.unwrap() < aVersion; +} + +//////////////////////////////////////////////////////////////////////////////// +// Compatibility +//////////////////////////////////////////////////////////////////////////////// + +uint32_t Compatibility::sConsumers = Compatibility::UNKNOWN; + +/** + * This function is safe to call multiple times. + */ +/* static */ +void Compatibility::InitConsumers() { + HMODULE jawsHandle = ::GetModuleHandleW(L"jhook"); + if (jawsHandle) { + sConsumers |= + IsModuleVersionLessThan(jawsHandle, MAKE_FILE_VERSION(19, 0, 0, 0)) + ? OLDJAWS + : JAWS; + } + + if (::GetModuleHandleW(L"gwm32inc")) sConsumers |= WE; + + if (::GetModuleHandleW(L"dolwinhk")) sConsumers |= DOLPHIN; + + if (::GetModuleHandleW(L"STSA32")) sConsumers |= SEROTEK; + + if (::GetModuleHandleW(L"nvdaHelperRemote")) sConsumers |= NVDA; + + if (::GetModuleHandleW(L"OsmHooks") || ::GetModuleHandleW(L"OsmHks64")) + sConsumers |= COBRA; + + if (::GetModuleHandleW(L"WebFinderRemote")) sConsumers |= ZOOMTEXT; + + if (::GetModuleHandleW(L"Kazahook")) sConsumers |= KAZAGURU; + + if (::GetModuleHandleW(L"TextExtractorImpl32") || + ::GetModuleHandleW(L"TextExtractorImpl64")) + sConsumers |= YOUDAO; + + if (::GetModuleHandleW(L"uiautomation") || + ::GetModuleHandleW(L"uiautomationcore")) + sConsumers |= UIAUTOMATION; + + if (::GetModuleHandleW(L"AccEventCache")) { + sConsumers |= VISPEROSHARED; + } + + // If we have a known consumer remove the unknown bit. + if (sConsumers != Compatibility::UNKNOWN) + sConsumers &= ~Compatibility::UNKNOWN; +} + +/* static */ +bool Compatibility::HasKnownNonUiaConsumer() { + InitConsumers(); + return sConsumers & ~(Compatibility::UNKNOWN | UIAUTOMATION); +} + +void Compatibility::Init() { + // Note we collect some AT statistics/telemetry here for convenience. + InitConsumers(); + + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::AccessibilityInProcClient, + nsPrintfCString("0x%X", sConsumers)); + + // Gather telemetry + uint32_t temp = sConsumers; + for (int i = 0; temp; i++) { + if (temp & 0x1) statistics::A11yConsumers(i); + + temp >>= 1; + } + + // Turn off new tab switching for Jaws and WE. + if (sConsumers & (JAWS | OLDJAWS | WE)) { + // Check to see if the pref for disallowing CtrlTab is already set. If so, + // bail out (respect the user settings). If not, set it. + if (!Preferences::HasUserValue("browser.ctrlTab.disallowForScreenReaders")) + Preferences::SetBool("browser.ctrlTab.disallowForScreenReaders", true); + } +} + +// static +void Compatibility::GetHumanReadableConsumersStr(nsAString& aResult) { + bool appened = false; + uint32_t index = 0; + for (uint32_t consumers = sConsumers; consumers; consumers = consumers >> 1) { + if (consumers & 0x1) { + if (appened) { + aResult.AppendLiteral(","); + } + aResult.Append(ConsumerStringMap[index]); + appened = true; + } + if (++index > CONSUMERS_ENUM_LEN) { + break; + } + } +} + +// Time when SuppressA11yForClipboardCopy() was called, as returned by +// ::GetTickCount(). +static DWORD sA11yClipboardCopySuppressionStartTime = 0; + +/* static */ +void Compatibility::SuppressA11yForClipboardCopy() { + // Bug 1774285: Windows Suggested Actions (introduced in Windows 11 22H2) + // might walk the a11y tree using UIA whenever anything is copied to the + // clipboard. This causes an unacceptable hang, particularly when the cache + // is disabled. + bool doSuppress = [&] { + switch ( + StaticPrefs::accessibility_windows_suppress_after_clipboard_copy()) { + case 0: + return false; + case 1: + return true; + default: + // Our workaround for Suggested Actions is needed from Windows 11 22H2 + return IsWin1122H2OrLater(); + } + }(); + + if (doSuppress) { + sA11yClipboardCopySuppressionStartTime = ::GetTickCount(); + } +} + +/* static */ +bool Compatibility::IsA11ySuppressedForClipboardCopy() { + constexpr DWORD kSuppressTimeout = 1500; // ms + if (!sA11yClipboardCopySuppressionStartTime) { + return false; + } + return ::GetTickCount() - sA11yClipboardCopySuppressionStartTime < + kSuppressTimeout; +} diff --git a/accessible/windows/msaa/Compatibility.h b/accessible/windows/msaa/Compatibility.h new file mode 100644 index 0000000000..d1e1b0e9ac --- /dev/null +++ b/accessible/windows/msaa/Compatibility.h @@ -0,0 +1,124 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef COMPATIBILITY_MANAGER_H +#define COMPATIBILITY_MANAGER_H + +#include <windows.h> +#include "nsTArray.h" +#include "nsString.h" + +#include <stdint.h> + +namespace mozilla { +namespace a11y { + +/** + * Used to get compatibility modes. Note, modes are computed at accessibility + * start up time and aren't changed during lifetime. + */ +class Compatibility { + public: + /** + * Return true if JAWS mode is enabled. + */ + static bool IsJAWS() { return !!(sConsumers & (JAWS | OLDJAWS)); } + + /** + * Return true if using an e10s incompatible Jaws. + */ + static bool IsOldJAWS() { return !!(sConsumers & OLDJAWS); } + + /** + * Return true if WE mode is enabled. + */ + static bool IsWE() { return !!(sConsumers & WE); } + + /** + * Return true if Dolphin mode is enabled. + */ + static bool IsDolphin() { return !!(sConsumers & DOLPHIN); } + + /** + * Return true if JAWS, ZoomText or ZoomText Fusion 2021 or later is being + * used. These products share common code for interacting with Firefox and + * all require window emulation to be enabled. + */ + static bool IsVisperoShared() { return !!(sConsumers & VISPEROSHARED); } + + /** + * Return a string describing sConsumers suitable for about:support. + * Exposed through nsIXULRuntime.accessibilityInstantiator. + */ + static void GetHumanReadableConsumersStr(nsAString& aResult); + + /** + * Initialize compatibility mode information. + */ + static void Init(); + + static void GetUiaClientPids(nsTArray<DWORD>& aPids); + + /** + * return true if a known, non-UIA a11y consumer is present + */ + static bool HasKnownNonUiaConsumer(); + + /** + * Return true if a module's version is lesser than the given version. + * Generally, the version should be provided using the MAKE_FILE_VERSION + * macro. + * If the version information cannot be retrieved, true is returned; i.e. + * no version information implies an earlier version. + */ + static bool IsModuleVersionLessThan(HMODULE aModuleHandle, + unsigned long long aVersion); + + static void SuppressA11yForClipboardCopy(); + static bool IsA11ySuppressedForClipboardCopy(); + + private: + Compatibility(); + Compatibility(const Compatibility&); + Compatibility& operator=(const Compatibility&); + + static void InitConsumers(); + + /** + * List of detected consumers of a11y (used for statistics/telemetry and + * compat) + */ + enum { + NVDA = 1 << 0, + JAWS = 1 << 1, + OLDJAWS = 1 << 2, + WE = 1 << 3, + DOLPHIN = 1 << 4, + SEROTEK = 1 << 5, + COBRA = 1 << 6, + ZOOMTEXT = 1 << 7, + KAZAGURU = 1 << 8, + YOUDAO = 1 << 9, + UNKNOWN = 1 << 10, + UIAUTOMATION = 1 << 11, + VISPEROSHARED = 1 << 12 + }; +#define CONSUMERS_ENUM_LEN 13 + + private: + static uint32_t sConsumers; +}; + +} // namespace a11y +} // namespace mozilla + +// Convert the 4 (decimal) components of a DLL version number into a +// single unsigned long long, as needed by +// mozilla::a11y::Compatibility::IsModuleVersionLessThan. +#define MAKE_FILE_VERSION(a, b, c, d) \ + ((a##ULL << 48) + (b##ULL << 32) + (c##ULL << 16) + d##ULL) + +#endif diff --git a/accessible/windows/msaa/CompatibilityUIA.cpp b/accessible/windows/msaa/CompatibilityUIA.cpp new file mode 100644 index 0000000000..1f46a35b20 --- /dev/null +++ b/accessible/windows/msaa/CompatibilityUIA.cpp @@ -0,0 +1,347 @@ +/* -*- 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 "Compatibility.h" + +#include "mozilla/a11y/Platform.h" +#include "mozilla/Telemetry.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WindowsVersion.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include "nsTHashSet.h" +#include "nsWindowsHelpers.h" + +#include "NtUndoc.h" + +using namespace mozilla; + +struct ByteArrayDeleter { + void operator()(void* aBuf) { delete[] reinterpret_cast<std::byte*>(aBuf); } +}; + +typedef UniquePtr<OBJECT_DIRECTORY_INFORMATION, ByteArrayDeleter> ObjDirInfoPtr; + +// ComparatorFnT returns true to continue searching, or else false to indicate +// search completion. +template <typename ComparatorFnT> +static bool FindNamedObject(const ComparatorFnT& aComparator) { + // We want to enumerate every named kernel object in our session. We do this + // by opening a directory object using a path constructed using the session + // id under which our process resides. + DWORD sessionId; + if (!::ProcessIdToSessionId(::GetCurrentProcessId(), &sessionId)) { + return false; + } + + nsAutoString path; + path.AppendPrintf("\\Sessions\\%lu\\BaseNamedObjects", sessionId); + + UNICODE_STRING baseNamedObjectsName; + ::RtlInitUnicodeString(&baseNamedObjectsName, path.get()); + + OBJECT_ATTRIBUTES attributes; + InitializeObjectAttributes(&attributes, &baseNamedObjectsName, 0, nullptr, + nullptr); + + HANDLE rawBaseNamedObjects; + NTSTATUS ntStatus = ::NtOpenDirectoryObject( + &rawBaseNamedObjects, DIRECTORY_QUERY | DIRECTORY_TRAVERSE, &attributes); + if (!NT_SUCCESS(ntStatus)) { + return false; + } + + nsAutoHandle baseNamedObjects(rawBaseNamedObjects); + + ULONG context = 0, returnedLen; + + ULONG objDirInfoBufLen = 1024 * sizeof(OBJECT_DIRECTORY_INFORMATION); + ObjDirInfoPtr objDirInfo(reinterpret_cast<OBJECT_DIRECTORY_INFORMATION*>( + new std::byte[objDirInfoBufLen])); + + // Now query that directory object for every named object that it contains. + + BOOL firstCall = TRUE; + + do { + ntStatus = ::NtQueryDirectoryObject(baseNamedObjects, objDirInfo.get(), + objDirInfoBufLen, FALSE, firstCall, + &context, &returnedLen); +#if defined(HAVE_64BIT_BUILD) + if (!NT_SUCCESS(ntStatus)) { + return false; + } +#else + if (ntStatus == STATUS_BUFFER_TOO_SMALL) { + // This case only occurs on 32-bit builds running atop WOW64. + // (See https://bugzilla.mozilla.org/show_bug.cgi?id=1423999#c3) + objDirInfo.reset(reinterpret_cast<OBJECT_DIRECTORY_INFORMATION*>( + new std::byte[returnedLen])); + objDirInfoBufLen = returnedLen; + continue; + } else if (!NT_SUCCESS(ntStatus)) { + return false; + } +#endif + + // NtQueryDirectoryObject gave us an array of OBJECT_DIRECTORY_INFORMATION + // structures whose final entry is zeroed out. + OBJECT_DIRECTORY_INFORMATION* curDir = objDirInfo.get(); + while (curDir->mName.Length && curDir->mTypeName.Length) { + // We use nsDependentSubstring here because UNICODE_STRINGs are not + // guaranteed to be null-terminated. + nsDependentSubstring objName(curDir->mName.Buffer, + curDir->mName.Length / sizeof(wchar_t)); + nsDependentSubstring typeName(curDir->mTypeName.Buffer, + curDir->mTypeName.Length / sizeof(wchar_t)); + + if (!aComparator(objName, typeName)) { + return true; + } + + ++curDir; + } + + firstCall = FALSE; + } while (ntStatus == STATUS_MORE_ENTRIES); + + return false; +} + +// ComparatorFnT returns true to continue searching, or else false to indicate +// search completion. +template <typename ComparatorFnT> +static bool FindHandle(const ComparatorFnT& aComparator) { + NTSTATUS ntStatus; + // First we must query for a list of all the open handles in the system. + UniquePtr<std::byte[]> handleInfoBuf; + ULONG handleInfoBufLen = sizeof(SYSTEM_HANDLE_INFORMATION_EX) + + 1024 * sizeof(SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX); + // We must query for handle information in a loop, since we are effectively + // asking the kernel to take a snapshot of all the handles on the system; + // the size of the required buffer may fluctuate between successive calls. + while (true) { + // These allocations can be hundreds of megabytes on some computers, so + // we should use fallible new here. + handleInfoBuf = MakeUniqueFallible<std::byte[]>(handleInfoBufLen); + if (!handleInfoBuf) { + return false; + } + ntStatus = ::NtQuerySystemInformation( + (SYSTEM_INFORMATION_CLASS)SystemExtendedHandleInformation, + handleInfoBuf.get(), handleInfoBufLen, &handleInfoBufLen); + if (ntStatus == STATUS_INFO_LENGTH_MISMATCH) { + continue; + } + if (!NT_SUCCESS(ntStatus)) { + return false; + } + break; + } + + auto handleInfo = + reinterpret_cast<SYSTEM_HANDLE_INFORMATION_EX*>(handleInfoBuf.get()); + for (ULONG index = 0; index < handleInfo->mHandleCount; ++index) { + SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX& info = handleInfo->mHandles[index]; + HANDLE handle = reinterpret_cast<HANDLE>(info.mHandle); + if (!aComparator(info, handle)) { + return true; + } + } + return false; +} + +static void GetUiaClientPidsWin11(nsTArray<DWORD>& aPids) { + const DWORD ourPid = ::GetCurrentProcessId(); + FindHandle([&](auto aInfo, auto aHandle) { + if (aInfo.mPid != ourPid) { + // We're only interested in handles in our own process. + return true; + } + // UIA creates a named pipe between the client and server processes. We want + // to find our handle to that pipe (if any). If this is a named pipe, get + // the process id of the remote end. We do this first because querying the + // name of the handle might hang in some cases. Counter-intuitively, for UIA + // pipes, we're the client and the remote process is the server. + ULONG pid = 0; + ::GetNamedPipeServerProcessId(aHandle, &pid); + if (!pid) { + return true; + } + // We know this is a named pipe and we have the pid. Now, get the name of + // the handle and check whether it's a UIA pipe. + ULONG objNameBufLen; + NTSTATUS ntStatus = ::NtQueryObject( + aHandle, (OBJECT_INFORMATION_CLASS)ObjectNameInformation, nullptr, 0, + &objNameBufLen); + if (ntStatus != STATUS_INFO_LENGTH_MISMATCH) { + return true; + } + auto objNameBuf = MakeUnique<std::byte[]>(objNameBufLen); + ntStatus = ::NtQueryObject(aHandle, + (OBJECT_INFORMATION_CLASS)ObjectNameInformation, + objNameBuf.get(), objNameBufLen, &objNameBufLen); + if (!NT_SUCCESS(ntStatus)) { + return true; + } + auto objNameInfo = + reinterpret_cast<OBJECT_NAME_INFORMATION*>(objNameBuf.get()); + if (!objNameInfo->Name.Length) { + return true; + } + nsDependentString objName(objNameInfo->Name.Buffer, + objNameInfo->Name.Length / sizeof(wchar_t)); + if (StringBeginsWith(objName, u"\\Device\\NamedPipe\\UIA_PIPE_"_ns)) { + aPids.AppendElement(pid); + } + return true; + }); +} + +static DWORD GetUiaClientPidWin10() { + // UIA creates a section of the form "HOOK_SHMEM_%08lx_%08lx_%08lx_%08lx" + constexpr auto kStrHookShmem = u"HOOK_SHMEM_"_ns; + // The second %08lx is the thread id. + nsAutoString sectionThread; + sectionThread.AppendPrintf("_%08lx_", ::GetCurrentThreadId()); + // This is the number of characters from the end of the section name where + // the sectionThread substring begins. + constexpr size_t sectionThreadRPos = 27; + // This is the length of sectionThread. + constexpr size_t sectionThreadLen = 10; + // Find any named Section that matches the naming convention of the UIA shared + // memory. There can only be one of these at a time, since this only exists + // while UIA is processing a request and it can only process a single request + // on a single thread. + nsAutoHandle section; + auto objectComparator = [&](const nsDependentSubstring& aName, + const nsDependentSubstring& aType) -> bool { + if (aType.Equals(u"Section"_ns) && FindInReadable(kStrHookShmem, aName) && + Substring(aName, aName.Length() - sectionThreadRPos, + sectionThreadLen) == sectionThread) { + // Get a handle to this section so we can get its kernel object and + // use that to find the handle for this section in the remote process. + section.own(::OpenFileMapping(GENERIC_READ, FALSE, + PromiseFlatString(aName).get())); + return false; + } + return true; + }; + if (!FindNamedObject(objectComparator) || !section) { + return 0; + } + + // Now, find the kernel object associated with our section, the handle in the + // remote process associated with that kernel object and thus the remote + // process id. + NTSTATUS ntStatus; + const DWORD ourPid = ::GetCurrentProcessId(); + Maybe<PVOID> kernelObject; + static Maybe<USHORT> sectionObjTypeIndex; + nsTHashSet<uint32_t> nonSectionObjTypes; + nsTHashMap<nsVoidPtrHashKey, DWORD> objMap; + DWORD remotePid = 0; + FindHandle([&](auto aInfo, auto aHandle) { + // The mapping of the aInfo.mObjectTypeIndex field depends on the + // underlying OS kernel. As we scan through the handle list, we record the + // type indices such that we may use those values to skip over handles that + // refer to non-section objects. + if (sectionObjTypeIndex) { + // If we know the type index for Sections, that's the fastest check... + if (sectionObjTypeIndex.value() != aInfo.mObjectTypeIndex) { + // Not a section + return true; + } + } else if (nonSectionObjTypes.Contains( + static_cast<uint32_t>(aInfo.mObjectTypeIndex))) { + // Otherwise we check whether or not the object type is definitely _not_ + // a Section... + return true; + } else if (ourPid == aInfo.mPid) { + // Otherwise we need to issue some system calls to find out the object + // type corresponding to the current handle's type index. + ULONG objTypeBufLen; + ntStatus = ::NtQueryObject(aHandle, ObjectTypeInformation, nullptr, 0, + &objTypeBufLen); + if (ntStatus != STATUS_INFO_LENGTH_MISMATCH) { + return true; + } + auto objTypeBuf = MakeUnique<std::byte[]>(objTypeBufLen); + ntStatus = + ::NtQueryObject(aHandle, ObjectTypeInformation, objTypeBuf.get(), + objTypeBufLen, &objTypeBufLen); + if (!NT_SUCCESS(ntStatus)) { + return true; + } + auto objType = + reinterpret_cast<PUBLIC_OBJECT_TYPE_INFORMATION*>(objTypeBuf.get()); + // Now we check whether the object's type name matches "Section" + nsDependentSubstring objTypeName( + objType->TypeName.Buffer, objType->TypeName.Length / sizeof(wchar_t)); + if (!objTypeName.Equals(u"Section"_ns)) { + nonSectionObjTypes.Insert( + static_cast<uint32_t>(aInfo.mObjectTypeIndex)); + return true; + } + sectionObjTypeIndex = Some(aInfo.mObjectTypeIndex); + } + + // At this point we know that aInfo references a Section object. + // Now we can do some actual tests on it. + if (ourPid != aInfo.mPid) { + if (kernelObject && kernelObject.value() == aInfo.mObject) { + // The kernel objects match -- we have found the remote pid! + remotePid = aInfo.mPid; + return false; + } + // An object that is not ours. Since we do not yet know which kernel + // object we're interested in, we'll save the current object for later. + objMap.InsertOrUpdate(aInfo.mObject, aInfo.mPid); + } else if (aHandle == section.get()) { + // This is the file mapping that we opened above. We save this mObject + // in order to compare to Section objects opened by other processes. + kernelObject = Some(aInfo.mObject); + } + return true; + }); + + if (remotePid) { + return remotePid; + } + if (!kernelObject) { + return 0; + } + + // If we reach here, we found kernelObject *after* we saw the remote process's + // copy. Now we must look it up in objMap. + if (objMap.Get(kernelObject.value(), &remotePid)) { + return remotePid; + } + + return 0; +} + +namespace mozilla { +namespace a11y { + +void Compatibility::GetUiaClientPids(nsTArray<DWORD>& aPids) { + if (!::GetModuleHandleW(L"uiautomationcore.dll")) { + // UIAutomationCore isn't loaded, so there is no UIA client. + return; + } + Telemetry::AutoTimer<Telemetry::A11Y_UIA_DETECTION_TIMING_MS> timer; + if (IsWin11OrLater()) { + GetUiaClientPidsWin11(aPids); + } else { + if (DWORD pid = GetUiaClientPidWin10()) { + aPids.AppendElement(pid); + } + } +} + +} // namespace a11y +} // namespace mozilla diff --git a/accessible/windows/msaa/DocAccessibleWrap.cpp b/accessible/windows/msaa/DocAccessibleWrap.cpp new file mode 100644 index 0000000000..e7ca74ce1a --- /dev/null +++ b/accessible/windows/msaa/DocAccessibleWrap.cpp @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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 "DocAccessibleWrap.h" + +#include "Compatibility.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/Document.h" +#include "DocAccessibleChild.h" +#include "nsWinUtils.h" +#include "RootAccessible.h" +#include "sdnDocAccessible.h" +#include "Statistics.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// DocAccessibleWrap +//////////////////////////////////////////////////////////////////////////////// + +DocAccessibleWrap::DocAccessibleWrap(dom::Document* aDocument, + PresShell* aPresShell) + : DocAccessible(aDocument, aPresShell), mHWND(nullptr) {} + +DocAccessibleWrap::~DocAccessibleWrap() {} + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible + +void DocAccessibleWrap::Shutdown() { + // Do window emulation specific shutdown if emulation was started. + if (nsWinUtils::IsWindowEmulationStarted()) { + // Destroy window created for root document. + if (mDocFlags & eTopLevelContentDocInProcess) { + MOZ_ASSERT(XRE_IsParentProcess()); + HWND hWnd = static_cast<HWND>(mHWND); + ::RemovePropW(hWnd, kPropNameDocAcc); + ::DestroyWindow(hWnd); + } + + mHWND = nullptr; + } + + DocAccessible::Shutdown(); +} + +//////////////////////////////////////////////////////////////////////////////// +// DocAccessible public + +void* DocAccessibleWrap::GetNativeWindow() const { + if (mHWND) { + return mHWND; + } + return DocAccessible::GetNativeWindow(); +} + +//////////////////////////////////////////////////////////////////////////////// +// DocAccessible protected + +void DocAccessibleWrap::DoInitialUpdate() { + DocAccessible::DoInitialUpdate(); + + if (nsWinUtils::IsWindowEmulationStarted()) { + // Create window for tab document. + if (mDocFlags & eTopLevelContentDocInProcess) { + MOZ_ASSERT(XRE_IsParentProcess()); + a11y::RootAccessible* rootDocument = RootAccessible(); + bool isActive = true; + LayoutDeviceIntRect rect(CW_USEDEFAULT, CW_USEDEFAULT, 0, 0); + if (Compatibility::IsDolphin()) { + rect = Bounds(); + LayoutDeviceIntRect rootRect = rootDocument->Bounds(); + rect.MoveToX(rootRect.X() - rect.X()); + rect.MoveByY(-rootRect.Y()); + + auto* bc = mDocumentNode->GetBrowsingContext(); + isActive = bc && bc->IsActive(); + } + + RefPtr<DocAccessibleWrap> self(this); + nsWinUtils::NativeWindowCreateProc onCreate([self](HWND aHwnd) -> void { + ::SetPropW(aHwnd, kPropNameDocAcc, + reinterpret_cast<HANDLE>(self.get())); + }); + + HWND parentWnd = reinterpret_cast<HWND>(rootDocument->GetNativeWindow()); + mHWND = nsWinUtils::CreateNativeWindow( + kClassNameTabContent, parentWnd, rect.X(), rect.Y(), rect.Width(), + rect.Height(), isActive, &onCreate); + } else { + DocAccessible* parentDocument = ParentDocument(); + if (parentDocument) mHWND = parentDocument->GetNativeWindow(); + } + } +} diff --git a/accessible/windows/msaa/DocAccessibleWrap.h b/accessible/windows/msaa/DocAccessibleWrap.h new file mode 100644 index 0000000000..5b1ae9d9b6 --- /dev/null +++ b/accessible/windows/msaa/DocAccessibleWrap.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef mozilla_a11y_DocAccessibleWrap_h__ +#define mozilla_a11y_DocAccessibleWrap_h__ + +#include "DocAccessible.h" + +namespace mozilla { + +class PresShell; + +namespace a11y { + +class DocAccessibleWrap : public DocAccessible { + public: + DocAccessibleWrap(dom::Document* aDocument, PresShell* aPresShell); + virtual ~DocAccessibleWrap(); + + // LocalAccessible + virtual void Shutdown(); + + // DocAccessible + virtual void* GetNativeWindow() const; + + protected: + void* mHWND; + + // DocAccessible + virtual void DoInitialUpdate(); +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/windows/msaa/EnumVariant.cpp b/accessible/windows/msaa/EnumVariant.cpp new file mode 100644 index 0000000000..866e8f69a9 --- /dev/null +++ b/accessible/windows/msaa/EnumVariant.cpp @@ -0,0 +1,90 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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 "EnumVariant.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// ChildrenEnumVariant +//////////////////////////////////////////////////////////////////////////////// + +IMPL_IUNKNOWN_QUERY_HEAD(ChildrenEnumVariant) +IMPL_IUNKNOWN_QUERY_IFACE(IEnumVARIANT) +IMPL_IUNKNOWN_QUERY_TAIL_AGGREGATED(mAnchorMsaa) + +STDMETHODIMP +ChildrenEnumVariant::Next(ULONG aCount, VARIANT FAR* aItems, + ULONG FAR* aCountFetched) { + if (!aItems || !aCountFetched) return E_INVALIDARG; + + *aCountFetched = 0; + + Accessible* anchor = mAnchorMsaa->Acc(); + if (!anchor || anchor->ChildAt(mCurIndex) != mCurAcc) { + return CO_E_OBJNOTCONNECTED; + } + + ULONG countFetched = 0; + while (mCurAcc && countFetched < aCount) { + VariantInit(aItems + countFetched); + + IDispatch* accNative = MsaaAccessible::NativeAccessible(mCurAcc); + + ++mCurIndex; + mCurAcc = anchor->ChildAt(mCurIndex); + + // Don't output the accessible and count it as having been fetched unless + // it is non-null + MOZ_ASSERT(accNative); + if (!accNative) { + continue; + } + + aItems[countFetched].pdispVal = accNative; + aItems[countFetched].vt = VT_DISPATCH; + ++countFetched; + } + + (*aCountFetched) = countFetched; + + return countFetched < aCount ? S_FALSE : S_OK; +} + +STDMETHODIMP +ChildrenEnumVariant::Skip(ULONG aCount) { + Accessible* anchor = mAnchorMsaa->Acc(); + if (!anchor || anchor->ChildAt(mCurIndex) != mCurAcc) { + return CO_E_OBJNOTCONNECTED; + } + + mCurIndex += aCount; + mCurAcc = anchor->ChildAt(mCurIndex); + + return mCurAcc ? S_OK : S_FALSE; +} + +STDMETHODIMP +ChildrenEnumVariant::Reset() { + Accessible* anchor = mAnchorMsaa->Acc(); + if (!anchor) return CO_E_OBJNOTCONNECTED; + + mCurIndex = 0; + mCurAcc = anchor->ChildAt(0); + + return S_OK; +} + +STDMETHODIMP +ChildrenEnumVariant::Clone(IEnumVARIANT** aEnumVariant) { + if (!aEnumVariant) return E_INVALIDARG; + + *aEnumVariant = new ChildrenEnumVariant(*this); + (*aEnumVariant)->AddRef(); + + return S_OK; +} diff --git a/accessible/windows/msaa/EnumVariant.h b/accessible/windows/msaa/EnumVariant.h new file mode 100644 index 0000000000..b584380e50 --- /dev/null +++ b/accessible/windows/msaa/EnumVariant.h @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef mozilla_a11y_EnumVariant_h__ +#define mozilla_a11y_EnumVariant_h__ + +#include "IUnknownImpl.h" +#include "MsaaAccessible.h" + +namespace mozilla { +namespace a11y { + +/** + * Used to fetch accessible children. + */ +class ChildrenEnumVariant final : public IEnumVARIANT { + public: + explicit ChildrenEnumVariant(MsaaAccessible* aAnchor) + : mAnchorMsaa(aAnchor), + mCurAcc(mAnchorMsaa->Acc()->ChildAt(0)), + mCurIndex(0) {} + + // IUnknown + DECL_IUNKNOWN + + // IEnumVariant + virtual /* [local] */ HRESULT STDMETHODCALLTYPE Next( + /* [in] */ ULONG aCount, + /* [length_is][size_is][out] */ VARIANT* aItems, + /* [out] */ ULONG* aCountFetched); + + virtual HRESULT STDMETHODCALLTYPE Skip( + /* [in] */ ULONG aCount); + + virtual HRESULT STDMETHODCALLTYPE Reset(); + + virtual HRESULT STDMETHODCALLTYPE Clone( + /* [out] */ IEnumVARIANT** aEnumVaraint); + + private: + ChildrenEnumVariant() = delete; + ChildrenEnumVariant& operator=(const ChildrenEnumVariant&) = delete; + + ChildrenEnumVariant(const ChildrenEnumVariant& aEnumVariant) + : mAnchorMsaa(aEnumVariant.mAnchorMsaa), + mCurAcc(aEnumVariant.mCurAcc), + mCurIndex(aEnumVariant.mCurIndex) {} + virtual ~ChildrenEnumVariant() {} + + protected: + RefPtr<MsaaAccessible> mAnchorMsaa; + Accessible* mCurAcc; + uint32_t mCurIndex; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/windows/msaa/IUnknownImpl.cpp b/accessible/windows/msaa/IUnknownImpl.cpp new file mode 100644 index 0000000000..b7c0ee0f39 --- /dev/null +++ b/accessible/windows/msaa/IUnknownImpl.cpp @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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 "IUnknownImpl.h" + +#include "nsDebug.h" + +namespace mozilla { +namespace a11y { + +HRESULT +GetHRESULT(nsresult aResult) { + switch (aResult) { + case NS_OK: + return S_OK; + + case NS_ERROR_INVALID_ARG: + return E_INVALIDARG; + + case NS_ERROR_OUT_OF_MEMORY: + return E_OUTOFMEMORY; + + case NS_ERROR_NOT_IMPLEMENTED: + return E_NOTIMPL; + + default: + return E_FAIL; + } +} + +} // namespace a11y +} // namespace mozilla diff --git a/accessible/windows/msaa/IUnknownImpl.h b/accessible/windows/msaa/IUnknownImpl.h new file mode 100644 index 0000000000..7935ebedda --- /dev/null +++ b/accessible/windows/msaa/IUnknownImpl.h @@ -0,0 +1,173 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. * + */ + +#ifndef mozilla_a11y_IUnknownImpl_h_ +#define mozilla_a11y_IUnknownImpl_h_ + +#include <windows.h> +#undef CreateEvent // thank you windows you're such a helper +#include "nsError.h" + +// Avoid warning C4509 like "nonstandard extension used: +// 'AccessibleWrap::[acc_getName]' uses SEH and 'name' has destructor. +// At this point we're catching a crash which is of much greater +// importance than the missing dereference for the nsCOMPtr<> +#ifdef _MSC_VER +# pragma warning(disable : 4509) +#endif + +#if defined(__GNUC__) || defined(__clang__) +# define ATTRIBUTE_UNUSED __attribute__((unused)) +#else +# define ATTRIBUTE_UNUSED +#endif + +namespace mozilla { +namespace a11y { + +class AutoRefCnt { + public: + AutoRefCnt() : mValue(0) {} + + ULONG operator++() { return ++mValue; } + ULONG operator--() { return --mValue; } + ULONG operator++(int) { return ++mValue; } + ULONG operator--(int) { return --mValue; } + + operator ULONG() const { return mValue; } + + private: + ULONG mValue; +}; + +} // namespace a11y +} // namespace mozilla + +#define DECL_IUNKNOWN \ + public: \ + virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void**); \ + ULONG STDMETHODCALLTYPE AddRef() override { \ + MOZ_ASSERT(int32_t(mRefCnt) >= 0, "illegal refcnt"); \ + ++mRefCnt; \ + return mRefCnt; \ + } \ + ULONG STDMETHODCALLTYPE Release() override { \ + MOZ_ASSERT(int32_t(mRefCnt) > 0, "dup release"); \ + --mRefCnt; \ + if (mRefCnt) return mRefCnt; \ + \ + delete this; \ + return 0; \ + } \ + \ + private: \ + mozilla::a11y::AutoRefCnt mRefCnt; \ + \ + public: + +#define DECL_IUNKNOWN_INHERITED \ + public: \ + virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void**); + +#define IMPL_IUNKNOWN_QUERY_HEAD(Class) \ + STDMETHODIMP \ + Class::QueryInterface(REFIID aIID, void** aInstancePtr) { \ + if (!aInstancePtr) return E_INVALIDARG; \ + *aInstancePtr = nullptr; \ + \ + HRESULT hr ATTRIBUTE_UNUSED = E_NOINTERFACE; + +#define IMPL_IUNKNOWN_QUERY_TAIL \ + return hr; \ + } + +#define IMPL_IUNKNOWN_QUERY_TAIL_AGGREGATED(Member) \ + return Member->QueryInterface(aIID, aInstancePtr); \ + } + +#define IMPL_IUNKNOWN_QUERY_TAIL_INHERITED(BaseClass) \ + return BaseClass::QueryInterface(aIID, aInstancePtr); \ + } + +#define IMPL_IUNKNOWN_QUERY_IFACE(Iface) \ + if (aIID == IID_##Iface) { \ + *aInstancePtr = static_cast<Iface*>(this); \ + AddRef(); \ + return S_OK; \ + } + +#define IMPL_IUNKNOWN_QUERY_IFACE_AMBIGIOUS(Iface, aResolveIface) \ + if (aIID == IID_##Iface) { \ + *aInstancePtr = static_cast<Iface*>(static_cast<aResolveIface*>(this)); \ + AddRef(); \ + return S_OK; \ + } + +#define IMPL_IUNKNOWN_QUERY_CLASS(Class) \ + hr = Class::QueryInterface(aIID, aInstancePtr); \ + if (SUCCEEDED(hr)) return hr; + +#define IMPL_IUNKNOWN_QUERY_CLASS_COND(Class, Cond) \ + if (Cond) { \ + hr = Class::QueryInterface(aIID, aInstancePtr); \ + if (SUCCEEDED(hr)) return hr; \ + } + +#define IMPL_IUNKNOWN_QUERY_AGGR_COND(Member, Cond) \ + if (Cond) { \ + hr = Member->QueryInterface(aIID, aInstancePtr); \ + if (SUCCEEDED(hr)) return hr; \ + } + +#define IMPL_IUNKNOWN1(Class, I1) \ + IMPL_IUNKNOWN_QUERY_HEAD(Class) \ + IMPL_IUNKNOWN_QUERY_IFACE(I1); \ + IMPL_IUNKNOWN_QUERY_IFACE(IUnknown); \ + IMPL_IUNKNOWN_QUERY_TAIL + +#define IMPL_IUNKNOWN2(Class, I1, I2) \ + IMPL_IUNKNOWN_QUERY_HEAD(Class) \ + IMPL_IUNKNOWN_QUERY_IFACE(I1); \ + IMPL_IUNKNOWN_QUERY_IFACE(I2); \ + IMPL_IUNKNOWN_QUERY_IFACE_AMBIGIOUS(IUnknown, I1); \ + IMPL_IUNKNOWN_QUERY_TAIL + +#define IMPL_IUNKNOWN_INHERITED1(Class, Super0, Super1) \ + IMPL_IUNKNOWN_QUERY_HEAD(Class) \ + IMPL_IUNKNOWN_QUERY_CLASS(Super1); \ + IMPL_IUNKNOWN_QUERY_TAIL_INHERITED(Super0) + +#define IMPL_IUNKNOWN_INHERITED2(Class, Super0, Super1, Super2) \ + IMPL_IUNKNOWN_QUERY_HEAD(Class) \ + IMPL_IUNKNOWN_QUERY_CLASS(Super1); \ + IMPL_IUNKNOWN_QUERY_CLASS(Super2); \ + IMPL_IUNKNOWN_QUERY_TAIL_INHERITED(Super0) + +/** + * Overrides AddRef and Release to call a specific base class. + * If you are inheriting a single class (e.g. to override some methods), you + * shouldn't need to use this. However, if you are inheriting from a COM + * implementation and also inheriting additional COM interfaces, you will need + * to use this to specify which base implements reference counting. + */ +#define IMPL_IUNKNOWN_REFCOUNTING_INHERITED(BaseClass) \ + public: \ + ULONG STDMETHODCALLTYPE AddRef() override { return BaseClass::AddRef(); } \ + ULONG STDMETHODCALLTYPE Release() override { return BaseClass::Release(); } + +namespace mozilla { +namespace a11y { + +/** + * Converts nsresult to HRESULT. + */ +HRESULT GetHRESULT(nsresult aResult); + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/windows/msaa/LazyInstantiator.cpp b/accessible/windows/msaa/LazyInstantiator.cpp new file mode 100644 index 0000000000..5f8ae9a8a4 --- /dev/null +++ b/accessible/windows/msaa/LazyInstantiator.cpp @@ -0,0 +1,779 @@ +/* -*- 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 "LazyInstantiator.h" + +#include "MainThreadUtils.h" +#include "mozilla/a11y/LocalAccessible.h" +#include "mozilla/a11y/Compatibility.h" +#include "mozilla/a11y/Platform.h" +#include "mozilla/Assertions.h" +#include "mozilla/mscom/ProcessRuntime.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "MsaaRootAccessible.h" +#include "nsAccessibilityService.h" +#include "nsWindowsHelpers.h" +#include "nsCOMPtr.h" +#include "nsIFile.h" +#include "nsXPCOM.h" +#include "WinUtils.h" +#include "prenv.h" + +#include <oaidl.h> + +#if !defined(STATE_SYSTEM_NORMAL) +# define STATE_SYSTEM_NORMAL (0) +#endif // !defined(STATE_SYSTEM_NORMAL) + +#define DLL_BLOCKLIST_ENTRY(name, ...) {L##name, __VA_ARGS__}, +#define DLL_BLOCKLIST_STRING_TYPE const wchar_t* +#include "mozilla/WindowsDllBlocklistA11yDefs.h" + +namespace mozilla { +namespace a11y { + +static const wchar_t kLazyInstantiatorProp[] = + L"mozilla::a11y::LazyInstantiator"; + +Maybe<bool> LazyInstantiator::sShouldBlockUia; + +/* static */ +already_AddRefed<IAccessible> LazyInstantiator::GetRootAccessible(HWND aHwnd) { + RefPtr<IAccessible> result; + // At this time we only want to check whether the acc service is running. We + // don't actually want to create the acc service yet. + if (!GetAccService()) { + // There must only be one LazyInstantiator per HWND. + // To track this, we set the kLazyInstantiatorProp on the HWND with a + // pointer to an existing instance. We only create a new LazyInstatiator if + // that prop has not already been set. + LazyInstantiator* existingInstantiator = + reinterpret_cast<LazyInstantiator*>( + ::GetProp(aHwnd, kLazyInstantiatorProp)); + + if (existingInstantiator) { + // Temporarily disable blind aggregation until we know that we have been + // marshaled. See EnableBlindAggregation for more information. + existingInstantiator->mAllowBlindAggregation = false; + result = existingInstantiator; + return result.forget(); + } + + // a11y is not running yet, there are no existing LazyInstantiators for this + // HWND, so create a new one and return it as a surrogate for the root + // accessible. + result = new LazyInstantiator(aHwnd); + return result.forget(); + } + + // a11y is running, so we just resolve the real root accessible. + a11y::LocalAccessible* rootAcc = + widget::WinUtils::GetRootAccessibleForHWND(aHwnd); + if (!rootAcc) { + return nullptr; + } + + if (!rootAcc->IsRoot()) { + // rootAcc might represent a popup as opposed to a true root accessible. + // In that case we just use the regular LocalAccessible::GetNativeInterface. + rootAcc->GetNativeInterface(getter_AddRefs(result)); + return result.forget(); + } + + auto msaaRoot = + static_cast<MsaaRootAccessible*>(MsaaAccessible::GetFrom(rootAcc)); + // Subtle: msaaRoot might still be wrapped by a LazyInstantiator, but we + // don't need LazyInstantiator's capabilities anymore (since a11y is already + // running). We can bypass LazyInstantiator by retrieving the internal + // unknown (which is not wrapped by the LazyInstantiator) and then querying + // that for IID_IAccessible. + RefPtr<IUnknown> punk(msaaRoot->GetInternalUnknown()); + + MOZ_ASSERT(punk); + if (!punk) { + return nullptr; + } + + punk->QueryInterface(IID_IAccessible, getter_AddRefs(result)); + return result.forget(); +} + +/** + * When marshaling an interface, COM makes a whole bunch of QueryInterface + * calls to determine what kind of marshaling the interface supports. We need + * to handle those queries without instantiating a11y, so we temporarily + * disable passing through of QueryInterface calls to a11y. Once we know that + * COM is finished marshaling, we call EnableBlindAggregation to re-enable + * QueryInterface passthrough. + */ +/* static */ +void LazyInstantiator::EnableBlindAggregation(HWND aHwnd) { + if (GetAccService()) { + // The accessibility service is already running. That means that + // LazyInstantiator::GetRootAccessible returned the real MsaaRootAccessible, + // rather than returning a LazyInstantiator with blind aggregation disabled. + // Thus, we have nothing to do here. + return; + } + + LazyInstantiator* existingInstantiator = reinterpret_cast<LazyInstantiator*>( + ::GetProp(aHwnd, kLazyInstantiatorProp)); + + if (!existingInstantiator) { + return; + } + + existingInstantiator->mAllowBlindAggregation = true; +} + +LazyInstantiator::LazyInstantiator(HWND aHwnd) + : mHwnd(aHwnd), + mAllowBlindAggregation(false), + mWeakMsaaRoot(nullptr), + mWeakAccessible(nullptr), + mWeakDispatch(nullptr) { + MOZ_ASSERT(aHwnd); + // Assign ourselves as the designated LazyInstantiator for aHwnd + DebugOnly<BOOL> setPropOk = + ::SetProp(aHwnd, kLazyInstantiatorProp, reinterpret_cast<HANDLE>(this)); + MOZ_ASSERT(setPropOk); +} + +LazyInstantiator::~LazyInstantiator() { + if (mRealRootUnk) { + // Disconnect ourselves from the root accessible. + RefPtr<IUnknown> dummy(mWeakMsaaRoot->Aggregate(nullptr)); + } + + ClearProp(); +} + +void LazyInstantiator::ClearProp() { + // Remove ourselves as the designated LazyInstantiator for mHwnd + DebugOnly<HANDLE> removedProp = ::RemoveProp(mHwnd, kLazyInstantiatorProp); + MOZ_ASSERT(!removedProp || + reinterpret_cast<LazyInstantiator*>(removedProp.value) == this); +} + +/** + * Get the process id of a remote (out-of-process) MSAA/IA2 client. + */ +DWORD LazyInstantiator::GetRemoteMsaaClientPid() { + nsAutoHandle callingThread( + ::OpenThread(THREAD_QUERY_LIMITED_INFORMATION, FALSE, + mscom::ProcessRuntime::GetClientThreadId())); + if (!callingThread) { + return 0; + } + return ::GetProcessIdOfThread(callingThread); +} + +/** + * This is the blocklist for known "bad" remote clients that instantiate a11y. + */ +static const char* gBlockedRemoteClients[] = { + "tbnotifier.exe", // Ask.com Toolbar, bug 1453876 + "flow.exe", // Conexant Flow causes performance issues, bug 1569712 + "rtop_bg.exe", // ByteFence Anti-Malware, bug 1713383 + "osk.exe", // Windows On-Screen Keyboard, bug 1424505 +}; + +/** + * Check for the presence of any known "bad" injected DLLs that may be trying + * to instantiate a11y. + * + * @return true to block a11y instantiation, otherwise false to continue + */ +bool LazyInstantiator::IsBlockedInjection() { + // Check debugging options see if we should disable the blocklist. + if (PR_GetEnv("MOZ_DISABLE_ACCESSIBLE_BLOCKLIST")) { + return false; + } + + for (size_t index = 0, len = ArrayLength(gBlockedInprocDlls); index < len; + ++index) { + const DllBlockInfo& blockedDll = gBlockedInprocDlls[index]; + HMODULE module = ::GetModuleHandleW(blockedDll.mName); + if (!module) { + // This dll isn't loaded. + continue; + } + + LauncherResult<ModuleVersion> version = GetModuleVersion(module); + return version.isOk() && blockedDll.IsVersionBlocked(version.unwrap()); + } + + return false; +} + +/** + * Given a remote client's process ID, determine whether we should proceed with + * a11y instantiation. This is where telemetry should be gathered and any + * potential blocking of unwanted a11y clients should occur. + * + * @return true if we should instantiate a11y + */ +bool LazyInstantiator::ShouldInstantiate(const DWORD aClientPid) { + a11y::SetInstantiator(aClientPid); + + nsCOMPtr<nsIFile> clientExe; + if (!a11y::GetInstantiator(getter_AddRefs(clientExe))) { + return true; + } + + nsresult rv; + if (!PR_GetEnv("MOZ_DISABLE_ACCESSIBLE_BLOCKLIST")) { + // Debugging option is not present, so check blocklist. + nsAutoString leafName; + rv = clientExe->GetLeafName(leafName); + if (NS_SUCCEEDED(rv)) { + for (size_t i = 0, len = ArrayLength(gBlockedRemoteClients); i < len; + ++i) { + if (leafName.EqualsIgnoreCase(gBlockedRemoteClients[i])) { + // If client exe is in our blocklist, do not instantiate. + return false; + } + } + } + } + + return true; +} + +/** + * Determine whether we should proceed with a11y instantiation, considering the + * various different types of clients. + */ +bool LazyInstantiator::ShouldInstantiate() { + if (Compatibility::IsA11ySuppressedForClipboardCopy()) { + // Bug 1774285: Windows Suggested Actions (introduced in Windows 11 22H2) + // walks the entire a11y tree using UIA whenever anything is copied to the + // clipboard. This causes an unacceptable hang, particularly when the cache + // is disabled. Don't allow a11y to be instantiated by this. + return false; + } + if (DWORD pid = GetRemoteMsaaClientPid()) { + return ShouldInstantiate(pid); + } + if (Compatibility::HasKnownNonUiaConsumer()) { + // We detected a known in-process client. + return true; + } + // UIA client detection can be expensive, so we cache the result. See the + // header comment for ResetUiaDetectionCache() for details. + if (sShouldBlockUia.isNothing()) { + // Unlike MSAA, we can't tell which specific UIA client is querying us right + // now. We can only determine which clients have tried querying us. + // Therefore, we must check all of them. + AutoTArray<DWORD, 1> uiaPids; + Compatibility::GetUiaClientPids(uiaPids); + if (uiaPids.IsEmpty()) { + // No UIA clients, so don't block UIA. However, we might block for + // non-UIA clients below. + sShouldBlockUia = Some(false); + } else { + for (const DWORD pid : uiaPids) { + if (ShouldInstantiate(pid)) { + sShouldBlockUia = Some(false); + return true; + } + } + // We didn't return in the loop above, so there are only blocked UIA + // clients. + sShouldBlockUia = Some(true); + } + } + if (*sShouldBlockUia) { + return false; + } + if (IsBlockedInjection()) { + return false; + } + return true; +} + +MsaaRootAccessible* LazyInstantiator::ResolveMsaaRoot() { + LocalAccessible* acc = widget::WinUtils::GetRootAccessibleForHWND(mHwnd); + if (!acc || !acc->IsRoot()) { + return nullptr; + } + + RefPtr<IAccessible> ia; + acc->GetNativeInterface(getter_AddRefs(ia)); + return static_cast<MsaaRootAccessible*>(ia.get()); +} + +/** + * With COM aggregation, the aggregated inner object usually delegates its + * reference counting to the outer object. In other words, we would expect + * mRealRootUnk to delegate its AddRef() and Release() to this LazyInstantiator. + * + * This scheme will not work in our case because the RootAccessibleWrap is + * cycle-collected! + * + * Instead, once a LazyInstantiator aggregates a RootAccessibleWrap, we transfer + * our strong references into mRealRootUnk. Any future calls to AddRef or + * Release now operate on mRealRootUnk instead of our intrinsic reference + * count. This is a bit strange, but it is the only way for these objects to + * share their reference count in a way that is safe for cycle collection. + * + * How do we know when it is safe to destroy ourselves? In + * LazyInstantiator::Release, we examine the result of mRealRootUnk->Release(). + * If mRealRootUnk's resulting refcount is 1, then we know that the only + * remaining reference to mRealRootUnk is the mRealRootUnk reference itself (and + * thus nobody else holds references to either this or mRealRootUnk). Therefore + * we may now delete ourselves. + */ +void LazyInstantiator::TransplantRefCnt() { + MOZ_ASSERT(mRefCnt > 0); + MOZ_ASSERT(mRealRootUnk); + + while (mRefCnt > 0) { + mRealRootUnk.get()->AddRef(); + --mRefCnt; + } +} + +HRESULT +LazyInstantiator::MaybeResolveRoot() { + if (!NS_IsMainThread()) { + MOZ_ASSERT_UNREACHABLE("Called on a background thread!"); + // Bug 1814780: This should never happen, since a caller should only be able + // to get this via AccessibleObjectFromWindow/AccessibleObjectFromEvent or + // WM_GETOBJECT/ObjectFromLresult, which should marshal any calls on + // a background thread to the main thread. Nevertheless, Windows sometimes + // calls QueryInterface from a background thread! To avoid crashes, fail + // gracefully here. + return RPC_E_WRONG_THREAD; + } + + if (mWeakAccessible) { + return S_OK; + } + + if (GetAccService() || ShouldInstantiate()) { + mWeakMsaaRoot = ResolveMsaaRoot(); + if (!mWeakMsaaRoot) { + return E_POINTER; + } + + // Wrap ourselves around the root accessible wrap + mRealRootUnk = mWeakMsaaRoot->Aggregate(static_cast<IAccessible*>(this)); + if (!mRealRootUnk) { + return E_FAIL; + } + + // Move our strong references into the root accessible (see the comments + // above TransplantRefCnt for explanation). + TransplantRefCnt(); + + // Now obtain mWeakAccessible which we use to forward our incoming calls + // to the real accesssible. + HRESULT hr = + mRealRootUnk->QueryInterface(IID_IAccessible, (void**)&mWeakAccessible); + if (FAILED(hr)) { + return hr; + } + + // mWeakAccessible is weak, so don't hold a strong ref + mWeakAccessible->Release(); + + // Now that a11y is running, we don't need to remain registered with our + // HWND anymore. + ClearProp(); + + return S_OK; + } + + return E_FAIL; +} + +#define RESOLVE_ROOT \ + { \ + HRESULT hr = MaybeResolveRoot(); \ + if (FAILED(hr)) { \ + return hr; \ + } \ + } + +IMPL_IUNKNOWN_QUERY_HEAD(LazyInstantiator) +IMPL_IUNKNOWN_QUERY_IFACE_AMBIGIOUS(IUnknown, IAccessible) +IMPL_IUNKNOWN_QUERY_IFACE(IAccessible) +IMPL_IUNKNOWN_QUERY_IFACE(IDispatch) +IMPL_IUNKNOWN_QUERY_IFACE(IServiceProvider) +// See EnableBlindAggregation for comments. +if (!mAllowBlindAggregation) { + return E_NOINTERFACE; +} + +if (aIID == IID_IAccIdentity) { + return E_NOINTERFACE; +} +// If the client queries for an interface that LazyInstantiator does not +// intrinsically support, then we must resolve the root accessible and pass +// on the QueryInterface call to mRealRootUnk. +RESOLVE_ROOT +IMPL_IUNKNOWN_QUERY_TAIL_AGGREGATED(mRealRootUnk) + +ULONG +LazyInstantiator::AddRef() { + // Always delegate refcounting to mRealRootUnk when it exists + if (mRealRootUnk) { + return mRealRootUnk.get()->AddRef(); + } + + return ++mRefCnt; +} + +ULONG +LazyInstantiator::Release() { + ULONG result; + + // Always delegate refcounting to mRealRootUnk when it exists + if (mRealRootUnk) { + result = mRealRootUnk.get()->Release(); + if (result == 1) { + // mRealRootUnk is the only strong reference left, so nothing else holds + // a strong reference to us. Drop result to zero so that we destroy + // ourselves (See the comments above LazyInstantiator::TransplantRefCnt + // for more info). + --result; + } + } else { + result = --mRefCnt; + } + + if (!result) { + delete this; + } + return result; +} + +/** + * Create a standard IDispatch implementation. mStdDispatch will translate any + * IDispatch::Invoke calls into real IAccessible calls. + */ +HRESULT +LazyInstantiator::ResolveDispatch() { + if (mWeakDispatch) { + return S_OK; + } + + // Extract IAccessible's type info + RefPtr<ITypeInfo> accTypeInfo = MsaaAccessible::GetTI(LOCALE_USER_DEFAULT); + if (!accTypeInfo) { + return E_UNEXPECTED; + } + + // Now create the standard IDispatch for IAccessible + HRESULT hr = ::CreateStdDispatch(static_cast<IAccessible*>(this), + static_cast<IAccessible*>(this), accTypeInfo, + getter_AddRefs(mStdDispatch)); + if (FAILED(hr)) { + return hr; + } + + hr = mStdDispatch->QueryInterface(IID_IDispatch, (void**)&mWeakDispatch); + if (FAILED(hr)) { + return hr; + } + + // WEAK reference + mWeakDispatch->Release(); + return S_OK; +} + +#define RESOLVE_IDISPATCH \ + { \ + HRESULT hr = ResolveDispatch(); \ + if (FAILED(hr)) { \ + return hr; \ + } \ + } + +/** + * The remaining methods implement IDispatch, IAccessible, and IServiceProvider, + * lazily resolving the real a11y objects and passing the call through. + */ + +HRESULT +LazyInstantiator::GetTypeInfoCount(UINT* pctinfo) { + RESOLVE_IDISPATCH; + return mWeakDispatch->GetTypeInfoCount(pctinfo); +} + +HRESULT +LazyInstantiator::GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo** ppTInfo) { + RESOLVE_IDISPATCH; + return mWeakDispatch->GetTypeInfo(iTInfo, lcid, ppTInfo); +} + +HRESULT +LazyInstantiator::GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames, UINT cNames, + LCID lcid, DISPID* rgDispId) { + RESOLVE_IDISPATCH; + return mWeakDispatch->GetIDsOfNames(riid, rgszNames, cNames, lcid, rgDispId); +} + +HRESULT +LazyInstantiator::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, + WORD wFlags, DISPPARAMS* pDispParams, + VARIANT* pVarResult, EXCEPINFO* pExcepInfo, + UINT* puArgErr) { + RESOLVE_IDISPATCH; + return mWeakDispatch->Invoke(dispIdMember, riid, lcid, wFlags, pDispParams, + pVarResult, pExcepInfo, puArgErr); +} + +HRESULT +LazyInstantiator::get_accParent(IDispatch** ppdispParent) { + if (!mWeakAccessible) { + // If we'd resolve the root right now this would be the codepath we'd end + // up in anyway. So we might as well return it here. + return ::CreateStdAccessibleObject(mHwnd, OBJID_WINDOW, IID_IAccessible, + (void**)ppdispParent); + } + RESOLVE_ROOT; + return mWeakAccessible->get_accParent(ppdispParent); +} + +HRESULT +LazyInstantiator::get_accChildCount(long* pcountChildren) { + if (!pcountChildren) { + return E_INVALIDARG; + } + + RESOLVE_ROOT; + return mWeakAccessible->get_accChildCount(pcountChildren); +} + +HRESULT +LazyInstantiator::get_accChild(VARIANT varChild, IDispatch** ppdispChild) { + if (!ppdispChild) { + return E_INVALIDARG; + } + + if (V_VT(&varChild) == VT_I4 && V_I4(&varChild) == CHILDID_SELF) { + RefPtr<IDispatch> disp(this); + disp.forget(ppdispChild); + return S_OK; + } + + RESOLVE_ROOT; + return mWeakAccessible->get_accChild(varChild, ppdispChild); +} + +HRESULT +LazyInstantiator::get_accName(VARIANT varChild, BSTR* pszName) { + if (!pszName) { + return E_INVALIDARG; + } + + RESOLVE_ROOT; + return mWeakAccessible->get_accName(varChild, pszName); +} + +HRESULT +LazyInstantiator::get_accValue(VARIANT varChild, BSTR* pszValue) { + if (!pszValue) { + return E_INVALIDARG; + } + + RESOLVE_ROOT; + return mWeakAccessible->get_accValue(varChild, pszValue); +} + +HRESULT +LazyInstantiator::get_accDescription(VARIANT varChild, BSTR* pszDescription) { + if (!pszDescription) { + return E_INVALIDARG; + } + + RESOLVE_ROOT; + return mWeakAccessible->get_accDescription(varChild, pszDescription); +} + +HRESULT +LazyInstantiator::get_accRole(VARIANT varChild, VARIANT* pvarRole) { + if (!pvarRole) { + return E_INVALIDARG; + } + + if (V_VT(&varChild) == VT_I4 && V_I4(&varChild) == CHILDID_SELF) { + V_VT(pvarRole) = VT_I4; + V_I4(pvarRole) = ROLE_SYSTEM_APPLICATION; + return S_OK; + } + + RESOLVE_ROOT; + return mWeakAccessible->get_accRole(varChild, pvarRole); +} + +HRESULT +LazyInstantiator::get_accState(VARIANT varChild, VARIANT* pvarState) { + if (!pvarState) { + return E_INVALIDARG; + } + + RESOLVE_ROOT; + return mWeakAccessible->get_accState(varChild, pvarState); +} + +HRESULT +LazyInstantiator::get_accHelp(VARIANT varChild, BSTR* pszHelp) { + return E_NOTIMPL; +} + +HRESULT +LazyInstantiator::get_accHelpTopic(BSTR* pszHelpFile, VARIANT varChild, + long* pidTopic) { + return E_NOTIMPL; +} + +HRESULT +LazyInstantiator::get_accKeyboardShortcut(VARIANT varChild, + BSTR* pszKeyboardShortcut) { + if (!pszKeyboardShortcut) { + return E_INVALIDARG; + } + + RESOLVE_ROOT; + return mWeakAccessible->get_accKeyboardShortcut(varChild, + pszKeyboardShortcut); +} + +HRESULT +LazyInstantiator::get_accFocus(VARIANT* pvarChild) { + if (!pvarChild) { + return E_INVALIDARG; + } + + RESOLVE_ROOT; + return mWeakAccessible->get_accFocus(pvarChild); +} + +HRESULT +LazyInstantiator::get_accSelection(VARIANT* pvarChildren) { + if (!pvarChildren) { + return E_INVALIDARG; + } + + RESOLVE_ROOT; + return mWeakAccessible->get_accSelection(pvarChildren); +} + +HRESULT +LazyInstantiator::get_accDefaultAction(VARIANT varChild, + BSTR* pszDefaultAction) { + if (!pszDefaultAction) { + return E_INVALIDARG; + } + + RESOLVE_ROOT; + return mWeakAccessible->get_accDefaultAction(varChild, pszDefaultAction); +} + +HRESULT +LazyInstantiator::accSelect(long flagsSelect, VARIANT varChild) { + RESOLVE_ROOT; + return mWeakAccessible->accSelect(flagsSelect, varChild); +} + +HRESULT +LazyInstantiator::accLocation(long* pxLeft, long* pyTop, long* pcxWidth, + long* pcyHeight, VARIANT varChild) { + RESOLVE_ROOT; + return mWeakAccessible->accLocation(pxLeft, pyTop, pcxWidth, pcyHeight, + varChild); +} + +HRESULT +LazyInstantiator::accNavigate(long navDir, VARIANT varStart, + VARIANT* pvarEndUpAt) { + if (!pvarEndUpAt) { + return E_INVALIDARG; + } + + RESOLVE_ROOT; + return mWeakAccessible->accNavigate(navDir, varStart, pvarEndUpAt); +} + +HRESULT +LazyInstantiator::accHitTest(long xLeft, long yTop, VARIANT* pvarChild) { + if (!pvarChild) { + return E_INVALIDARG; + } + + RESOLVE_ROOT; + return mWeakAccessible->accHitTest(xLeft, yTop, pvarChild); +} + +HRESULT +LazyInstantiator::accDoDefaultAction(VARIANT varChild) { + RESOLVE_ROOT; + return mWeakAccessible->accDoDefaultAction(varChild); +} + +HRESULT +LazyInstantiator::put_accName(VARIANT varChild, BSTR szName) { + return E_NOTIMPL; +} + +HRESULT +LazyInstantiator::put_accValue(VARIANT varChild, BSTR szValue) { + return E_NOTIMPL; +} + +static const GUID kUnsupportedServices[] = { + // clang-format off + // Unknown, queried by Windows on devices with touch screens or similar devices + // connected. + {0x33f139ee, 0xe509, 0x47f7, {0xbf, 0x39, 0x83, 0x76, 0x44, 0xf7, 0x45, 0x76}}, + // Unknown, queried by Windows + {0xFDA075CF, 0x7C8B, 0x498C, { 0xB5, 0x14, 0xA9, 0xCB, 0x52, 0x1B, 0xBF, 0xB4 }}, + // Unknown, queried by Windows + {0x8EDAA462, 0x21F4, 0x4C87, { 0xA0, 0x12, 0xB3, 0xCD, 0xA3, 0xAB, 0x01, 0xFC }}, + // Unknown, queried by Windows + {0xacd46652, 0x829d, 0x41cb, { 0xa5, 0xfc, 0x17, 0xac, 0xf4, 0x36, 0x61, 0xac }}, + // SID_IsUIAutomationObject (undocumented), queried by Windows + {0xb96fdb85, 0x7204, 0x4724, { 0x84, 0x2b, 0xc7, 0x05, 0x9d, 0xed, 0xb9, 0xd0 }}, + // IIS_IsOleaccProxy (undocumented), queried by Windows + {0x902697FA, 0x80E4, 0x4560, {0x80, 0x2A, 0xA1, 0x3F, 0x22, 0xA6, 0x47, 0x09}}, + // IID_IHTMLElement, queried by JAWS + {0x3050F1FF, 0x98B5, 0x11CF, {0xBB, 0x82, 0x00, 0xAA, 0x00, 0xBD, 0xCE, 0x0B}} + // clang-format on +}; + +HRESULT +LazyInstantiator::QueryService(REFGUID aServiceId, REFIID aServiceIid, + void** aOutInterface) { + if (!aOutInterface) { + return E_INVALIDARG; + } + + for (const GUID& unsupportedService : kUnsupportedServices) { + if (aServiceId == unsupportedService) { + return E_NOINTERFACE; + } + } + + *aOutInterface = nullptr; + + RESOLVE_ROOT; + + RefPtr<IServiceProvider> servProv; + HRESULT hr = mRealRootUnk->QueryInterface(IID_IServiceProvider, + getter_AddRefs(servProv)); + if (FAILED(hr)) { + return hr; + } + + return servProv->QueryService(aServiceId, aServiceIid, aOutInterface); +} + +} // namespace a11y +} // namespace mozilla diff --git a/accessible/windows/msaa/LazyInstantiator.h b/accessible/windows/msaa/LazyInstantiator.h new file mode 100644 index 0000000000..00fa4ba6ed --- /dev/null +++ b/accessible/windows/msaa/LazyInstantiator.h @@ -0,0 +1,142 @@ +/* -*- 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/. */ + +#ifndef mozilla_a11y_LazyInstantiator_h +#define mozilla_a11y_LazyInstantiator_h + +#include "IUnknownImpl.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" +#include "nsString.h" + +#include <oleacc.h> + +class nsIFile; + +namespace mozilla { +namespace a11y { + +class MsaaRootAccessible; + +/** + * LazyInstantiator is an IAccessible that initially acts as a placeholder. + * The a11y service is not actually started until two conditions are met: + * + * (1) A method is called on the LazyInstantiator that would require a11y + * services in order to fulfill; and + * (2) LazyInstantiator::ShouldInstantiate returns true. + */ +class LazyInstantiator final : public IAccessible, public IServiceProvider { + public: + [[nodiscard]] static already_AddRefed<IAccessible> GetRootAccessible( + HWND aHwnd); + static void EnableBlindAggregation(HWND aHwnd); + + // IUnknown + STDMETHODIMP QueryInterface(REFIID aIid, void** aOutInterface) override; + STDMETHODIMP_(ULONG) AddRef() override; + STDMETHODIMP_(ULONG) Release() override; + + // IDispatch + STDMETHODIMP GetTypeInfoCount(UINT* pctinfo) override; + STDMETHODIMP GetTypeInfo(UINT iTInfo, LCID lcid, + ITypeInfo** ppTInfo) override; + STDMETHODIMP GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames, UINT cNames, + LCID lcid, DISPID* rgDispId) override; + STDMETHODIMP Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, + DISPPARAMS* pDispParams, VARIANT* pVarResult, + EXCEPINFO* pExcepInfo, UINT* puArgErr) override; + + // IAccessible + STDMETHODIMP get_accParent(IDispatch** ppdispParent) override; + STDMETHODIMP get_accChildCount(long* pcountChildren) override; + STDMETHODIMP get_accChild(VARIANT varChild, IDispatch** ppdispChild) override; + STDMETHODIMP get_accName(VARIANT varChild, BSTR* pszName) override; + STDMETHODIMP get_accValue(VARIANT varChild, BSTR* pszValue) override; + STDMETHODIMP get_accDescription(VARIANT varChild, + BSTR* pszDescription) override; + STDMETHODIMP get_accRole(VARIANT varChild, VARIANT* pvarRole) override; + STDMETHODIMP get_accState(VARIANT varChild, VARIANT* pvarState) override; + STDMETHODIMP get_accHelp(VARIANT varChild, BSTR* pszHelp) override; + STDMETHODIMP get_accHelpTopic(BSTR* pszHelpFile, VARIANT varChild, + long* pidTopic) override; + STDMETHODIMP get_accKeyboardShortcut(VARIANT varChild, + BSTR* pszKeyboardShortcut) override; + STDMETHODIMP get_accFocus(VARIANT* pvarChild) override; + STDMETHODIMP get_accSelection(VARIANT* pvarChildren) override; + STDMETHODIMP get_accDefaultAction(VARIANT varChild, + BSTR* pszDefaultAction) override; + STDMETHODIMP accSelect(long flagsSelect, VARIANT varChild) override; + STDMETHODIMP accLocation(long* pxLeft, long* pyTop, long* pcxWidth, + long* pcyHeight, VARIANT varChild) override; + STDMETHODIMP accNavigate(long navDir, VARIANT varStart, + VARIANT* pvarEndUpAt) override; + STDMETHODIMP accHitTest(long xLeft, long yTop, VARIANT* pvarChild) override; + STDMETHODIMP accDoDefaultAction(VARIANT varChild) override; + STDMETHODIMP put_accName(VARIANT varChild, BSTR szName) override; + STDMETHODIMP put_accValue(VARIANT varChild, BSTR szValue) override; + + // IServiceProvider + STDMETHODIMP QueryService(REFGUID aServiceId, REFIID aServiceIid, + void** aOutInterface) override; + + /** + * We cache the result of UIA detection because it could be expensive if a + * client repeatedly queries us. This function is called to reset that cache + * when one of our windows comes to the foreground. If there is a new UIA + * client that isn't blocked, instantiation will subsequently be allowed. The + * hope is that a user will probably need to switch apps in order to start a + * new client. + */ + static void ResetUiaDetectionCache() { sShouldBlockUia = Nothing(); } + + private: + explicit LazyInstantiator(HWND aHwnd); + ~LazyInstantiator(); + + bool IsBlockedInjection(); + bool ShouldInstantiate(const DWORD aClientPid); + bool ShouldInstantiate(); + + DWORD GetRemoteMsaaClientPid(); + + /** + * @return S_OK if we have a valid mRealRoot to invoke methods on + */ + HRESULT MaybeResolveRoot(); + + /** + * @return S_OK if we have a valid mWeakDispatch to invoke methods on + */ + HRESULT ResolveDispatch(); + + MsaaRootAccessible* ResolveMsaaRoot(); + void TransplantRefCnt(); + void ClearProp(); + + private: + mozilla::a11y::AutoRefCnt mRefCnt; + HWND mHwnd; + bool mAllowBlindAggregation; + RefPtr<IUnknown> mRealRootUnk; + RefPtr<IUnknown> mStdDispatch; + /** + * mWeakMsaaRoot, mWeakAccessible and mWeakDispatch are weak because they + * are interfaces that come from objects that we aggregate. Aggregated object + * interfaces share refcount methods with ours, so if we were to hold strong + * references to them, we would be holding strong references to ourselves, + * creating a cycle. + */ + MsaaRootAccessible* mWeakMsaaRoot; + IAccessible* mWeakAccessible; + IDispatch* mWeakDispatch; + static Maybe<bool> sShouldBlockUia; +}; + +} // namespace a11y +} // namespace mozilla + +#endif // mozilla_a11y_LazyInstantiator_h diff --git a/accessible/windows/msaa/MsaaAccessible.cpp b/accessible/windows/msaa/MsaaAccessible.cpp new file mode 100644 index 0000000000..702f8341dc --- /dev/null +++ b/accessible/windows/msaa/MsaaAccessible.cpp @@ -0,0 +1,1368 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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 "EnumVariant.h" +#include "ia2AccessibleApplication.h" +#include "ia2AccessibleHypertext.h" +#include "ia2AccessibleImage.h" +#include "ia2AccessibleTable.h" +#include "ia2AccessibleTableCell.h" +#include "LocalAccessible-inl.h" +#include "mozilla/a11y/AccessibleWrap.h" +#include "mozilla/a11y/Compatibility.h" +#include "mozilla/a11y/DocAccessibleParent.h" +#include "MsaaAccessible.h" +#include "MsaaDocAccessible.h" +#include "MsaaRootAccessible.h" +#include "MsaaXULMenuAccessible.h" +#include "nsEventMap.h" +#include "nsViewManager.h" +#include "nsWinUtils.h" +#include "Relation.h" +#include "sdnAccessible.h" +#include "sdnTextAccessible.h" +#include "HyperTextAccessible-inl.h" +#include "ServiceProvider.h" +#include "Statistics.h" +#include "ARIAMap.h" +#include "mozilla/PresShell.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +static const VARIANT kVarChildIdSelf = {{{VT_I4}}}; + +// Used internally to safely get an MsaaAccessible from a COM pointer provided +// to us by a client. +static const GUID IID_MsaaAccessible = { + /* a94aded3-1a9c-4afc-a32c-d6b5c010046b */ + 0xa94aded3, + 0x1a9c, + 0x4afc, + {0xa3, 0x2c, 0xd6, 0xb5, 0xc0, 0x10, 0x04, 0x6b}}; + +MsaaIdGenerator MsaaAccessible::sIDGen; +ITypeInfo* MsaaAccessible::gTypeInfo = nullptr; + +/* static */ +MsaaAccessible* MsaaAccessible::Create(Accessible* aAcc) { + // This should only ever be called in the parent process. + MOZ_ASSERT(XRE_IsParentProcess()); + // The order of some of these is important! For example, when isRoot is true, + // IsDoc will also be true, so we must check IsRoot first. IsTable/Cell and + // IsHyperText are a similar case. + if (aAcc->IsRoot()) { + MOZ_ASSERT(aAcc->IsLocal()); + return new MsaaRootAccessible(aAcc); + } + if (aAcc->IsDoc()) { + return new MsaaDocAccessible(aAcc); + } + if (aAcc->IsTable()) { + return new ia2AccessibleTable(aAcc); + } + if (aAcc->IsTableCell()) { + return new ia2AccessibleTableCell(aAcc); + } + if (aAcc->IsApplication()) { + MOZ_ASSERT(aAcc->IsLocal()); + return new ia2AccessibleApplication(aAcc); + } + if (aAcc->IsImage()) { + return new ia2AccessibleImage(aAcc); + } + if (LocalAccessible* localAcc = aAcc->AsLocal()) { + if (localAcc->GetContent() && + localAcc->GetContent()->IsXULElement(nsGkAtoms::menuitem)) { + return new MsaaXULMenuitemAccessible(aAcc); + } + } + if (aAcc->IsHyperText()) { + return new ia2AccessibleHypertext(aAcc); + } + return new MsaaAccessible(aAcc); +} + +MsaaAccessible::MsaaAccessible(Accessible* aAcc) : mAcc(aAcc), mID(kNoID) {} + +MsaaAccessible::~MsaaAccessible() { + MOZ_ASSERT(!mAcc, "MsaaShutdown wasn't called!"); + if (mID != kNoID) { + sIDGen.ReleaseID(WrapNotNull(this)); + } +} + +void MsaaAccessible::MsaaShutdown() { + // Accessibles can be shut down twice in some cases. If that happens, + // MsaaShutdown will also be called twice because AccessibleWrap holds + // the reference until its destructor is called; see the comments in + // AccessibleWrap::Shutdown. + if (!mAcc) { + return; + } + + if (mID != kNoID) { + auto doc = MsaaDocAccessible::GetFromOwned(mAcc); + MOZ_ASSERT(doc); + doc->RemoveID(mID); + } + + mAcc = nullptr; +} + +int32_t MsaaAccessible::GetChildIDFor(Accessible* aAccessible) { + // A child ID of the window is required, when we use NotifyWinEvent, + // so that the 3rd party application can call back and get the IAccessible + // the event occurred on. + + if (!aAccessible) { + return 0; + } + + auto doc = MsaaDocAccessible::GetFromOwned(aAccessible); + if (!doc) { + return 0; + } + + uint32_t* id = &MsaaAccessible::GetFrom(aAccessible)->mID; + if (*id != kNoID) return *id; + + *id = sIDGen.GetID(); + doc->AddID(*id, aAccessible); + + return *id; +} + +/* static */ +void MsaaAccessible::AssignChildIDTo(NotNull<sdnAccessible*> aSdnAcc) { + aSdnAcc->SetUniqueID(sIDGen.GetID()); +} + +/* static */ +void MsaaAccessible::ReleaseChildID(NotNull<sdnAccessible*> aSdnAcc) { + sIDGen.ReleaseID(aSdnAcc); +} + +HWND MsaaAccessible::GetHWNDFor(Accessible* aAccessible) { + if (!aAccessible) { + return nullptr; + } + + LocalAccessible* localAcc = aAccessible->AsLocal(); + if (!localAcc) { + RemoteAccessible* proxy = aAccessible->AsRemote(); + if (!proxy) { + return nullptr; + } + + // If window emulation is enabled, retrieve the emulated window from the + // containing document document proxy. + if (nsWinUtils::IsWindowEmulationStarted()) { + DocAccessibleParent* doc = proxy->Document(); + HWND hWnd = doc->GetEmulatedWindowHandle(); + if (hWnd) { + return hWnd; + } + } + + // Accessibles in child processes are said to have the HWND of the window + // their tab is within. Popups are always in the parent process, and so + // never proxied, which means this is basically correct. + LocalAccessible* outerDoc = proxy->OuterDocOfRemoteBrowser(); + if (!outerDoc) { + // In some cases, the outer document accessible may be unattached from its + // document at this point, if it is scheduled for removal. Do not assert + // in such case. An example: putting aria-hidden="true" on HTML:iframe + // element will destroy iframe's document asynchroniously, but + // the document may be a target of selection events until then, and thus + // it may attempt to deliever these events to MSAA clients. + return nullptr; + } + + return GetHWNDFor(outerDoc); + } + + DocAccessible* document = localAcc->Document(); + if (!document) return nullptr; + + // Popup lives in own windows, use its HWND until the popup window is + // hidden to make old JAWS versions work with collapsed comboboxes (see + // discussion in bug 379678). + nsIFrame* frame = localAcc->GetFrame(); + if (frame) { + nsIWidget* widget = frame->GetNearestWidget(); + if (widget && widget->IsVisible()) { + if (nsViewManager* vm = document->PresShellPtr()->GetViewManager()) { + nsCOMPtr<nsIWidget> rootWidget = vm->GetRootWidget(); + // Make sure the accessible belongs to popup. If not then use + // document HWND (which might be different from root widget in the + // case of window emulation). + if (rootWidget != widget) + return static_cast<HWND>(widget->GetNativeData(NS_NATIVE_WINDOW)); + } + } + } + + return static_cast<HWND>(document->GetNativeWindow()); +} + +void MsaaAccessible::FireWinEvent(Accessible* aTarget, uint32_t aEventType) { + MOZ_ASSERT(XRE_IsParentProcess()); + static_assert(sizeof(gWinEventMap) / sizeof(gWinEventMap[0]) == + nsIAccessibleEvent::EVENT_LAST_ENTRY, + "MSAA event map skewed"); + + if (aEventType == 0 || aEventType >= ArrayLength(gWinEventMap)) { + MOZ_ASSERT_UNREACHABLE("invalid event type"); + return; + } + + uint32_t winEvent = gWinEventMap[aEventType]; + if (!winEvent) return; + + int32_t childID = MsaaAccessible::GetChildIDFor(aTarget); + if (!childID) return; // Can't fire an event without a child ID + + HWND hwnd = GetHWNDFor(aTarget); + if (!hwnd) { + return; + } + + // Fire MSAA event for client area window. + ::NotifyWinEvent(winEvent, hwnd, OBJID_CLIENT, childID); +} + +AccessibleWrap* MsaaAccessible::LocalAcc() { + if (!mAcc || mAcc->IsRemote()) { + return nullptr; + } + auto acc = static_cast<AccessibleWrap*>(mAcc); + MOZ_ASSERT(!acc || !acc->IsDefunct(), + "mAcc defunct but MsaaShutdown wasn't called"); + return acc; +} + +/** + * This function is a helper for implementing IAccessible methods that accept + * a Child ID as a parameter. If the child ID is CHILDID_SELF, the function + * returns S_OK but a null *aOutInterface. Otherwise, *aOutInterface points + * to the resolved IAccessible. + * + * The CHILDID_SELF case is special because in that case we actually execute + * the implementation of the IAccessible method, whereas in the non-self case, + * we delegate the method call to that object for execution. + * + * A sample invocation of this would look like: + * + * RefPtr<IAccessible> accessible; + * HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); + * if (FAILED(hr)) { + * return hr; + * } + * + * if (accessible) { + * return accessible->get_accFoo(kVarChildIdSelf, pszName); + * } + * + * // Implementation for CHILDID_SELF case goes here + */ +HRESULT +MsaaAccessible::ResolveChild(const VARIANT& aVarChild, + IAccessible** aOutInterface) { + MOZ_ASSERT(aOutInterface); + *aOutInterface = nullptr; + + if (aVarChild.vt != VT_I4) { + return E_INVALIDARG; + } + + if (!mAcc) { + return CO_E_OBJNOTCONNECTED; + } + + if (aVarChild.lVal == CHILDID_SELF) { + return S_OK; + } + + bool isDefunct = false; + RefPtr<IAccessible> accessible = GetIAccessibleFor(aVarChild, &isDefunct); + if (!accessible) { + return E_INVALIDARG; + } + + if (isDefunct) { + return CO_E_OBJNOTCONNECTED; + } + + accessible.forget(aOutInterface); + return S_OK; +} + +static Accessible* GetAccessibleInSubtree(DocAccessible* aDoc, uint32_t aID) { + Accessible* child = MsaaDocAccessible::GetFrom(aDoc)->GetAccessibleByID(aID); + if (child) return child; + + uint32_t childDocCount = aDoc->ChildDocumentCount(); + for (uint32_t i = 0; i < childDocCount; i++) { + child = GetAccessibleInSubtree(aDoc->GetChildDocumentAt(i), aID); + if (child) return child; + } + + return nullptr; +} + +static Accessible* GetAccessibleInSubtree(DocAccessibleParent* aDoc, + uint32_t aID) { + Accessible* child = MsaaDocAccessible::GetFrom(aDoc)->GetAccessibleByID(aID); + if (child) { + return child; + } + + size_t childDocCount = aDoc->ChildDocCount(); + for (size_t i = 0; i < childDocCount; i++) { + child = GetAccessibleInSubtree(aDoc->ChildDocAt(i), aID); + if (child) { + return child; + } + } + + return nullptr; +} + +static bool IsInclusiveDescendantOf(DocAccessible* aAncestor, + DocAccessible* aDescendant) { + for (DocAccessible* doc = aDescendant; doc; doc = doc->ParentDocument()) { + if (doc == aAncestor) { + return true; + } + } + return false; +} + +already_AddRefed<IAccessible> MsaaAccessible::GetIAccessibleFor( + const VARIANT& aVarChild, bool* aIsDefunct) { + if (aVarChild.vt != VT_I4) return nullptr; + + VARIANT varChild = aVarChild; + + MOZ_ASSERT(aIsDefunct); + *aIsDefunct = false; + + RefPtr<IAccessible> result; + + if (!mAcc) { + *aIsDefunct = true; + return nullptr; + } + + if (varChild.lVal == CHILDID_SELF) { + result = this; + return result.forget(); + } + + if (varChild.ulVal != GetExistingID() && nsAccUtils::MustPrune(mAcc)) { + // This accessible should have no subtree in platform, return null for its + // children. + return nullptr; + } + + if (varChild.lVal > 0) { + // Gecko child indices are 0-based in contrast to indices used in MSAA. + Accessible* xpAcc = mAcc->ChildAt(varChild.lVal - 1); + if (!xpAcc) { + return nullptr; + } + MOZ_ASSERT(xpAcc->IsRemote() || !xpAcc->AsLocal()->IsDefunct(), + "Shouldn't get a defunct child"); + result = MsaaAccessible::GetFrom(xpAcc); + return result.forget(); + } + + // If lVal negative then it is treated as child ID and we should look for + // accessible through whole accessible subtree including subdocuments. + Accessible* doc = nullptr; + Accessible* child = nullptr; + auto id = static_cast<uint32_t>(varChild.lVal); + if (LocalAccessible* localAcc = mAcc->AsLocal()) { + DocAccessible* localDoc = localAcc->Document(); + doc = localDoc; + child = GetAccessibleInSubtree(localDoc, id); + if (!child) { + // Search remote documents which are descendants of this local document. + const auto remoteDocs = DocManager::TopLevelRemoteDocs(); + if (!remoteDocs) { + return nullptr; + } + for (DocAccessibleParent* remoteDoc : *remoteDocs) { + LocalAccessible* outerDoc = remoteDoc->OuterDocOfRemoteBrowser(); + if (!outerDoc || + !IsInclusiveDescendantOf(localDoc, outerDoc->Document())) { + continue; + } + child = GetAccessibleInSubtree(remoteDoc, id); + if (child) { + break; + } + } + } + } else { + DocAccessibleParent* remoteDoc = mAcc->AsRemote()->Document(); + doc = remoteDoc; + child = GetAccessibleInSubtree(remoteDoc, id); + } + if (!child) { + return nullptr; + } + + MOZ_ASSERT(child->IsRemote() || !child->AsLocal()->IsDefunct(), + "Shouldn't get a defunct child"); + // If this method is being called on the document we searched, we can just + // return child. + if (mAcc == doc) { + result = MsaaAccessible::GetFrom(child); + return result.forget(); + } + + // Otherwise, this method was called on a descendant, so we searched an + // ancestor. We must check whether child is really a descendant. This is used + // for ARIA documents and popups. + Accessible* parent = child; + while (parent && parent != doc) { + if (parent == mAcc) { + result = MsaaAccessible::GetFrom(child); + return result.forget(); + } + + parent = parent->Parent(); + } + + return nullptr; +} + +IDispatch* MsaaAccessible::NativeAccessible(Accessible* aAccessible) { + if (!aAccessible) { + NS_WARNING("Not passing in an aAccessible"); + return nullptr; + } + + RefPtr<IDispatch> disp; + disp = MsaaAccessible::GetFrom(aAccessible); + IDispatch* rawDisp; + disp.forget(&rawDisp); + return rawDisp; +} + +ITypeInfo* MsaaAccessible::GetTI(LCID lcid) { + if (gTypeInfo) return gTypeInfo; + + ITypeLib* typeLib = nullptr; + HRESULT hr = LoadRegTypeLib(LIBID_Accessibility, 1, 0, lcid, &typeLib); + if (FAILED(hr)) return nullptr; + + hr = typeLib->GetTypeInfoOfGuid(IID_IAccessible, &gTypeInfo); + typeLib->Release(); + + if (FAILED(hr)) return nullptr; + + return gTypeInfo; +} + +/* static */ +MsaaAccessible* MsaaAccessible::GetFrom(Accessible* aAcc) { + if (!aAcc) { + return nullptr; + } + + if (RemoteAccessible* remoteAcc = aAcc->AsRemote()) { + return reinterpret_cast<MsaaAccessible*>(remoteAcc->GetWrapper()); + } + return static_cast<AccessibleWrap*>(aAcc)->GetMsaa(); +} + +/* static */ +Accessible* MsaaAccessible::GetAccessibleFrom(IUnknown* aUnknown) { + RefPtr<MsaaAccessible> msaa; + aUnknown->QueryInterface(IID_MsaaAccessible, getter_AddRefs(msaa)); + if (!msaa) { + return nullptr; + } + return msaa->Acc(); +} + +// IUnknown methods +STDMETHODIMP +MsaaAccessible::QueryInterface(REFIID iid, void** ppv) { + if (!ppv) return E_INVALIDARG; + + *ppv = nullptr; + + if (IID_IClientSecurity == iid) { + // Some code might QI(IID_IClientSecurity) to detect whether or not we are + // a proxy. Right now that can potentially happen off the main thread, so we + // look for this condition immediately so that we don't trigger other code + // that might not be thread-safe. + return E_NOINTERFACE; + } + + // These interfaces are always available. We can support querying to them + // even if the Accessible is dead. + if (IID_IUnknown == iid) { + *ppv = static_cast<IAccessible*>(this); + } else if (IID_MsaaAccessible == iid) { + *ppv = static_cast<MsaaAccessible*>(this); + } else if (IID_IDispatch == iid || IID_IAccessible == iid) { + *ppv = static_cast<IAccessible*>(this); + } else if (IID_IServiceProvider == iid) { + *ppv = new ServiceProvider(this); + } else { + HRESULT hr = ia2Accessible::QueryInterface(iid, ppv); + if (SUCCEEDED(hr)) { + return hr; + } + } + if (*ppv) { + (reinterpret_cast<IUnknown*>(*ppv))->AddRef(); + return S_OK; + } + + // For interfaces below this point, we have to query the Accessible to + // determine if they are available. + if (!mAcc) { + // mscom::Interceptor (and maybe other callers) expects either S_OK or + // E_NOINTERFACE, so don't return CO_E_OBJNOTCONNECTED like we normally + // would for a dead object. + return E_NOINTERFACE; + } + AccessibleWrap* localAcc = LocalAcc(); + if (IID_IEnumVARIANT == iid) { + // We don't support this interface for leaf elements. + if (!mAcc->HasChildren() || nsAccUtils::MustPrune(mAcc)) { + return E_NOINTERFACE; + } + *ppv = static_cast<IEnumVARIANT*>(new ChildrenEnumVariant(this)); + } else if (IID_ISimpleDOMNode == iid) { + if (mAcc->IsDoc() || (localAcc && !localAcc->HasOwnContent())) { + return E_NOINTERFACE; + } + + *ppv = static_cast<ISimpleDOMNode*>(new sdnAccessible(WrapNotNull(this))); + } else if (iid == IID_ISimpleDOMText && localAcc && localAcc->IsTextLeaf()) { + statistics::ISimpleDOMUsed(); + *ppv = static_cast<ISimpleDOMText*>(new sdnTextAccessible(this)); + static_cast<IUnknown*>(*ppv)->AddRef(); + return S_OK; + } + + if (!*ppv && localAcc) { + HRESULT hr = ia2AccessibleComponent::QueryInterface(iid, ppv); + if (SUCCEEDED(hr)) return hr; + } + + if (!*ppv) { + HRESULT hr = ia2AccessibleHyperlink::QueryInterface(iid, ppv); + if (SUCCEEDED(hr)) return hr; + } + + if (!*ppv) { + HRESULT hr = ia2AccessibleValue::QueryInterface(iid, ppv); + if (SUCCEEDED(hr)) return hr; + } + + if (nullptr == *ppv) return E_NOINTERFACE; + + (reinterpret_cast<IUnknown*>(*ppv))->AddRef(); + return S_OK; +} + +// IAccessible methods + +STDMETHODIMP +MsaaAccessible::get_accParent(IDispatch __RPC_FAR* __RPC_FAR* ppdispParent) { + if (!ppdispParent) return E_INVALIDARG; + + *ppdispParent = nullptr; + + if (!mAcc) { + return CO_E_OBJNOTCONNECTED; + } + + Accessible* xpParentAcc = mAcc->Parent(); + if (!xpParentAcc) return S_FALSE; + + *ppdispParent = NativeAccessible(xpParentAcc); + return S_OK; +} + +STDMETHODIMP +MsaaAccessible::get_accChildCount(long __RPC_FAR* pcountChildren) { + if (!pcountChildren) return E_INVALIDARG; + + *pcountChildren = 0; + + if (!mAcc) return CO_E_OBJNOTCONNECTED; + + if (Compatibility::IsA11ySuppressedForClipboardCopy() && mAcc->IsRoot()) { + // Bug 1798098: Windows Suggested Actions (introduced in Windows 11 22H2) + // might walk the entire a11y tree using UIA whenever anything is copied to + // the clipboard. This causes an unacceptable hang, particularly when the + // cache is disabled. We prevent this tree walk by returning a 0 child count + // for the root window, from which Windows might walk. + return S_OK; + } + + if (nsAccUtils::MustPrune(mAcc)) return S_OK; + + *pcountChildren = mAcc->ChildCount(); + return S_OK; +} + +STDMETHODIMP +MsaaAccessible::get_accChild( + /* [in] */ VARIANT varChild, + /* [retval][out] */ IDispatch __RPC_FAR* __RPC_FAR* ppdispChild) { + if (!ppdispChild) return E_INVALIDARG; + + *ppdispChild = nullptr; + if (!mAcc) return CO_E_OBJNOTCONNECTED; + + // IAccessible::accChild is used to return this accessible or child accessible + // at the given index or to get an accessible by child ID in the case of + // document accessible. + // The getting an accessible by child ID is used by + // AccessibleObjectFromEvent() called by AT when AT handles our MSAA event. + bool isDefunct = false; + RefPtr<IAccessible> child = GetIAccessibleFor(varChild, &isDefunct); + if (!child) { + return E_INVALIDARG; + } + + if (isDefunct) { + return CO_E_OBJNOTCONNECTED; + } + + child.forget(ppdispChild); + return S_OK; +} + +STDMETHODIMP +MsaaAccessible::get_accName( + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ BSTR __RPC_FAR* pszName) { + if (!pszName || varChild.vt != VT_I4) return E_INVALIDARG; + + *pszName = nullptr; + + RefPtr<IAccessible> accessible; + HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); + if (FAILED(hr)) { + return hr; + } + + if (accessible) { + return accessible->get_accName(kVarChildIdSelf, pszName); + } + + nsAutoString name; + Acc()->Name(name); + + if (name.IsVoid()) return S_FALSE; + + *pszName = ::SysAllocStringLen(name.get(), name.Length()); + if (!*pszName) return E_OUTOFMEMORY; + return S_OK; +} + +STDMETHODIMP +MsaaAccessible::get_accValue( + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ BSTR __RPC_FAR* pszValue) { + if (!pszValue) return E_INVALIDARG; + + *pszValue = nullptr; + + RefPtr<IAccessible> accessible; + HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); + if (FAILED(hr)) { + return hr; + } + + if (accessible) { + return accessible->get_accValue(kVarChildIdSelf, pszValue); + } + + nsAutoString value; + Acc()->Value(value); + + // See bug 438784: need to expose URL on doc's value attribute. For this, + // reverting part of fix for bug 425693 to make this MSAA method behave + // IAccessible2-style. + if (value.IsEmpty()) return S_FALSE; + + *pszValue = ::SysAllocStringLen(value.get(), value.Length()); + if (!*pszValue) return E_OUTOFMEMORY; + return S_OK; +} + +STDMETHODIMP +MsaaAccessible::get_accDescription(VARIANT varChild, + BSTR __RPC_FAR* pszDescription) { + if (!pszDescription) return E_INVALIDARG; + + *pszDescription = nullptr; + + RefPtr<IAccessible> accessible; + HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); + if (FAILED(hr)) { + return hr; + } + + if (accessible) { + return accessible->get_accDescription(kVarChildIdSelf, pszDescription); + } + + nsAutoString description; + Acc()->Description(description); + + *pszDescription = + ::SysAllocStringLen(description.get(), description.Length()); + return *pszDescription ? S_OK : E_OUTOFMEMORY; +} + +STDMETHODIMP +MsaaAccessible::get_accRole( + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ VARIANT __RPC_FAR* pvarRole) { + if (!pvarRole) return E_INVALIDARG; + + VariantInit(pvarRole); + + RefPtr<IAccessible> accessible; + HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); + if (FAILED(hr)) { + return hr; + } + + if (accessible) { + return accessible->get_accRole(kVarChildIdSelf, pvarRole); + } + + a11y::role geckoRole; +#ifdef DEBUG + if (mAcc->IsLocal()) { + NS_ASSERTION(nsAccUtils::IsTextInterfaceSupportCorrect(mAcc->AsLocal()), + "Does not support Text when it should"); + } +#endif + geckoRole = mAcc->Role(); + + uint32_t msaaRole = 0; + +#define ROLE(_geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \ + _msaaRole, ia2Role, androidClass, nameRule) \ + case roles::_geckoRole: \ + msaaRole = _msaaRole; \ + break; + + switch (geckoRole) { +#include "RoleMap.h" + default: + MOZ_CRASH("Unknown role."); + } + +#undef ROLE + + // Special case, if there is a ROLE_ROW inside of a ROLE_TREE_TABLE, then call + // the MSAA role a ROLE_OUTLINEITEM for consistency and compatibility. We need + // this because ARIA has a role of "row" for both grid and treegrid + if (geckoRole == roles::ROW) { + Accessible* xpParent = mAcc->Parent(); + if (xpParent && xpParent->Role() == roles::TREE_TABLE) + msaaRole = ROLE_SYSTEM_OUTLINEITEM; + } + + pvarRole->vt = VT_I4; + pvarRole->lVal = msaaRole; + return S_OK; +} + +STDMETHODIMP +MsaaAccessible::get_accState( + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ VARIANT __RPC_FAR* pvarState) { + if (!pvarState) return E_INVALIDARG; + + VariantInit(pvarState); + pvarState->vt = VT_I4; + pvarState->lVal = 0; + + RefPtr<IAccessible> accessible; + HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); + if (FAILED(hr)) { + return hr; + } + + if (accessible) { + return accessible->get_accState(kVarChildIdSelf, pvarState); + } + + // MSAA only has 31 states and the lowest 31 bits of our state bit mask + // are the same states as MSAA. + // Note: we map the following Gecko states to different MSAA states: + // REQUIRED -> ALERT_LOW + // ALERT -> ALERT_MEDIUM + // INVALID -> ALERT_HIGH + // CHECKABLE -> MARQUEED + + uint64_t state = Acc()->State(); + + uint32_t msaaState = 0; + nsAccUtils::To32States(state, &msaaState, nullptr); + pvarState->lVal = msaaState; + return S_OK; +} + +STDMETHODIMP +MsaaAccessible::get_accHelp( + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ BSTR __RPC_FAR* pszHelp) { + if (!pszHelp) return E_INVALIDARG; + + *pszHelp = nullptr; + return S_FALSE; +} + +STDMETHODIMP +MsaaAccessible::get_accHelpTopic( + /* [out] */ BSTR __RPC_FAR* pszHelpFile, + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ long __RPC_FAR* pidTopic) { + if (!pszHelpFile || !pidTopic) return E_INVALIDARG; + + *pszHelpFile = nullptr; + *pidTopic = 0; + return S_FALSE; +} + +STDMETHODIMP +MsaaAccessible::get_accKeyboardShortcut( + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ BSTR __RPC_FAR* pszKeyboardShortcut) { + if (!pszKeyboardShortcut) return E_INVALIDARG; + *pszKeyboardShortcut = nullptr; + + RefPtr<IAccessible> accessible; + HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); + if (FAILED(hr)) { + return hr; + } + + if (accessible) { + return accessible->get_accKeyboardShortcut(kVarChildIdSelf, + pszKeyboardShortcut); + } + + KeyBinding keyBinding = mAcc->AccessKey(); + if (keyBinding.IsEmpty()) { + if (LocalAccessible* localAcc = mAcc->AsLocal()) { + keyBinding = localAcc->KeyboardShortcut(); + } + } + + nsAutoString shortcut; + keyBinding.ToString(shortcut); + + *pszKeyboardShortcut = ::SysAllocStringLen(shortcut.get(), shortcut.Length()); + return *pszKeyboardShortcut ? S_OK : E_OUTOFMEMORY; +} + +STDMETHODIMP +MsaaAccessible::get_accFocus( + /* [retval][out] */ VARIANT __RPC_FAR* pvarChild) { + if (!pvarChild) return E_INVALIDARG; + + VariantInit(pvarChild); + + // clang-format off + // VT_EMPTY: None. This object does not have the keyboard focus itself + // and does not contain a child that has the keyboard focus. + // VT_I4: lVal is CHILDID_SELF. The object itself has the keyboard focus. + // VT_I4: lVal contains the child ID of the child element with the keyboard focus. + // VT_DISPATCH: pdispVal member is the address of the IDispatch interface + // for the child object with the keyboard focus. + // clang-format on + if (!mAcc) { + return CO_E_OBJNOTCONNECTED; + } + // Return the current IAccessible child that has focus + Accessible* focusedAccessible = mAcc->FocusedChild(); + if (focusedAccessible == mAcc) { + pvarChild->vt = VT_I4; + pvarChild->lVal = CHILDID_SELF; + } else if (focusedAccessible) { + pvarChild->vt = VT_DISPATCH; + pvarChild->pdispVal = NativeAccessible(focusedAccessible); + } else { + pvarChild->vt = VT_EMPTY; // No focus or focus is not a child + } + + return S_OK; +} + +/** + * This helper class implements IEnumVARIANT for a nsTArray containing + * accessible objects. + */ +class AccessibleEnumerator final : public IEnumVARIANT { + public: + explicit AccessibleEnumerator(const nsTArray<Accessible*>& aArray) + : mArray(aArray.Clone()), mCurIndex(0) {} + AccessibleEnumerator(const AccessibleEnumerator& toCopy) + : mArray(toCopy.mArray.Clone()), mCurIndex(toCopy.mCurIndex) {} + ~AccessibleEnumerator() {} + + // IUnknown + DECL_IUNKNOWN + + // IEnumVARIANT + STDMETHODIMP Next(unsigned long celt, VARIANT FAR* rgvar, + unsigned long FAR* pceltFetched); + STDMETHODIMP Skip(unsigned long celt); + STDMETHODIMP Reset() { + mCurIndex = 0; + return S_OK; + } + STDMETHODIMP Clone(IEnumVARIANT FAR* FAR* ppenum); + + private: + nsTArray<Accessible*> mArray; + uint32_t mCurIndex; +}; + +STDMETHODIMP +AccessibleEnumerator::QueryInterface(REFIID iid, void** ppvObject) { + if (iid == IID_IEnumVARIANT) { + *ppvObject = static_cast<IEnumVARIANT*>(this); + AddRef(); + return S_OK; + } + if (iid == IID_IUnknown) { + *ppvObject = static_cast<IUnknown*>(this); + AddRef(); + return S_OK; + } + + *ppvObject = nullptr; + return E_NOINTERFACE; +} + +STDMETHODIMP +AccessibleEnumerator::Next(unsigned long celt, VARIANT FAR* rgvar, + unsigned long FAR* pceltFetched) { + uint32_t length = mArray.Length(); + HRESULT hr = S_OK; + + // Can't get more elements than there are... + if (celt > length - mCurIndex) { + hr = S_FALSE; + celt = length - mCurIndex; + } + + // Copy the elements of the array into rgvar. + for (uint32_t i = 0; i < celt; ++i, ++mCurIndex) { + rgvar[i].vt = VT_DISPATCH; + rgvar[i].pdispVal = MsaaAccessible::NativeAccessible(mArray[mCurIndex]); + } + + if (pceltFetched) *pceltFetched = celt; + + return hr; +} + +STDMETHODIMP +AccessibleEnumerator::Clone(IEnumVARIANT FAR* FAR* ppenum) { + *ppenum = new AccessibleEnumerator(*this); + NS_ADDREF(*ppenum); + return S_OK; +} + +STDMETHODIMP +AccessibleEnumerator::Skip(unsigned long celt) { + uint32_t length = mArray.Length(); + // Check if we can skip the requested number of elements + if (celt > length - mCurIndex) { + mCurIndex = length; + return S_FALSE; + } + mCurIndex += celt; + return S_OK; +} + +/** + * This method is called when a client wants to know which children of a node + * are selected. Note that this method can only find selected children for + * accessible object which implement SelectAccessible. + * + * The VARIANT return value arguement is expected to either contain a single + * IAccessible or an IEnumVARIANT of IAccessibles. We return the IEnumVARIANT + * regardless of the number of children selected, unless there are none selected + * in which case we return an empty VARIANT. + * + * We get the selected options from the select's accessible object and wrap + * those in an AccessibleEnumerator which we then put in the return VARIANT. + * + * returns a VT_EMPTY VARIANT if: + * - there are no selected children for this object + * - the object is not the type that can have children selected + */ +STDMETHODIMP +MsaaAccessible::get_accSelection(VARIANT __RPC_FAR* pvarChildren) { + if (!pvarChildren) return E_INVALIDARG; + + VariantInit(pvarChildren); + pvarChildren->vt = VT_EMPTY; + + if (!mAcc) { + return CO_E_OBJNOTCONNECTED; + } + Accessible* acc = Acc(); + + if (!acc->IsSelect()) { + return S_OK; + } + + AutoTArray<Accessible*, 10> selectedItems; + acc->SelectedItems(&selectedItems); + uint32_t count = selectedItems.Length(); + if (count == 1) { + pvarChildren->vt = VT_DISPATCH; + pvarChildren->pdispVal = NativeAccessible(selectedItems[0]); + } else if (count > 1) { + RefPtr<AccessibleEnumerator> pEnum = + new AccessibleEnumerator(selectedItems); + pvarChildren->vt = + VT_UNKNOWN; // this must be VT_UNKNOWN for an IEnumVARIANT + NS_ADDREF(pvarChildren->punkVal = pEnum); + } + // If count == 0, vt is already VT_EMPTY, so there's nothing else to do. + + return S_OK; +} + +STDMETHODIMP +MsaaAccessible::get_accDefaultAction( + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ BSTR __RPC_FAR* pszDefaultAction) { + if (!pszDefaultAction) return E_INVALIDARG; + + *pszDefaultAction = nullptr; + + RefPtr<IAccessible> accessible; + HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); + if (FAILED(hr)) { + return hr; + } + + if (accessible) { + return accessible->get_accDefaultAction(kVarChildIdSelf, pszDefaultAction); + } + + nsAutoString defaultAction; + mAcc->ActionNameAt(0, defaultAction); + + *pszDefaultAction = + ::SysAllocStringLen(defaultAction.get(), defaultAction.Length()); + return *pszDefaultAction ? S_OK : E_OUTOFMEMORY; +} + +STDMETHODIMP +MsaaAccessible::accSelect( + /* [in] */ long flagsSelect, + /* [optional][in] */ VARIANT varChild) { + RefPtr<IAccessible> accessible; + HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); + if (FAILED(hr)) { + return hr; + } + + if (accessible) { + return accessible->accSelect(flagsSelect, kVarChildIdSelf); + } + + if (flagsSelect & SELFLAG_TAKEFOCUS) { + mAcc->TakeFocus(); + return S_OK; + } + + if (flagsSelect & SELFLAG_TAKESELECTION) { + mAcc->TakeSelection(); + return S_OK; + } + + if (flagsSelect & SELFLAG_ADDSELECTION) { + mAcc->SetSelected(true); + return S_OK; + } + + if (flagsSelect & SELFLAG_REMOVESELECTION) { + mAcc->SetSelected(false); + return S_OK; + } + + return E_FAIL; +} + +STDMETHODIMP +MsaaAccessible::accLocation( + /* [out] */ long __RPC_FAR* pxLeft, + /* [out] */ long __RPC_FAR* pyTop, + /* [out] */ long __RPC_FAR* pcxWidth, + /* [out] */ long __RPC_FAR* pcyHeight, + /* [optional][in] */ VARIANT varChild) { + if (!pxLeft || !pyTop || !pcxWidth || !pcyHeight) return E_INVALIDARG; + + *pxLeft = 0; + *pyTop = 0; + *pcxWidth = 0; + *pcyHeight = 0; + + RefPtr<IAccessible> accessible; + HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); + if (FAILED(hr)) { + return hr; + } + + if (accessible) { + return accessible->accLocation(pxLeft, pyTop, pcxWidth, pcyHeight, + kVarChildIdSelf); + } + + LayoutDeviceIntRect rect = Acc()->Bounds(); + *pxLeft = rect.X(); + *pyTop = rect.Y(); + *pcxWidth = rect.Width(); + *pcyHeight = rect.Height(); + return S_OK; +} + +STDMETHODIMP +MsaaAccessible::accNavigate( + /* [in] */ long navDir, + /* [optional][in] */ VARIANT varStart, + /* [retval][out] */ VARIANT __RPC_FAR* pvarEndUpAt) { + if (!pvarEndUpAt) return E_INVALIDARG; + + VariantInit(pvarEndUpAt); + + RefPtr<IAccessible> accessible; + HRESULT hr = ResolveChild(varStart, getter_AddRefs(accessible)); + if (FAILED(hr)) { + return hr; + } + + if (accessible) { + return accessible->accNavigate(navDir, kVarChildIdSelf, pvarEndUpAt); + } + + Accessible* navAccessible = nullptr; + Maybe<RelationType> xpRelation; + +#define RELATIONTYPE(geckoType, stringType, atkType, msaaType, ia2Type) \ + case msaaType: \ + xpRelation.emplace(RelationType::geckoType); \ + break; + + switch (navDir) { + case NAVDIR_FIRSTCHILD: + if (!nsAccUtils::MustPrune(mAcc)) { + navAccessible = mAcc->FirstChild(); + } + break; + case NAVDIR_LASTCHILD: + if (!nsAccUtils::MustPrune(mAcc)) { + navAccessible = mAcc->LastChild(); + } + break; + case NAVDIR_NEXT: + navAccessible = mAcc->NextSibling(); + break; + case NAVDIR_PREVIOUS: + navAccessible = mAcc->PrevSibling(); + break; + case NAVDIR_DOWN: + case NAVDIR_LEFT: + case NAVDIR_RIGHT: + case NAVDIR_UP: + return E_NOTIMPL; + + // MSAA relationship extensions to accNavigate +#include "RelationTypeMap.h" + + default: + return E_INVALIDARG; + } + +#undef RELATIONTYPE + + pvarEndUpAt->vt = VT_EMPTY; + + if (xpRelation) { + Relation rel = mAcc->RelationByType(*xpRelation); + navAccessible = rel.Next(); + } + + if (!navAccessible) return E_FAIL; + + pvarEndUpAt->pdispVal = NativeAccessible(navAccessible); + pvarEndUpAt->vt = VT_DISPATCH; + return S_OK; +} + +STDMETHODIMP +MsaaAccessible::accHitTest( + /* [in] */ long xLeft, + /* [in] */ long yTop, + /* [retval][out] */ VARIANT __RPC_FAR* pvarChild) { + if (!pvarChild) return E_INVALIDARG; + + VariantInit(pvarChild); + + if (!mAcc) { + return CO_E_OBJNOTCONNECTED; + } + + // The MSAA documentation says accHitTest should return a child. However, + // clients call AccessibleObjectFromPoint, which ends up walking the + // descendants calling accHitTest on each one. Since clients want the + // deepest descendant anyway, it's faster and probably more accurate to + // just do this ourselves. + Accessible* accessible = mAcc->ChildAtPoint( + xLeft, yTop, Accessible::EWhichChildAtPoint::DeepestChild); + + // if we got a child + if (accessible) { + if (accessible != mAcc && accessible->IsTextLeaf()) { + Accessible* parent = accessible->Parent(); + if (parent != mAcc && parent->Role() == roles::LINK) { + // Bug 1843832: The UI Automation -> IAccessible2 proxy barfs if we + // return the text leaf child of a link when hit testing an ancestor of + // the link. Therefore, we return the link instead. MSAA clients which + // call AccessibleObjectFromPoint will still get to the text leaf, since + // AccessibleObjectFromPoint keeps calling accHitTest until it can't + // descend any further. We should remove this tragic hack once we have + // a native UIA implementation. + accessible = parent; + } + } + if (accessible == mAcc) { + pvarChild->vt = VT_I4; + pvarChild->lVal = CHILDID_SELF; + } else { + pvarChild->vt = VT_DISPATCH; + pvarChild->pdispVal = NativeAccessible(accessible); + } + } else { + // no child at that point + pvarChild->vt = VT_EMPTY; + return S_FALSE; + } + return S_OK; +} + +STDMETHODIMP +MsaaAccessible::accDoDefaultAction( + /* [optional][in] */ VARIANT varChild) { + RefPtr<IAccessible> accessible; + HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); + if (FAILED(hr)) { + return hr; + } + + if (accessible) { + return accessible->accDoDefaultAction(kVarChildIdSelf); + } + + return mAcc->DoAction(0) ? S_OK : E_INVALIDARG; +} + +STDMETHODIMP +MsaaAccessible::put_accName( + /* [optional][in] */ VARIANT varChild, + /* [in] */ BSTR szName) { + return E_NOTIMPL; +} + +STDMETHODIMP +MsaaAccessible::put_accValue( + /* [optional][in] */ VARIANT varChild, + /* [in] */ BSTR szValue) { + RefPtr<IAccessible> accessible; + HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible)); + if (FAILED(hr)) { + return hr; + } + + if (accessible) { + return accessible->put_accValue(kVarChildIdSelf, szValue); + } + + HyperTextAccessibleBase* ht = mAcc->AsHyperTextBase(); + if (!ht) { + return E_NOTIMPL; + } + + uint32_t length = ::SysStringLen(szValue); + nsAutoString text(szValue, length); + ht->ReplaceText(text); + return S_OK; +} + +// IDispatch methods + +STDMETHODIMP +MsaaAccessible::GetTypeInfoCount(UINT* pctinfo) { + if (!pctinfo) return E_INVALIDARG; + + *pctinfo = 1; + return S_OK; +} + +STDMETHODIMP +MsaaAccessible::GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo** ppTInfo) { + if (!ppTInfo) return E_INVALIDARG; + + *ppTInfo = nullptr; + + if (iTInfo != 0) return DISP_E_BADINDEX; + + ITypeInfo* typeInfo = GetTI(lcid); + if (!typeInfo) return E_FAIL; + + typeInfo->AddRef(); + *ppTInfo = typeInfo; + + return S_OK; +} + +STDMETHODIMP +MsaaAccessible::GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames, UINT cNames, + LCID lcid, DISPID* rgDispId) { + ITypeInfo* typeInfo = GetTI(lcid); + if (!typeInfo) return E_FAIL; + + HRESULT hr = DispGetIDsOfNames(typeInfo, rgszNames, cNames, rgDispId); + return hr; +} + +STDMETHODIMP +MsaaAccessible::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, + DISPPARAMS* pDispParams, VARIANT* pVarResult, + EXCEPINFO* pExcepInfo, UINT* puArgErr) { + ITypeInfo* typeInfo = GetTI(lcid); + if (!typeInfo) return E_FAIL; + + return typeInfo->Invoke(static_cast<IAccessible*>(this), dispIdMember, wFlags, + pDispParams, pVarResult, pExcepInfo, puArgErr); +} diff --git a/accessible/windows/msaa/MsaaAccessible.h b/accessible/windows/msaa/MsaaAccessible.h new file mode 100644 index 0000000000..034db860ed --- /dev/null +++ b/accessible/windows/msaa/MsaaAccessible.h @@ -0,0 +1,204 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef mozilla_a11y_MsaaAccessible_h_ +#define mozilla_a11y_MsaaAccessible_h_ + +#include "ia2Accessible.h" +#include "ia2AccessibleComponent.h" +#include "ia2AccessibleHyperlink.h" +#include "ia2AccessibleValue.h" +#include "IUnknownImpl.h" +#include "mozilla/a11y/MsaaIdGenerator.h" +#include "nsXULAppAPI.h" + +namespace mozilla { +namespace a11y { +class Accessible; +class AccessibleWrap; +class LocalAccessible; +class sdnAccessible; + +class MsaaAccessible : public ia2Accessible, + public ia2AccessibleComponent, + public ia2AccessibleHyperlink, + public ia2AccessibleValue { + public: + static MsaaAccessible* Create(Accessible* aAcc); + + Accessible* Acc() { return mAcc; } + AccessibleWrap* LocalAcc(); + + uint32_t GetExistingID() const { return mID; } + static const uint32_t kNoID = 0; + + static int32_t GetChildIDFor(Accessible* aAccessible); + static void AssignChildIDTo(NotNull<sdnAccessible*> aSdnAcc); + static void ReleaseChildID(NotNull<sdnAccessible*> aSdnAcc); + static HWND GetHWNDFor(Accessible* aAccessible); + static void FireWinEvent(Accessible* aTarget, uint32_t aEventType); + + /** + * Find an accessible by the given child ID in cached documents. + */ + [[nodiscard]] already_AddRefed<IAccessible> GetIAccessibleFor( + const VARIANT& aVarChild, bool* aIsDefunct); + + void MsaaShutdown(); + + static IDispatch* NativeAccessible(Accessible* aAccessible); + + static MsaaAccessible* GetFrom(Accessible* aAcc); + + /** + * Creates ITypeInfo for LIBID_Accessibility if it's needed and returns it. + */ + static ITypeInfo* GetTI(LCID lcid); + + static Accessible* GetAccessibleFrom(IUnknown* aUnknown); + + DECL_IUNKNOWN + + // IAccessible + virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accParent( + /* [retval][out] */ IDispatch __RPC_FAR* __RPC_FAR* ppdispParent) + override; + virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accChildCount( + /* [retval][out] */ long __RPC_FAR* pcountChildren) override; + virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accChild( + /* [in] */ VARIANT varChild, + /* [retval][out] */ IDispatch __RPC_FAR* __RPC_FAR* ppdispChild) override; + virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accName( + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ BSTR __RPC_FAR* pszName) override; + virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accValue( + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ BSTR __RPC_FAR* pszValue) override; + virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accDescription( + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ BSTR __RPC_FAR* pszDescription) override; + virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accRole( + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ VARIANT __RPC_FAR* pvarRole) override; + virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accState( + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ VARIANT __RPC_FAR* pvarState) override; + virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accHelp( + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ BSTR __RPC_FAR* pszHelp) override; + virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accHelpTopic( + /* [out] */ BSTR __RPC_FAR* pszHelpFile, + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ long __RPC_FAR* pidTopic) override; + virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accKeyboardShortcut( + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ BSTR __RPC_FAR* pszKeyboardShortcut) override; + virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accFocus( + /* [retval][out] */ VARIANT __RPC_FAR* pvarChild) override; + virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accSelection( + /* [retval][out] */ VARIANT __RPC_FAR* pvarChildren) override; + virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accDefaultAction( + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ BSTR __RPC_FAR* pszDefaultAction) override; + virtual /* [id] */ HRESULT STDMETHODCALLTYPE accSelect( + /* [in] */ long flagsSelect, + /* [optional][in] */ VARIANT varChild) override; + virtual /* [id] */ HRESULT STDMETHODCALLTYPE accLocation( + /* [out] */ long __RPC_FAR* pxLeft, + /* [out] */ long __RPC_FAR* pyTop, + /* [out] */ long __RPC_FAR* pcxWidth, + /* [out] */ long __RPC_FAR* pcyHeight, + /* [optional][in] */ VARIANT varChild) override; + virtual /* [id] */ HRESULT STDMETHODCALLTYPE accNavigate( + /* [in] */ long navDir, + /* [optional][in] */ VARIANT varStart, + /* [retval][out] */ VARIANT __RPC_FAR* pvarEndUpAt) override; + virtual /* [id] */ HRESULT STDMETHODCALLTYPE accHitTest( + /* [in] */ long xLeft, + /* [in] */ long yTop, + /* [retval][out] */ VARIANT __RPC_FAR* pvarChild) override; + virtual /* [id] */ HRESULT STDMETHODCALLTYPE accDoDefaultAction( + /* [optional][in] */ VARIANT varChild) override; + virtual /* [id][propput] */ HRESULT STDMETHODCALLTYPE put_accName( + /* [optional][in] */ VARIANT varChild, + /* [in] */ BSTR szName) override; + virtual /* [id][propput] */ HRESULT STDMETHODCALLTYPE put_accValue( + /* [optional][in] */ VARIANT varChild, + /* [in] */ BSTR szValue) override; + + // IDispatch (support of scripting languages like VB) + virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount(UINT* pctinfo) override; + virtual HRESULT STDMETHODCALLTYPE GetTypeInfo(UINT iTInfo, LCID lcid, + ITypeInfo** ppTInfo) override; + virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames(REFIID riid, + LPOLESTR* rgszNames, + UINT cNames, LCID lcid, + DISPID* rgDispId) override; + virtual HRESULT STDMETHODCALLTYPE Invoke(DISPID dispIdMember, REFIID riid, + LCID lcid, WORD wFlags, + DISPPARAMS* pDispParams, + VARIANT* pVarResult, + EXCEPINFO* pExcepInfo, + UINT* puArgErr) override; + + protected: + explicit MsaaAccessible(Accessible* aAcc); + virtual ~MsaaAccessible(); + + Accessible* mAcc; + + uint32_t mID; + static MsaaIdGenerator sIDGen; + + HRESULT + ResolveChild(const VARIANT& aVarChild, IAccessible** aOutInterface); + + enum navRelations { + NAVRELATION_CONTROLLED_BY = 0x1000, + NAVRELATION_CONTROLLER_FOR = 0x1001, + NAVRELATION_LABEL_FOR = 0x1002, + NAVRELATION_LABELLED_BY = 0x1003, + NAVRELATION_MEMBER_OF = 0x1004, + NAVRELATION_NODE_CHILD_OF = 0x1005, + NAVRELATION_FLOWS_TO = 0x1006, + NAVRELATION_FLOWS_FROM = 0x1007, + NAVRELATION_SUBWINDOW_OF = 0x1008, + NAVRELATION_EMBEDS = 0x1009, + NAVRELATION_EMBEDDED_BY = 0x100a, + NAVRELATION_POPUP_FOR = 0x100b, + NAVRELATION_PARENT_WINDOW_OF = 0x100c, + NAVRELATION_DEFAULT_BUTTON = 0x100d, + NAVRELATION_DESCRIBED_BY = 0x100e, + NAVRELATION_DESCRIPTION_FOR = 0x100f, + NAVRELATION_NODE_PARENT_OF = 0x1010, + NAVRELATION_CONTAINING_DOCUMENT = 0x1011, + NAVRELATION_CONTAINING_TAB_PANE = 0x1012, + NAVRELATION_CONTAINING_WINDOW = 0x1013, + NAVRELATION_CONTAINING_APPLICATION = 0x1014, + NAVRELATION_DETAILS = 0x1015, + NAVRELATION_DETAILS_FOR = 0x1016, + NAVRELATION_ERROR = 0x1017, + NAVRELATION_ERROR_FOR = 0x1018, + NAVRELATION_LINKS_TO = 0x1019 + }; + + private: + static ITypeInfo* gTypeInfo; +}; + +} // namespace a11y +} // namespace mozilla + +#ifdef XP_WIN +// Undo the windows.h damage +# undef GetMessage +# undef CreateEvent +# undef GetClassName +# undef GetBinaryType +# undef RemoveDirectory +#endif + +#endif diff --git a/accessible/windows/msaa/MsaaDocAccessible.cpp b/accessible/windows/msaa/MsaaDocAccessible.cpp new file mode 100644 index 0000000000..615f2e5d39 --- /dev/null +++ b/accessible/windows/msaa/MsaaDocAccessible.cpp @@ -0,0 +1,133 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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 "MsaaDocAccessible.h" + +#include "MsaaDocAccessible.h" +#include "DocAccessibleChild.h" +#include "mozilla/a11y/DocAccessibleParent.h" +#include "nsAccessibilityService.h" +#include "nsAccUtils.h" +#include "nsWinUtils.h" +#include "Statistics.h" +#include "sdnDocAccessible.h" +#include "mozilla/a11y/Role.h" +#include "ISimpleDOM.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +DocAccessible* MsaaDocAccessible::DocAcc() { + return static_cast<DocAccessible*>(LocalAcc()); +} + +/* static */ +MsaaDocAccessible* MsaaDocAccessible::GetFrom(DocAccessible* aDoc) { + return static_cast<MsaaDocAccessible*>(MsaaAccessible::GetFrom(aDoc)); +} + +/* static */ +MsaaDocAccessible* MsaaDocAccessible::GetFrom(DocAccessibleParent* aDoc) { + return static_cast<MsaaDocAccessible*>( + reinterpret_cast<MsaaAccessible*>(aDoc->GetWrapper())); +} + +/* static */ +MsaaDocAccessible* MsaaDocAccessible::GetFromOwned(Accessible* aAcc) { + if (RemoteAccessible* remoteAcc = aAcc->AsRemote()) { + DocAccessibleParent* doc = remoteAcc->Document(); + if (!doc) { + return nullptr; + } + return MsaaDocAccessible::GetFrom(doc); + } + DocAccessible* doc = aAcc->AsLocal()->Document(); + if (!doc) { + return nullptr; + } + return MsaaDocAccessible::GetFrom(doc); +} + +// IUnknown +IMPL_IUNKNOWN_QUERY_HEAD(MsaaDocAccessible) +if (aIID == IID_ISimpleDOMDocument && LocalAcc()) { + statistics::ISimpleDOMUsed(); + *aInstancePtr = static_cast<ISimpleDOMDocument*>(new sdnDocAccessible(this)); + static_cast<IUnknown*>(*aInstancePtr)->AddRef(); + return S_OK; +} +IMPL_IUNKNOWN_QUERY_TAIL_INHERITED(ia2AccessibleHypertext) + +STDMETHODIMP +MsaaDocAccessible::get_accParent( + /* [retval][out] */ IDispatch __RPC_FAR* __RPC_FAR* ppdispParent) { + if (!mAcc) { + return CO_E_OBJNOTCONNECTED; + } + + if (mAcc->IsRemote()) { + DocAccessibleParent* remoteDoc = mAcc->AsRemote()->AsDoc(); + if (nsWinUtils::IsWindowEmulationStarted() && remoteDoc->IsTopLevel()) { + // Window emulation is enabled and this is a top level document. Return + // window system accessible object. + HWND hwnd = remoteDoc->GetEmulatedWindowHandle(); + MOZ_ASSERT(hwnd); + if (hwnd && + SUCCEEDED(::CreateStdAccessibleObject( + hwnd, OBJID_WINDOW, IID_IAccessible, (void**)ppdispParent))) { + return S_OK; + } + } + return MsaaAccessible::get_accParent(ppdispParent); + } + + DocAccessible* docAcc = DocAcc(); + MOZ_ASSERT(docAcc); + + // Return window system accessible object for root document accessibles, as + // well as tab document accessibles if window emulation is enabled. + MOZ_ASSERT(XRE_IsParentProcess()); + if ((!docAcc->ParentDocument() || + (nsWinUtils::IsWindowEmulationStarted() && + nsCoreUtils::IsTopLevelContentDocInProcess(docAcc->DocumentNode())))) { + HWND hwnd = static_cast<HWND>(docAcc->GetNativeWindow()); + if (hwnd && + SUCCEEDED(::CreateStdAccessibleObject( + hwnd, OBJID_WINDOW, IID_IAccessible, (void**)ppdispParent))) { + return S_OK; + } + } + + return MsaaAccessible::get_accParent(ppdispParent); +} + +STDMETHODIMP +MsaaDocAccessible::get_accValue(VARIANT aVarChild, BSTR __RPC_FAR* aValue) { + if (!aValue) return E_INVALIDARG; + *aValue = nullptr; + + // For backwards-compat, we still support old MSAA hack to provide URL in + // accValue Check for real value first + HRESULT hr = MsaaAccessible::get_accValue(aVarChild, aValue); + if (FAILED(hr) || *aValue || aVarChild.lVal != CHILDID_SELF) return hr; + + // MsaaAccessible::get_accValue should have failed (and thus we should have + // returned early) if the Accessible is dead. + MOZ_ASSERT(mAcc); + // If document is being used to create a widget, don't use the URL hack + roles::Role role = mAcc->Role(); + if (role != roles::DOCUMENT && role != roles::APPLICATION && + role != roles::DIALOG && role != roles::ALERT && + role != roles::NON_NATIVE_DOCUMENT) + return hr; + + nsAutoString url; + nsAccUtils::DocumentURL(mAcc, url); + if (url.IsEmpty()) return S_FALSE; + + *aValue = ::SysAllocStringLen(url.get(), url.Length()); + return *aValue ? S_OK : E_OUTOFMEMORY; +} diff --git a/accessible/windows/msaa/MsaaDocAccessible.h b/accessible/windows/msaa/MsaaDocAccessible.h new file mode 100644 index 0000000000..4bb36670a4 --- /dev/null +++ b/accessible/windows/msaa/MsaaDocAccessible.h @@ -0,0 +1,71 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef mozilla_a11y_MsaaDocAccessible_h__ +#define mozilla_a11y_MsaaDocAccessible_h__ + +#include "ia2AccessibleHypertext.h" +#include "nsTHashMap.h" + +namespace mozilla { + +namespace a11y { +class Accessible; +class DocAccessible; +class DocAccessibleParent; + +class MsaaDocAccessible : public ia2AccessibleHypertext { + public: + DocAccessible* DocAcc(); + + // IUnknown + DECL_IUNKNOWN_INHERITED + IMPL_IUNKNOWN_REFCOUNTING_INHERITED(ia2AccessibleHypertext) + + // IAccessible + + // Override get_accParent for e10s + virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accParent( + /* [retval][out] */ IDispatch __RPC_FAR* __RPC_FAR* ppdispParent) + override; + + // Override get_accValue to provide URL when no other value is available + virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accValue( + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ BSTR __RPC_FAR* pszValue) override; + + /** + * Manage the mapping from id to Accessible. + */ + void AddID(uint32_t aID, Accessible* aAcc) { + mIDToAccessibleMap.InsertOrUpdate(aID, aAcc); + } + void RemoveID(uint32_t aID) { mIDToAccessibleMap.Remove(aID); } + Accessible* GetAccessibleByID(uint32_t aID) const { + return mIDToAccessibleMap.Get(aID); + } + + static MsaaDocAccessible* GetFrom(DocAccessible* aDoc); + static MsaaDocAccessible* GetFrom(DocAccessibleParent* aDoc); + + /** + * Get the MsaaDocAccessible for the document which owns the given Accessible. + */ + static MsaaDocAccessible* GetFromOwned(Accessible* aAcc); + + protected: + using ia2AccessibleHypertext::ia2AccessibleHypertext; + + /* + * This provides a mapping from 32 bit id to accessible objects. + */ + nsTHashMap<nsUint32HashKey, Accessible*> mIDToAccessibleMap; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/windows/msaa/MsaaIdGenerator.cpp b/accessible/windows/msaa/MsaaIdGenerator.cpp new file mode 100644 index 0000000000..9f220365e3 --- /dev/null +++ b/accessible/windows/msaa/MsaaIdGenerator.cpp @@ -0,0 +1,87 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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 "MsaaIdGenerator.h" + +#include "mozilla/a11y/MsaaAccessible.h" +#include "mozilla/Assertions.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Unused.h" +#include "nsAccessibilityService.h" +#include "sdnAccessible.h" + +namespace mozilla { +namespace a11y { + +uint32_t MsaaIdGenerator::GetID() { + if (!mGetIDCalled) { + mGetIDCalled = true; + // this is a static instance, so capturing this here is safe. + RunOnShutdown([this] { + if (mReleaseIDTimer) { + mReleaseIDTimer->Cancel(); + ReleasePendingIDs(); + } + }); + } + uint32_t id = mIDSet.GetID(); + MOZ_ASSERT(id <= ((1UL << kNumFullIDBits) - 1UL)); + return ~id; +} + +void MsaaIdGenerator::ReleasePendingIDs() { + for (auto id : mIDsToRelease) { + mIDSet.ReleaseID(~id); + } + mIDsToRelease.Clear(); + mReleaseIDTimer = nullptr; +} + +bool MsaaIdGenerator::ReleaseID(uint32_t aID) { + MOZ_ASSERT(aID != MsaaAccessible::kNoID); + // Releasing an id means it can be reused. Reusing ids too quickly can + // cause problems for clients which process events asynchronously. + // Therefore, we release ids after a short delay. This doesn't seem to be + // necessary when the cache is disabled, perhaps because the COM runtime + // holds references to our objects for longer. + if (nsAccessibilityService::IsShutdown()) { + // If accessibility is shut down, no more Accessibles will be created. + // Also, if the service is shut down, it's possible XPCOM is also shutting + // down, in which case timers won't work. Thus, we release the id + // immediately. + mIDSet.ReleaseID(~aID); + return true; + } + const uint32_t kReleaseDelay = 1000; + mIDsToRelease.AppendElement(aID); + if (mReleaseIDTimer) { + mReleaseIDTimer->SetDelay(kReleaseDelay); + } else { + NS_NewTimerWithCallback( + getter_AddRefs(mReleaseIDTimer), + // mReleaseIDTimer is cancelled on shutdown and this is a static + // instance, so capturing this here is safe. + [this](nsITimer* aTimer) { ReleasePendingIDs(); }, kReleaseDelay, + nsITimer::TYPE_ONE_SHOT, "a11y::MsaaIdGenerator::ReleaseIDDelayed"); + } + return true; +} + +void MsaaIdGenerator::ReleaseID(NotNull<MsaaAccessible*> aMsaaAcc) { + ReleaseID(aMsaaAcc->GetExistingID()); +} + +void MsaaIdGenerator::ReleaseID(NotNull<sdnAccessible*> aSdnAcc) { + Maybe<uint32_t> id = aSdnAcc->ReleaseUniqueID(); + if (id.isSome()) { + DebugOnly<bool> released = ReleaseID(id.value()); + MOZ_ASSERT(released); + } +} + +} // namespace a11y +} // namespace mozilla diff --git a/accessible/windows/msaa/MsaaIdGenerator.h b/accessible/windows/msaa/MsaaIdGenerator.h new file mode 100644 index 0000000000..2063a32f71 --- /dev/null +++ b/accessible/windows/msaa/MsaaIdGenerator.h @@ -0,0 +1,47 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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/. */ + +#ifndef mozilla_a11y_MsaaIdGenerator_h +#define mozilla_a11y_MsaaIdGenerator_h + +#include "mozilla/a11y/IDSet.h" + +#include "mozilla/NotNull.h" +#include "nsITimer.h" + +namespace mozilla { +namespace a11y { + +class MsaaAccessible; +class sdnAccessible; + +/** + * This class is responsible for generating child IDs used by our MSAA + * implementation. + */ +class MsaaIdGenerator { + public: + uint32_t GetID(); + void ReleaseID(NotNull<MsaaAccessible*> aMsaaAcc); + void ReleaseID(NotNull<sdnAccessible*> aSdnAcc); + + private: + bool ReleaseID(uint32_t aID); + void ReleasePendingIDs(); + + private: + static constexpr uint32_t kNumFullIDBits = 31UL; + IDSet mIDSet{kNumFullIDBits}; + nsTArray<uint32_t> mIDsToRelease; + nsCOMPtr<nsITimer> mReleaseIDTimer; + // Whether GetID has been called yet this session. + bool mGetIDCalled = false; +}; + +} // namespace a11y +} // namespace mozilla + +#endif // mozilla_a11y_MsaaIdGenerator_h diff --git a/accessible/windows/msaa/MsaaRootAccessible.cpp b/accessible/windows/msaa/MsaaRootAccessible.cpp new file mode 100644 index 0000000000..ac747ff3d1 --- /dev/null +++ b/accessible/windows/msaa/MsaaRootAccessible.cpp @@ -0,0 +1,67 @@ +/* -*- 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 "mozilla/a11y/DocAccessibleParent.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/WindowsVersion.h" +#include "MsaaRootAccessible.h" +#include "Relation.h" +#include "RootAccessible.h" +#include "EnumVariant.h" + +#include <oleauto.h> + +using namespace mozilla; +using namespace mozilla::a11y; + +RootAccessible* MsaaRootAccessible::RootAcc() { + return static_cast<RootAccessible*>(LocalAcc()); +} + +//////////////////////////////////////////////////////////////////////////////// +// Aggregated IUnknown +HRESULT +MsaaRootAccessible::InternalQueryInterface(REFIID aIid, void** aOutInterface) { + if (!aOutInterface) { + return E_INVALIDARG; + } + + // InternalQueryInterface should always return its internal unknown + // when queried for IID_IUnknown... + if (aIid == IID_IUnknown) { + RefPtr<IUnknown> punk(&mInternalUnknown); + punk.forget(aOutInterface); + return S_OK; + } + + // ...Otherwise we pass through to the base COM implementation of + // QueryInterface which is provided by MsaaDocAccessible. + return MsaaDocAccessible::QueryInterface(aIid, aOutInterface); +} + +ULONG +MsaaRootAccessible::InternalAddRef() { return MsaaDocAccessible::AddRef(); } + +ULONG +MsaaRootAccessible::InternalRelease() { return MsaaDocAccessible::Release(); } + +already_AddRefed<IUnknown> MsaaRootAccessible::Aggregate(IUnknown* aOuter) { + MOZ_ASSERT(mOuter && + (mOuter == &mInternalUnknown || mOuter == aOuter || !aOuter)); + if (!aOuter) { + // If there is no aOuter then we should always set mOuter to + // mInternalUnknown. This is standard COM aggregation stuff. + mOuter = &mInternalUnknown; + return nullptr; + } + + mOuter = aOuter; + return GetInternalUnknown(); +} + +already_AddRefed<IUnknown> MsaaRootAccessible::GetInternalUnknown() { + RefPtr<IUnknown> result(&mInternalUnknown); + return result.forget(); +} diff --git a/accessible/windows/msaa/MsaaRootAccessible.h b/accessible/windows/msaa/MsaaRootAccessible.h new file mode 100644 index 0000000000..a51533d7ba --- /dev/null +++ b/accessible/windows/msaa/MsaaRootAccessible.h @@ -0,0 +1,50 @@ +/* -*- 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/. */ + +#ifndef mozilla_a11y_MsaaRootAccessible_h__ +#define mozilla_a11y_MsaaRootAccessible_h__ + +#include "mozilla/mscom/Aggregation.h" +#include "MsaaDocAccessible.h" + +namespace mozilla { + +namespace a11y { + +class MsaaRootAccessible : public MsaaDocAccessible { + public: + explicit MsaaRootAccessible(Accessible* aAcc) + : MsaaDocAccessible(aAcc), mOuter(&mInternalUnknown) {} + + /** + * This method enables a RootAccessibleWrap to be wrapped by a + * LazyInstantiator. + * + * @param aOuter The IUnknown of the object that is wrapping this + * RootAccessibleWrap, or nullptr to unwrap the aOuter from + * a previous call. + * @return This objects own IUnknown (as opposed to aOuter's IUnknown). + */ + already_AddRefed<IUnknown> Aggregate(IUnknown* aOuter); + + /** + * @return This object's own IUnknown, as opposed to its wrapper's IUnknown + * which is what would be returned by QueryInterface(IID_IUnknown). + */ + already_AddRefed<IUnknown> GetInternalUnknown(); + + private: + // DECLARE_AGGREGATABLE declares the internal IUnknown methods as well as + // mInternalUnknown. + DECLARE_AGGREGATABLE(MsaaRootAccessible); + IUnknown* mOuter; + + RootAccessible* RootAcc(); +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/windows/msaa/MsaaXULMenuAccessible.cpp b/accessible/windows/msaa/MsaaXULMenuAccessible.cpp new file mode 100644 index 0000000000..9d65c6d21e --- /dev/null +++ b/accessible/windows/msaa/MsaaXULMenuAccessible.cpp @@ -0,0 +1,83 @@ +/* -*- 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 "MsaaXULMenuAccessible.h" +#include "mozilla/dom/NameSpaceConstants.h" +#include "mozilla/dom/Element.h" +#include "mozilla/a11y/AccessibleWrap.h" +#include "LocalAccessible-inl.h" +#include "nsGkAtoms.h" + +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// MsaaXULMenuitemAccessible +//////////////////////////////////////////////////////////////////////////////// + +STDMETHODIMP +MsaaXULMenuitemAccessible::get_accKeyboardShortcut( + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ BSTR __RPC_FAR* pszKeyboardShortcut) { + if (!pszKeyboardShortcut) return E_INVALIDARG; + *pszKeyboardShortcut = nullptr; + + if (varChild.vt != VT_I4 || varChild.lVal != CHILDID_SELF) { + return MsaaAccessible::get_accKeyboardShortcut(varChild, + pszKeyboardShortcut); + } + + LocalAccessible* acc = LocalAcc(); + if (!acc) { + return CO_E_OBJNOTCONNECTED; + } + + KeyBinding keyBinding = acc->AccessKey(); + if (keyBinding.IsEmpty()) { + return S_FALSE; + } + + nsAutoString shortcut; + keyBinding.ToString(shortcut); + + *pszKeyboardShortcut = ::SysAllocStringLen(shortcut.get(), shortcut.Length()); + return *pszKeyboardShortcut ? S_OK : E_OUTOFMEMORY; +} + +STDMETHODIMP +MsaaXULMenuitemAccessible::get_accName( + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ BSTR __RPC_FAR* pszName) { + if (varChild.vt != VT_I4 || varChild.lVal != CHILDID_SELF) { + return MsaaAccessible::get_accName(varChild, pszName); + } + if (!pszName) { + return E_INVALIDARG; + } + LocalAccessible* acc = LocalAcc(); + if (!acc) { + return CO_E_OBJNOTCONNECTED; + } + + *pszName = nullptr; + nsAutoString name; + acc->Name(name); + if (name.IsVoid()) { + return S_FALSE; + } + + nsAutoString accel; + if (dom::Element* el = acc->Elm()) { + el->GetAttr(nsGkAtoms::acceltext, accel); + } + if (!accel.IsEmpty()) { + name += u"\t"_ns + accel; + } + + *pszName = ::SysAllocStringLen(name.get(), name.Length()); + if (!*pszName) { + return E_OUTOFMEMORY; + } + return S_OK; +} diff --git a/accessible/windows/msaa/MsaaXULMenuAccessible.h b/accessible/windows/msaa/MsaaXULMenuAccessible.h new file mode 100644 index 0000000000..ed5d5a4aa2 --- /dev/null +++ b/accessible/windows/msaa/MsaaXULMenuAccessible.h @@ -0,0 +1,30 @@ +/* -*- 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/. */ + +#ifndef mozilla_a11y_MsaaXULMenuAccessible_h__ +#define mozilla_a11y_MsaaXULMenuAccessible_h__ + +#include "MsaaAccessible.h" + +namespace mozilla { +namespace a11y { + +class MsaaXULMenuitemAccessible : public MsaaAccessible { + public: + virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accKeyboardShortcut( + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ BSTR __RPC_FAR* pszKeyboardShortcut) override; + virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accName( + /* [optional][in] */ VARIANT varChild, + /* [retval][out] */ BSTR __RPC_FAR* pszName) override; + + protected: + using MsaaAccessible::MsaaAccessible; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/windows/msaa/NtUndoc.h b/accessible/windows/msaa/NtUndoc.h new file mode 100644 index 0000000000..b11b791dfe --- /dev/null +++ b/accessible/windows/msaa/NtUndoc.h @@ -0,0 +1,85 @@ +/* -*- 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/. */ + +#ifndef mozilla_NtUndoc_h +#define mozilla_NtUndoc_h + +#include <winternl.h> + +#if defined(__cplusplus) +extern "C" { +#endif + +#ifndef STATUS_INFO_LENGTH_MISMATCH +# define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L) +#endif + +#ifndef STATUS_BUFFER_TOO_SMALL +# define STATUS_BUFFER_TOO_SMALL ((NTSTATUS)0xC0000023L) +#endif + +#ifndef STATUS_MORE_ENTRIES +# define STATUS_MORE_ENTRIES ((NTSTATUS)0x00000105L) +#endif + +enum UndocSystemInformationClass { SystemExtendedHandleInformation = 64 }; + +struct SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX { + PVOID mObject; + ULONG_PTR mPid; + ULONG_PTR mHandle; + ACCESS_MASK mGrantedAccess; + USHORT mCreatorBackTraceIndex; + USHORT mObjectTypeIndex; + ULONG mAttributes; + ULONG mReserved; +}; + +struct SYSTEM_HANDLE_INFORMATION_EX { + ULONG_PTR mHandleCount; + ULONG_PTR mReserved; + SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX mHandles[1]; +}; + +#ifndef __MINGW32__ +enum UndocObjectInformationClass { ObjectNameInformation = 1 }; + +struct OBJECT_NAME_INFORMATION { + UNICODE_STRING Name; +}; +#endif + +// The following declarations are documented on MSDN but are not included in +// public user-mode headers. + +enum DirectoryObjectAccessFlags { + DIRECTORY_QUERY = 0x0001, + DIRECTORY_TRAVERSE = 0x0002, + DIRECTORY_CREATE_OBJECT = 0x0004, + DIRECTORY_CREATE_SUBDIRECTORY = 0x0008, + DIRECTORY_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | 0x000F +}; + +NTSTATUS WINAPI NtOpenDirectoryObject(PHANDLE aDirectoryHandle, + ACCESS_MASK aDesiredAccess, + POBJECT_ATTRIBUTES aObjectAttributes); + +struct OBJECT_DIRECTORY_INFORMATION { + UNICODE_STRING mName; + UNICODE_STRING mTypeName; +}; + +NTSTATUS WINAPI NtQueryDirectoryObject(HANDLE aDirectoryHandle, + PVOID aOutBuffer, ULONG aBufferLength, + BOOLEAN aReturnSingleEntry, + BOOLEAN aRestartScan, PULONG aContext, + PULONG aOutReturnLength); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // mozilla_NtUndoc_h diff --git a/accessible/windows/msaa/Platform.cpp b/accessible/windows/msaa/Platform.cpp new file mode 100644 index 0000000000..40dddac215 --- /dev/null +++ b/accessible/windows/msaa/Platform.cpp @@ -0,0 +1,264 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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 "Platform.h" + +#include "AccEvent.h" +#include "Compatibility.h" +#include "HyperTextAccessible.h" +#include "nsWinUtils.h" +#include "mozilla/a11y/DocAccessibleParent.h" +#include "mozilla/a11y/RemoteAccessible.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/WindowsVersion.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "WinUtils.h" +#include "ia2AccessibleText.h" + +#include <tuple> + +#if defined(MOZ_TELEMETRY_REPORTING) +# include "mozilla/Telemetry.h" +#endif // defined(MOZ_TELEMETRY_REPORTING) + +using namespace mozilla; +using namespace mozilla::a11y; +using namespace mozilla::mscom; + +static StaticRefPtr<nsIFile> gInstantiator; + +void a11y::PlatformInit() { + nsWinUtils::MaybeStartWindowEmulation(); + ia2AccessibleText::InitTextChangeData(); +} + +void a11y::PlatformShutdown() { + ::DestroyCaret(); + + nsWinUtils::ShutdownWindowEmulation(); + + if (gInstantiator) { + gInstantiator = nullptr; + } +} + +void a11y::ProxyCreated(RemoteAccessible* aProxy) { + MsaaAccessible* msaa = MsaaAccessible::Create(aProxy); + msaa->AddRef(); + aProxy->SetWrapper(reinterpret_cast<uintptr_t>(msaa)); +} + +void a11y::ProxyDestroyed(RemoteAccessible* aProxy) { + MsaaAccessible* msaa = + reinterpret_cast<MsaaAccessible*>(aProxy->GetWrapper()); + if (!msaa) { + return; + } + msaa->MsaaShutdown(); + aProxy->SetWrapper(0); + msaa->Release(); + + if (aProxy->IsDoc() && nsWinUtils::IsWindowEmulationStarted()) { + aProxy->AsDoc()->SetEmulatedWindowHandle(nullptr); + } +} + +void a11y::PlatformEvent(Accessible* aTarget, uint32_t aEventType) { + MsaaAccessible::FireWinEvent(aTarget, aEventType); +} + +void a11y::PlatformStateChangeEvent(Accessible* aTarget, uint64_t, bool) { + MsaaAccessible::FireWinEvent(aTarget, nsIAccessibleEvent::EVENT_STATE_CHANGE); +} + +void a11y::PlatformFocusEvent(Accessible* aTarget, + const LayoutDeviceIntRect& aCaretRect) { + if (aTarget->IsRemote() && FocusMgr() && + FocusMgr()->FocusedLocalAccessible()) { + // This is a focus event from a remote document, but focus has moved out + // of that document into the chrome since that event was sent. For example, + // this can happen when choosing File menu -> New Tab. See bug 1471466. + // Note that this does not handle the case where a focus event is sent from + // one remote document, but focus moved into a second remote document + // since that event was sent. However, this isn't something anyone has been + // able to trigger. + return; + } + + AccessibleWrap::UpdateSystemCaretFor(aTarget, aCaretRect); + MsaaAccessible::FireWinEvent(aTarget, nsIAccessibleEvent::EVENT_FOCUS); +} + +void a11y::PlatformCaretMoveEvent(Accessible* aTarget, int32_t aOffset, + bool aIsSelectionCollapsed, + int32_t aGranularity, + const LayoutDeviceIntRect& aCaretRect, + bool aFromUser) { + AccessibleWrap::UpdateSystemCaretFor(aTarget, aCaretRect); + MsaaAccessible::FireWinEvent(aTarget, + nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED); +} + +void a11y::PlatformTextChangeEvent(Accessible* aText, const nsAString& aStr, + int32_t aStart, uint32_t aLen, bool aInsert, + bool) { + uint32_t eventType = aInsert ? nsIAccessibleEvent::EVENT_TEXT_INSERTED + : nsIAccessibleEvent::EVENT_TEXT_REMOVED; + MOZ_ASSERT(aText->IsHyperText()); + ia2AccessibleText::UpdateTextChangeData(aText->AsHyperTextBase(), aInsert, + aStr, aStart, aLen); + MsaaAccessible::FireWinEvent(aText, eventType); +} + +void a11y::PlatformShowHideEvent(Accessible* aTarget, Accessible*, bool aInsert, + bool) { + uint32_t event = + aInsert ? nsIAccessibleEvent::EVENT_SHOW : nsIAccessibleEvent::EVENT_HIDE; + MsaaAccessible::FireWinEvent(aTarget, event); +} + +void a11y::PlatformSelectionEvent(Accessible* aTarget, Accessible*, + uint32_t aType) { + MsaaAccessible::FireWinEvent(aTarget, aType); +} + +static bool GetInstantiatorExecutable(const DWORD aPid, + nsIFile** aOutClientExe) { + nsAutoHandle callingProcess( + ::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, aPid)); + if (!callingProcess) { + return false; + } + + DWORD bufLen = MAX_PATH; + UniquePtr<wchar_t[]> buf; + + while (true) { + buf = MakeUnique<wchar_t[]>(bufLen); + if (::QueryFullProcessImageName(callingProcess, 0, buf.get(), &bufLen)) { + break; + } + + DWORD lastError = ::GetLastError(); + MOZ_ASSERT(lastError == ERROR_INSUFFICIENT_BUFFER); + if (lastError != ERROR_INSUFFICIENT_BUFFER) { + return false; + } + + bufLen *= 2; + } + + nsCOMPtr<nsIFile> file; + nsresult rv = NS_NewLocalFile(nsDependentString(buf.get(), bufLen), false, + getter_AddRefs(file)); + if (NS_FAILED(rv)) { + return false; + } + + file.forget(aOutClientExe); + return NS_SUCCEEDED(rv); +} + +/** + * Appends version information in the format "|a.b.c.d". + * If there is no version information, we append nothing. + */ +static void AppendVersionInfo(nsIFile* aClientExe, nsAString& aStrToAppend) { + MOZ_ASSERT(!NS_IsMainThread()); + + LauncherResult<ModuleVersion> version = GetModuleVersion(aClientExe); + if (version.isErr()) { + return; + } + + auto [major, minor, patch, build] = version.unwrap().AsTuple(); + + aStrToAppend.AppendLiteral(u"|"); + + constexpr auto dot = u"."_ns; + + aStrToAppend.AppendInt(major); + aStrToAppend.Append(dot); + aStrToAppend.AppendInt(minor); + aStrToAppend.Append(dot); + aStrToAppend.AppendInt(patch); + aStrToAppend.Append(dot); + aStrToAppend.AppendInt(build); +} + +static void AccumulateInstantiatorTelemetry(const nsAString& aValue) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!aValue.IsEmpty()) { +#if defined(MOZ_TELEMETRY_REPORTING) + Telemetry::ScalarSet(Telemetry::ScalarID::A11Y_INSTANTIATORS, aValue); +#endif // defined(MOZ_TELEMETRY_REPORTING) + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::AccessibilityClient, + NS_ConvertUTF16toUTF8(aValue)); + } +} + +static void GatherInstantiatorTelemetry(nsIFile* aClientExe) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsString value; + nsresult rv = aClientExe->GetLeafName(value); + if (NS_SUCCEEDED(rv)) { + AppendVersionInfo(aClientExe, value); + } + + nsCOMPtr<nsIRunnable> runnable( + NS_NewRunnableFunction("a11y::AccumulateInstantiatorTelemetry", + [value = std::move(value)]() -> void { + AccumulateInstantiatorTelemetry(value); + })); + + // Now that we've (possibly) obtained version info, send the resulting + // string back to the main thread to accumulate in telemetry. + NS_DispatchToMainThread(runnable.forget()); +} + +void a11y::SetInstantiator(const uint32_t aPid) { + nsCOMPtr<nsIFile> clientExe; + if (!GetInstantiatorExecutable(aPid, getter_AddRefs(clientExe))) { + AccumulateInstantiatorTelemetry( + u"(Failed to retrieve client image name)"_ns); + return; + } + + // Only record the instantiator if it is the first instantiator, or if it does + // not match the previous one. Some blocked clients are repeatedly requesting + // a11y over and over so we don't want to be spawning countless telemetry + // threads. + if (gInstantiator) { + bool equal; + nsresult rv = gInstantiator->Equals(clientExe, &equal); + if (NS_SUCCEEDED(rv) && equal) { + return; + } + } + + gInstantiator = clientExe; + + nsCOMPtr<nsIRunnable> runnable( + NS_NewRunnableFunction("a11y::GatherInstantiatorTelemetry", + [clientExe = std::move(clientExe)]() -> void { + GatherInstantiatorTelemetry(clientExe); + })); + + DebugOnly<nsresult> rv = + NS_DispatchBackgroundTask(runnable.forget(), NS_DISPATCH_EVENT_MAY_BLOCK); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +bool a11y::GetInstantiator(nsIFile** aOutInstantiator) { + if (!gInstantiator) { + return false; + } + + return NS_SUCCEEDED(gInstantiator->Clone(aOutInstantiator)); +} diff --git a/accessible/windows/msaa/RootAccessibleWrap.cpp b/accessible/windows/msaa/RootAccessibleWrap.cpp new file mode 100644 index 0000000000..3a2477e877 --- /dev/null +++ b/accessible/windows/msaa/RootAccessibleWrap.cpp @@ -0,0 +1,44 @@ +/* -*- 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 "RootAccessibleWrap.h" + +#include "Compatibility.h" +#include "mozilla/PresShell.h" +#include "nsCoreUtils.h" +#include "nsWinUtils.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// Constructor/destructor + +RootAccessibleWrap::RootAccessibleWrap(dom::Document* aDocument, + PresShell* aPresShell) + : RootAccessible(aDocument, aPresShell) {} + +RootAccessibleWrap::~RootAccessibleWrap() {} + +//////////////////////////////////////////////////////////////////////////////// +// RootAccessible + +void RootAccessibleWrap::DocumentActivated(DocAccessible* aDocument) { + // This check will never work with e10s enabled, in other words, as of + // Firefox 57. + if (Compatibility::IsDolphin() && + nsCoreUtils::IsTopLevelContentDocInProcess(aDocument->DocumentNode())) { + MOZ_ASSERT(XRE_IsParentProcess()); + uint32_t count = mChildDocuments.Length(); + for (uint32_t idx = 0; idx < count; idx++) { + DocAccessible* childDoc = mChildDocuments[idx]; + HWND childDocHWND = static_cast<HWND>(childDoc->GetNativeWindow()); + if (childDoc != aDocument) + nsWinUtils::HideNativeWindow(childDocHWND); + else + nsWinUtils::ShowNativeWindow(childDocHWND); + } + } +} diff --git a/accessible/windows/msaa/RootAccessibleWrap.h b/accessible/windows/msaa/RootAccessibleWrap.h new file mode 100644 index 0000000000..20b3e8edfc --- /dev/null +++ b/accessible/windows/msaa/RootAccessibleWrap.h @@ -0,0 +1,33 @@ +/* -*- 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/. */ + +#ifndef mozilla_a11y_RootAccessibleWrap_h__ +#define mozilla_a11y_RootAccessibleWrap_h__ + +#include "RootAccessible.h" + +namespace mozilla { + +class PresShell; + +namespace a11y { + +/** + * Windows specific functionality for the node at a root of the accessibility + * tree: see the RootAccessible superclass for further details. + */ +class RootAccessibleWrap : public RootAccessible { + public: + RootAccessibleWrap(dom::Document* aDocument, PresShell* aPresShell); + virtual ~RootAccessibleWrap(); + + // RootAccessible + virtual void DocumentActivated(DocAccessible* aDocument); +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/windows/msaa/ServiceProvider.cpp b/accessible/windows/msaa/ServiceProvider.cpp new file mode 100644 index 0000000000..b2b005f3b4 --- /dev/null +++ b/accessible/windows/msaa/ServiceProvider.cpp @@ -0,0 +1,106 @@ +/* -*- 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 "ServiceProvider.h" + +#include "AccessibleApplication_i.c" +#include "ApplicationAccessibleWrap.h" +#include "DocAccessible.h" +#include "nsAccUtils.h" +#include "nsCoreUtils.h" +#include "Relation.h" +#include "RootAccessible.h" +#include "uiaRawElmProvider.h" + +#include "mozilla/a11y/DocAccessibleChild.h" +#include "mozilla/Preferences.h" + +#include "ISimpleDOM.h" + +namespace mozilla { +namespace a11y { + +IMPL_IUNKNOWN_QUERY_HEAD(ServiceProvider) +IMPL_IUNKNOWN_QUERY_IFACE(IServiceProvider) +IMPL_IUNKNOWN_QUERY_TAIL_AGGREGATED(mMsaa) + +//////////////////////////////////////////////////////////////////////////////// +// IServiceProvider + +STDMETHODIMP +ServiceProvider::QueryService(REFGUID aGuidService, REFIID aIID, + void** aInstancePtr) { + if (!aInstancePtr) return E_INVALIDARG; + + *aInstancePtr = nullptr; + Accessible* acc = mMsaa->Acc(); + if (!acc) { + return CO_E_OBJNOTCONNECTED; + } + AccessibleWrap* localAcc = mMsaa->LocalAcc(); + + // UIA IAccessibleEx + if (aGuidService == IID_IAccessibleEx && + Preferences::GetBool("accessibility.uia.enable") && localAcc) { + uiaRawElmProvider* accEx = new uiaRawElmProvider(localAcc); + HRESULT hr = accEx->QueryInterface(aIID, aInstancePtr); + if (FAILED(hr)) delete accEx; + + return hr; + } + + // Provide a special service ID for getting the accessible for the browser tab + // document that contains this accessible object. If this accessible object + // is not inside a browser tab then the service fails with E_NOINTERFACE. + // A use case for this is for screen readers that need to switch context or + // 'virtual buffer' when focus moves from one browser tab area to another. + static const GUID SID_IAccessibleContentDocument = { + 0xa5d8e1f3, + 0x3571, + 0x4d8f, + {0x95, 0x21, 0x07, 0xed, 0x28, 0xfb, 0x07, 0x2e}}; + if (aGuidService == SID_IAccessibleContentDocument) { + if (aIID != IID_IAccessible) return E_NOINTERFACE; + + Relation rel = acc->RelationByType(RelationType::CONTAINING_TAB_PANE); + RefPtr<IAccessible> next = MsaaAccessible::GetFrom(rel.Next()); + if (!next) { + return E_NOINTERFACE; + } + + next.forget(aInstancePtr); + return S_OK; + } + + // Can get to IAccessibleApplication from any node via QS + // Note: in case of JAWS we want to check if aIID is + // IID_IAccessibleApplication. + if (aGuidService == IID_IAccessibleApplication || + aIID == IID_IAccessibleApplication) { + ApplicationAccessibleWrap* applicationAcc = + static_cast<ApplicationAccessibleWrap*>(ApplicationAcc()); + if (!applicationAcc) return E_NOINTERFACE; + + RefPtr<IAccessible> appIa; + applicationAcc->GetNativeInterface(getter_AddRefs(appIa)); + return appIa->QueryInterface(aIID, aInstancePtr); + } + + static const GUID IID_SimpleDOMDeprecated = { + 0x0c539790, + 0x12e4, + 0x11cf, + {0xb6, 0x61, 0x00, 0xaa, 0x00, 0x4c, 0xd6, 0xd8}}; + if (aGuidService == IID_ISimpleDOMNode || + aGuidService == IID_SimpleDOMDeprecated || + aGuidService == IID_IAccessible || aGuidService == IID_IAccessible2) + return mMsaa->QueryInterface(aIID, aInstancePtr); + + return E_INVALIDARG; +} + +} // namespace a11y +} // namespace mozilla diff --git a/accessible/windows/msaa/ServiceProvider.h b/accessible/windows/msaa/ServiceProvider.h new file mode 100644 index 0000000000..0545bccf62 --- /dev/null +++ b/accessible/windows/msaa/ServiceProvider.h @@ -0,0 +1,37 @@ +/* -*- 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/. */ + +#ifndef mozilla_a11y_ServiceProvider_h_ +#define mozilla_a11y_ServiceProvider_h_ + +#include <servprov.h> + +#include "IUnknownImpl.h" +#include "MsaaAccessible.h" + +namespace mozilla { +namespace a11y { + +class ServiceProvider final : public IServiceProvider { + public: + explicit ServiceProvider(MsaaAccessible* aMsaa) : mMsaa(aMsaa) {} + ~ServiceProvider() {} + + DECL_IUNKNOWN + + // IServiceProvider + virtual HRESULT STDMETHODCALLTYPE QueryService(REFGUID aGuidService, + REFIID aIID, + void** aInstancePtr); + + private: + RefPtr<MsaaAccessible> mMsaa; +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/windows/msaa/moz.build b/accessible/windows/msaa/moz.build new file mode 100644 index 0000000000..1b56c2eacf --- /dev/null +++ b/accessible/windows/msaa/moz.build @@ -0,0 +1,73 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +EXPORTS += [ + "IUnknownImpl.h", +] + +EXPORTS.mozilla.a11y += [ + "AccessibleWrap.h", + "Compatibility.h", + "LazyInstantiator.h", + "MsaaAccessible.h", + "MsaaIdGenerator.h", + "nsWinUtils.h", +] + +UNIFIED_SOURCES += [ + "AccessibleWrap.cpp", + "ApplicationAccessibleWrap.cpp", + "Compatibility.cpp", + "CompatibilityUIA.cpp", + "DocAccessibleWrap.cpp", + "EnumVariant.cpp", + "IUnknownImpl.cpp", + "LazyInstantiator.cpp", + "MsaaAccessible.cpp", + "MsaaDocAccessible.cpp", + "MsaaIdGenerator.cpp", + "MsaaRootAccessible.cpp", + "MsaaXULMenuAccessible.cpp", + "nsWinUtils.cpp", + "Platform.cpp", + "RootAccessibleWrap.cpp", +] + +SOURCES += [ + # This file cannot be built in unified mode because it includes ISimpleDOMNode_i.c. + "ServiceProvider.cpp", +] + +OS_LIBS += [ + "ntdll", +] + +LOCAL_INCLUDES += [ + "/accessible/base", + "/accessible/generic", + "/accessible/html", + "/accessible/ipc", + "/accessible/windows", + "/accessible/windows/ia2", + "/accessible/windows/sdn", + "/accessible/windows/uia", + "/accessible/xpcom", + "/accessible/xul", + "/dom/base", + "/layout/style", +] + +# The Windows MIDL code generator creates things like: +# +# #endif !_MIDL_USE_GUIDDEF_ +# +# which clang-cl complains about. MSVC doesn't, so turn this warning off. +if CONFIG["CC_TYPE"] == "clang-cl": + CXXFLAGS += ["-Wno-extra-tokens"] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/accessible/windows/msaa/nsEventMap.h b/accessible/windows/msaa/nsEventMap.h new file mode 100644 index 0000000000..9030f0e551 --- /dev/null +++ b/accessible/windows/msaa/nsEventMap.h @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=4:tabstop=4: + */ +/* 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 <winuser.h> +#include "AccessibleEventId.h" + +const uint32_t kEVENT_WIN_UNKNOWN = 0x00000000; + +static const uint32_t gWinEventMap[] = { + // clang-format off + kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent doesn't have 0 constant + EVENT_OBJECT_SHOW, // nsIAccessibleEvent::EVENT_SHOW + EVENT_OBJECT_HIDE, // nsIAccessibleEvent::EVENT_HIDE + EVENT_OBJECT_REORDER, // nsIAccessibleEvent::EVENT_REORDER + EVENT_OBJECT_FOCUS, // nsIAccessibleEvent::EVENT_FOCUS + EVENT_OBJECT_STATECHANGE, // nsIAccessibleEvent::EVENT_STATE_CHANGE + EVENT_OBJECT_NAMECHANGE, // nsIAccessibleEvent::EVENT_NAME_CHANGE + EVENT_OBJECT_DESCRIPTIONCHANGE, // nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE + EVENT_OBJECT_VALUECHANGE, // nsIAccessibleEvent::EVENT_VALUE_CHANGE + EVENT_OBJECT_SELECTION, // nsIAccessibleEvent::EVENT_SELECTION + EVENT_OBJECT_SELECTIONADD, // nsIAccessibleEvent::EVENT_SELECTION_ADD + EVENT_OBJECT_SELECTIONREMOVE, // nsIAccessibleEvent::EVENT_SELECTION_REMOVE + EVENT_OBJECT_SELECTIONWITHIN, // nsIAccessibleEvent::EVENT_SELECTION_WITHIN + EVENT_SYSTEM_ALERT, // nsIAccessibleEvent::EVENT_ALERT + EVENT_SYSTEM_MENUSTART, // nsIAccessibleEvent::EVENT_MENU_START + EVENT_SYSTEM_MENUEND, // nsIAccessibleEvent::EVENT_MENU_END + EVENT_SYSTEM_MENUPOPUPSTART, // nsIAccessibleEvent::EVENT_MENUPOPUP_START + EVENT_SYSTEM_MENUPOPUPEND, // nsIAccessibleEvent::EVENT_MENUPOPUP_END + EVENT_SYSTEM_DRAGDROPSTART, // nsIAccessibleEvent::EVENT_DRAGDROP_START + EVENT_SYSTEM_SCROLLINGSTART, // nsIAccessibleEvent::EVENT_SCROLLING_START + EVENT_SYSTEM_SCROLLINGEND, // nsIAccessibleEvent::EVENT_SCROLLING_END + IA2_EVENT_DOCUMENT_LOAD_COMPLETE, // nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE + IA2_EVENT_DOCUMENT_RELOAD, // nsIAccessibleEvent::EVENT_DOCUMENT_RELOAD + IA2_EVENT_DOCUMENT_LOAD_STOPPED, // nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_STOPPED + IA2_EVENT_TEXT_ATTRIBUTE_CHANGED, // nsIAccessibleEvent::EVENT_TEXT_ATTRIBUTE_CHANGED + IA2_EVENT_TEXT_CARET_MOVED, // nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED + IA2_EVENT_TEXT_INSERTED, // nsIAccessibleEvent::EVENT_TEXT_INSERTED + IA2_EVENT_TEXT_REMOVED, // nsIAccessibleEvent::EVENT_TEXT_REMOVED + IA2_EVENT_TEXT_SELECTION_CHANGED, // nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED + kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_WINDOW_ACTIVATE + kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_WINDOW_DEACTIVATE + kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_WINDOW_MAXIMIZE + kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_WINDOW_MINIMIZE + kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_WINDOW_RESTORE + IA2_EVENT_OBJECT_ATTRIBUTE_CHANGED, // nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED + EVENT_OBJECT_VALUECHANGE, // nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE + kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_SCROLLING + kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_ANNOUNCEMENT + kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED + kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED + kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_INNER_REORDER + // clang-format on +}; diff --git a/accessible/windows/msaa/nsWinUtils.cpp b/accessible/windows/msaa/nsWinUtils.cpp new file mode 100644 index 0000000000..6eb7d290dd --- /dev/null +++ b/accessible/windows/msaa/nsWinUtils.cpp @@ -0,0 +1,167 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=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 "nsWinUtils.h" + +#include "Compatibility.h" +#include "DocAccessible.h" +#include "nsAccessibilityService.h" +#include "nsCoreUtils.h" + +#include "mozilla/a11y/DocAccessibleParent.h" +#include "mozilla/Preferences.h" +#include "nsArrayUtils.h" +#include "nsICSSDeclaration.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "nsXULAppAPI.h" + +using namespace mozilla; +using namespace mozilla::a11y; +using mozilla::dom::Element; + +// Window property used by ipc related code in identifying accessible +// tab windows. +const wchar_t* kPropNameTabContent = L"AccessibleTabWindow"; + +/** + * WindowProc to process WM_GETOBJECT messages, used in windows emulation mode. + */ +static LRESULT CALLBACK WindowProc(HWND hWnd, UINT msg, WPARAM wParam, + LPARAM lParam); + +bool nsWinUtils::sWindowEmulationStarted = false; + +already_AddRefed<nsICSSDeclaration> nsWinUtils::GetComputedStyleDeclaration( + nsIContent* aContent) { + nsIContent* elm = nsCoreUtils::GetDOMElementFor(aContent); + if (!elm) return nullptr; + + // Returns number of items in style declaration + nsCOMPtr<nsPIDOMWindowInner> window = elm->OwnerDoc()->GetInnerWindow(); + if (!window) return nullptr; + + ErrorResult dummy; + nsCOMPtr<Element> domElement(do_QueryInterface(elm)); + nsCOMPtr<nsICSSDeclaration> cssDecl = + window->GetComputedStyle(*domElement, u""_ns, dummy); + dummy.SuppressException(); + return cssDecl.forget(); +} + +bool nsWinUtils::MaybeStartWindowEmulation() { + // Register window class that'll be used for document accessibles associated + // with tabs. + if (IPCAccessibilityActive()) return false; + + if (Compatibility::IsJAWS() || Compatibility::IsWE() || + Compatibility::IsDolphin() || Compatibility::IsVisperoShared()) { + RegisterNativeWindow(kClassNameTabContent); + sWindowEmulationStarted = true; + return true; + } + + return false; +} + +void nsWinUtils::ShutdownWindowEmulation() { + // Unregister window call that's used for document accessibles associated + // with tabs. + if (IsWindowEmulationStarted()) { + ::UnregisterClassW(kClassNameTabContent, GetModuleHandle(nullptr)); + sWindowEmulationStarted = false; + } +} + +void nsWinUtils::RegisterNativeWindow(LPCWSTR aWindowClass) { + WNDCLASSW wc; + wc.style = CS_GLOBALCLASS; + wc.lpfnWndProc = WindowProc; + wc.cbClsExtra = 0; + wc.cbWndExtra = 0; + wc.hInstance = GetModuleHandle(nullptr); + wc.hIcon = nullptr; + wc.hCursor = nullptr; + wc.hbrBackground = nullptr; + wc.lpszMenuName = nullptr; + wc.lpszClassName = aWindowClass; + ::RegisterClassW(&wc); +} + +HWND nsWinUtils::CreateNativeWindow(LPCWSTR aWindowClass, HWND aParentWnd, + int aX, int aY, int aWidth, int aHeight, + bool aIsActive, + NativeWindowCreateProc* aOnCreateProc) { + return ::CreateWindowExW( + WS_EX_TRANSPARENT, aWindowClass, L"NetscapeDispatchWnd", + WS_CHILD | (aIsActive ? WS_VISIBLE : 0), aX, aY, aWidth, aHeight, + aParentWnd, nullptr, GetModuleHandle(nullptr), aOnCreateProc); +} + +void nsWinUtils::ShowNativeWindow(HWND aWnd) { ::ShowWindow(aWnd, SW_SHOW); } + +void nsWinUtils::HideNativeWindow(HWND aWnd) { + ::SetWindowPos( + aWnd, nullptr, 0, 0, 0, 0, + SWP_HIDEWINDOW | SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); +} + +LRESULT CALLBACK WindowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { + // Note, this window's message handling should not invoke any call that + // may result in a cross-process ipc call. Doing so may violate RPC + // message semantics. + + switch (msg) { + case WM_CREATE: { + // Mark this window so that ipc related code can identify it. + ::SetPropW(hWnd, kPropNameTabContent, reinterpret_cast<HANDLE>(1)); + + auto createStruct = reinterpret_cast<CREATESTRUCT*>(lParam); + auto createProc = reinterpret_cast<nsWinUtils::NativeWindowCreateProc*>( + createStruct->lpCreateParams); + + if (createProc && *createProc) { + (*createProc)(hWnd); + } + + return 0; + } + case WM_GETOBJECT: { + // Do explicit casting to make it working on 64bit systems (see bug 649236 + // for details). + int32_t objId = static_cast<DWORD>(lParam); + if (objId == OBJID_CLIENT) { + RefPtr<IAccessible> msaaAccessible; + DocAccessible* document = + reinterpret_cast<DocAccessible*>(::GetPropW(hWnd, kPropNameDocAcc)); + if (document) { + document->GetNativeInterface(getter_AddRefs(msaaAccessible)); + } else { + DocAccessibleParent* docParent = static_cast<DocAccessibleParent*>( + ::GetPropW(hWnd, kPropNameDocAccParent)); + if (docParent) { + msaaAccessible = MsaaAccessible::GetFrom(docParent); + } + } + if (msaaAccessible) { + LRESULT result = + ::LresultFromObject(IID_IAccessible, wParam, + msaaAccessible); // does an addref + return result; + } + } + return 0; + } + case WM_NCHITTEST: { + LRESULT lRet = ::DefWindowProc(hWnd, msg, wParam, lParam); + if (HTCLIENT == lRet) lRet = HTTRANSPARENT; + return lRet; + } + } + + return ::DefWindowProcW(hWnd, msg, wParam, lParam); +} diff --git a/accessible/windows/msaa/nsWinUtils.h b/accessible/windows/msaa/nsWinUtils.h new file mode 100644 index 0000000000..7ae0cadc5d --- /dev/null +++ b/accessible/windows/msaa/nsWinUtils.h @@ -0,0 +1,105 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=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/. */ + +#ifndef nsWinUtils_h_ +#define nsWinUtils_h_ + +#include <functional> +#include <windows.h> + +#include "nsICSSDeclaration.h" +#include "nsCOMPtr.h" + +class nsIContent; + +namespace mozilla { +namespace a11y { + +class DocAccessible; + +const LPCWSTR kClassNameRoot = L"MozillaUIWindowClass"; +const LPCWSTR kClassNameTabContent = L"MozillaContentWindowClass"; +const LPCWSTR kPropNameDocAcc = L"MozDocAccessible"; +const LPCWSTR kPropNameDocAccParent = L"MozDocAccessibleParent"; + +class nsWinUtils { + public: + /** + * Return computed styles declaration for the given node. + * + * @note Please use it carefully since it can shutdown the accessible tree + * you operate on. + */ + static already_AddRefed<nsICSSDeclaration> GetComputedStyleDeclaration( + nsIContent* aContent); + + /** + * Start window emulation if presence of specific AT is detected. + */ + static bool MaybeStartWindowEmulation(); + + /** + * Free resources used for window emulation. + */ + static void ShutdownWindowEmulation(); + + /** + * Return true if window emulation is started. + */ + static bool IsWindowEmulationStarted() { return sWindowEmulationStarted; } + + /** + * Helper to register window class. + */ + static void RegisterNativeWindow(LPCWSTR aWindowClass); + + typedef std::function<void(HWND)> NativeWindowCreateProc; + + /** + * Helper to create a window. + * + * NB: If additional setup needs to be done once the window has been created, + * you should do so via aOnCreateProc. Hooks will fire during the + * CreateNativeWindow call, thus triggering events in the AT. + * Using aOnCreateProc guarantees that your additional initialization will + * have completed prior to the AT receiving window creation events. + * + * For example: + * + * nsWinUtils::NativeWindowCreateProc onCreate([](HWND aHwnd) -> void { + * DoSomeAwesomeInitializationStuff(aHwnd); + * DoMoreAwesomeInitializationStuff(aHwnd); + * }); + * HWND hwnd = nsWinUtils::CreateNativeWindow(..., &onCreate); + * // Doing further initialization work to hwnd on this line is too late! + */ + static HWND CreateNativeWindow( + LPCWSTR aWindowClass, HWND aParentWnd, int aX, int aY, int aWidth, + int aHeight, bool aIsActive, + NativeWindowCreateProc* aOnCreateProc = nullptr); + + /** + * Helper to show window. + */ + static void ShowNativeWindow(HWND aWnd); + + /** + * Helper to hide window. + */ + static void HideNativeWindow(HWND aWnd); + + private: + /** + * Flag that indicates if window emulation is started. + */ + static bool sWindowEmulationStarted; +}; + +} // namespace a11y +} // namespace mozilla + +#endif |