summaryrefslogtreecommitdiffstats
path: root/accessible/windows/msaa/MsaaAccessible.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /accessible/windows/msaa/MsaaAccessible.cpp
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'accessible/windows/msaa/MsaaAccessible.cpp')
-rw-r--r--accessible/windows/msaa/MsaaAccessible.cpp1793
1 files changed, 1793 insertions, 0 deletions
diff --git a/accessible/windows/msaa/MsaaAccessible.cpp b/accessible/windows/msaa/MsaaAccessible.cpp
new file mode 100644
index 0000000000..851edbb6c3
--- /dev/null
+++ b/accessible/windows/msaa/MsaaAccessible.cpp
@@ -0,0 +1,1793 @@
+/* -*- 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 "mozilla/a11y/AccessibleWrap.h"
+#include "mozilla/a11y/Compatibility.h"
+#include "mozilla/a11y/DocAccessibleParent.h"
+#include "mozilla/dom/BrowserBridgeChild.h"
+#include "mozilla/dom/BrowserBridgeParent.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/mscom/Interceptor.h"
+#include "mozilla/StaticPrefs_accessibility.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"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+const uint32_t USE_ROLE_STRING = 0;
+static const VARIANT kVarChildIdSelf = {{{VT_I4}}};
+
+MsaaIdGenerator MsaaAccessible::sIDGen;
+ITypeInfo* MsaaAccessible::gTypeInfo = nullptr;
+
+/* static */
+MsaaAccessible* MsaaAccessible::Create(Accessible* aAcc) {
+ // If the cache is enabled, this should only ever be called in the parent
+ // process.
+ MOZ_ASSERT(!StaticPrefs::accessibility_cache_enabled_AtStartup() ||
+ XRE_IsParentProcess());
+ if (!StaticPrefs::accessibility_cache_enabled_AtStartup() &&
+ aAcc->IsRemote()) {
+ // MsaaAccessible is just a stub to hold the id in this case.
+ return new MsaaAccessible(aAcc);
+ }
+ // 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.
+ LocalAccessible* localAcc = aAcc->AsLocal();
+ if (localAcc && aAcc->IsRoot()) {
+ 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 (localAcc) {
+ // XXX These classes don't support RemoteAccessible yet.
+ if (aAcc->IsApplication()) {
+ return new ia2AccessibleApplication(aAcc);
+ }
+ if (aAcc->IsImage()) {
+ return new ia2AccessibleImage(aAcc);
+ }
+ 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 (!StaticPrefs::accessibility_cache_enabled_AtStartup() &&
+ mAcc->IsRemote()) {
+ // This MsaaAccessible is just a stub. We just need to clear mAcc.
+ mAcc = nullptr;
+ return;
+ }
+
+ if (mID != kNoID) {
+ auto doc = MsaaDocAccessible::GetFromOwned(mAcc);
+ MOZ_ASSERT(doc);
+ doc->RemoveID(mID);
+ }
+
+ if (XRE_IsContentProcess()) {
+ // Bug 1434822: To improve performance for cross-process COM, we disable COM
+ // garbage collection. However, this means we never receive Release calls
+ // from clients, so defunct accessibles can never be deleted. Since we
+ // know when an accessible is shutting down, we can work around this by
+ // forcing COM to disconnect this object from all of its remote clients,
+ // which will cause associated references to be released.
+ IUnknown* unk = static_cast<IAccessible*>(this);
+ mscom::Interceptor::DisconnectRemotesForTarget(unk);
+ // If an accessible was retrieved via IAccessibleHypertext::hyperlink*,
+ // it will have a different Interceptor that won't be matched by the above
+ // call, even though it's the same object. Therefore, call it again with
+ // the IAccessibleHyperlink pointer. We can remove this horrible hack once
+ // bug 1440267 is fixed.
+ unk = static_cast<IAccessibleHyperlink*>(this);
+ mscom::Interceptor::DisconnectRemotesForTarget(unk);
+ for (auto& assocUnk : mAssociatedCOMObjectsForDisconnection) {
+ mscom::Interceptor::DisconnectRemotesForTarget(assocUnk);
+ }
+ mAssociatedCOMObjectsForDisconnection.Clear();
+ }
+
+ mAcc = nullptr;
+}
+
+void MsaaAccessible::SetID(uint32_t aID) {
+ MOZ_ASSERT(XRE_IsParentProcess() && mAcc &&
+ !StaticPrefs::accessibility_cache_enabled_AtStartup());
+ mID = aID;
+}
+
+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;
+ }
+
+ // If the cache is disabled, chrome should use mID which has been generated by
+ // the content process.
+ if (!StaticPrefs::accessibility_cache_enabled_AtStartup() &&
+ aAccessible->IsRemote()) {
+ const uint32_t id = MsaaAccessible::GetFrom(aAccessible)->mID;
+ MOZ_ASSERT(id != kNoID);
+ return id;
+ }
+
+ 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 */
+uint32_t MsaaAccessible::GetContentProcessIdFor(
+ dom::ContentParentId aIPCContentId) {
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ return 0;
+ }
+ return sIDGen.GetContentProcessIDFor(aIPCContentId);
+}
+
+/* static */
+void MsaaAccessible::ReleaseContentProcessIdFor(
+ dom::ContentParentId aIPCContentId) {
+ sIDGen.ReleaseContentProcessIDFor(aIPCContentId);
+}
+
+/* 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());
+}
+
+static bool IsHandlerInvalidationNeeded(uint32_t aEvent) {
+ // We want to return true for any events that would indicate that something
+ // in the handler cache is out of date.
+ switch (aEvent) {
+ case EVENT_OBJECT_STATECHANGE:
+ case EVENT_OBJECT_LOCATIONCHANGE:
+ case EVENT_OBJECT_NAMECHANGE:
+ case EVENT_OBJECT_DESCRIPTIONCHANGE:
+ case EVENT_OBJECT_VALUECHANGE:
+ case EVENT_OBJECT_FOCUS:
+ case IA2_EVENT_ACTION_CHANGED:
+ case IA2_EVENT_DOCUMENT_LOAD_COMPLETE:
+ case IA2_EVENT_DOCUMENT_LOAD_STOPPED:
+ case IA2_EVENT_DOCUMENT_ATTRIBUTE_CHANGED:
+ case IA2_EVENT_DOCUMENT_CONTENT_CHANGED:
+ case IA2_EVENT_PAGE_CHANGED:
+ case IA2_EVENT_TEXT_ATTRIBUTE_CHANGED:
+ case IA2_EVENT_TEXT_CHANGED:
+ case IA2_EVENT_TEXT_INSERTED:
+ case IA2_EVENT_TEXT_REMOVED:
+ case IA2_EVENT_TEXT_UPDATED:
+ case IA2_EVENT_OBJECT_ATTRIBUTE_CHANGED:
+ return true;
+ default:
+ return false;
+ }
+}
+
+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");
+
+ NS_ASSERTION(aEventType > 0 && aEventType < ArrayLength(gWinEventMap),
+ "invalid event type");
+
+ 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;
+ }
+
+ if (IsHandlerInvalidationNeeded(winEvent)) {
+ AccessibleWrap::InvalidateHandlers();
+ }
+
+ // 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;
+}
+
+/**
+ * If this is an OOP iframe in a content process, return the COM proxy for the
+ * child document.
+ * This will only ever return something when the cache is disabled. When the
+ * cache is enabled, traversing to OOP iframe documents is handled in the
+ * parent process via the RemoteAccessible hierarchy.
+ */
+static already_AddRefed<IDispatch> MaybeGetOOPIframeDocCOMProxy(
+ Accessible* aAcc) {
+ if (!XRE_IsContentProcess() || !aAcc->IsOuterDoc()) {
+ return nullptr;
+ }
+ MOZ_ASSERT(!StaticPrefs::accessibility_cache_enabled_AtStartup());
+ LocalAccessible* outerDoc = aAcc->AsLocal();
+ auto bridge = dom::BrowserBridgeChild::GetFrom(outerDoc->GetContent());
+ if (!bridge) {
+ // This isn't an OOP iframe.
+ return nullptr;
+ }
+ return bridge->GetEmbeddedDocAccessible();
+}
+
+/**
+ * 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;
+ }
+
+ if (aVarChild.lVal == 1 && RefPtr{MaybeGetOOPIframeDocCOMProxy(mAcc)}) {
+ // We can't access an OOP iframe document.
+ return E_FAIL;
+ }
+
+ 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 already_AddRefed<IDispatch> GetProxiedAccessibleInSubtree(
+ DocAccessibleParent* aDoc, const VARIANT& aVarChild) {
+ MOZ_ASSERT(!StaticPrefs::accessibility_cache_enabled_AtStartup());
+ RefPtr<IAccessible> comProxy;
+ int32_t docWrapperChildId = MsaaAccessible::GetChildIDFor(aDoc);
+ // Only document accessible proxies at the top level of their content process
+ // are created with a pointer to their COM proxy.
+ if (aDoc->IsTopLevelInContentProcess()) {
+ aDoc->GetCOMInterface(getter_AddRefs(comProxy));
+ } else {
+ auto tab = static_cast<dom::BrowserParent*>(aDoc->Manager());
+ MOZ_ASSERT(tab);
+ DocAccessibleParent* topLevelDoc = tab->GetTopLevelDocAccessible();
+ MOZ_ASSERT(topLevelDoc && topLevelDoc->IsTopLevelInContentProcess());
+ VARIANT docId = {{{VT_I4}}};
+ docId.lVal = docWrapperChildId;
+ RefPtr<IDispatch> disp = GetProxiedAccessibleInSubtree(topLevelDoc, docId);
+ if (!disp) {
+ return nullptr;
+ }
+
+ DebugOnly<HRESULT> hr =
+ disp->QueryInterface(IID_IAccessible, getter_AddRefs(comProxy));
+ MOZ_ASSERT(SUCCEEDED(hr));
+ }
+
+ MOZ_ASSERT(comProxy);
+ if (!comProxy) {
+ return nullptr;
+ }
+
+ if (docWrapperChildId == aVarChild.lVal) {
+ return comProxy.forget();
+ }
+
+ RefPtr<IDispatch> disp;
+ if (FAILED(comProxy->get_accChild(aVarChild, getter_AddRefs(disp)))) {
+ return nullptr;
+ }
+
+ return disp.forget();
+}
+
+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;
+ }
+
+ AccessibleWrap* localAcc = static_cast<AccessibleWrap*>(mAcc->AsLocal());
+ if (varChild.lVal == CHILDID_SELF) {
+ MOZ_ASSERT(localAcc ||
+ StaticPrefs::accessibility_cache_enabled_AtStartup());
+ 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 the MSAA ID is not a chrome id then we already know that we won't
+ // find it here and should look remotely instead. This handles the case when
+ // accessible is part of the chrome process and is part of the xul browser
+ // window and the child id points in the content documents.
+ // Bug 1422674: We must only handle remote ids here (< 0), not child indices.
+ // Child indices (> 0) are handled below for both local and remote children.
+ if (XRE_IsParentProcess() && localAcc && varChild.lVal < 0 &&
+ !sIDGen.IsChromeID(varChild.lVal)) {
+ MOZ_ASSERT(!StaticPrefs::accessibility_cache_enabled_AtStartup());
+ if (!localAcc->IsRootForHWND()) {
+ // Bug 1422201, 1424657: accChild with a remote id is only valid on the
+ // root accessible for an HWND.
+ // Otherwise, we might return remote accessibles which aren't descendants
+ // of this accessible. This would confuse clients which use accChild to
+ // check whether something is a descendant of a document.
+ return nullptr;
+ }
+ return GetRemoteIAccessibleFor(varChild);
+ }
+
+ if (varChild.lVal > 0) {
+ // Gecko child indices are 0-based in contrast to indices used in MSAA.
+ if (varChild.lVal == 1) {
+ RefPtr<IDispatch> disp = MaybeGetOOPIframeDocCOMProxy(localAcc);
+ if (disp) {
+ // We need to return an IAccessible here, but
+ // MaybeGetOOPIframeDocCOMProxy returns an IDispatch. We know this
+ // IDispatch is also an IAccessible, so this code is safe, albeit ugly.
+ disp.forget((void**)getter_AddRefs(result));
+ return result.forget();
+ }
+ }
+ Accessible* xpAcc = mAcc->ChildAt(varChild.lVal - 1);
+ if (!xpAcc) {
+ return nullptr;
+ }
+ MOZ_ASSERT(xpAcc->IsRemote() || !xpAcc->AsLocal()->IsDefunct(),
+ "Shouldn't get a defunct child");
+ if (!StaticPrefs::accessibility_cache_enabled_AtStartup() && localAcc &&
+ xpAcc->IsRemote()) {
+ // We're crossing from a local OuterDoc to a remote document and caching
+ // isn't enabled. We must return the COM proxy.
+ MOZ_ASSERT(localAcc->IsOuterDoc());
+ xpAcc->AsRemote()->GetCOMInterface(getter_AddRefs(result));
+ return result.forget();
+ }
+ 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.
+ // First see about the case that both this accessible and the target one are
+ // RemoteAccessibles and the cache is disabled.
+ if (!localAcc && !StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ DocAccessibleParent* proxyDoc = mAcc->AsRemote()->Document();
+ RefPtr<IDispatch> disp = GetProxiedAccessibleInSubtree(proxyDoc, varChild);
+ if (!disp) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(mscom::IsProxy(disp));
+ DebugOnly<HRESULT> hr =
+ disp->QueryInterface(IID_IAccessible, getter_AddRefs(result));
+ MOZ_ASSERT(SUCCEEDED(hr));
+ return result.forget();
+ }
+
+ Accessible* doc = nullptr;
+ Accessible* child = nullptr;
+ auto id = static_cast<uint32_t>(varChild.lVal);
+ if (localAcc) {
+ DocAccessible* localDoc = localAcc->Document();
+ doc = localDoc;
+ child = GetAccessibleInSubtree(localDoc, id);
+ if (!child && StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ // 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 if (StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ DocAccessibleParent* remoteDoc = mAcc->AsRemote()->Document();
+ doc = remoteDoc;
+ child = GetAccessibleInSubtree(remoteDoc, id);
+ }
+ if (!child) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(child->IsLocal() ||
+ StaticPrefs::accessibility_cache_enabled_AtStartup());
+ 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;
+}
+
+/**
+ * Visit DocAccessibleParent descendants of `aBrowser` that are at the top
+ * level of their content process.
+ * That is, IsTopLevelInContentProcess() will be true for each visited actor.
+ * Each visited actor will be an embedded document in a different content
+ * process to its embedder.
+ * The DocAccessibleParent for `aBrowser` itself is excluded.
+ * `aCallback` will be called for each DocAccessibleParent.
+ * The callback should return true to continue traversal, false to cease.
+ */
+template <typename Callback>
+static bool VisitDocAccessibleParentDescendantsAtTopLevelInContentProcess(
+ dom::BrowserParent* aBrowser, Callback aCallback) {
+ // We can't use BrowserBridgeParent::VisitAllDescendants because it doesn't
+ // provide a way to stop the search.
+ const auto& bridges = aBrowser->ManagedPBrowserBridgeParent();
+ return std::all_of(bridges.cbegin(), bridges.cend(), [&](const auto& key) {
+ auto* bridge = static_cast<dom::BrowserBridgeParent*>(key);
+ dom::BrowserParent* childBrowser = bridge->GetBrowserParent();
+ DocAccessibleParent* childDocAcc = childBrowser->GetTopLevelDocAccessible();
+ if (!childDocAcc || childDocAcc->IsShutdown()) {
+ return true;
+ }
+ if (!aCallback(childDocAcc)) {
+ return false; // Stop traversal.
+ }
+ if (!VisitDocAccessibleParentDescendantsAtTopLevelInContentProcess(
+ childBrowser, aCallback)) {
+ return false; // Stop traversal.
+ }
+ return true; // Continue traversal.
+ });
+}
+
+already_AddRefed<IAccessible> MsaaAccessible::GetRemoteIAccessibleFor(
+ const VARIANT& aVarChild) {
+ a11y::RootAccessible* root = LocalAcc()->RootAccessible();
+ const nsTArray<DocAccessibleParent*>* rawRemoteDocs =
+ DocManager::TopLevelRemoteDocs();
+ if (!rawRemoteDocs) {
+ return nullptr;
+ }
+ nsTArray<RefPtr<DocAccessibleParent>> remoteDocs(rawRemoteDocs->Length());
+ for (auto rawRemoteDoc : *rawRemoteDocs) {
+ remoteDocs.AppendElement(rawRemoteDoc);
+ }
+
+ RefPtr<IAccessible> result;
+
+ for (auto topRemoteDoc : remoteDocs) {
+ if (topRemoteDoc->IsShutdown()) {
+ continue;
+ }
+ LocalAccessible* outerDoc = topRemoteDoc->OuterDocOfRemoteBrowser();
+ if (!outerDoc) {
+ continue;
+ }
+
+ if (outerDoc->RootAccessible() != root) {
+ continue;
+ }
+
+ RefPtr<IDispatch> disp;
+ auto checkDoc = [&aVarChild,
+ &disp](DocAccessibleParent* aRemoteDoc) -> bool {
+ uint32_t remoteDocMsaaId =
+ MsaaAccessible::GetFrom(aRemoteDoc)->GetExistingID();
+ if (!sIDGen.IsSameContentProcessFor(aVarChild.lVal, remoteDocMsaaId)) {
+ return true; // Continue the search.
+ }
+ if ((disp = GetProxiedAccessibleInSubtree(aRemoteDoc, aVarChild))) {
+ return false; // Found it! Stop traversal!
+ }
+ return true; // Continue the search.
+ };
+
+ // Check the top level document for this id.
+ checkDoc(topRemoteDoc);
+ if (!disp) {
+ // The top level document doesn't contain this id. Recursively check any
+ // out-of-process iframe documents it embeds.
+ VisitDocAccessibleParentDescendantsAtTopLevelInContentProcess(
+ static_cast<dom::BrowserParent*>(topRemoteDoc->Manager()), checkDoc);
+ }
+
+ if (!disp) {
+ continue;
+ }
+
+ DebugOnly<HRESULT> hr =
+ disp->QueryInterface(IID_IAccessible, getter_AddRefs(result));
+ // QI can fail on rare occasions if the LocalAccessible dies after we
+ // fetched disp but before we QI.
+ NS_WARNING_ASSERTION(SUCCEEDED(hr), "QI failed on remote IDispatch");
+ return result.forget();
+ }
+
+ return nullptr;
+}
+
+IDispatch* MsaaAccessible::NativeAccessible(Accessible* aAccessible) {
+ if (!aAccessible) {
+ NS_WARNING("Not passing in an aAccessible");
+ return nullptr;
+ }
+
+ RefPtr<IDispatch> disp;
+ if (!StaticPrefs::accessibility_cache_enabled_AtStartup() &&
+ aAccessible->IsRemote()) {
+ // This is a RemoteAccessible and caching isn't enabled. We must return
+ // the COM proxy.
+ aAccessible->AsRemote()->GetCOMInterface(getter_AddRefs(disp));
+ } else {
+ 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();
+}
+
+// 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_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) {
+ if (
+ // Don't support this interface for leaf elements.
+ !mAcc->HasChildren() || nsAccUtils::MustPrune(mAcc) ||
+ // We also don't support this for local OuterDocAccessibles with remote
+ // document children when the cache is disabled. Handling this is tricky
+ // and there's no benefit to IEnumVARIANT when there's only one child
+ // anyway.
+ (localAcc && localAcc->IsOuterDoc() &&
+ !StaticPrefs::accessibility_cache_enabled_AtStartup() &&
+ localAcc->FirstChild()->IsRemote())) {
+ 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 (!*ppv && iid == IID_IGeckoCustom) {
+ MOZ_ASSERT(XRE_IsContentProcess() &&
+ !StaticPrefs::accessibility_cache_enabled_AtStartup());
+ RefPtr<GeckoCustom> gkCrap = new GeckoCustom(this);
+ gkCrap.forget(ppv);
+ return S_OK;
+ }
+
+ 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;
+ MOZ_ASSERT(StaticPrefs::accessibility_cache_enabled_AtStartup() ||
+ xpParentAcc->IsLocal());
+
+ *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();
+ if (*pcountChildren == 0 && RefPtr{MaybeGetOOPIframeDocCOMProxy(mAcc)}) {
+ *pcountChildren = 1;
+ }
+ 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, 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;
+ }
+
+ // -- Try enumerated role
+ if (msaaRole != USE_ROLE_STRING) {
+ pvarRole->vt = VT_I4;
+ pvarRole->lVal = msaaRole; // Normal enumerated role
+ return S_OK;
+ }
+
+ // -- Try BSTR role
+ // Could not map to known enumerated MSAA role like ROLE_BUTTON
+ // Use BSTR role to expose role attribute or tag name + namespace
+ // XXX We should remove this hack and map to standard MSAA roles, even though
+ // they're lossy. See bug 798492.
+ if (mAcc->IsRemote()) {
+ // We don't support unknown or multiple ARIA roles for RemoteAccessible
+ // here, nor can we support namespaces. No one should be relying on this
+ // anyway, so this is fine. We just want to avoid returning a failure here.
+ nsAtom* val = nullptr;
+ const nsRoleMapEntry* roleMap = mAcc->ARIARoleMap();
+ if (roleMap && roleMap->roleAtom != nsGkAtoms::_empty) {
+ val = roleMap->roleAtom;
+ } else {
+ val = mAcc->TagName();
+ }
+ if (!val) {
+ return E_FAIL;
+ }
+ pvarRole->vt = VT_BSTR;
+ pvarRole->bstrVal = ::SysAllocString(val->GetUTF16String());
+ return S_OK;
+ }
+
+ LocalAccessible* localAcc = mAcc->AsLocal();
+ MOZ_ASSERT(localAcc);
+ nsIContent* content = localAcc->GetContent();
+ if (!content) return E_FAIL;
+
+ if (content->IsElement()) {
+ nsAutoString roleString;
+ // Try the role attribute.
+ nsAccUtils::GetARIAAttr(content->AsElement(), nsGkAtoms::role, roleString);
+
+ if (roleString.IsEmpty()) {
+ // No role attribute (or it is an empty string).
+ // Use the tag name.
+ dom::Document* document = content->GetUncomposedDoc();
+ if (!document) return E_FAIL;
+
+ dom::NodeInfo* nodeInfo = content->NodeInfo();
+ nodeInfo->GetName(roleString);
+
+ // Only append name space if different from that of current document.
+ if (!nodeInfo->NamespaceEquals(document->GetDefaultNamespaceID())) {
+ nsAutoString nameSpaceURI;
+ nodeInfo->GetNamespaceURI(nameSpaceURI);
+ roleString += u", "_ns + nameSpaceURI;
+ }
+ }
+
+ if (!roleString.IsEmpty()) {
+ pvarRole->vt = VT_BSTR;
+ pvarRole->bstrVal = ::SysAllocString(roleString.get());
+ return S_OK;
+ }
+ }
+
+ return E_FAIL;
+}
+
+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);
+ AssociateCOMObjectForDisconnection(pEnum);
+ 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) {
+ if (XRE_IsContentProcess()) {
+ // In this case we might have been invoked while the IPC MessageChannel is
+ // waiting on a sync reply. We cannot dispatch additional IPC while that
+ // is happening, so we dispatch TakeFocus from the main thread to
+ // guarantee that we are outside any IPC.
+ MOZ_ASSERT(mAcc->IsLocal(),
+ "Content process should only have local accessibles");
+ nsCOMPtr<nsIRunnable> runnable = mozilla::NewRunnableMethod(
+ "LocalAccessible::TakeFocus", mAcc->AsLocal(),
+ &LocalAccessible::TakeFocus);
+ NS_DispatchToMainThread(runnable, NS_DISPATCH_NORMAL);
+ return S_OK;
+ }
+ 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);
+ }
+ // This should never be called on a RemoteAccessible if the cache is disabled.
+ MOZ_ASSERT(mAcc->IsLocal() ||
+ StaticPrefs::accessibility_cache_enabled_AtStartup());
+
+ 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 (RefPtr<IDispatch> disp = MaybeGetOOPIframeDocCOMProxy(mAcc)) {
+ pvarEndUpAt->vt = VT_DISPATCH;
+ disp.forget(&pvarEndUpAt->pdispVal);
+ return S_OK;
+ } else if (!nsAccUtils::MustPrune(mAcc)) {
+ navAccessible = mAcc->FirstChild();
+ }
+ break;
+ case NAVDIR_LASTCHILD:
+ if (RefPtr<IDispatch> disp = MaybeGetOOPIframeDocCOMProxy(mAcc)) {
+ pvarEndUpAt->vt = VT_DISPATCH;
+ disp.forget(&pvarEndUpAt->pdispVal);
+ return S_OK;
+ } else 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;
+ }
+
+ Accessible* accessible = mAcc->ChildAtPoint(
+ xLeft, yTop,
+ // 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. For now, we keep this behind the cache pref.
+ StaticPrefs::accessibility_cache_enabled_AtStartup()
+ ? Accessible::EWhichChildAtPoint::DeepestChild
+ : Accessible::EWhichChildAtPoint::DirectChild);
+
+ // if we got a child
+ if (accessible) {
+ 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
+ if (RefPtr<IDispatch> disp = MaybeGetOOPIframeDocCOMProxy(mAcc)) {
+ // This is an OOP iframe. ChildAtPoint can't traverse inside it. If the
+ // coordinates are inside this iframe, return the COM proxy for the
+ // OOP document.
+ LayoutDeviceIntRect docRect = mAcc->AsLocal()->Bounds();
+ if (docRect.Contains(xLeft, yTop)) {
+ pvarChild->vt = VT_DISPATCH;
+ disp.forget(&pvarChild->pdispVal);
+ return S_OK;
+ }
+ }
+ 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);
+ }
+ if (mAcc->IsRemote()) {
+ return E_NOTIMPL; // XXX Not supported for RemoteAccessible yet.
+ }
+
+ HyperTextAccessible* ht = LocalAcc()->AsHyperText();
+ 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);
+}