diff options
Diffstat (limited to '')
-rw-r--r-- | accessible/ipc/DocAccessibleChild.cpp | 431 | ||||
-rw-r--r-- | accessible/ipc/DocAccessibleChild.h | 176 | ||||
-rw-r--r-- | accessible/ipc/DocAccessibleParent.cpp | 1266 | ||||
-rw-r--r-- | accessible/ipc/DocAccessibleParent.h | 400 | ||||
-rw-r--r-- | accessible/ipc/DocAccessibleTypes.ipdlh | 19 | ||||
-rw-r--r-- | accessible/ipc/IPCTypes.h | 189 | ||||
-rw-r--r-- | accessible/ipc/PDocAccessible.ipdl | 164 | ||||
-rw-r--r-- | accessible/ipc/RemoteAccessible.cpp | 2092 | ||||
-rw-r--r-- | accessible/ipc/RemoteAccessible.h | 511 | ||||
-rw-r--r-- | accessible/ipc/moz.build | 62 |
10 files changed, 5310 insertions, 0 deletions
diff --git a/accessible/ipc/DocAccessibleChild.cpp b/accessible/ipc/DocAccessibleChild.cpp new file mode 100644 index 0000000000..5f9d1a0a63 --- /dev/null +++ b/accessible/ipc/DocAccessibleChild.cpp @@ -0,0 +1,431 @@ +/* -*- 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 "chrome/common/ipc_channel.h" +#include "mozilla/a11y/DocAccessibleChild.h" +#include "mozilla/a11y/CacheConstants.h" +#include "mozilla/a11y/FocusManager.h" +#include "mozilla/ipc/ProcessChild.h" +#include "nsAccessibilityService.h" + +#include "LocalAccessible-inl.h" +#ifdef A11Y_LOG +# include "Logging.h" +#endif +#include "TextLeafRange.h" + +namespace mozilla { +namespace a11y { + +/* static */ +void DocAccessibleChild::FlattenTree(LocalAccessible* aRoot, + nsTArray<LocalAccessible*>& aTree) { + MOZ_ASSERT(!aRoot->IsDoc(), "documents shouldn't be serialized"); + + aTree.AppendElement(aRoot); + // OuterDocAccessibles are special because we don't want to serialize the + // child doc here, we'll call PDocAccessibleConstructor in + // NotificationController. + uint32_t childCount = aRoot->IsOuterDoc() ? 0 : aRoot->ChildCount(); + + for (uint32_t i = 0; i < childCount; i++) { + FlattenTree(aRoot->LocalChildAt(i), aTree); + } +} + +/* static */ +AccessibleData DocAccessibleChild::SerializeAcc(LocalAccessible* aAcc) { + uint32_t genericTypes = aAcc->mGenericTypes; + if (aAcc->ARIAHasNumericValue()) { + // XXX: We need to do this because this requires a state check. + genericTypes |= eNumericValue; + } + if (aAcc->IsTextLeaf() || aAcc->IsImage()) { + // Ideally, we'd set eActionable for any Accessible with an ancedstor + // action. However, that requires an ancestor walk which is too expensive + // here. eActionable is only used by ATK. For now, we only expose ancestor + // actions on text leaf and image Accessibles. This means that we don't + // support "click ancestor" for ATK. + if (aAcc->ActionCount()) { + genericTypes |= eActionable; + } + } else if (aAcc->HasPrimaryAction()) { + genericTypes |= eActionable; + } + + RefPtr<AccAttributes> fields; + // Even though we send moves as a hide and a show, we don't want to + // push the cache again for moves. + if (!aAcc->Document()->IsAccessibleBeingMoved(aAcc)) { + fields = + aAcc->BundleFieldsForCache(CacheDomain::All, CacheUpdateType::Initial); + if (fields->Count() == 0) { + fields = nullptr; + } + } + + return AccessibleData(aAcc->ID(), aAcc->Role(), aAcc->LocalParent()->ID(), + static_cast<int32_t>(aAcc->IndexInParent()), + static_cast<AccType>(aAcc->mType), + static_cast<AccGenericType>(genericTypes), + aAcc->mRoleMapEntryIndex, fields); +} + +void DocAccessibleChild::InsertIntoIpcTree(LocalAccessible* aChild, + bool aSuppressShowEvent) { + nsTArray<LocalAccessible*> shownTree; + FlattenTree(aChild, shownTree); + uint32_t totalAccs = shownTree.Length(); + // Exceeding the IPDL maximum message size will cause a crash. Try to avoid + // this by only including kMaxAccsPerMessage Accessibels in a single IPDL + // call. If there are Accessibles beyond this, they will be split across + // multiple calls. + constexpr uint32_t kMaxAccsPerMessage = + IPC::Channel::kMaximumMessageSize / (2 * 1024); + nsTArray<AccessibleData> data(std::min(kMaxAccsPerMessage, totalAccs)); + for (LocalAccessible* child : shownTree) { + if (data.Length() == kMaxAccsPerMessage) { + if (ipc::ProcessChild::ExpectingShutdown()) { + return; + } + SendShowEvent(data, aSuppressShowEvent, false, false); + data.ClearAndRetainStorage(); + } + data.AppendElement(SerializeAcc(child)); + } + if (ipc::ProcessChild::ExpectingShutdown()) { + return; + } + if (!data.IsEmpty()) { + SendShowEvent(data, aSuppressShowEvent, true, false); + } +} + +void DocAccessibleChild::ShowEvent(AccShowEvent* aShowEvent) { + LocalAccessible* child = aShowEvent->GetAccessible(); + InsertIntoIpcTree(child, false); +} + +mozilla::ipc::IPCResult DocAccessibleChild::RecvTakeFocus(const uint64_t& aID) { + LocalAccessible* acc = IdToAccessible(aID); + if (acc) { + acc->TakeFocus(); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleChild::RecvScrollTo( + const uint64_t& aID, const uint32_t& aScrollType) { + LocalAccessible* acc = IdToAccessible(aID); + if (acc) { + RefPtr<PresShell> presShell = acc->Document()->PresShellPtr(); + nsCOMPtr<nsIContent> content = acc->GetContent(); + nsCoreUtils::ScrollTo(presShell, content, aScrollType); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleChild::RecvTakeSelection( + const uint64_t& aID) { + LocalAccessible* acc = IdToAccessible(aID); + if (acc) { + acc->TakeSelection(); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleChild::RecvSetSelected( + const uint64_t& aID, const bool& aSelect) { + LocalAccessible* acc = IdToAccessible(aID); + if (acc) { + acc->SetSelected(aSelect); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleChild::RecvVerifyCache( + const uint64_t& aID, const uint64_t& aCacheDomain, AccAttributes* aFields) { +#ifdef A11Y_LOG + LocalAccessible* acc = IdToAccessible(aID); + if (!acc) { + return IPC_OK(); + } + + RefPtr<AccAttributes> localFields = + acc->BundleFieldsForCache(aCacheDomain, CacheUpdateType::Update); + bool mismatches = false; + + for (auto prop : *localFields) { + if (prop.Value<DeleteEntry>()) { + if (aFields->HasAttribute(prop.Name())) { + if (!mismatches) { + logging::MsgBegin("Mismatch!", "Local and remote values differ"); + logging::AccessibleInfo("", acc); + mismatches = true; + } + nsAutoCString propName; + prop.Name()->ToUTF8String(propName); + nsAutoString val; + aFields->GetAttribute(prop.Name(), val); + logging::MsgEntry( + "Remote value for %s should be empty, but instead it is '%s'", + propName.get(), NS_ConvertUTF16toUTF8(val).get()); + } + continue; + } + + nsAutoString localVal; + prop.ValueAsString(localVal); + nsAutoString remoteVal; + aFields->GetAttribute(prop.Name(), remoteVal); + if (!localVal.Equals(remoteVal)) { + if (!mismatches) { + logging::MsgBegin("Mismatch!", "Local and remote values differ"); + logging::AccessibleInfo("", acc); + mismatches = true; + } + nsAutoCString propName; + prop.Name()->ToUTF8String(propName); + logging::MsgEntry("Fields differ: %s '%s' != '%s'", propName.get(), + NS_ConvertUTF16toUTF8(remoteVal).get(), + NS_ConvertUTF16toUTF8(localVal).get()); + } + } + if (mismatches) { + logging::MsgEnd(); + } +#endif // A11Y_LOG + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleChild::RecvDoActionAsync( + const uint64_t& aID, const uint8_t& aIndex) { + if (LocalAccessible* acc = IdToAccessible(aID)) { + Unused << acc->DoAction(aIndex); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleChild::RecvSetCaretOffset( + const uint64_t& aID, const int32_t& aOffset) { + HyperTextAccessible* acc = IdToHyperTextAccessible(aID); + if (acc && acc->IsTextRole() && acc->IsValidOffset(aOffset)) { + acc->SetCaretOffset(aOffset); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleChild::RecvSetTextSelection( + const uint64_t& aStartID, const int32_t& aStartOffset, + const uint64_t& aEndID, const int32_t& aEndOffset, + const int32_t& aSelectionNum) { + TextLeafRange range(TextLeafPoint(IdToAccessible(aStartID), aStartOffset), + TextLeafPoint(IdToAccessible(aEndID), aEndOffset)); + if (range) { + range.SetSelection(aSelectionNum); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleChild::RecvScrollTextLeafRangeIntoView( + const uint64_t& aStartID, const int32_t& aStartOffset, + const uint64_t& aEndID, const int32_t& aEndOffset, + const uint32_t& aScrollType) { + TextLeafRange range(TextLeafPoint(IdToAccessible(aStartID), aStartOffset), + TextLeafPoint(IdToAccessible(aEndID), aEndOffset)); + if (range) { + range.ScrollIntoView(aScrollType); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleChild::RecvRemoveTextSelection( + const uint64_t& aID, const int32_t& aSelectionNum) { + HyperTextAccessible* acc = IdToHyperTextAccessible(aID); + if (acc && acc->IsTextRole()) { + acc->RemoveFromSelection(aSelectionNum); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleChild::RecvSetCurValue( + const uint64_t& aID, const double& aValue) { + LocalAccessible* acc = IdToAccessible(aID); + if (acc) { + acc->SetCurValue(aValue); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleChild::RecvReplaceText( + const uint64_t& aID, const nsAString& aText) { + HyperTextAccessible* acc = IdToHyperTextAccessible(aID); + if (acc && acc->IsTextRole()) { + acc->ReplaceText(aText); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleChild::RecvInsertText( + const uint64_t& aID, const nsAString& aText, const int32_t& aPosition) { + HyperTextAccessible* acc = IdToHyperTextAccessible(aID); + if (acc && acc->IsTextRole()) { + acc->InsertText(aText, aPosition); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleChild::RecvCopyText( + const uint64_t& aID, const int32_t& aStartPos, const int32_t& aEndPos) { + HyperTextAccessible* acc = IdToHyperTextAccessible(aID); + if (acc && acc->IsTextRole()) { + acc->CopyText(aStartPos, aEndPos); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleChild::RecvCutText( + const uint64_t& aID, const int32_t& aStartPos, const int32_t& aEndPos) { + HyperTextAccessible* acc = IdToHyperTextAccessible(aID); + if (acc && acc->IsTextRole()) { + acc->CutText(aStartPos, aEndPos); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleChild::RecvDeleteText( + const uint64_t& aID, const int32_t& aStartPos, const int32_t& aEndPos) { + HyperTextAccessible* acc = IdToHyperTextAccessible(aID); + if (acc && acc->IsTextRole()) { + acc->DeleteText(aStartPos, aEndPos); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleChild::RecvPasteText( + const uint64_t& aID, const int32_t& aPosition) { + RefPtr<HyperTextAccessible> acc = IdToHyperTextAccessible(aID); + if (acc && acc->IsTextRole()) { + acc->PasteText(aPosition); + } + + return IPC_OK(); +} + +ipc::IPCResult DocAccessibleChild::RecvRestoreFocus() { + if (FocusManager* focusMgr = FocusMgr()) { + focusMgr->ForceFocusEvent(); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleChild::RecvScrollToPoint( + const uint64_t& aID, const uint32_t& aScrollType, const int32_t& aX, + const int32_t& aY) { + LocalAccessible* acc = IdToAccessible(aID); + if (acc) { + acc->ScrollToPoint(aScrollType, aX, aY); + } + + return IPC_OK(); +} + +LayoutDeviceIntRect DocAccessibleChild::GetCaretRectFor(const uint64_t& aID) { +#if defined(XP_WIN) + LocalAccessible* target; + + if (aID) { + target = reinterpret_cast<LocalAccessible*>(aID); + } else { + target = mDoc; + } + + MOZ_ASSERT(target); + + HyperTextAccessible* text = target->AsHyperText(); + if (!text) { + return LayoutDeviceIntRect(); + } + + nsIWidget* widget = nullptr; + return text->GetCaretRect(&widget); +#else + // The caret rect is only used on Windows, so just return an empty rect + // on other platforms. + return LayoutDeviceIntRect(); +#endif // defined(XP_WIN) +} + +bool DocAccessibleChild::SendFocusEvent(const uint64_t& aID) { + return PDocAccessibleChild::SendFocusEvent(aID, GetCaretRectFor(aID)); +} + +bool DocAccessibleChild::SendCaretMoveEvent(const uint64_t& aID, + const int32_t& aOffset, + const bool& aIsSelectionCollapsed, + const bool& aIsAtEndOfLine, + const int32_t& aGranularity, + bool aFromUser) { + return PDocAccessibleChild::SendCaretMoveEvent( + aID, GetCaretRectFor(aID), aOffset, aIsSelectionCollapsed, aIsAtEndOfLine, + aGranularity, aFromUser); +} + +#if !defined(XP_WIN) +mozilla::ipc::IPCResult DocAccessibleChild::RecvAnnounce( + const uint64_t& aID, const nsAString& aAnnouncement, + const uint16_t& aPriority) { + LocalAccessible* acc = IdToAccessible(aID); + if (acc) { + acc->Announce(aAnnouncement, aPriority); + } + + return IPC_OK(); +} +#endif // !defined(XP_WIN) + +mozilla::ipc::IPCResult DocAccessibleChild::RecvScrollSubstringToPoint( + const uint64_t& aID, const int32_t& aStartOffset, const int32_t& aEndOffset, + const uint32_t& aCoordinateType, const int32_t& aX, const int32_t& aY) { + HyperTextAccessible* acc = IdToHyperTextAccessible(aID); + if (acc) { + acc->ScrollSubstringToPoint(aStartOffset, aEndOffset, aCoordinateType, aX, + aY); + } + + return IPC_OK(); +} + +LocalAccessible* DocAccessibleChild::IdToAccessible(const uint64_t& aID) const { + if (!aID) return mDoc; + + if (!mDoc) return nullptr; + + return mDoc->GetAccessibleByUniqueID(reinterpret_cast<void*>(aID)); +} + +HyperTextAccessible* DocAccessibleChild::IdToHyperTextAccessible( + const uint64_t& aID) const { + LocalAccessible* acc = IdToAccessible(aID); + return acc && acc->IsHyperText() ? acc->AsHyperText() : nullptr; +} + +} // namespace a11y +} // namespace mozilla diff --git a/accessible/ipc/DocAccessibleChild.h b/accessible/ipc/DocAccessibleChild.h new file mode 100644 index 0000000000..0a6164cce8 --- /dev/null +++ b/accessible/ipc/DocAccessibleChild.h @@ -0,0 +1,176 @@ +/* -*- 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_DocAccessibleChild_h +#define mozilla_a11y_DocAccessibleChild_h + +#include "mozilla/a11y/DocAccessible.h" +#include "mozilla/a11y/PDocAccessibleChild.h" +#include "mozilla/Unused.h" +#include "nsISupportsImpl.h" + +namespace mozilla { +namespace a11y { + +class LocalAccessible; +class AccShowEvent; + +/** + * These objects handle content side communication for an accessible document, + * and their lifetime is the same as the document they represent. + */ +class DocAccessibleChild : public PDocAccessibleChild { + public: + DocAccessibleChild(DocAccessible* aDoc, IProtocol* aManager) : mDoc(aDoc) { + MOZ_COUNT_CTOR(DocAccessibleChild); + SetManager(aManager); + } + + ~DocAccessibleChild() { + // Shutdown() should have been called, but maybe it isn't if the process is + // killed? + MOZ_ASSERT(!mDoc); + if (mDoc) { + mDoc->SetIPCDoc(nullptr); + } + + MOZ_COUNT_DTOR(DocAccessibleChild); + } + + virtual void Shutdown() { + DetachDocument(); + SendShutdown(); + } + + /** + * Serializes a shown tree and sends it to the chrome process. + */ + void InsertIntoIpcTree(LocalAccessible* aChild, bool aSuppressShowEvent); + void ShowEvent(AccShowEvent* aShowEvent); + + virtual void ActorDestroy(ActorDestroyReason) override { + if (!mDoc) { + return; + } + + mDoc->SetIPCDoc(nullptr); + mDoc = nullptr; + } + + virtual mozilla::ipc::IPCResult RecvTakeFocus(const uint64_t& aID) override; + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + virtual mozilla::ipc::IPCResult RecvScrollTo( + const uint64_t& aID, const uint32_t& aScrollType) override; + + virtual mozilla::ipc::IPCResult RecvTakeSelection( + const uint64_t& aID) override; + virtual mozilla::ipc::IPCResult RecvSetSelected(const uint64_t& aID, + const bool& aSelect) override; + + virtual mozilla::ipc::IPCResult RecvVerifyCache( + const uint64_t& aID, const uint64_t& aCacheDomain, + AccAttributes* aFields) override; + + virtual mozilla::ipc::IPCResult RecvDoActionAsync( + const uint64_t& aID, const uint8_t& aIndex) override; + + virtual mozilla::ipc::IPCResult RecvSetCaretOffset( + const uint64_t& aID, const int32_t& aOffset) override; + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + virtual mozilla::ipc::IPCResult RecvSetTextSelection( + const uint64_t& aStartID, const int32_t& aStartOffset, + const uint64_t& aEndID, const int32_t& aEndOffset, + const int32_t& aSelectionNum) override; + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + virtual mozilla::ipc::IPCResult RecvScrollTextLeafRangeIntoView( + const uint64_t& aStartID, const int32_t& aStartOffset, + const uint64_t& aEndID, const int32_t& aEndOffset, + const uint32_t& aScrollType) override; + + virtual mozilla::ipc::IPCResult RecvRemoveTextSelection( + const uint64_t& aID, const int32_t& aSelectionNum) override; + + virtual mozilla::ipc::IPCResult RecvSetCurValue( + const uint64_t& aID, const double& aValue) override; + + virtual mozilla::ipc::IPCResult RecvReplaceText( + const uint64_t& aID, const nsAString& aText) override; + + virtual mozilla::ipc::IPCResult RecvInsertText( + const uint64_t& aID, const nsAString& aText, + const int32_t& aPosition) override; + + virtual mozilla::ipc::IPCResult RecvCopyText(const uint64_t& aID, + const int32_t& aStartPos, + const int32_t& aEndPos) override; + + virtual mozilla::ipc::IPCResult RecvCutText(const uint64_t& aID, + const int32_t& aStartPos, + const int32_t& aEndPos) override; + + virtual mozilla::ipc::IPCResult RecvDeleteText( + const uint64_t& aID, const int32_t& aStartPos, + const int32_t& aEndPos) override; + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + virtual mozilla::ipc::IPCResult RecvPasteText( + const uint64_t& aID, const int32_t& aPosition) override; + + virtual mozilla::ipc::IPCResult RecvRestoreFocus() override; + + virtual mozilla::ipc::IPCResult RecvScrollToPoint(const uint64_t& aID, + const uint32_t& aScrollType, + const int32_t& aX, + const int32_t& aY) override; + + bool SendCaretMoveEvent(const uint64_t& aID, const int32_t& aOffset, + const bool& aIsSelectionCollapsed, + const bool& aIsAtEndOfLine, + const int32_t& aGranularity, bool aFromUser); + bool SendFocusEvent(const uint64_t& aID); + +#if !defined(XP_WIN) + virtual mozilla::ipc::IPCResult RecvAnnounce( + const uint64_t& aID, const nsAString& aAnnouncement, + const uint16_t& aPriority) override; +#endif // !defined(XP_WIN) + + virtual mozilla::ipc::IPCResult RecvScrollSubstringToPoint( + const uint64_t& aID, const int32_t& aStartOffset, + const int32_t& aEndOffset, const uint32_t& aCoordinateType, + const int32_t& aX, const int32_t& aY) override; + + private: + LayoutDeviceIntRect GetCaretRectFor(const uint64_t& aID); + + protected: + static void FlattenTree(LocalAccessible* aRoot, + nsTArray<LocalAccessible*>& aTree); + + static AccessibleData SerializeAcc(LocalAccessible* aAcc); + + void DetachDocument() { + if (mDoc) { + mDoc->SetIPCDoc(nullptr); + mDoc = nullptr; + } + } + + LocalAccessible* IdToAccessible(const uint64_t& aID) const; + HyperTextAccessible* IdToHyperTextAccessible(const uint64_t& aID) const; + + DocAccessible* mDoc; + + friend void DocAccessible::DoInitialUpdate(); +}; + +} // namespace a11y +} // namespace mozilla + +#endif // mozilla_a11y_DocAccessibleChild_h diff --git a/accessible/ipc/DocAccessibleParent.cpp b/accessible/ipc/DocAccessibleParent.cpp new file mode 100644 index 0000000000..1c96bd38cd --- /dev/null +++ b/accessible/ipc/DocAccessibleParent.cpp @@ -0,0 +1,1266 @@ +/* -*- 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 "ARIAMap.h" +#include "CachedTableAccessible.h" +#include "DocAccessibleParent.h" +#include "mozilla/a11y/Platform.h" +#include "mozilla/Components.h" // for mozilla::components +#include "mozilla/dom/BrowserBridgeParent.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "nsAccessibilityService.h" +#include "xpcAccessibleDocument.h" +#include "xpcAccEvents.h" +#include "nsAccUtils.h" +#include "nsIIOService.h" +#include "TextRange.h" +#include "Relation.h" +#include "RootAccessible.h" + +#if defined(XP_WIN) +# include "Compatibility.h" +# include "nsWinUtils.h" +#endif + +#if defined(ANDROID) +# define ACQUIRE_ANDROID_LOCK \ + MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor()); +#else +# define ACQUIRE_ANDROID_LOCK \ + do { \ + } while (0); +#endif + +namespace mozilla { + +namespace a11y { +uint64_t DocAccessibleParent::sMaxDocID = 0; + +DocAccessibleParent::DocAccessibleParent() + : RemoteAccessible(this), + mParentDoc(kNoParentDoc), +#if defined(XP_WIN) + mEmulatedWindowHandle(nullptr), +#endif // defined(XP_WIN) + mTopLevel(false), + mTopLevelInContentProcess(false), + mShutdown(false), + mFocus(0), + mCaretId(0), + mCaretOffset(-1), + mIsCaretAtEndOfLine(false) { + sMaxDocID++; + mActorID = sMaxDocID; + MOZ_ASSERT(!LiveDocs().Get(mActorID)); + LiveDocs().InsertOrUpdate(mActorID, this); +} + +DocAccessibleParent::~DocAccessibleParent() { + UnregisterWeakMemoryReporter(this); + LiveDocs().Remove(mActorID); + MOZ_ASSERT(mChildDocs.Length() == 0); + MOZ_ASSERT(!ParentDoc()); +} + +already_AddRefed<DocAccessibleParent> DocAccessibleParent::New() { + RefPtr<DocAccessibleParent> dap(new DocAccessibleParent()); + // We need to do this with a non-zero reference count. The easiest way is to + // do it in this static method and hide the constructor. + RegisterWeakMemoryReporter(dap); + return dap.forget(); +} + +void DocAccessibleParent::SetBrowsingContext( + dom::CanonicalBrowsingContext* aBrowsingContext) { + mBrowsingContext = aBrowsingContext; +} + +mozilla::ipc::IPCResult DocAccessibleParent::RecvShowEvent( + nsTArray<AccessibleData>&& aNewTree, const bool& aEventSuppressed, + const bool& aComplete, const bool& aFromUser) { + ACQUIRE_ANDROID_LOCK + if (mShutdown) return IPC_OK(); + + MOZ_ASSERT(CheckDocTree()); + + if (aNewTree.IsEmpty()) { + return IPC_FAIL(this, "No children being added"); + } + + RemoteAccessible* root = nullptr; + RemoteAccessible* rootParent = nullptr; + RemoteAccessible* lastParent = this; + uint64_t lastParentID = 0; + for (const auto& accData : aNewTree) { + RemoteAccessible* parent = accData.ParentID() == lastParentID + ? lastParent + : GetAccessible(accData.ParentID()); + // XXX This should really never happen, but sometimes we fail to fire the + // required show events. + if (!parent) { + NS_ERROR("adding child to unknown accessible"); +#ifdef DEBUG + return IPC_FAIL(this, "unknown parent accessible"); +#else + return IPC_OK(); +#endif + } + + uint32_t childIdx = accData.IndexInParent(); + if (childIdx > parent->ChildCount()) { + NS_ERROR("invalid index to add child at"); +#ifdef DEBUG + return IPC_FAIL(this, "invalid index"); +#else + return IPC_OK(); +#endif + } + + RemoteAccessible* child = CreateAcc(accData); + if (!child) { + // This shouldn't happen. + return IPC_FAIL(this, "failed to add children"); + } + if (!root && !mPendingShowChild) { + // This is the first Accessible, which is the root of the shown subtree. + root = child; + rootParent = parent; + } + // If this show event has been split across multiple messages and this is + // not the last message, don't attach the shown root to the tree yet. + // Otherwise, clients might crawl the incomplete subtree and they won't get + // mutation events for the remaining pieces. + if (aComplete || root != child) { + AttachChild(parent, childIdx, child); + } + } + + MOZ_ASSERT(CheckDocTree()); + + if (!aComplete && !mPendingShowChild) { + // This is the first message for a show event split across multiple + // messages. Save the show target for subsequent messages and return. + const auto& accData = aNewTree[0]; + mPendingShowChild = accData.ID(); + mPendingShowParent = accData.ParentID(); + mPendingShowIndex = accData.IndexInParent(); + return IPC_OK(); + } + if (!aComplete) { + // This show event has been split into multiple messages, but this is + // neither the first nor the last message. There's nothing more to do here. + return IPC_OK(); + } + MOZ_ASSERT(aComplete); + if (mPendingShowChild) { + // This is the last message for a show event split across multiple + // messages. Retrieve the saved show target, attach it to the tree and fire + // an event if appropriate. + rootParent = GetAccessible(mPendingShowParent); + MOZ_ASSERT(rootParent); + root = GetAccessible(mPendingShowChild); + MOZ_ASSERT(root); + AttachChild(rootParent, mPendingShowIndex, root); + mPendingShowChild = 0; + mPendingShowParent = 0; + mPendingShowIndex = 0; + } + + // Just update, no events. + if (aEventSuppressed) { + return IPC_OK(); + } + + PlatformShowHideEvent(root, rootParent, true, aFromUser); + + if (nsCOMPtr<nsIObserverService> obsService = + services::GetObserverService()) { + obsService->NotifyObservers(nullptr, NS_ACCESSIBLE_CACHE_TOPIC, nullptr); + } + + if (!nsCoreUtils::AccEventObserversExist()) { + return IPC_OK(); + } + + uint32_t type = nsIAccessibleEvent::EVENT_SHOW; + xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(root); + xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this); + nsINode* node = nullptr; + RefPtr<xpcAccEvent> event = + new xpcAccEvent(type, xpcAcc, doc, node, aFromUser); + nsCoreUtils::DispatchAccEvent(std::move(event)); + + return IPC_OK(); +} + +RemoteAccessible* DocAccessibleParent::CreateAcc( + const AccessibleData& aAccData) { + RemoteAccessible* newProxy; + if ((newProxy = GetAccessible(aAccData.ID()))) { + // This is a move. Reuse the Accessible; don't destroy it. + MOZ_ASSERT(!newProxy->RemoteParent()); + return newProxy; + } + + if (!aria::IsRoleMapIndexValid(aAccData.RoleMapEntryIndex())) { + MOZ_ASSERT_UNREACHABLE("Invalid role map entry index"); + return nullptr; + } + + newProxy = new RemoteAccessible(aAccData.ID(), this, aAccData.Role(), + aAccData.Type(), aAccData.GenericTypes(), + aAccData.RoleMapEntryIndex()); + mAccessibles.PutEntry(aAccData.ID())->mProxy = newProxy; + + if (RefPtr<AccAttributes> fields = aAccData.CacheFields()) { + newProxy->ApplyCache(CacheUpdateType::Initial, fields); + } + + return newProxy; +} + +void DocAccessibleParent::AttachChild(RemoteAccessible* aParent, + uint32_t aIndex, + RemoteAccessible* aChild) { + aParent->AddChildAt(aIndex, aChild); + aChild->SetParent(aParent); + // ProxyCreated might have already been called if aChild is being moved. + if (!aChild->GetWrapper()) { + ProxyCreated(aChild); + } + if (aChild->IsTableCell()) { + CachedTableAccessible::Invalidate(aChild); + } + if (aChild->IsOuterDoc()) { + // We can only do this after ProxyCreated is called because it will fire an + // event on aChild. + mPendingOOPChildDocs.RemoveIf([&](dom::BrowserBridgeParent* bridge) { + MOZ_ASSERT(bridge->GetBrowserParent(), + "Pending BrowserBridgeParent should be alive"); + if (bridge->GetEmbedderAccessibleId() != aChild->ID()) { + return false; + } + MOZ_ASSERT(bridge->GetEmbedderAccessibleDoc() == this); + if (DocAccessibleParent* childDoc = bridge->GetDocAccessibleParent()) { + AddChildDoc(childDoc, aChild->ID(), false); + } + return true; + }); + } +} + +void DocAccessibleParent::ShutdownOrPrepareForMove(RemoteAccessible* aAcc) { + // Children might be removed or moved. Handle them the same way. We do this + // before checking the moving IDs set in order to ensure that we handle moved + // descendants properly. Avoid descending into the children of outer documents + // for moves since they are added and removed differently to normal children. + if (!aAcc->IsOuterDoc()) { + // Even if some children are kept, those will be re-attached when we handle + // the show event. For now, clear all of them by moving them to a temporary. + auto children{std::move(aAcc->mChildren)}; + for (RemoteAccessible* child : children) { + ShutdownOrPrepareForMove(child); + } + } + + const uint64_t id = aAcc->ID(); + if (!mMovingIDs.Contains(id)) { + // This Accessible is being removed. + aAcc->Shutdown(); + return; + } + // This is a move. Moves are sent as a hide and then a show, but for a move, + // we want to keep the Accessible alive for reuse later. + if (aAcc->IsTable() || aAcc->IsTableCell()) { + // For table cells, it's important that we do this before the parent is + // cleared because CachedTableAccessible::Invalidate needs the ancestry. + CachedTableAccessible::Invalidate(aAcc); + } + if (aAcc->IsHyperText()) { + aAcc->InvalidateCachedHyperTextOffsets(); + } + aAcc->SetParent(nullptr); + mMovingIDs.EnsureRemoved(id); +} + +mozilla::ipc::IPCResult DocAccessibleParent::RecvHideEvent( + const uint64_t& aRootID, const bool& aFromUser) { + ACQUIRE_ANDROID_LOCK + if (mShutdown) return IPC_OK(); + + MOZ_ASSERT(CheckDocTree()); + + // We shouldn't actually need this because mAccessibles shouldn't have an + // entry for the document itself, but it doesn't hurt to be explicit. + if (!aRootID) { + return IPC_FAIL(this, "Trying to hide entire document?"); + } + + ProxyEntry* rootEntry = mAccessibles.GetEntry(aRootID); + if (!rootEntry) { + NS_ERROR("invalid root being removed!"); + return IPC_OK(); + } + + RemoteAccessible* root = rootEntry->mProxy; + if (!root) { + NS_ERROR("invalid root being removed!"); + return IPC_OK(); + } + + RemoteAccessible* parent = root->RemoteParent(); + PlatformShowHideEvent(root, parent, false, aFromUser); + + RefPtr<xpcAccHideEvent> event = nullptr; + if (nsCoreUtils::AccEventObserversExist()) { + uint32_t type = nsIAccessibleEvent::EVENT_HIDE; + xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(root); + xpcAccessibleGeneric* xpcParent = GetXPCAccessible(parent); + RemoteAccessible* next = root->RemoteNextSibling(); + xpcAccessibleGeneric* xpcNext = next ? GetXPCAccessible(next) : nullptr; + RemoteAccessible* prev = root->RemotePrevSibling(); + xpcAccessibleGeneric* xpcPrev = prev ? GetXPCAccessible(prev) : nullptr; + xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this); + nsINode* node = nullptr; + event = new xpcAccHideEvent(type, xpcAcc, doc, node, aFromUser, xpcParent, + xpcNext, xpcPrev); + } + + parent->RemoveChild(root); + ShutdownOrPrepareForMove(root); + + MOZ_ASSERT(CheckDocTree()); + + if (event) { + nsCoreUtils::DispatchAccEvent(std::move(event)); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleParent::RecvEvent( + const uint64_t& aID, const uint32_t& aEventType) { + ACQUIRE_ANDROID_LOCK + if (mShutdown) { + return IPC_OK(); + } + if (aEventType == 0 || aEventType >= nsIAccessibleEvent::EVENT_LAST_ENTRY) { + MOZ_ASSERT_UNREACHABLE("Invalid event"); + return IPC_FAIL(this, "Invalid event"); + } + + RemoteAccessible* remote = GetAccessible(aID); + if (!remote) { + NS_ERROR("no proxy for event!"); + return IPC_OK(); + } + + FireEvent(remote, aEventType); + return IPC_OK(); +} + +void DocAccessibleParent::FireEvent(RemoteAccessible* aAcc, + const uint32_t& aEventType) { + if (aEventType == nsIAccessibleEvent::EVENT_REORDER || + aEventType == nsIAccessibleEvent::EVENT_INNER_REORDER) { + uint32_t count = aAcc->ChildCount(); + for (uint32_t c = 0; c < count; ++c) { + aAcc->RemoteChildAt(c)->InvalidateGroupInfo(); + } + } else if (aEventType == nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE && + aAcc == this) { + // A DocAccessible gets the STALE state while it is still loading, but we + // don't fire a state change for that. That state might have been + // included in the initial cache push, so clear it here. + // We also clear the BUSY state here. Although we do fire a state change + // for that, we fire it after doc load complete. It doesn't make sense + // for the document to report BUSY after doc load complete and doing so + // confuses JAWS. + UpdateStateCache(states::STALE | states::BUSY, false); + } + + PlatformEvent(aAcc, aEventType); + + if (!nsCoreUtils::AccEventObserversExist()) { + return; + } + + xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(aAcc); + xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this); + nsINode* node = nullptr; + bool fromUser = true; // XXX fix me + RefPtr<xpcAccEvent> event = + new xpcAccEvent(aEventType, xpcAcc, doc, node, fromUser); + nsCoreUtils::DispatchAccEvent(std::move(event)); +} + +mozilla::ipc::IPCResult DocAccessibleParent::RecvStateChangeEvent( + const uint64_t& aID, const uint64_t& aState, const bool& aEnabled) { + ACQUIRE_ANDROID_LOCK + if (mShutdown) { + return IPC_OK(); + } + + RemoteAccessible* target = GetAccessible(aID); + if (!target) { + NS_ERROR("we don't know about the target of a state change event!"); + return IPC_OK(); + } + + target->UpdateStateCache(aState, aEnabled); + if (nsCOMPtr<nsIObserverService> obsService = + services::GetObserverService()) { + obsService->NotifyObservers(nullptr, NS_ACCESSIBLE_CACHE_TOPIC, nullptr); + } + PlatformStateChangeEvent(target, aState, aEnabled); + + if (!nsCoreUtils::AccEventObserversExist()) { + return IPC_OK(); + } + + xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(target); + xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this); + uint32_t type = nsIAccessibleEvent::EVENT_STATE_CHANGE; + bool extra; + uint32_t state = nsAccUtils::To32States(aState, &extra); + bool fromUser = true; // XXX fix this + nsINode* node = nullptr; // XXX can we do better? + RefPtr<xpcAccStateChangeEvent> event = new xpcAccStateChangeEvent( + type, xpcAcc, doc, node, fromUser, state, extra, aEnabled); + nsCoreUtils::DispatchAccEvent(std::move(event)); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleParent::RecvCaretMoveEvent( + const uint64_t& aID, const LayoutDeviceIntRect& aCaretRect, + const int32_t& aOffset, const bool& aIsSelectionCollapsed, + const bool& aIsAtEndOfLine, const int32_t& aGranularity, + const bool& aFromUser) { + ACQUIRE_ANDROID_LOCK + if (mShutdown) { + return IPC_OK(); + } + + RemoteAccessible* proxy = GetAccessible(aID); + if (!proxy) { + NS_ERROR("unknown caret move event target!"); + return IPC_OK(); + } + + mCaretId = aID; + mCaretOffset = aOffset; + mIsCaretAtEndOfLine = aIsAtEndOfLine; + if (aIsSelectionCollapsed) { + // We don't fire selection events for collapsed selections, but we need to + // ensure we don't have a stale cached selection; e.g. when selecting + // forward and then unselecting backward. + mTextSelections.ClearAndRetainStorage(); + mTextSelections.AppendElement(TextRangeData(aID, aID, aOffset, aOffset)); + } + + PlatformCaretMoveEvent(proxy, aOffset, aIsSelectionCollapsed, aGranularity, + aCaretRect, aFromUser); + + if (!nsCoreUtils::AccEventObserversExist()) { + return IPC_OK(); + } + + xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(proxy); + xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this); + nsINode* node = nullptr; + bool fromUser = true; // XXX fix me + uint32_t type = nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED; + RefPtr<xpcAccCaretMoveEvent> event = new xpcAccCaretMoveEvent( + type, xpcAcc, doc, node, fromUser, aOffset, aIsSelectionCollapsed, + aIsAtEndOfLine, aGranularity); + nsCoreUtils::DispatchAccEvent(std::move(event)); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleParent::RecvTextChangeEvent( + const uint64_t& aID, const nsAString& aStr, const int32_t& aStart, + const uint32_t& aLen, const bool& aIsInsert, const bool& aFromUser) { + ACQUIRE_ANDROID_LOCK + if (mShutdown) { + return IPC_OK(); + } + + RemoteAccessible* target = GetAccessible(aID); + if (!target) { + NS_ERROR("text change event target is unknown!"); + return IPC_OK(); + } + + PlatformTextChangeEvent(target, aStr, aStart, aLen, aIsInsert, aFromUser); + + if (!nsCoreUtils::AccEventObserversExist()) { + return IPC_OK(); + } + + xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(target); + xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this); + uint32_t type = aIsInsert ? nsIAccessibleEvent::EVENT_TEXT_INSERTED + : nsIAccessibleEvent::EVENT_TEXT_REMOVED; + nsINode* node = nullptr; + RefPtr<xpcAccTextChangeEvent> event = new xpcAccTextChangeEvent( + type, xpcAcc, doc, node, aFromUser, aStart, aLen, aIsInsert, aStr); + nsCoreUtils::DispatchAccEvent(std::move(event)); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleParent::RecvSelectionEvent( + const uint64_t& aID, const uint64_t& aWidgetID, const uint32_t& aType) { + ACQUIRE_ANDROID_LOCK + if (mShutdown) { + return IPC_OK(); + } + if (aType == 0 || aType >= nsIAccessibleEvent::EVENT_LAST_ENTRY) { + MOZ_ASSERT_UNREACHABLE("Invalid event"); + return IPC_FAIL(this, "Invalid event"); + } + + RemoteAccessible* target = GetAccessible(aID); + RemoteAccessible* widget = GetAccessible(aWidgetID); + if (!target || !widget) { + NS_ERROR("invalid id in selection event"); + return IPC_OK(); + } + + PlatformSelectionEvent(target, widget, aType); + if (!nsCoreUtils::AccEventObserversExist()) { + return IPC_OK(); + } + xpcAccessibleGeneric* xpcTarget = GetXPCAccessible(target); + xpcAccessibleDocument* xpcDoc = GetAccService()->GetXPCDocument(this); + RefPtr<xpcAccEvent> event = + new xpcAccEvent(aType, xpcTarget, xpcDoc, nullptr, false); + nsCoreUtils::DispatchAccEvent(std::move(event)); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleParent::RecvScrollingEvent( + const uint64_t& aID, const uint64_t& aType, const uint32_t& aScrollX, + const uint32_t& aScrollY, const uint32_t& aMaxScrollX, + const uint32_t& aMaxScrollY) { + ACQUIRE_ANDROID_LOCK + if (mShutdown) { + return IPC_OK(); + } + if (aType == 0 || aType >= nsIAccessibleEvent::EVENT_LAST_ENTRY) { + MOZ_ASSERT_UNREACHABLE("Invalid event"); + return IPC_FAIL(this, "Invalid event"); + } + + RemoteAccessible* target = GetAccessible(aID); + if (!target) { + NS_ERROR("no proxy for event!"); + return IPC_OK(); + } + +#if defined(ANDROID) + PlatformScrollingEvent(target, aType, aScrollX, aScrollY, aMaxScrollX, + aMaxScrollY); +#else + PlatformEvent(target, aType); +#endif + + if (!nsCoreUtils::AccEventObserversExist()) { + return IPC_OK(); + } + + xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(target); + xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this); + nsINode* node = nullptr; + bool fromUser = true; // XXX: Determine if this was from user input. + RefPtr<xpcAccScrollingEvent> event = + new xpcAccScrollingEvent(aType, xpcAcc, doc, node, fromUser, aScrollX, + aScrollY, aMaxScrollX, aMaxScrollY); + nsCoreUtils::DispatchAccEvent(std::move(event)); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleParent::RecvCache( + const mozilla::a11y::CacheUpdateType& aUpdateType, + nsTArray<CacheData>&& aData) { + ACQUIRE_ANDROID_LOCK + if (mShutdown) { + return IPC_OK(); + } + + for (auto& entry : aData) { + RemoteAccessible* remote = GetAccessible(entry.ID()); + if (!remote) { + MOZ_ASSERT_UNREACHABLE("No remote found!"); + continue; + } + + remote->ApplyCache(aUpdateType, entry.Fields()); + } + + if (nsCOMPtr<nsIObserverService> obsService = + services::GetObserverService()) { + obsService->NotifyObservers(nullptr, NS_ACCESSIBLE_CACHE_TOPIC, nullptr); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleParent::RecvSelectedAccessiblesChanged( + nsTArray<uint64_t>&& aSelectedIDs, nsTArray<uint64_t>&& aUnselectedIDs) { + ACQUIRE_ANDROID_LOCK + if (mShutdown) { + return IPC_OK(); + } + + for (auto& id : aSelectedIDs) { + RemoteAccessible* remote = GetAccessible(id); + if (!remote) { + MOZ_ASSERT_UNREACHABLE("No remote found!"); + continue; + } + + remote->UpdateStateCache(states::SELECTED, true); + } + + for (auto& id : aUnselectedIDs) { + RemoteAccessible* remote = GetAccessible(id); + if (!remote) { + MOZ_ASSERT_UNREACHABLE("No remote found!"); + continue; + } + + remote->UpdateStateCache(states::SELECTED, false); + } + + if (nsCOMPtr<nsIObserverService> obsService = + services::GetObserverService()) { + obsService->NotifyObservers(nullptr, NS_ACCESSIBLE_CACHE_TOPIC, nullptr); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleParent::RecvAccessiblesWillMove( + nsTArray<uint64_t>&& aIDs) { + for (uint64_t id : aIDs) { + mMovingIDs.EnsureInserted(id); + } + return IPC_OK(); +} + +#if !defined(XP_WIN) +mozilla::ipc::IPCResult DocAccessibleParent::RecvAnnouncementEvent( + const uint64_t& aID, const nsAString& aAnnouncement, + const uint16_t& aPriority) { + ACQUIRE_ANDROID_LOCK + if (mShutdown) { + return IPC_OK(); + } + + RemoteAccessible* target = GetAccessible(aID); + if (!target) { + NS_ERROR("no proxy for event!"); + return IPC_OK(); + } + +# if defined(ANDROID) + PlatformAnnouncementEvent(target, aAnnouncement, aPriority); +# endif + + if (!nsCoreUtils::AccEventObserversExist()) { + return IPC_OK(); + } + + xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(target); + xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this); + RefPtr<xpcAccAnnouncementEvent> event = new xpcAccAnnouncementEvent( + nsIAccessibleEvent::EVENT_ANNOUNCEMENT, xpcAcc, doc, nullptr, false, + aAnnouncement, aPriority); + nsCoreUtils::DispatchAccEvent(std::move(event)); + + return IPC_OK(); +} +#endif // !defined(XP_WIN) + +mozilla::ipc::IPCResult DocAccessibleParent::RecvTextSelectionChangeEvent( + const uint64_t& aID, nsTArray<TextRangeData>&& aSelection) { + ACQUIRE_ANDROID_LOCK + if (mShutdown) { + return IPC_OK(); + } + + RemoteAccessible* target = GetAccessible(aID); + if (!target) { + NS_ERROR("no proxy for event!"); + return IPC_OK(); + } + + mTextSelections.ClearAndRetainStorage(); + mTextSelections.AppendElements(aSelection); + +#ifdef MOZ_WIDGET_COCOA + AutoTArray<TextRange, 1> ranges; + SelectionRanges(&ranges); + PlatformTextSelectionChangeEvent(target, ranges); +#else + PlatformEvent(target, nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED); +#endif + + if (!nsCoreUtils::AccEventObserversExist()) { + return IPC_OK(); + } + xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(target); + xpcAccessibleDocument* doc = nsAccessibilityService::GetXPCDocument(this); + nsINode* node = nullptr; + bool fromUser = true; // XXX fix me + RefPtr<xpcAccEvent> event = + new xpcAccEvent(nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED, xpcAcc, + doc, node, fromUser); + nsCoreUtils::DispatchAccEvent(std::move(event)); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleParent::RecvRoleChangedEvent( + const a11y::role& aRole, const uint8_t& aRoleMapEntryIndex) { + ACQUIRE_ANDROID_LOCK + if (mShutdown) { + return IPC_OK(); + } + if (!aria::IsRoleMapIndexValid(aRoleMapEntryIndex)) { + MOZ_ASSERT_UNREACHABLE("Invalid role map entry index"); + return IPC_FAIL(this, "Invalid role map entry index"); + } + + mRole = aRole; + mRoleMapEntryIndex = aRoleMapEntryIndex; + +#ifdef MOZ_WIDGET_COCOA + PlatformRoleChangedEvent(this, aRole, aRoleMapEntryIndex); +#endif + + return IPC_OK(); +} + +mozilla::ipc::IPCResult DocAccessibleParent::RecvBindChildDoc( + NotNull<PDocAccessibleParent*> aChildDoc, const uint64_t& aID) { + ACQUIRE_ANDROID_LOCK + // One document should never directly be the child of another. + // We should always have at least an outer doc accessible in between. + MOZ_ASSERT(aID); + if (!aID) return IPC_FAIL(this, "ID is 0!"); + + if (mShutdown) { + return IPC_OK(); + } + + MOZ_ASSERT(CheckDocTree()); + + auto childDoc = static_cast<DocAccessibleParent*>(aChildDoc.get()); + childDoc->Unbind(); + ipc::IPCResult result = AddChildDoc(childDoc, aID, false); + MOZ_ASSERT(result); + MOZ_ASSERT(CheckDocTree()); +#ifdef DEBUG + if (!result) { + return result; + } +#else + result = IPC_OK(); +#endif + + return result; +} + +ipc::IPCResult DocAccessibleParent::AddChildDoc(DocAccessibleParent* aChildDoc, + uint64_t aParentID, + bool aCreating) { + // We do not use GetAccessible here because we want to be sure to not get the + // document it self. + ProxyEntry* e = mAccessibles.GetEntry(aParentID); + if (!e) { +#ifndef FUZZING_SNAPSHOT + // This diagnostic assert and the one down below expect a well-behaved + // child process. In IPC fuzzing, we directly fuzz parameters of each + // method over IPDL and the asserts are not valid under these conditions. + MOZ_DIAGNOSTIC_ASSERT(false, "Binding to nonexistent proxy!"); +#endif + return IPC_FAIL(this, "binding to nonexistant proxy!"); + } + + RemoteAccessible* outerDoc = e->mProxy; + MOZ_ASSERT(outerDoc); + + // OuterDocAccessibles are expected to only have a document as a child. + // However for compatibility we tolerate replacing one document with another + // here. + if (!outerDoc->IsOuterDoc() || outerDoc->ChildCount() > 1 || + (outerDoc->ChildCount() == 1 && !outerDoc->RemoteChildAt(0)->IsDoc())) { +#ifndef FUZZING_SNAPSHOT + MOZ_DIAGNOSTIC_ASSERT(false, + "Binding to parent that isn't a valid OuterDoc!"); +#endif + return IPC_FAIL(this, "Binding to parent that isn't a valid OuterDoc!"); + } + + if (outerDoc->ChildCount() == 1) { + MOZ_ASSERT(outerDoc->RemoteChildAt(0)->AsDoc()); + outerDoc->RemoteChildAt(0)->AsDoc()->Unbind(); + } + + aChildDoc->SetParent(outerDoc); + outerDoc->SetChildDoc(aChildDoc); + mChildDocs.AppendElement(aChildDoc->mActorID); + aChildDoc->mParentDoc = mActorID; + + if (aCreating) { + ProxyCreated(aChildDoc); + } + + if (aChildDoc->IsTopLevelInContentProcess()) { + // aChildDoc is an embedded document in a different content process to + // this document. + auto embeddedBrowser = + static_cast<dom::BrowserParent*>(aChildDoc->Manager()); + dom::BrowserBridgeParent* bridge = + embeddedBrowser->GetBrowserBridgeParent(); + if (bridge) { +#if defined(XP_WIN) + if (nsWinUtils::IsWindowEmulationStarted()) { + aChildDoc->SetEmulatedWindowHandle(mEmulatedWindowHandle); + } +#endif // defined(XP_WIN) + // We need to fire a reorder event on the outer doc accessible. + // For same-process documents, this is fired by the content process, but + // this isn't possible when the document is in a different process to its + // embedder. + // FireEvent fires both OS and XPCOM events. + FireEvent(outerDoc, nsIAccessibleEvent::EVENT_REORDER); + } + } + + return IPC_OK(); +} + +ipc::IPCResult DocAccessibleParent::AddChildDoc( + dom::BrowserBridgeParent* aBridge) { + MOZ_ASSERT(aBridge->GetEmbedderAccessibleDoc() == this); + uint64_t parentId = aBridge->GetEmbedderAccessibleId(); + MOZ_ASSERT(parentId); + if (!mAccessibles.GetEntry(parentId)) { + // Sometimes, this gets called before the embedder sends us the + // OuterDocAccessible. We must add the child when the OuterDocAccessible + // gets created later. + mPendingOOPChildDocs.Insert(aBridge); + return IPC_OK(); + } + return AddChildDoc(aBridge->GetDocAccessibleParent(), parentId, + /* aCreating */ false); +} + +mozilla::ipc::IPCResult DocAccessibleParent::RecvShutdown() { + ACQUIRE_ANDROID_LOCK + Destroy(); + + auto mgr = static_cast<dom::BrowserParent*>(Manager()); + if (!mgr->IsDestroyed()) { + if (!PDocAccessibleParent::Send__delete__(this)) { + return IPC_FAIL_NO_REASON(mgr); + } + } + + return IPC_OK(); +} + +void DocAccessibleParent::Destroy() { + // If we are already shutdown that is because our containing tab parent is + // shutting down in which case we don't need to do anything. + if (mShutdown) { + return; + } + + mShutdown = true; + mBrowsingContext = nullptr; + +#ifdef ANDROID + if (FocusMgr() && FocusMgr()->IsFocusedRemoteDoc(this)) { + FocusMgr()->SetFocusedRemoteDoc(nullptr); + } +#endif + + MOZ_DIAGNOSTIC_ASSERT(LiveDocs().Contains(mActorID)); + uint32_t childDocCount = mChildDocs.Length(); + for (uint32_t i = 0; i < childDocCount; i++) { + for (uint32_t j = i + 1; j < childDocCount; j++) { + MOZ_DIAGNOSTIC_ASSERT(mChildDocs[i] != mChildDocs[j]); + } + } + + // XXX This indirection through the hash map of live documents shouldn't be + // needed, but be paranoid for now. + int32_t actorID = mActorID; + for (uint32_t i = childDocCount - 1; i < childDocCount; i--) { + DocAccessibleParent* thisDoc = LiveDocs().Get(actorID); + MOZ_ASSERT(thisDoc); + if (!thisDoc) { + return; + } + + thisDoc->ChildDocAt(i)->Destroy(); + } + + for (auto iter = mAccessibles.Iter(); !iter.Done(); iter.Next()) { + RemoteAccessible* acc = iter.Get()->mProxy; + MOZ_ASSERT(acc != this); + if (acc->IsTable()) { + CachedTableAccessible::Invalidate(acc); + } + ProxyDestroyed(acc); + iter.Remove(); + } + + DocAccessibleParent* thisDoc = LiveDocs().Get(actorID); + MOZ_ASSERT(thisDoc); + if (!thisDoc) { + return; + } + + mChildren.Clear(); + // The code above should have already completely cleared these, but to be + // extra safe make sure they are cleared here. + thisDoc->mAccessibles.Clear(); + thisDoc->mChildDocs.Clear(); + + DocManager::NotifyOfRemoteDocShutdown(thisDoc); + thisDoc = LiveDocs().Get(actorID); + MOZ_ASSERT(thisDoc); + if (!thisDoc) { + return; + } + + ProxyDestroyed(thisDoc); + thisDoc = LiveDocs().Get(actorID); + MOZ_ASSERT(thisDoc); + if (!thisDoc) { + return; + } + + if (DocAccessibleParent* parentDoc = thisDoc->ParentDoc()) { + parentDoc->RemoveChildDoc(thisDoc); + } else if (IsTopLevel()) { + GetAccService()->RemoteDocShutdown(this); + } +} + +void DocAccessibleParent::ActorDestroy(ActorDestroyReason aWhy) { + MOZ_ASSERT(CheckDocTree()); + if (!mShutdown) { + ACQUIRE_ANDROID_LOCK + Destroy(); + } +} + +DocAccessibleParent* DocAccessibleParent::ParentDoc() const { + if (mParentDoc == kNoParentDoc) { + return nullptr; + } + + return LiveDocs().Get(mParentDoc); +} + +bool DocAccessibleParent::CheckDocTree() const { + size_t childDocs = mChildDocs.Length(); + for (size_t i = 0; i < childDocs; i++) { + const DocAccessibleParent* childDoc = ChildDocAt(i); + if (!childDoc || childDoc->ParentDoc() != this) return false; + + if (!childDoc->CheckDocTree()) { + return false; + } + } + + return true; +} + +xpcAccessibleGeneric* DocAccessibleParent::GetXPCAccessible( + RemoteAccessible* aProxy) { + xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this); + MOZ_ASSERT(doc); + + return doc->GetAccessible(aProxy); +} + +#if defined(XP_WIN) +void DocAccessibleParent::MaybeInitWindowEmulation() { + if (!nsWinUtils::IsWindowEmulationStarted()) { + return; + } + + // XXX get the bounds from the browserParent instead of poking at accessibles + // which might not exist yet. + LocalAccessible* outerDoc = OuterDocOfRemoteBrowser(); + if (!outerDoc) { + return; + } + + RootAccessible* rootDocument = outerDoc->RootAccessible(); + MOZ_ASSERT(rootDocument); + + 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.MoveToY(rect.Y() - rootRect.Y()); + + auto browserParent = static_cast<dom::BrowserParent*>(Manager()); + isActive = browserParent->GetDocShellIsActive(); + } + + // onCreate is guaranteed to be called synchronously by + // nsWinUtils::CreateNativeWindow, so this reference isn't really necessary. + // However, static analysis complains without it. + RefPtr<DocAccessibleParent> thisRef = this; + nsWinUtils::NativeWindowCreateProc onCreate([thisRef](HWND aHwnd) -> void { + ::SetPropW(aHwnd, kPropNameDocAccParent, + reinterpret_cast<HANDLE>(thisRef.get())); + thisRef->SetEmulatedWindowHandle(aHwnd); + }); + + HWND parentWnd = reinterpret_cast<HWND>(rootDocument->GetNativeWindow()); + DebugOnly<HWND> hWnd = nsWinUtils::CreateNativeWindow( + kClassNameTabContent, parentWnd, rect.X(), rect.Y(), rect.Width(), + rect.Height(), isActive, &onCreate); + MOZ_ASSERT(hWnd); +} + +void DocAccessibleParent::SetEmulatedWindowHandle(HWND aWindowHandle) { + if (!aWindowHandle && mEmulatedWindowHandle && IsTopLevel()) { + ::DestroyWindow(mEmulatedWindowHandle); + } + mEmulatedWindowHandle = aWindowHandle; +} +#endif // defined(XP_WIN) + +mozilla::ipc::IPCResult DocAccessibleParent::RecvFocusEvent( + const uint64_t& aID, const LayoutDeviceIntRect& aCaretRect) { + ACQUIRE_ANDROID_LOCK + if (mShutdown) { + return IPC_OK(); + } + + RemoteAccessible* proxy = GetAccessible(aID); + if (!proxy) { + NS_ERROR("no proxy for event!"); + return IPC_OK(); + } + +#ifdef ANDROID + if (FocusMgr()) { + FocusMgr()->SetFocusedRemoteDoc(this); + } +#endif + + mFocus = aID; + PlatformFocusEvent(proxy, aCaretRect); + + if (!nsCoreUtils::AccEventObserversExist()) { + return IPC_OK(); + } + + xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(proxy); + xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this); + nsINode* node = nullptr; + bool fromUser = true; // XXX fix me + RefPtr<xpcAccEvent> event = new xpcAccEvent(nsIAccessibleEvent::EVENT_FOCUS, + xpcAcc, doc, node, fromUser); + nsCoreUtils::DispatchAccEvent(std::move(event)); + + return IPC_OK(); +} + +void DocAccessibleParent::SelectionRanges(nsTArray<TextRange>* aRanges) const { + aRanges->SetCapacity(mTextSelections.Length()); + for (const auto& data : mTextSelections) { + // Selection ranges should usually be in sync with the tree. However, tree + // and selection updates happen using separate IPDL calls, so it's possible + // for a client selection query to arrive between them. Thus, we validate + // the Accessibles and offsets here. + auto* startAcc = + const_cast<RemoteAccessible*>(GetAccessible(data.StartID())); + auto* endAcc = const_cast<RemoteAccessible*>(GetAccessible(data.EndID())); + if (!startAcc || !endAcc) { + continue; + } + uint32_t startCount = startAcc->CharacterCount(); + if (startCount == 0 || + data.StartOffset() > static_cast<int32_t>(startCount)) { + continue; + } + uint32_t endCount = endAcc->CharacterCount(); + if (endCount == 0 || data.EndOffset() > static_cast<int32_t>(endCount)) { + continue; + } + aRanges->AppendElement(TextRange(const_cast<DocAccessibleParent*>(this), + startAcc, data.StartOffset(), endAcc, + data.EndOffset())); + } +} + +Accessible* DocAccessibleParent::FocusedChild() { + LocalAccessible* outerDoc = OuterDocOfRemoteBrowser(); + if (!outerDoc) { + return nullptr; + } + + RootAccessible* rootDocument = outerDoc->RootAccessible(); + return rootDocument->FocusedChild(); +} + +void DocAccessibleParent::URL(nsACString& aURL) const { + if (!mBrowsingContext) { + return; + } + nsCOMPtr<nsIURI> uri = mBrowsingContext->GetCurrentURI(); + if (!uri) { + return; + } + // Let's avoid treating too long URI in the main process for avoiding + // memory fragmentation as far as possible. + if (uri->SchemeIs("data") || uri->SchemeIs("blob")) { + return; + } + nsCOMPtr<nsIIOService> io = mozilla::components::IO::Service(); + if (NS_WARN_IF(!io)) { + return; + } + nsCOMPtr<nsIURI> exposableURI; + if (NS_FAILED(io->CreateExposableURI(uri, getter_AddRefs(exposableURI))) || + MOZ_UNLIKELY(!exposableURI)) { + return; + } + exposableURI->GetSpec(aURL); +} + +void DocAccessibleParent::URL(nsAString& aURL) const { + nsAutoCString url; + URL(url); + CopyUTF8toUTF16(url, aURL); +} + +void DocAccessibleParent::MimeType(nsAString& aMime) const { + if (mCachedFields) { + mCachedFields->GetAttribute(CacheKey::MimeType, aMime); + } +} + +Relation DocAccessibleParent::RelationByType(RelationType aType) const { + // If the accessible is top-level, provide the NODE_CHILD_OF relation so that + // MSAA clients can easily get to true parent instead of getting to oleacc's + // ROLE_WINDOW accessible when window emulation is enabled which will prevent + // us from going up further (because it is system generated and has no idea + // about the hierarchy above it). + if (aType == RelationType::NODE_CHILD_OF && IsTopLevel()) { + return Relation(Parent()); + } + + return RemoteAccessible::RelationByType(aType); +} + +DocAccessibleParent* DocAccessibleParent::GetFrom( + dom::BrowsingContext* aBrowsingContext) { + if (!aBrowsingContext) { + return nullptr; + } + + dom::BrowserParent* bp = aBrowsingContext->Canonical()->GetBrowserParent(); + if (!bp) { + return nullptr; + } + + const ManagedContainer<PDocAccessibleParent>& docs = + bp->ManagedPDocAccessibleParent(); + for (auto* key : docs) { + // Iterate over our docs until we find one with a browsing + // context that matches the one we passed in. Return that + // document. + auto* doc = static_cast<a11y::DocAccessibleParent*>(key); + if (doc->GetBrowsingContext() == aBrowsingContext) { + return doc; + } + } + + return nullptr; +} + +size_t DocAccessibleParent::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) { + size_t size = 0; + + size += RemoteAccessible::SizeOfExcludingThis(aMallocSizeOf); + + size += mReverseRelations.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (auto i = mReverseRelations.Iter(); !i.Done(); i.Next()) { + size += i.Data().ShallowSizeOfExcludingThis(aMallocSizeOf); + for (auto j = i.Data().Iter(); !j.Done(); j.Next()) { + size += j.Data().ShallowSizeOfExcludingThis(aMallocSizeOf); + } + } + + size += mOnScreenAccessibles.ShallowSizeOfExcludingThis(aMallocSizeOf); + + size += mChildDocs.ShallowSizeOfExcludingThis(aMallocSizeOf); + + size += mAccessibles.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (auto i = mAccessibles.Iter(); !i.Done(); i.Next()) { + size += i.Get()->mProxy->SizeOfIncludingThis(aMallocSizeOf); + } + + size += mPendingOOPChildDocs.ShallowSizeOfExcludingThis(aMallocSizeOf); + + // The mTextSelections array contains structs of integers. We can count them + // by counting the size of the array - there's no deep structure here. + size += mTextSelections.ShallowSizeOfExcludingThis(aMallocSizeOf); + + return size; +} + +MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOfAccessibilityCache); + +NS_IMETHODIMP +DocAccessibleParent::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnon) { + nsAutoCString path; + + if (aAnon) { + path = nsPrintfCString("explicit/a11y/cache(%" PRIu64 ")", mActorID); + } else { + nsCString url; + URL(url); + url.ReplaceChar( + '/', '\\'); // Tell the memory reporter this is not a path seperator. + path = nsPrintfCString("explicit/a11y/cache(%s)", url.get()); + } + + aHandleReport->Callback( + /* process */ ""_ns, path, KIND_HEAP, UNITS_BYTES, + SizeOfIncludingThis(MallocSizeOfAccessibilityCache), + nsLiteralCString("Size of the accessability cache for this document."), + aData); + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(DocAccessibleParent, nsIMemoryReporter); + +} // namespace a11y +} // namespace mozilla diff --git a/accessible/ipc/DocAccessibleParent.h b/accessible/ipc/DocAccessibleParent.h new file mode 100644 index 0000000000..bb05fbafad --- /dev/null +++ b/accessible/ipc/DocAccessibleParent.h @@ -0,0 +1,400 @@ +/* -*- 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_DocAccessibleParent_h +#define mozilla_a11y_DocAccessibleParent_h + +#include "nsAccessibilityService.h" +#include "mozilla/a11y/PDocAccessibleParent.h" +#include "mozilla/a11y/RemoteAccessible.h" +#include "mozilla/dom/BrowserBridgeParent.h" +#include "nsClassHashtable.h" +#include "nsHashKeys.h" +#include "nsISupportsImpl.h" + +namespace mozilla { +namespace dom { +class CanonicalBrowsingContext; +} + +namespace a11y { + +class TextRange; +class xpcAccessibleGeneric; + +/* + * These objects live in the main process and comunicate with and represent + * an accessible document in a content process. + */ +class DocAccessibleParent : public RemoteAccessible, + public PDocAccessibleParent, + public nsIMemoryReporter { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMEMORYREPORTER + + private: + DocAccessibleParent(); + + public: + static already_AddRefed<DocAccessibleParent> New(); + + /** + * Set this as a top level document; i.e. it is not embedded by another remote + * document. This also means it is a top level document in its content + * process. + * Tab documents are top level documents. + */ + void SetTopLevel() { + mTopLevel = true; + mTopLevelInContentProcess = true; + } + bool IsTopLevel() const { return mTopLevel; } + + /** + * Set this as a top level document in its content process. + * Note that this could be an out-of-process iframe embedded by a remote + * embedder document. In that case, IsToplevel() will return false, but + * IsTopLevelInContentProcess() will return true. + */ + void SetTopLevelInContentProcess() { mTopLevelInContentProcess = true; } + bool IsTopLevelInContentProcess() const { return mTopLevelInContentProcess; } + + /** + * Determine whether this is an out-of-process iframe document, embedded by a + * remote embedder document. + */ + bool IsOOPIframeDoc() const { + return !mTopLevel && mTopLevelInContentProcess; + } + + bool IsShutdown() const { return mShutdown; } + + /** + * Mark this actor as shutdown without doing any cleanup. This should only + * be called on actors that have just been initialized, so probably only from + * RecvPDocAccessibleConstructor. + */ + void MarkAsShutdown() { + MOZ_ASSERT(mChildDocs.IsEmpty()); + MOZ_ASSERT(mAccessibles.Count() == 0); + MOZ_ASSERT(!mBrowsingContext); + mShutdown = true; + } + + void SetBrowsingContext(dom::CanonicalBrowsingContext* aBrowsingContext); + + dom::CanonicalBrowsingContext* GetBrowsingContext() const { + return mBrowsingContext; + } + + /* + * Called when a message from a document in a child process notifies the main + * process it is firing an event. + */ + virtual mozilla::ipc::IPCResult RecvEvent(const uint64_t& aID, + const uint32_t& aType) override; + + virtual mozilla::ipc::IPCResult RecvShowEvent( + nsTArray<AccessibleData>&& aNewTree, const bool& aEventSuppressed, + const bool& aComplete, const bool& aFromUser) override; + virtual mozilla::ipc::IPCResult RecvHideEvent(const uint64_t& aRootID, + const bool& aFromUser) override; + mozilla::ipc::IPCResult RecvStateChangeEvent(const uint64_t& aID, + const uint64_t& aState, + const bool& aEnabled) final; + + mozilla::ipc::IPCResult RecvCaretMoveEvent( + const uint64_t& aID, const LayoutDeviceIntRect& aCaretRect, + const int32_t& aOffset, const bool& aIsSelectionCollapsed, + const bool& aIsAtEndOfLine, const int32_t& aGranularity, + const bool& aFromUser) final; + + virtual mozilla::ipc::IPCResult RecvTextChangeEvent( + const uint64_t& aID, const nsAString& aStr, const int32_t& aStart, + const uint32_t& aLen, const bool& aIsInsert, + const bool& aFromUser) override; + + virtual mozilla::ipc::IPCResult RecvFocusEvent( + const uint64_t& aID, const LayoutDeviceIntRect& aCaretRect) override; + + virtual mozilla::ipc::IPCResult RecvSelectionEvent( + const uint64_t& aID, const uint64_t& aWidgetID, + const uint32_t& aType) override; + + virtual mozilla::ipc::IPCResult RecvScrollingEvent( + const uint64_t& aID, const uint64_t& aType, const uint32_t& aScrollX, + const uint32_t& aScrollY, const uint32_t& aMaxScrollX, + const uint32_t& aMaxScrollY) override; + + virtual mozilla::ipc::IPCResult RecvCache( + const mozilla::a11y::CacheUpdateType& aUpdateType, + nsTArray<CacheData>&& aData) override; + + virtual mozilla::ipc::IPCResult RecvSelectedAccessiblesChanged( + nsTArray<uint64_t>&& aSelectedIDs, + nsTArray<uint64_t>&& aUnselectedIDs) override; + + virtual mozilla::ipc::IPCResult RecvAccessiblesWillMove( + nsTArray<uint64_t>&& aIDs) override; + +#if !defined(XP_WIN) + virtual mozilla::ipc::IPCResult RecvAnnouncementEvent( + const uint64_t& aID, const nsAString& aAnnouncement, + const uint16_t& aPriority) override; +#endif + + virtual mozilla::ipc::IPCResult RecvTextSelectionChangeEvent( + const uint64_t& aID, nsTArray<TextRangeData>&& aSelection) override; + + mozilla::ipc::IPCResult RecvRoleChangedEvent( + const a11y::role& aRole, const uint8_t& aRoleMapEntryIndex) final; + + virtual mozilla::ipc::IPCResult RecvBindChildDoc( + NotNull<PDocAccessibleParent*> aChildDoc, const uint64_t& aID) override; + + void Unbind() { + if (DocAccessibleParent* parent = ParentDoc()) { + parent->RemoveChildDoc(this); + } + + SetParent(nullptr); + } + + virtual mozilla::ipc::IPCResult RecvShutdown() override; + void Destroy(); + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + /* + * Return the main processes representation of the parent document (if any) + * of the document this object represents. + */ + DocAccessibleParent* ParentDoc() const; + static const uint64_t kNoParentDoc = UINT64_MAX; + + /** + * Called when a document in a content process notifies the main process of a + * new child document. + * Although this is called internally for OOP child documents, these should be + * added via the BrowserBridgeParent version of this method, as the parent id + * might not exist yet in that case. + */ + ipc::IPCResult AddChildDoc(DocAccessibleParent* aChildDoc, uint64_t aParentID, + bool aCreating = true); + + /** + * Called when a document in a content process notifies the main process of a + * new OOP child document. + */ + ipc::IPCResult AddChildDoc(dom::BrowserBridgeParent* aBridge); + + void RemovePendingOOPChildDoc(dom::BrowserBridgeParent* aBridge) { + mPendingOOPChildDocs.Remove(aBridge); + } + + /* + * Called when the document in the content process this object represents + * notifies the main process a child document has been removed. + */ + void RemoveChildDoc(DocAccessibleParent* aChildDoc) { + RemoteAccessible* parent = aChildDoc->RemoteParent(); + MOZ_ASSERT(parent); + if (parent) { + aChildDoc->RemoteParent()->ClearChildDoc(aChildDoc); + } + DebugOnly<bool> result = mChildDocs.RemoveElement(aChildDoc->mActorID); + aChildDoc->mParentDoc = kNoParentDoc; + MOZ_ASSERT(result); + } + + void RemoveAccessible(RemoteAccessible* aAccessible) { + MOZ_DIAGNOSTIC_ASSERT(mAccessibles.GetEntry(aAccessible->ID())); + mAccessibles.RemoveEntry(aAccessible->ID()); + } + + /** + * Return the accessible for given id. + */ + RemoteAccessible* GetAccessible(uintptr_t aID) { + if (!aID) return this; + + ProxyEntry* e = mAccessibles.GetEntry(aID); + return e ? e->mProxy : nullptr; + } + + const RemoteAccessible* GetAccessible(uintptr_t aID) const { + return const_cast<DocAccessibleParent*>(this)->GetAccessible(aID); + } + + size_t ChildDocCount() const { return mChildDocs.Length(); } + const DocAccessibleParent* ChildDocAt(size_t aIdx) const { + return const_cast<DocAccessibleParent*>(this)->ChildDocAt(aIdx); + } + DocAccessibleParent* ChildDocAt(size_t aIdx) { + return LiveDocs().Get(mChildDocs[aIdx]); + } + +#if defined(XP_WIN) + void MaybeInitWindowEmulation(); + + /** + * Set emulated native window handle for a document. + * @param aWindowHandle emulated native window handle + */ + void SetEmulatedWindowHandle(HWND aWindowHandle); + HWND GetEmulatedWindowHandle() const { return mEmulatedWindowHandle; } +#endif + + // Accessible + virtual Accessible* Parent() const override { + if (IsTopLevel()) { + return OuterDocOfRemoteBrowser(); + } + return RemoteParent(); + } + + virtual int32_t IndexInParent() const override { + if (IsTopLevel() && OuterDocOfRemoteBrowser()) { + // An OuterDoc can only have 1 child. + return 0; + } + return RemoteAccessible::IndexInParent(); + } + + /** + * Get the focused Accessible in this document, if any. + */ + RemoteAccessible* GetFocusedAcc() const { + return const_cast<DocAccessibleParent*>(this)->GetAccessible(mFocus); + } + + /** + * Get the HyperText Accessible containing the caret and the offset of the + * caret within. If there is no caret in this document, returns + * {nullptr, -1}. + */ + std::pair<RemoteAccessible*, int32_t> GetCaret() const { + if (mCaretOffset == -1) { + return {nullptr, -1}; + } + RemoteAccessible* acc = + const_cast<DocAccessibleParent*>(this)->GetAccessible(mCaretId); + if (!acc) { + return {nullptr, -1}; + } + return {acc, mCaretOffset}; + } + + bool IsCaretAtEndOfLine() const { return mIsCaretAtEndOfLine; } + + virtual void SelectionRanges(nsTArray<TextRange>* aRanges) const override; + + virtual Accessible* FocusedChild() override; + + void URL(nsAString& aURL) const; + void URL(nsACString& aURL) const; + + void MimeType(nsAString& aURL) const; + + virtual Relation RelationByType(RelationType aType) const override; + + // Tracks cached reverse relations (ie. those not set explicitly by an + // attribute like aria-labelledby) for accessibles in this doc. This map is of + // the form: {accID, {relationType, [targetAccID, targetAccID, ...]}} + nsTHashMap<uint64_t, nsTHashMap<RelationType, nsTArray<uint64_t>>> + mReverseRelations; + + // Computed from the viewport cache, the accs referenced by these ids + // are currently on screen (making any acc not in this list offscreen). + nsTHashSet<uint64_t> mOnScreenAccessibles; + + static DocAccessibleParent* GetFrom(dom::BrowsingContext* aBrowsingContext); + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) override; + + private: + ~DocAccessibleParent(); + + class ProxyEntry : public PLDHashEntryHdr { + public: + explicit ProxyEntry(const void*) : mProxy(nullptr) {} + ProxyEntry(ProxyEntry&& aOther) : mProxy(aOther.mProxy) { + aOther.mProxy = nullptr; + } + ~ProxyEntry() { delete mProxy; } + + typedef uint64_t KeyType; + typedef const void* KeyTypePointer; + + bool KeyEquals(const void* aKey) const { + return mProxy->ID() == (uint64_t)aKey; + } + + static const void* KeyToPointer(uint64_t aKey) { return (void*)aKey; } + + static PLDHashNumber HashKey(const void* aKey) { return (uint64_t)aKey; } + + enum { ALLOW_MEMMOVE = true }; + + RemoteAccessible* mProxy; + }; + + RemoteAccessible* CreateAcc(const AccessibleData& aAccData); + void AttachChild(RemoteAccessible* aParent, uint32_t aIndex, + RemoteAccessible* aChild); + [[nodiscard]] bool CheckDocTree() const; + xpcAccessibleGeneric* GetXPCAccessible(RemoteAccessible* aProxy); + + void FireEvent(RemoteAccessible* aAcc, const uint32_t& aType); + + /** + * If this Accessible is being moved, prepare it for reuse. Otherwise, it is + * being removed, so shut it down. + */ + void ShutdownOrPrepareForMove(RemoteAccessible* aAcc); + + nsTArray<uint64_t> mChildDocs; + uint64_t mParentDoc; + +#if defined(XP_WIN) + // The handle associated with the emulated window that contains this document + HWND mEmulatedWindowHandle; +#endif // defined(XP_WIN) + + /* + * Conceptually this is a map from IDs to proxies, but we store the ID in the + * proxy object so we can't use a real map. + */ + nsTHashtable<ProxyEntry> mAccessibles; + uint64_t mPendingShowChild = 0; + uint64_t mPendingShowParent = 0; + uint32_t mPendingShowIndex = 0; + nsTHashSet<uint64_t> mMovingIDs; + uint64_t mActorID; + bool mTopLevel; + bool mTopLevelInContentProcess; + bool mShutdown; + RefPtr<dom::CanonicalBrowsingContext> mBrowsingContext; + + nsTHashSet<RefPtr<dom::BrowserBridgeParent>> mPendingOOPChildDocs; + + uint64_t mFocus; + uint64_t mCaretId; + int32_t mCaretOffset; + bool mIsCaretAtEndOfLine; + nsTArray<TextRangeData> mTextSelections; + + static uint64_t sMaxDocID; + static nsTHashMap<nsUint64HashKey, DocAccessibleParent*>& LiveDocs() { + static nsTHashMap<nsUint64HashKey, DocAccessibleParent*> sLiveDocs; + return sLiveDocs; + } +}; + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/ipc/DocAccessibleTypes.ipdlh b/accessible/ipc/DocAccessibleTypes.ipdlh new file mode 100644 index 0000000000..3ef67fc431 --- /dev/null +++ b/accessible/ipc/DocAccessibleTypes.ipdlh @@ -0,0 +1,19 @@ +/* -*- 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/. */ + +[RefCounted] using mozilla::a11y::AccAttributes from "mozilla/a11y/IPCTypes.h"; + +namespace mozilla { +namespace a11y { + +struct CacheData +{ + uint64_t ID; + nullable AccAttributes Fields; +}; + +} +} diff --git a/accessible/ipc/IPCTypes.h b/accessible/ipc/IPCTypes.h new file mode 100644 index 0000000000..2e911724bb --- /dev/null +++ b/accessible/ipc/IPCTypes.h @@ -0,0 +1,189 @@ +/* -*- 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_IPCTypes_h +#define mozilla_a11y_IPCTypes_h + +#ifdef ACCESSIBILITY +# include "mozilla/a11y/AccAttributes.h" +# include "mozilla/a11y/AccTypes.h" +# include "mozilla/a11y/CacheConstants.h" +# include "mozilla/a11y/Role.h" +# include "mozilla/a11y/AccGroupInfo.h" +# include "mozilla/GfxMessageUtils.h" +# include "ipc/EnumSerializer.h" +# include "ipc/IPCMessageUtilsSpecializations.h" + +namespace IPC { + +template <> +struct ParamTraits<mozilla::a11y::role> + : public ContiguousEnumSerializerInclusive<mozilla::a11y::role, + mozilla::a11y::role::NOTHING, + mozilla::a11y::role::LAST_ROLE> { +}; + +template <> +struct ParamTraits<mozilla::a11y::AccType> + : public ContiguousEnumSerializerInclusive< + mozilla::a11y::AccType, mozilla::a11y::AccType::eNoType, + mozilla::a11y::AccType::eLastAccType> {}; + +template <> +struct ParamTraits<mozilla::a11y::AccGenericType> + : public BitFlagsEnumSerializer< + mozilla::a11y::AccGenericType, + mozilla::a11y::AccGenericType::eAllGenericTypes> {}; + +template <> +struct ParamTraits<mozilla::a11y::CacheUpdateType> + : public ContiguousEnumSerializerInclusive< + mozilla::a11y::CacheUpdateType, + mozilla::a11y::CacheUpdateType::Initial, + mozilla::a11y::CacheUpdateType::Update> {}; + +template <> +struct ParamTraits<mozilla::a11y::FontSize> { + typedef mozilla::a11y::FontSize paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mValue); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + return ReadParam(aReader, &(aResult->mValue)); + } +}; + +template <> +struct ParamTraits<mozilla::a11y::DeleteEntry> { + typedef mozilla::a11y::DeleteEntry paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mValue); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + return ReadParam(aReader, &(aResult->mValue)); + } +}; + +template <> +struct ParamTraits<mozilla::a11y::AccGroupInfo> { + typedef mozilla::a11y::AccGroupInfo paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + MOZ_ASSERT_UNREACHABLE("Cannot serialize AccGroupInfo"); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + MOZ_ASSERT_UNREACHABLE("Cannot de-serialize AccGroupInfo"); + return false; + } +}; + +template <> +struct ParamTraits<mozilla::a11y::Color> { + typedef mozilla::a11y::Color paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mValue); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + return ReadParam(aReader, &(aResult->mValue)); + } +}; + +template <> +struct ParamTraits<mozilla::a11y::AccAttributes*> { + typedef mozilla::a11y::AccAttributes paramType; + + static void Write(MessageWriter* aWriter, const paramType* aParam) { + if (!aParam) { + WriteParam(aWriter, true); + return; + } + + WriteParam(aWriter, false); + uint32_t count = aParam->mData.Count(); + WriteParam(aWriter, count); + for (auto iter = aParam->mData.ConstIter(); !iter.Done(); iter.Next()) { + RefPtr<nsAtom> key = iter.Key(); + WriteParam(aWriter, key); + const paramType::AttrValueType& data = iter.Data(); + WriteParam(aWriter, data); + } + } + + static bool Read(MessageReader* aReader, RefPtr<paramType>* aResult) { + bool isNull = false; + if (!ReadParam(aReader, &isNull)) { + return false; + } + + if (isNull) { + *aResult = nullptr; + return true; + } + + *aResult = mozilla::MakeRefPtr<mozilla::a11y::AccAttributes>(); + uint32_t count; + if (!ReadParam(aReader, &count)) { + return false; + } + for (uint32_t i = 0; i < count; ++i) { + RefPtr<nsAtom> key; + if (!ReadParam(aReader, &key)) { + return false; + } + paramType::AttrValueType val(0); + if (!ReadParam(aReader, &val)) { + return false; + } + (*aResult)->mData.InsertOrUpdate(key, std::move(val)); + } + return true; + } +}; + +} // namespace IPC +#else +namespace mozilla { +namespace a11y { +typedef uint32_t role; +} // namespace a11y +} // namespace mozilla +#endif // ACCESSIBILITY + +/** + * Since IPDL does not support preprocessing, this header file allows us to + * define types used by PDocAccessible differently depending on platform. + */ + +#if defined(MOZ_WIDGET_COCOA) +# if defined(ACCESSIBILITY) +# include "mozilla/a11y/PlatformExtTypes.h" +namespace IPC { + +template <> +struct ParamTraits<mozilla::a11y::EWhichRange> + : public ContiguousEnumSerializerInclusive< + mozilla::a11y::EWhichRange, mozilla::a11y::EWhichRange::eLeftWord, + mozilla::a11y::EWhichRange::eStyle> {}; + +} // namespace IPC + +# else +namespace mozilla { +namespace a11y { +typedef uint32_t EWhichRange; +} // namespace a11y +} // namespace mozilla +# endif // defined(ACCESSIBILITY) +#endif // defined(MOZ_WIDGET_COCOA) + +#endif // mozilla_a11y_IPCTypes_h diff --git a/accessible/ipc/PDocAccessible.ipdl b/accessible/ipc/PDocAccessible.ipdl new file mode 100644 index 0000000000..d43fad823e --- /dev/null +++ b/accessible/ipc/PDocAccessible.ipdl @@ -0,0 +1,164 @@ +/* -*- 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 protocol PBrowser; + +include DocAccessibleTypes; + +include "mozilla/GfxMessageUtils.h"; + +using mozilla::LayoutDeviceIntRect from "Units.h"; +using mozilla::LayoutDeviceIntPoint from "Units.h"; +using mozilla::LayoutDeviceIntSize from "Units.h"; +using mozilla::a11y::role from "mozilla/a11y/IPCTypes.h"; +using mozilla::a11y::AccType from "mozilla/a11y/IPCTypes.h"; +using mozilla::a11y::AccGenericType from "mozilla/a11y/IPCTypes.h"; +[RefCounted] using mozilla::a11y::AccAttributes from "mozilla/a11y/IPCTypes.h"; +using mozilla::a11y::CacheUpdateType from "mozilla/a11y/IPCTypes.h"; +using mozilla::gfx::Matrix4x4 from "mozilla/gfx/Matrix.h"; + +namespace mozilla { +namespace a11y { + +struct AccessibleData +{ + uint64_t ID; + role Role; + uint64_t ParentID; + uint32_t IndexInParent; + AccType Type; + AccGenericType GenericTypes; + uint8_t RoleMapEntryIndex; + nullable AccAttributes CacheFields; +}; + +struct TextRangeData +{ + uint64_t StartID; + uint64_t EndID; + int32_t StartOffset; + int32_t EndOffset; +}; + +[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual] +protocol PDocAccessible +{ + manager PBrowser; + +parent: + async Shutdown(); + + /* + * Notify the parent process the document in the child process is firing an + * event. + */ + async Event(uint64_t aID, uint32_t type); + async ShowEvent(AccessibleData[] aNewTree, bool aEventSuppressed, + bool aComplete, bool aFromuser); + async HideEvent(uint64_t aRootID, bool aFromUser); + async StateChangeEvent(uint64_t aID, uint64_t aState, bool aEnabled); + async CaretMoveEvent(uint64_t aID, + LayoutDeviceIntRect aCaretRect, + int32_t aOffset, + bool aIsSelectionCollapsed, bool aIsAtEndOfLine, + int32_t aGranularity, bool aFromUser); + async TextChangeEvent(uint64_t aID, nsString aStr, int32_t aStart, uint32_t aLen, + bool aIsInsert, bool aFromUser); + async SelectionEvent(uint64_t aID, uint64_t aWidgetID, uint32_t aType); + async RoleChangedEvent(role aRole, uint8_t aRoleMapEntryIndex); + async FocusEvent(uint64_t aID, LayoutDeviceIntRect aCaretRect); + async ScrollingEvent(uint64_t aID, uint64_t aType, + uint32_t aScrollX, uint32_t aScrollY, + uint32_t aMaxScrollX, uint32_t aMaxScrollY); +#ifndef XP_WIN + async AnnouncementEvent(uint64_t aID, + nsString aAnnouncement, + uint16_t aPriority); +#endif + async TextSelectionChangeEvent(uint64_t aID, TextRangeData[] aSelection); + + /* + * Tell the parent document to bind the existing document as a new child + * document. + */ + async BindChildDoc(PDocAccessible aChildDoc, uint64_t aID); + + /* + * Cache The World + */ + async Cache(CacheUpdateType aUpdateType, CacheData[] aData); + + /* + * Lists of accessibles that either gained or lost a selected state. + */ + async SelectedAccessiblesChanged(uint64_t[] aSelectedIDs, uint64_t[] aUnselectedIDs); + + /* + * Tell the parent process that the given Accessibles are about to be moved + * via subsequent hide and show events. + */ + async AccessiblesWillMove(uint64_t[] aIDs); + +child: + async __delete__(); + + /* + * Called as a result of focus shifting from chrome to content + * elements through keyboard navigation. + */ + async RestoreFocus(); + + // LocalAccessible + async ScrollTo(uint64_t aID, uint32_t aScrollType); + async ScrollToPoint(uint64_t aID, uint32_t aScrollType, int32_t aX, + int32_t aY); +#ifndef XP_WIN + async Announce(uint64_t aID, nsString aAnnouncement, uint16_t aPriority); +#endif + + // AccessibleText + + async SetCaretOffset(uint64_t aID, int32_t aOffset); + + async SetTextSelection(uint64_t aStartID, int32_t aStartOffset, + uint64_t aEndID, int32_t aEndOffset, + int32_t aSelectionNum); + async RemoveTextSelection(uint64_t aID, int32_t aSelectionNum); + + async ScrollTextLeafRangeIntoView(uint64_t aStartID, int32_t aStartOffset, + uint64_t aEndID, int32_t aEndOffset, + uint32_t aScrollType); + async ScrollSubstringToPoint(uint64_t aID, + int32_t aStartOffset, + int32_t aEndOffset, + uint32_t aCoordinateType, + int32_t aX, int32_t aY); + + async ReplaceText(uint64_t aID, nsString aText); + async InsertText(uint64_t aID, nsString aText, int32_t aPosition); + async CopyText(uint64_t aID, int32_t aStartPos, int32_t aEndPos); + async CutText(uint64_t aID, int32_t aStartPos, int32_t aEndPos); + async DeleteText(uint64_t aID, int32_t aStartPos, int32_t aEndPos); + async PasteText(uint64_t aID, int32_t aPosition); + + async TakeSelection(uint64_t aID); + async SetSelected(uint64_t aID, bool aSelected); + + async DoActionAsync(uint64_t aID, uint8_t aIndex); + + async SetCurValue(uint64_t aID, double aValue); + + async TakeFocus(uint64_t aID); + + /* + * Verify the cache. Used for testing purposes. + */ + async VerifyCache(uint64_t aID, uint64_t aCacheDomain, nullable AccAttributes aFields); + +}; + +} +} diff --git a/accessible/ipc/RemoteAccessible.cpp b/accessible/ipc/RemoteAccessible.cpp new file mode 100644 index 0000000000..772fc58776 --- /dev/null +++ b/accessible/ipc/RemoteAccessible.cpp @@ -0,0 +1,2092 @@ +/* -*- 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 "ARIAMap.h" +#include "CachedTableAccessible.h" +#include "RemoteAccessible.h" +#include "mozilla/a11y/DocAccessibleParent.h" +#include "mozilla/a11y/DocManager.h" +#include "mozilla/a11y/Platform.h" +#include "mozilla/a11y/TableAccessible.h" +#include "mozilla/a11y/TableCellAccessible.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/gfx/Matrix.h" +#include "nsAccessibilityService.h" +#include "mozilla/Unused.h" +#include "nsAccUtils.h" +#include "nsTextEquivUtils.h" +#include "Pivot.h" +#include "Relation.h" +#include "mozilla/a11y/RelationType.h" +#include "xpcAccessibleDocument.h" + +#ifdef A11Y_LOG +# include "Logging.h" +# define VERIFY_CACHE(domain) \ + if (logging::IsEnabled(logging::eCache)) { \ + Unused << mDoc->SendVerifyCache(mID, domain, mCachedFields); \ + } +#else +# define VERIFY_CACHE(domain) \ + do { \ + } while (0) + +#endif + +namespace mozilla { +namespace a11y { + +void RemoteAccessible::Shutdown() { + MOZ_DIAGNOSTIC_ASSERT(!IsDoc()); + xpcAccessibleDocument* xpcDoc = + GetAccService()->GetCachedXPCDocument(Document()); + if (xpcDoc) { + xpcDoc->NotifyOfShutdown(static_cast<RemoteAccessible*>(this)); + } + + if (IsTable() || IsTableCell()) { + CachedTableAccessible::Invalidate(this); + } + + // Remove this acc's relation map from the doc's map of + // reverse relations. Prune forward relations associated with this + // acc's reverse relations. This also removes the acc's map of reverse + // rels from the mDoc's mReverseRelations. + PruneRelationsOnShutdown(); + + // XXX Ideally this wouldn't be necessary, but it seems OuterDoc + // accessibles can be destroyed before the doc they own. + uint32_t childCount = mChildren.Length(); + if (!IsOuterDoc()) { + for (uint32_t idx = 0; idx < childCount; idx++) mChildren[idx]->Shutdown(); + } else { + if (childCount > 1) { + MOZ_CRASH("outer doc has too many documents!"); + } else if (childCount == 1) { + mChildren[0]->AsDoc()->Unbind(); + } + } + + mChildren.Clear(); + ProxyDestroyed(static_cast<RemoteAccessible*>(this)); + mDoc->RemoveAccessible(static_cast<RemoteAccessible*>(this)); +} + +void RemoteAccessible::SetChildDoc(DocAccessibleParent* aChildDoc) { + MOZ_ASSERT(aChildDoc); + MOZ_ASSERT(mChildren.Length() == 0); + mChildren.AppendElement(aChildDoc); +} + +void RemoteAccessible::ClearChildDoc(DocAccessibleParent* aChildDoc) { + MOZ_ASSERT(aChildDoc); + // This is possible if we're replacing one document with another: Doc 1 + // has not had a chance to remove itself, but was already replaced by Doc 2 + // in SetChildDoc(). This could result in two subsequent calls to + // ClearChildDoc() even though mChildren.Length() == 1. + MOZ_ASSERT(mChildren.Length() <= 1); + mChildren.RemoveElement(aChildDoc); +} + +uint32_t RemoteAccessible::EmbeddedChildCount() { + size_t count = 0, kids = mChildren.Length(); + for (size_t i = 0; i < kids; i++) { + if (mChildren[i]->IsEmbeddedObject()) { + count++; + } + } + + return count; +} + +int32_t RemoteAccessible::IndexOfEmbeddedChild(Accessible* aChild) { + size_t index = 0, kids = mChildren.Length(); + for (size_t i = 0; i < kids; i++) { + if (mChildren[i]->IsEmbeddedObject()) { + if (mChildren[i] == aChild) { + return index; + } + + index++; + } + } + + return -1; +} + +Accessible* RemoteAccessible::EmbeddedChildAt(uint32_t aChildIdx) { + size_t index = 0, kids = mChildren.Length(); + for (size_t i = 0; i < kids; i++) { + if (!mChildren[i]->IsEmbeddedObject()) { + continue; + } + + if (index == aChildIdx) { + return mChildren[i]; + } + + index++; + } + + return nullptr; +} + +LocalAccessible* RemoteAccessible::OuterDocOfRemoteBrowser() const { + auto tab = static_cast<dom::BrowserParent*>(mDoc->Manager()); + dom::Element* frame = tab->GetOwnerElement(); + NS_ASSERTION(frame, "why isn't the tab in a frame!"); + if (!frame) return nullptr; + + DocAccessible* chromeDoc = GetExistingDocAccessible(frame->OwnerDoc()); + + return chromeDoc ? chromeDoc->GetAccessible(frame) : nullptr; +} + +void RemoteAccessible::SetParent(RemoteAccessible* aParent) { + if (!aParent) { + mParent = kNoParent; + } else { + MOZ_ASSERT(!IsDoc() || !aParent->IsDoc()); + mParent = aParent->ID(); + } +} + +RemoteAccessible* RemoteAccessible::RemoteParent() const { + if (mParent == kNoParent) { + return nullptr; + } + + // if we are not a document then are parent is another proxy in the same + // document. That means we can just ask our document for the proxy with our + // parent id. + if (!IsDoc()) { + return Document()->GetAccessible(mParent); + } + + // If we are a top level document then our parent is not a proxy. + if (AsDoc()->IsTopLevel()) { + return nullptr; + } + + // Finally if we are a non top level document then our parent id is for a + // proxy in our parent document so get the proxy from there. + DocAccessibleParent* parentDoc = AsDoc()->ParentDoc(); + MOZ_ASSERT(parentDoc); + MOZ_ASSERT(mParent); + return parentDoc->GetAccessible(mParent); +} + +void RemoteAccessible::ApplyCache(CacheUpdateType aUpdateType, + AccAttributes* aFields) { + if (!aFields) { + MOZ_ASSERT_UNREACHABLE("ApplyCache called with aFields == null"); + return; + } + + const nsTArray<bool> relUpdatesNeeded = PreProcessRelations(aFields); + if (auto maybeViewportCache = + aFields->GetAttribute<nsTArray<uint64_t>>(CacheKey::Viewport)) { + // Updating the viewport cache means the offscreen state of this + // document's accessibles has changed. Update the HashSet we use for + // checking offscreen state here. + MOZ_ASSERT(IsDoc(), + "Fetched the viewport cache from a non-doc accessible?"); + AsDoc()->mOnScreenAccessibles.Clear(); + for (auto id : *maybeViewportCache) { + AsDoc()->mOnScreenAccessibles.Insert(id); + } + } + + if (aUpdateType == CacheUpdateType::Initial) { + mCachedFields = aFields; + } else { + if (!mCachedFields) { + // The fields cache can be uninitialized if there were no cache-worthy + // fields in the initial cache push. + // We don't do a simple assign because we don't want to store the + // DeleteEntry entries. + mCachedFields = new AccAttributes(); + } + mCachedFields->Update(aFields); + } + + if (IsTextLeaf()) { + RemoteAccessible* parent = RemoteParent(); + if (parent && parent->IsHyperText()) { + parent->InvalidateCachedHyperTextOffsets(); + } + } + + PostProcessRelations(relUpdatesNeeded); +} + +ENameValueFlag RemoteAccessible::Name(nsString& aName) const { + ENameValueFlag nameFlag = eNameOK; + if (mCachedFields) { + if (IsText()) { + mCachedFields->GetAttribute(CacheKey::Text, aName); + return eNameOK; + } + auto cachedNameFlag = + mCachedFields->GetAttribute<int32_t>(CacheKey::NameValueFlag); + if (cachedNameFlag) { + nameFlag = static_cast<ENameValueFlag>(*cachedNameFlag); + } + if (mCachedFields->GetAttribute(CacheKey::Name, aName)) { + VERIFY_CACHE(CacheDomain::NameAndDescription); + return nameFlag; + } + } + + MOZ_ASSERT(aName.IsEmpty()); + aName.SetIsVoid(true); + return nameFlag; +} + +void RemoteAccessible::Description(nsString& aDescription) const { + if (mCachedFields) { + mCachedFields->GetAttribute(CacheKey::Description, aDescription); + VERIFY_CACHE(CacheDomain::NameAndDescription); + } +} + +void RemoteAccessible::Value(nsString& aValue) const { + if (mCachedFields) { + if (mCachedFields->HasAttribute(CacheKey::TextValue)) { + mCachedFields->GetAttribute(CacheKey::TextValue, aValue); + VERIFY_CACHE(CacheDomain::Value); + return; + } + + if (HasNumericValue()) { + double checkValue = CurValue(); + if (!std::isnan(checkValue)) { + aValue.AppendFloat(checkValue); + } + return; + } + + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + // Value of textbox is a textified subtree. + if (roleMapEntry && roleMapEntry->Is(nsGkAtoms::textbox)) { + nsTextEquivUtils::GetTextEquivFromSubtree(this, aValue); + return; + } + + if (IsCombobox()) { + // For combo boxes, rely on selection state to determine the value. + const Accessible* option = + const_cast<RemoteAccessible*>(this)->GetSelectedItem(0); + if (option) { + option->Name(aValue); + } else { + // If no selected item, determine the value from descendant elements. + nsTextEquivUtils::GetTextEquivFromSubtree(this, aValue); + } + return; + } + + if (IsTextLeaf() || IsImage()) { + if (const Accessible* actionAcc = ActionAncestor()) { + if (const_cast<Accessible*>(actionAcc)->State() & states::LINKED) { + // Text and image descendants of links expose the link URL as the + // value. + return actionAcc->Value(aValue); + } + } + } + } +} + +double RemoteAccessible::CurValue() const { + if (mCachedFields) { + if (auto value = + mCachedFields->GetAttribute<double>(CacheKey::NumericValue)) { + VERIFY_CACHE(CacheDomain::Value); + return *value; + } + } + + return UnspecifiedNaN<double>(); +} + +double RemoteAccessible::MinValue() const { + if (mCachedFields) { + if (auto min = mCachedFields->GetAttribute<double>(CacheKey::MinValue)) { + VERIFY_CACHE(CacheDomain::Value); + return *min; + } + } + + return UnspecifiedNaN<double>(); +} + +double RemoteAccessible::MaxValue() const { + if (mCachedFields) { + if (auto max = mCachedFields->GetAttribute<double>(CacheKey::MaxValue)) { + VERIFY_CACHE(CacheDomain::Value); + return *max; + } + } + + return UnspecifiedNaN<double>(); +} + +double RemoteAccessible::Step() const { + if (mCachedFields) { + if (auto step = mCachedFields->GetAttribute<double>(CacheKey::Step)) { + VERIFY_CACHE(CacheDomain::Value); + return *step; + } + } + + return UnspecifiedNaN<double>(); +} + +bool RemoteAccessible::SetCurValue(double aValue) { + if (!HasNumericValue() || IsProgress()) { + return false; + } + + const uint32_t kValueCannotChange = states::READONLY | states::UNAVAILABLE; + if (State() & kValueCannotChange) { + return false; + } + + double checkValue = MinValue(); + if (!std::isnan(checkValue) && aValue < checkValue) { + return false; + } + + checkValue = MaxValue(); + if (!std::isnan(checkValue) && aValue > checkValue) { + return false; + } + + Unused << mDoc->SendSetCurValue(mID, aValue); + return true; +} + +bool RemoteAccessible::ContainsPoint(int32_t aX, int32_t aY) { + if (!BoundsWithOffset(Nothing(), true).Contains(aX, aY)) { + return false; + } + if (!IsTextLeaf()) { + if (IsImage() || IsImageMap() || !HasChildren() || + RefPtr{DisplayStyle()} != nsGkAtoms::inlinevalue) { + // This isn't an inline element that might contain text, so we don't need + // to walk lines. It's enough that our rect contains the point. + return true; + } + // Non-image inline elements with children can wrap across lines just like + // text leaves; see below. + // Walk the children, which will walk the lines of text in any text leaves. + uint32_t count = ChildCount(); + for (uint32_t c = 0; c < count; ++c) { + RemoteAccessible* child = RemoteChildAt(c); + if (child->Role() == roles::TEXT_CONTAINER && child->IsClipped()) { + // There is a clipped child. This is a candidate for fuzzy hit testing. + // See RemoteAccessible::DoFuzzyHittesting. + return true; + } + if (child->ContainsPoint(aX, aY)) { + return true; + } + } + // None of our descendants contain the point, so nor do we. + return false; + } + // This is a text leaf. The text might wrap across lines, which means our + // rect might cover a wider area than the actual text. For example, if the + // text begins in the middle of the first line and wraps on to the second, + // the rect will cover the start of the first line and the end of the second. + auto lines = GetCachedTextLines(); + if (!lines) { + // This means the text is empty or occupies a single line (but does not + // begin the line). In that case, the Bounds check above is sufficient, + // since there's only one rect. + return true; + } + uint32_t length = lines->Length(); + MOZ_ASSERT(length > 0, + "Line starts shouldn't be in cache if there aren't any"); + if (length == 0 || (length == 1 && (*lines)[0] == 0)) { + // This means the text begins and occupies a single line. Again, the Bounds + // check above is sufficient. + return true; + } + // Walk the lines of the text. Even if this text doesn't start at the + // beginning of a line (i.e. lines[0] > 0), we always want to consider its + // first line. + int32_t lineStart = 0; + for (uint32_t index = 0; index <= length; ++index) { + int32_t lineEnd; + if (index < length) { + int32_t nextLineStart = (*lines)[index]; + if (nextLineStart == 0) { + // This Accessible starts at the beginning of a line. Here, we always + // treat 0 as the first line start anyway. + MOZ_ASSERT(index == 0); + continue; + } + lineEnd = nextLineStart - 1; + } else { + // This is the last line. + lineEnd = static_cast<int32_t>(nsAccUtils::TextLength(this)) - 1; + } + MOZ_ASSERT(lineEnd >= lineStart); + nsRect lineRect = GetCachedCharRect(lineStart); + if (lineEnd > lineStart) { + lineRect.UnionRect(lineRect, GetCachedCharRect(lineEnd)); + } + if (BoundsWithOffset(Some(lineRect), true).Contains(aX, aY)) { + return true; + } + lineStart = lineEnd + 1; + } + return false; +} + +RemoteAccessible* RemoteAccessible::DoFuzzyHittesting() { + uint32_t childCount = ChildCount(); + if (!childCount) { + return nullptr; + } + // Check if this match has a clipped child. + // This usually indicates invisible text, and we're + // interested in returning the inner text content + // even if it doesn't contain the point we're hittesting. + RemoteAccessible* clippedContainer = nullptr; + for (uint32_t i = 0; i < childCount; i++) { + RemoteAccessible* child = RemoteChildAt(i); + if (child->Role() == roles::TEXT_CONTAINER) { + if (child->IsClipped()) { + clippedContainer = child; + break; + } + } + } + // If we found a clipped container, descend it in search of + // meaningful text leaves. Ignore non-text-leaf/text-container + // siblings. + RemoteAccessible* container = clippedContainer; + while (container) { + RemoteAccessible* textLeaf = nullptr; + bool continueSearch = false; + childCount = container->ChildCount(); + for (uint32_t i = 0; i < childCount; i++) { + RemoteAccessible* child = container->RemoteChildAt(i); + if (child->Role() == roles::TEXT_CONTAINER) { + container = child; + continueSearch = true; + break; + } + if (child->IsTextLeaf()) { + textLeaf = child; + // Don't break here -- it's possible a text container + // exists as another sibling, and we should descend as + // deep as possible. + } + } + if (textLeaf) { + return textLeaf; + } + if (!continueSearch) { + // We didn't find anything useful in this set of siblings. + // Don't keep searching + break; + } + } + return nullptr; +} + +Accessible* RemoteAccessible::ChildAtPoint( + int32_t aX, int32_t aY, LocalAccessible::EWhichChildAtPoint aWhichChild) { + // Elements that are partially on-screen should have their bounds masked by + // their containing scroll area so hittesting yields results that are + // consistent with the content's visual representation. Pass this value to + // bounds calculation functions to indicate that we're hittesting. + const bool hitTesting = true; + + if (IsOuterDoc() && aWhichChild == EWhichChildAtPoint::DirectChild) { + // This is an iframe, which is as deep as the viewport cache goes. The + // caller wants a direct child, which can only be the embedded document. + if (BoundsWithOffset(Nothing(), hitTesting).Contains(aX, aY)) { + return RemoteFirstChild(); + } + return nullptr; + } + + RemoteAccessible* lastMatch = nullptr; + // If `this` is a document, use its viewport cache instead of + // the cache of its parent document. + if (DocAccessibleParent* doc = IsDoc() ? AsDoc() : mDoc) { + if (!doc->mCachedFields) { + // A client call might arrive after we've constructed doc but before we + // get a cache push for it. + return nullptr; + } + if (auto maybeViewportCache = + doc->mCachedFields->GetAttribute<nsTArray<uint64_t>>( + CacheKey::Viewport)) { + // The retrieved viewport cache contains acc IDs in hittesting order. + // That is, items earlier in the list have z-indexes that are larger than + // those later in the list. If you were to build a tree by z-index, where + // chilren have larger z indices than their parents, iterating this list + // is essentially a postorder tree traversal. + const nsTArray<uint64_t>& viewportCache = *maybeViewportCache; + + for (auto id : viewportCache) { + RemoteAccessible* acc = doc->GetAccessible(id); + if (!acc) { + // This can happen if the acc died in between + // pushing the viewport cache and doing this hittest + continue; + } + + if (acc->IsOuterDoc() && + aWhichChild == EWhichChildAtPoint::DeepestChild && + acc->BoundsWithOffset(Nothing(), hitTesting).Contains(aX, aY)) { + // acc is an iframe, which is as deep as the viewport cache goes. This + // iframe contains the requested point. + RemoteAccessible* innerDoc = acc->RemoteFirstChild(); + if (innerDoc) { + MOZ_ASSERT(innerDoc->IsDoc()); + // Search the embedded document's viewport cache so we return the + // deepest descendant in that embedded document. + Accessible* deepestAcc = innerDoc->ChildAtPoint( + aX, aY, EWhichChildAtPoint::DeepestChild); + MOZ_ASSERT(!deepestAcc || deepestAcc->IsRemote()); + lastMatch = deepestAcc ? deepestAcc->AsRemote() : nullptr; + break; + } + // If there is no embedded document, the iframe itself is the deepest + // descendant. + lastMatch = acc; + break; + } + + if (acc == this) { + MOZ_ASSERT(!acc->IsOuterDoc()); + // Even though we're searching from the doc's cache + // this call shouldn't pass the boundary defined by + // the acc this call originated on. If we hit `this`, + // return our most recent match. + if (!lastMatch && + BoundsWithOffset(Nothing(), hitTesting).Contains(aX, aY)) { + // If we haven't found a match, but `this` contains the point we're + // looking for, set it as our temp last match so we can + // (potentially) do fuzzy hittesting on it below. + lastMatch = acc; + } + break; + } + + if (acc->ContainsPoint(aX, aY)) { + // Because our rects are in hittesting order, the + // first match we encounter is guaranteed to be the + // deepest match. + lastMatch = acc; + break; + } + } + if (lastMatch) { + RemoteAccessible* fuzzyMatch = lastMatch->DoFuzzyHittesting(); + lastMatch = fuzzyMatch ? fuzzyMatch : lastMatch; + } + } + } + + if (aWhichChild == EWhichChildAtPoint::DirectChild && lastMatch) { + // lastMatch is the deepest match. Walk up to the direct child of this. + RemoteAccessible* parent = lastMatch->RemoteParent(); + for (;;) { + if (parent == this) { + break; + } + if (!parent || parent->IsDoc()) { + // `this` is not an ancestor of lastMatch. Ignore lastMatch. + lastMatch = nullptr; + break; + } + lastMatch = parent; + parent = parent->RemoteParent(); + } + } else if (aWhichChild == EWhichChildAtPoint::DeepestChild && lastMatch && + !IsDoc() && !IsAncestorOf(lastMatch)) { + // If we end up with a match that is not in the ancestor chain + // of the accessible this call originated on, we should ignore it. + // This can happen when the aX, aY given are outside `this`. + lastMatch = nullptr; + } + + if (!lastMatch && BoundsWithOffset(Nothing(), hitTesting).Contains(aX, aY)) { + // Even though the hit target isn't inside `this`, the point is still + // within our bounds, so fall back to `this`. + return this; + } + + return lastMatch; +} + +Maybe<nsRect> RemoteAccessible::RetrieveCachedBounds() const { + if (!mCachedFields) { + return Nothing(); + } + + Maybe<const nsTArray<int32_t>&> maybeArray = + mCachedFields->GetAttribute<nsTArray<int32_t>>( + CacheKey::ParentRelativeBounds); + if (maybeArray) { + const nsTArray<int32_t>& relativeBoundsArr = *maybeArray; + MOZ_ASSERT(relativeBoundsArr.Length() == 4, + "Incorrectly sized bounds array"); + nsRect relativeBoundsRect(relativeBoundsArr[0], relativeBoundsArr[1], + relativeBoundsArr[2], relativeBoundsArr[3]); + return Some(relativeBoundsRect); + } + + return Nothing(); +} + +void RemoteAccessible::ApplyCrossDocOffset(nsRect& aBounds) const { + if (!IsDoc()) { + // We should only apply cross-doc offsets to documents. If we're anything + // else, return early here. + return; + } + + RemoteAccessible* parentAcc = RemoteParent(); + if (!parentAcc || !parentAcc->IsOuterDoc()) { + return; + } + + Maybe<const nsTArray<int32_t>&> maybeOffset = + parentAcc->mCachedFields->GetAttribute<nsTArray<int32_t>>( + CacheKey::CrossDocOffset); + if (!maybeOffset) { + return; + } + + MOZ_ASSERT(maybeOffset->Length() == 2); + const nsTArray<int32_t>& offset = *maybeOffset; + // Our retrieved value is in app units, so we don't need to do any + // unit conversion here. + aBounds.MoveBy(offset[0], offset[1]); +} + +bool RemoteAccessible::ApplyTransform(nsRect& aCumulativeBounds) const { + // First, attempt to retrieve the transform from the cache. + Maybe<const UniquePtr<gfx::Matrix4x4>&> maybeTransform = + mCachedFields->GetAttribute<UniquePtr<gfx::Matrix4x4>>( + CacheKey::TransformMatrix); + if (!maybeTransform) { + return false; + } + + auto mtxInPixels = gfx::Matrix4x4Typed<CSSPixel, CSSPixel>::FromUnknownMatrix( + *(*maybeTransform)); + + // Our matrix is in CSS Pixels, so we need our rect to be in CSS + // Pixels too. Convert before applying. + auto boundsInPixels = CSSRect::FromAppUnits(aCumulativeBounds); + boundsInPixels = mtxInPixels.TransformBounds(boundsInPixels); + aCumulativeBounds = CSSRect::ToAppUnits(boundsInPixels); + + return true; +} + +bool RemoteAccessible::ApplyScrollOffset(nsRect& aBounds) const { + Maybe<const nsTArray<int32_t>&> maybeScrollPosition = + mCachedFields->GetAttribute<nsTArray<int32_t>>(CacheKey::ScrollPosition); + + if (!maybeScrollPosition || maybeScrollPosition->Length() != 2) { + return false; + } + // Our retrieved value is in app units, so we don't need to do any + // unit conversion here. + const nsTArray<int32_t>& scrollPosition = *maybeScrollPosition; + + // Scroll position is an inverse representation of scroll offset (since the + // further the scroll bar moves down the page, the further the page content + // moves up/closer to the origin). + nsPoint scrollOffset(-scrollPosition[0], -scrollPosition[1]); + + aBounds.MoveBy(scrollOffset.x, scrollOffset.y); + + // Return true here even if the scroll offset was 0,0 because the RV is used + // as a scroll container indicator. Non-scroll containers won't have cached + // scroll position. + return true; +} + +nsRect RemoteAccessible::BoundsInAppUnits() const { + if (dom::CanonicalBrowsingContext* cbc = mDoc->GetBrowsingContext()->Top()) { + if (dom::BrowserParent* bp = cbc->GetBrowserParent()) { + DocAccessibleParent* topDoc = bp->GetTopLevelDocAccessible(); + if (topDoc && topDoc->mCachedFields) { + auto appUnitsPerDevPixel = topDoc->mCachedFields->GetAttribute<int32_t>( + CacheKey::AppUnitsPerDevPixel); + MOZ_ASSERT(appUnitsPerDevPixel); + return LayoutDeviceIntRect::ToAppUnits(Bounds(), *appUnitsPerDevPixel); + } + } + } + return LayoutDeviceIntRect::ToAppUnits(Bounds(), AppUnitsPerCSSPixel()); +} + +bool RemoteAccessible::IsFixedPos() const { + MOZ_ASSERT(mCachedFields); + if (auto maybePosition = + mCachedFields->GetAttribute<RefPtr<nsAtom>>(CacheKey::CssPosition)) { + return *maybePosition == nsGkAtoms::fixed; + } + + return false; +} + +bool RemoteAccessible::IsOverflowHidden() const { + MOZ_ASSERT(mCachedFields); + if (auto maybeOverflow = + mCachedFields->GetAttribute<RefPtr<nsAtom>>(CacheKey::CSSOverflow)) { + return *maybeOverflow == nsGkAtoms::hidden; + } + + return false; +} + +bool RemoteAccessible::IsClipped() const { + MOZ_ASSERT(mCachedFields); + if (mCachedFields->GetAttribute<bool>(CacheKey::IsClipped)) { + return true; + } + + return false; +} + +LayoutDeviceIntRect RemoteAccessible::BoundsWithOffset( + Maybe<nsRect> aOffset, bool aBoundsAreForHittesting) const { + Maybe<nsRect> maybeBounds = RetrieveCachedBounds(); + if (maybeBounds) { + nsRect bounds = *maybeBounds; + // maybeBounds is parent-relative. However, the transform matrix we cache + // (if any) is meant to operate on self-relative rects. Therefore, make + // bounds self-relative until after we transform. + bounds.MoveTo(0, 0); + const DocAccessibleParent* topDoc = IsDoc() ? AsDoc() : nullptr; + + if (aOffset.isSome()) { + // The rect we've passed in is in app units, so no conversion needed. + nsRect internalRect = *aOffset; + bounds.SetRectX(bounds.x + internalRect.x, internalRect.width); + bounds.SetRectY(bounds.y + internalRect.y, internalRect.height); + } + + Unused << ApplyTransform(bounds); + // Now apply the parent-relative offset. + bounds.MoveBy(maybeBounds->TopLeft()); + + ApplyCrossDocOffset(bounds); + + LayoutDeviceIntRect devPxBounds; + const Accessible* acc = Parent(); + bool encounteredFixedContainer = IsFixedPos(); + while (acc && acc->IsRemote()) { + // Return early if we're hit testing and our cumulative bounds are empty, + // since walking the ancestor chain won't produce any hits. + if (aBoundsAreForHittesting && bounds.IsEmpty()) { + return LayoutDeviceIntRect{}; + } + + RemoteAccessible* remoteAcc = const_cast<Accessible*>(acc)->AsRemote(); + + if (Maybe<nsRect> maybeRemoteBounds = remoteAcc->RetrieveCachedBounds()) { + nsRect remoteBounds = *maybeRemoteBounds; + // We need to take into account a non-1 resolution set on the + // presshell. This happens with async pinch zooming, among other + // things. We can't reliably query this value in the parent process, + // so we retrieve it from the document's cache. + if (remoteAcc->IsDoc()) { + // Apply the document's resolution to the bounds we've gathered + // thus far. We do this before applying the document's offset + // because document accs should not have their bounds scaled by + // their own resolution. They should be scaled by the resolution + // of their containing document (if any). + Maybe<float> res = + remoteAcc->AsDoc()->mCachedFields->GetAttribute<float>( + CacheKey::Resolution); + MOZ_ASSERT(res, "No cached document resolution found."); + bounds.ScaleRoundOut(res.valueOr(1.0f)); + + topDoc = remoteAcc->AsDoc(); + } + + // We don't account for the document offset of iframes when + // computing parent-relative bounds. Instead, we store this value + // separately on all iframes and apply it here. See the comments in + // LocalAccessible::BundleFieldsForCache where we set the + // nsGkAtoms::crossorigin attribute. + remoteAcc->ApplyCrossDocOffset(remoteBounds); + if (!encounteredFixedContainer) { + // Apply scroll offset, if applicable. Only the contents of an + // element are affected by its scroll offset, which is why this call + // happens in this loop instead of both inside and outside of + // the loop (like ApplyTransform). + // Never apply scroll offsets past a fixed container. + const bool hasScrollArea = remoteAcc->ApplyScrollOffset(bounds); + + // If we are hit testing and the Accessible has a scroll area, ensure + // that the bounds we've calculated so far are constrained to the + // bounds of the scroll area. Without this, we'll "hit" the off-screen + // portions of accs that are are partially (but not fully) within the + // scroll area. This is also a problem for accs with overflow:hidden; + if (aBoundsAreForHittesting && + (hasScrollArea || remoteAcc->IsOverflowHidden())) { + nsRect selfRelativeVisibleBounds(0, 0, remoteBounds.width, + remoteBounds.height); + bounds = bounds.SafeIntersect(selfRelativeVisibleBounds); + } + } + if (remoteAcc->IsDoc()) { + // Fixed elements are document relative, so if we've hit a + // document we're now subject to that document's styling + // (including scroll offsets that operate on it). + // This ordering is important, we don't want to apply scroll + // offsets on this doc's content. + encounteredFixedContainer = false; + } + if (!encounteredFixedContainer) { + // The transform matrix we cache (if any) is meant to operate on + // self-relative rects. Therefore, we must apply the transform before + // we make bounds parent-relative. + Unused << remoteAcc->ApplyTransform(bounds); + // Regardless of whether this is a doc, we should offset `bounds` + // by the bounds retrieved here. This is how we build screen + // coordinates from relative coordinates. + bounds.MoveBy(remoteBounds.X(), remoteBounds.Y()); + } + + if (remoteAcc->IsFixedPos()) { + encounteredFixedContainer = true; + } + // we can't just break here if we're scroll suppressed because we still + // need to find the top doc. + } + acc = acc->Parent(); + } + + MOZ_ASSERT(topDoc); + if (topDoc) { + // We use the top documents app-units-per-dev-pixel even though + // theoretically nested docs can have different values. Practically, + // that isn't likely since we only offer zoom controls for the top + // document and all subdocuments inherit from it. + auto appUnitsPerDevPixel = topDoc->mCachedFields->GetAttribute<int32_t>( + CacheKey::AppUnitsPerDevPixel); + MOZ_ASSERT(appUnitsPerDevPixel); + if (appUnitsPerDevPixel) { + // Convert our existing `bounds` rect from app units to dev pixels + devPxBounds = LayoutDeviceIntRect::FromAppUnitsToNearest( + bounds, *appUnitsPerDevPixel); + } + } + +#if !defined(ANDROID) + // This block is not thread safe because it queries a LocalAccessible. + // It is also not needed in Android since the only local accessible is + // the outer doc browser that has an offset of 0. + // acc could be null if the OuterDocAccessible died before the top level + // DocAccessibleParent. + if (LocalAccessible* localAcc = + acc ? const_cast<Accessible*>(acc)->AsLocal() : nullptr) { + // LocalAccessible::Bounds returns screen-relative bounds in + // dev pixels. + LayoutDeviceIntRect localBounds = localAcc->Bounds(); + + // The root document will always have an APZ resolution of 1, + // so we don't factor in its scale here. We also don't scale + // by GetFullZoom because LocalAccessible::Bounds already does + // that. + devPxBounds.MoveBy(localBounds.X(), localBounds.Y()); + } +#endif + + return devPxBounds; + } + + return LayoutDeviceIntRect(); +} + +LayoutDeviceIntRect RemoteAccessible::Bounds() const { + return BoundsWithOffset(Nothing()); +} + +Relation RemoteAccessible::RelationByType(RelationType aType) const { + // We are able to handle some relations completely in the + // parent process, without the help of the cache. Those + // relations are enumerated here. Other relations, whose + // types are stored in kRelationTypeAtoms, are processed + // below using the cache. + if (aType == RelationType::CONTAINING_TAB_PANE) { + if (dom::CanonicalBrowsingContext* cbc = mDoc->GetBrowsingContext()) { + if (dom::CanonicalBrowsingContext* topCbc = cbc->Top()) { + if (dom::BrowserParent* bp = topCbc->GetBrowserParent()) { + return Relation(bp->GetTopLevelDocAccessible()); + } + } + } + return Relation(); + } + + if (aType == RelationType::LINKS_TO && Role() == roles::LINK) { + Pivot p = Pivot(mDoc); + nsString href; + Value(href); + int32_t i = href.FindChar('#'); + int32_t len = static_cast<int32_t>(href.Length()); + if (i != -1 && i < (len - 1)) { + nsDependentSubstring anchorName = Substring(href, i + 1, len); + MustPruneSameDocRule rule; + Accessible* nameMatch = nullptr; + for (Accessible* match = p.Next(mDoc, rule); match; + match = p.Next(match, rule)) { + nsString currID; + match->DOMNodeID(currID); + MOZ_ASSERT(match->IsRemote()); + if (anchorName.Equals(currID)) { + return Relation(match->AsRemote()); + } + if (!nameMatch) { + nsString currName = match->AsRemote()->GetCachedHTMLNameAttribute(); + if (match->TagName() == nsGkAtoms::a && anchorName.Equals(currName)) { + // If we find an element with a matching ID, we should return + // that, but if we don't we should return the first anchor with + // a matching name. To avoid doing two traversals, store the first + // name match here. + nameMatch = match; + } + } + } + return nameMatch ? Relation(nameMatch->AsRemote()) : Relation(); + } + + return Relation(); + } + + // Handle ARIA tree, treegrid parent/child relations. Each of these cases + // relies on cached group info. To find the parent of an accessible, use the + // unified conceptual parent. + if (aType == RelationType::NODE_CHILD_OF) { + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (roleMapEntry && (roleMapEntry->role == roles::OUTLINEITEM || + roleMapEntry->role == roles::LISTITEM || + roleMapEntry->role == roles::ROW)) { + if (const AccGroupInfo* groupInfo = + const_cast<RemoteAccessible*>(this)->GetOrCreateGroupInfo()) { + return Relation(groupInfo->ConceptualParent()); + } + } + return Relation(); + } + + // To find the children of a parent, provide an iterator through its items. + if (aType == RelationType::NODE_PARENT_OF) { + const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); + if (roleMapEntry && (roleMapEntry->role == roles::OUTLINEITEM || + roleMapEntry->role == roles::LISTITEM || + roleMapEntry->role == roles::ROW || + roleMapEntry->role == roles::OUTLINE || + roleMapEntry->role == roles::LIST || + roleMapEntry->role == roles::TREE_TABLE)) { + return Relation(new ItemIterator(this)); + } + return Relation(); + } + + if (aType == RelationType::MEMBER_OF) { + Relation rel = Relation(); + // HTML radio buttons with cached names should be grouped. + if (IsHTMLRadioButton()) { + nsString name = GetCachedHTMLNameAttribute(); + if (name.IsEmpty()) { + return rel; + } + + RemoteAccessible* ancestor = RemoteParent(); + while (ancestor && ancestor->Role() != roles::FORM && ancestor != mDoc) { + ancestor = ancestor->RemoteParent(); + } + if (ancestor) { + // Sometimes we end up with an unparented acc here, potentially + // because the acc is being moved. See bug 1807639. + // Pivot expects to be created with a non-null mRoot. + Pivot p = Pivot(ancestor); + PivotRadioNameRule rule(name); + Accessible* match = p.Next(ancestor, rule); + while (match) { + rel.AppendTarget(match->AsRemote()); + match = p.Next(match, rule); + } + } + return rel; + } + + if (IsARIARole(nsGkAtoms::radio)) { + // ARIA radio buttons should be grouped by their radio group + // parent, if one exists. + RemoteAccessible* currParent = RemoteParent(); + while (currParent && currParent->Role() != roles::RADIO_GROUP) { + currParent = currParent->RemoteParent(); + } + + if (currParent && currParent->Role() == roles::RADIO_GROUP) { + // If we found a radiogroup parent, search for all + // roles::RADIOBUTTON children and add them to our relation. + // This search will include the radio button this method + // was called from, which is expected. + Pivot p = Pivot(currParent); + PivotRoleRule rule(roles::RADIOBUTTON); + Accessible* match = p.Next(currParent, rule); + while (match) { + MOZ_ASSERT(match->IsRemote(), + "We should only be traversing the remote tree."); + rel.AppendTarget(match->AsRemote()); + match = p.Next(match, rule); + } + } + } + // By webkit's standard, aria radio buttons do not get grouped + // if they lack a group parent, so we return an empty + // relation here if the above check fails. + return rel; + } + + Relation rel; + if (!mCachedFields) { + return rel; + } + + for (const auto& data : kRelationTypeAtoms) { + if (data.mType != aType || + (data.mValidTag && TagName() != data.mValidTag)) { + continue; + } + + if (auto maybeIds = + mCachedFields->GetAttribute<nsTArray<uint64_t>>(data.mAtom)) { + rel.AppendIter(new RemoteAccIterator(*maybeIds, Document())); + } + // Each relation type has only one relevant cached attribute, + // so break after we've handled the attr for this type, + // even if we didn't find any targets. + break; + } + + if (auto accRelMapEntry = mDoc->mReverseRelations.Lookup(ID())) { + if (auto reverseIdsEntry = accRelMapEntry.Data().Lookup(aType)) { + rel.AppendIter(new RemoteAccIterator(reverseIdsEntry.Data(), Document())); + } + } + + // We handle these relations here rather than before cached relations because + // the cached relations need to take precedence. For example, a <figure> with + // both aria-labelledby and a <figcaption> must return two LABELLED_BY + // targets: the aria-labelledby and then the <figcaption>. + if (aType == RelationType::LABELLED_BY && TagName() == nsGkAtoms::figure) { + uint32_t count = ChildCount(); + for (uint32_t c = 0; c < count; ++c) { + RemoteAccessible* child = RemoteChildAt(c); + MOZ_ASSERT(child); + if (child->TagName() == nsGkAtoms::figcaption) { + rel.AppendTarget(child); + } + } + } else if (aType == RelationType::LABEL_FOR && + TagName() == nsGkAtoms::figcaption) { + if (RemoteAccessible* parent = RemoteParent()) { + if (parent->TagName() == nsGkAtoms::figure) { + rel.AppendTarget(parent); + } + } + } + + return rel; +} + +void RemoteAccessible::AppendTextTo(nsAString& aText, uint32_t aStartOffset, + uint32_t aLength) { + if (IsText()) { + if (mCachedFields) { + if (auto text = mCachedFields->GetAttribute<nsString>(CacheKey::Text)) { + aText.Append(Substring(*text, aStartOffset, aLength)); + } + VERIFY_CACHE(CacheDomain::Text); + } + return; + } + + if (aStartOffset != 0 || aLength == 0) { + return; + } + + if (IsHTMLBr()) { + aText += kForcedNewLineChar; + } else if (RemoteParent() && nsAccUtils::MustPrune(RemoteParent())) { + // Expose the embedded object accessible as imaginary embedded object + // character if its parent hypertext accessible doesn't expose children to + // AT. + aText += kImaginaryEmbeddedObjectChar; + } else { + aText += kEmbeddedObjectChar; + } +} + +nsTArray<bool> RemoteAccessible::PreProcessRelations(AccAttributes* aFields) { + nsTArray<bool> updateTracker(ArrayLength(kRelationTypeAtoms)); + for (auto const& data : kRelationTypeAtoms) { + if (data.mValidTag) { + // The relation we're currently processing only applies to particular + // elements. Check to see if we're one of them. + nsAtom* tag = TagName(); + if (!tag) { + // TagName() returns null on an initial cache push -- check aFields + // for a tag name instead. + if (auto maybeTag = + aFields->GetAttribute<RefPtr<nsAtom>>(CacheKey::TagName)) { + tag = *maybeTag; + } + } + MOZ_ASSERT( + tag || IsTextLeaf() || IsDoc(), + "Could not fetch tag via TagName() or from initial cache push!"); + if (tag != data.mValidTag) { + // If this rel doesn't apply to us, do no pre-processing. Also, + // note in our updateTracker that we should do no post-processing. + updateTracker.AppendElement(false); + continue; + } + } + + nsStaticAtom* const relAtom = data.mAtom; + auto newRelationTargets = + aFields->GetAttribute<nsTArray<uint64_t>>(relAtom); + bool shouldAddNewImplicitRels = + newRelationTargets && newRelationTargets->Length(); + + // Remove existing implicit relations if we need to perform an update, or + // if we've received a DeleteEntry(). Only do this if mCachedFields is + // initialized. If mCachedFields is not initialized, we still need to + // construct the update array so we correctly handle reverse rels in + // PostProcessRelations. + if ((shouldAddNewImplicitRels || + aFields->GetAttribute<DeleteEntry>(relAtom)) && + mCachedFields) { + if (auto maybeOldIDs = + mCachedFields->GetAttribute<nsTArray<uint64_t>>(relAtom)) { + for (uint64_t id : *maybeOldIDs) { + // For each target, fetch its reverse relation map + // We need to call `Lookup` here instead of `LookupOrInsert` because + // it's possible the ID we're querying is from an acc that has since + // been Shutdown(), and so has intentionally removed its reverse rels + // from the doc's reverse rel cache. + if (auto reverseRels = Document()->mReverseRelations.Lookup(id)) { + // Then fetch its reverse relation's ID list. This should be safe + // to do via LookupOrInsert because by the time we've gotten here, + // we know the acc and `this` are still alive in the doc. If we hit + // the following assert, we don't have parity on implicit/explicit + // rels and something is wrong. + nsTArray<uint64_t>& reverseRelIDs = + reverseRels->LookupOrInsert(data.mReverseType); + // There might be other reverse relations stored for this acc, so + // remove our ID instead of deleting the array entirely. + DebugOnly<bool> removed = reverseRelIDs.RemoveElement(ID()); + MOZ_ASSERT(removed, "Can't find old reverse relation"); + } + } + } + } + + updateTracker.AppendElement(shouldAddNewImplicitRels); + } + + return updateTracker; +} + +void RemoteAccessible::PostProcessRelations(const nsTArray<bool>& aToUpdate) { + size_t updateCount = aToUpdate.Length(); + MOZ_ASSERT(updateCount == ArrayLength(kRelationTypeAtoms), + "Did not note update status for every relation type!"); + for (size_t i = 0; i < updateCount; i++) { + if (aToUpdate.ElementAt(i)) { + // Since kRelationTypeAtoms was used to generate aToUpdate, we + // know the ith entry of aToUpdate corresponds to the relation type in + // the ith entry of kRelationTypeAtoms. Fetch the related data here. + auto const& data = kRelationTypeAtoms[i]; + + const nsTArray<uint64_t>& newIDs = + *mCachedFields->GetAttribute<nsTArray<uint64_t>>(data.mAtom); + for (uint64_t id : newIDs) { + nsTHashMap<RelationType, nsTArray<uint64_t>>& relations = + Document()->mReverseRelations.LookupOrInsert(id); + nsTArray<uint64_t>& ids = relations.LookupOrInsert(data.mReverseType); + ids.AppendElement(ID()); + } + } + } +} + +void RemoteAccessible::PruneRelationsOnShutdown() { + auto reverseRels = mDoc->mReverseRelations.Lookup(ID()); + if (!reverseRels) { + return; + } + for (auto const& data : kRelationTypeAtoms) { + // Fetch the list of targets for this reverse relation + auto reverseTargetList = reverseRels->Lookup(data.mReverseType); + if (!reverseTargetList) { + continue; + } + for (uint64_t id : *reverseTargetList) { + // For each target, retrieve its corresponding forward relation target + // list + RemoteAccessible* affectedAcc = mDoc->GetAccessible(id); + if (!affectedAcc) { + // It's possible the affect acc also shut down, in which case + // we don't have anything to update. + continue; + } + if (auto forwardTargetList = + affectedAcc->mCachedFields + ->GetMutableAttribute<nsTArray<uint64_t>>(data.mAtom)) { + forwardTargetList->RemoveElement(ID()); + if (!forwardTargetList->Length()) { + // The ID we removed was the only thing in the list, so remove the + // entry from the cache entirely -- don't leave an empty array. + affectedAcc->mCachedFields->Remove(data.mAtom); + } + } + } + } + // Remove this ID from the document's map of reverse relations. + reverseRels.Remove(); +} + +uint32_t RemoteAccessible::GetCachedTextLength() { + MOZ_ASSERT(!HasChildren()); + if (!mCachedFields) { + return 0; + } + VERIFY_CACHE(CacheDomain::Text); + auto text = mCachedFields->GetAttribute<nsString>(CacheKey::Text); + if (!text) { + return 0; + } + return text->Length(); +} + +Maybe<const nsTArray<int32_t>&> RemoteAccessible::GetCachedTextLines() { + MOZ_ASSERT(!HasChildren()); + if (!mCachedFields) { + return Nothing(); + } + VERIFY_CACHE(CacheDomain::Text); + return mCachedFields->GetAttribute<nsTArray<int32_t>>( + CacheKey::TextLineStarts); +} + +nsRect RemoteAccessible::GetCachedCharRect(int32_t aOffset) { + MOZ_ASSERT(IsText()); + if (!mCachedFields) { + return nsRect(); + } + + if (Maybe<const nsTArray<int32_t>&> maybeCharData = + mCachedFields->GetAttribute<nsTArray<int32_t>>( + CacheKey::TextBounds)) { + const nsTArray<int32_t>& charData = *maybeCharData; + const int32_t index = aOffset * kNumbersInRect; + if (index < static_cast<int32_t>(charData.Length())) { + return nsRect(charData[index], charData[index + 1], charData[index + 2], + charData[index + 3]); + } + // It is valid for a client to call this with an offset 1 after the last + // character because of the insertion point at the end of text boxes. + MOZ_ASSERT(index == static_cast<int32_t>(charData.Length())); + } + + return nsRect(); +} + +void RemoteAccessible::DOMNodeID(nsString& aID) const { + if (mCachedFields) { + mCachedFields->GetAttribute(CacheKey::DOMNodeID, aID); + VERIFY_CACHE(CacheDomain::DOMNodeIDAndClass); + } +} + +void RemoteAccessible::ScrollToPoint(uint32_t aScrollType, int32_t aX, + int32_t aY) { + Unused << mDoc->SendScrollToPoint(mID, aScrollType, aX, aY); +} + +#if !defined(XP_WIN) +void RemoteAccessible::Announce(const nsString& aAnnouncement, + uint16_t aPriority) { + Unused << mDoc->SendAnnounce(mID, aAnnouncement, aPriority); +} +#endif // !defined(XP_WIN) + +void RemoteAccessible::ScrollSubstringToPoint(int32_t aStartOffset, + int32_t aEndOffset, + uint32_t aCoordinateType, + int32_t aX, int32_t aY) { + Unused << mDoc->SendScrollSubstringToPoint(mID, aStartOffset, aEndOffset, + aCoordinateType, aX, aY); +} + +RefPtr<const AccAttributes> RemoteAccessible::GetCachedTextAttributes() { + MOZ_ASSERT(IsText() || IsHyperText()); + if (mCachedFields) { + auto attrs = mCachedFields->GetAttributeRefPtr<AccAttributes>( + CacheKey::TextAttributes); + VERIFY_CACHE(CacheDomain::Text); + return attrs; + } + return nullptr; +} + +already_AddRefed<AccAttributes> RemoteAccessible::DefaultTextAttributes() { + RefPtr<const AccAttributes> attrs = GetCachedTextAttributes(); + RefPtr<AccAttributes> result = new AccAttributes(); + if (attrs) { + attrs->CopyTo(result); + } + return result.forget(); +} + +RefPtr<const AccAttributes> RemoteAccessible::GetCachedARIAAttributes() const { + if (mCachedFields) { + auto attrs = mCachedFields->GetAttributeRefPtr<AccAttributes>( + CacheKey::ARIAAttributes); + VERIFY_CACHE(CacheDomain::ARIA); + return attrs; + } + return nullptr; +} + +nsString RemoteAccessible::GetCachedHTMLNameAttribute() const { + if (mCachedFields) { + if (auto maybeName = + mCachedFields->GetAttribute<nsString>(CacheKey::DOMName)) { + return *maybeName; + } + } + return nsString(); +} + +uint64_t RemoteAccessible::State() { + uint64_t state = 0; + if (mCachedFields) { + if (auto rawState = + mCachedFields->GetAttribute<uint64_t>(CacheKey::State)) { + VERIFY_CACHE(CacheDomain::State); + state = *rawState; + // Handle states that are derived from other states. + if (!(state & states::UNAVAILABLE)) { + state |= states::ENABLED | states::SENSITIVE; + } + if (state & states::EXPANDABLE && !(state & states::EXPANDED)) { + state |= states::COLLAPSED; + } + } + + ApplyImplicitState(state); + + auto* cbc = mDoc->GetBrowsingContext(); + if (cbc && !cbc->IsActive()) { + // If our browsing context is _not_ active, we're in a background tab + // and inherently offscreen. + state |= states::OFFSCREEN; + } else { + // If we're in an active browsing context, there are a few scenarios we + // need to address: + // - We are an iframe document in the visual viewport + // - We are an iframe document out of the visual viewport + // - We are non-iframe content in the visual viewport + // - We are non-iframe content out of the visual viewport + // We assume top level tab docs are on screen if their BC is active, so + // we don't need additional handling for them here. + if (!mDoc->IsTopLevel()) { + // Here we handle iframes and iframe content. + // We use an iframe's outer doc's position in the embedding document's + // viewport to determine if the iframe has been scrolled offscreen. + Accessible* docParent = mDoc->Parent(); + // In rare cases, we might not have an outer doc yet. Return if that's + // the case. + if (NS_WARN_IF(!docParent || !docParent->IsRemote())) { + return state; + } + + RemoteAccessible* outerDoc = docParent->AsRemote(); + DocAccessibleParent* embeddingDocument = outerDoc->Document(); + if (embeddingDocument && + !embeddingDocument->mOnScreenAccessibles.Contains(outerDoc->ID())) { + // Our embedding document's viewport cache doesn't contain the ID of + // our outer doc, so this iframe (and any of its content) is + // offscreen. + state |= states::OFFSCREEN; + } else if (this != mDoc && !mDoc->mOnScreenAccessibles.Contains(ID())) { + // Our embedding document's viewport cache contains the ID of our + // outer doc, but the iframe's viewport cache doesn't contain our ID. + // We are offscreen. + state |= states::OFFSCREEN; + } + } else if (this != mDoc && !mDoc->mOnScreenAccessibles.Contains(ID())) { + // We are top level tab content (but not a top level tab doc). + // If our tab doc's viewport cache doesn't contain our ID, we're + // offscreen. + state |= states::OFFSCREEN; + } + } + } + + return state; +} + +already_AddRefed<AccAttributes> RemoteAccessible::Attributes() { + RefPtr<AccAttributes> attributes = new AccAttributes(); + nsAccessibilityService* accService = GetAccService(); + if (!accService) { + // The service can be shut down before RemoteAccessibles. If it is shut + // down, we can't calculate some attributes. We're about to die anyway. + return attributes.forget(); + } + + if (mCachedFields) { + // We use GetAttribute instead of GetAttributeRefPtr because we need + // nsAtom, not const nsAtom. + if (auto tag = + mCachedFields->GetAttribute<RefPtr<nsAtom>>(CacheKey::TagName)) { + attributes->SetAttribute(nsGkAtoms::tag, *tag); + } + + GroupPos groupPos = GroupPosition(); + nsAccUtils::SetAccGroupAttrs(attributes, groupPos.level, groupPos.setSize, + groupPos.posInSet); + + bool hierarchical = false; + uint32_t itemCount = AccGroupInfo::TotalItemCount(this, &hierarchical); + if (itemCount) { + attributes->SetAttribute(nsGkAtoms::child_item_count, + static_cast<int32_t>(itemCount)); + } + + if (hierarchical) { + attributes->SetAttribute(nsGkAtoms::tree, true); + } + + if (auto inputType = + mCachedFields->GetAttribute<RefPtr<nsAtom>>(CacheKey::InputType)) { + attributes->SetAttribute(nsGkAtoms::textInputType, *inputType); + } + + if (RefPtr<nsAtom> display = DisplayStyle()) { + attributes->SetAttribute(nsGkAtoms::display, display); + } + + if (TableCellAccessible* cell = AsTableCell()) { + TableAccessible* table = cell->Table(); + uint32_t row = cell->RowIdx(); + uint32_t col = cell->ColIdx(); + int32_t cellIdx = table->CellIndexAt(row, col); + if (cellIdx != -1) { + attributes->SetAttribute(nsGkAtoms::tableCellIndex, cellIdx); + } + } + + if (bool layoutGuess = TableIsProbablyForLayout()) { + attributes->SetAttribute(nsGkAtoms::layout_guess, layoutGuess); + } + + accService->MarkupAttributes(this, attributes); + + const nsRoleMapEntry* roleMap = ARIARoleMap(); + nsAutoString role; + mCachedFields->GetAttribute(CacheKey::ARIARole, role); + if (role.IsEmpty()) { + if (roleMap && roleMap->roleAtom != nsGkAtoms::_empty) { + // Single, known role. + attributes->SetAttribute(nsGkAtoms::xmlroles, roleMap->roleAtom); + } else if (nsAtom* landmark = LandmarkRole()) { + // Landmark role from markup; e.g. HTML <main>. + attributes->SetAttribute(nsGkAtoms::xmlroles, landmark); + } + } else { + // Unknown role or multiple roles. + attributes->SetAttribute(nsGkAtoms::xmlroles, std::move(role)); + } + + if (roleMap) { + nsAutoString live; + if (nsAccUtils::GetLiveAttrValue(roleMap->liveAttRule, live)) { + attributes->SetAttribute(nsGkAtoms::aria_live, std::move(live)); + } + } + + if (auto ariaAttrs = GetCachedARIAAttributes()) { + ariaAttrs->CopyTo(attributes); + } + + nsAccUtils::SetLiveContainerAttributes(attributes, this); + + nsString id; + DOMNodeID(id); + if (!id.IsEmpty()) { + attributes->SetAttribute(nsGkAtoms::id, std::move(id)); + } + + nsString className; + mCachedFields->GetAttribute(CacheKey::DOMNodeClass, className); + if (!className.IsEmpty()) { + attributes->SetAttribute(nsGkAtoms::_class, std::move(className)); + } + + if (IsImage()) { + nsString src; + mCachedFields->GetAttribute(CacheKey::SrcURL, src); + if (!src.IsEmpty()) { + attributes->SetAttribute(nsGkAtoms::src, std::move(src)); + } + } + + if (IsTextField()) { + nsString placeholder; + mCachedFields->GetAttribute(CacheKey::HTMLPlaceholder, placeholder); + if (!placeholder.IsEmpty()) { + attributes->SetAttribute(nsGkAtoms::placeholder, + std::move(placeholder)); + attributes->Remove(nsGkAtoms::aria_placeholder); + } + } + + nsString popupType; + mCachedFields->GetAttribute(CacheKey::PopupType, popupType); + if (!popupType.IsEmpty()) { + attributes->SetAttribute(nsGkAtoms::ispopup, std::move(popupType)); + } + } + + nsAutoString name; + if (Name(name) != eNameFromSubtree && !name.IsVoid()) { + attributes->SetAttribute(nsGkAtoms::explicit_name, true); + } + + // Expose the string value via the valuetext attribute. We test for the value + // interface because we don't want to expose traditional Value() information + // such as URLs on links and documents, or text in an input. + // XXX This is only needed for ATK, since other APIs have native ways to + // retrieve value text. We should probably move this into ATK specific code. + // For now, we do this because LocalAccessible does it. + if (HasNumericValue()) { + nsString valuetext; + Value(valuetext); + attributes->SetAttribute(nsGkAtoms::aria_valuetext, std::move(valuetext)); + } + + return attributes.forget(); +} + +nsAtom* RemoteAccessible::TagName() const { + if (mCachedFields) { + if (auto tag = + mCachedFields->GetAttribute<RefPtr<nsAtom>>(CacheKey::TagName)) { + return *tag; + } + } + + return nullptr; +} + +already_AddRefed<nsAtom> RemoteAccessible::InputType() const { + if (mCachedFields) { + if (auto inputType = + mCachedFields->GetAttribute<RefPtr<nsAtom>>(CacheKey::InputType)) { + RefPtr<nsAtom> result = *inputType; + return result.forget(); + } + } + + return nullptr; +} + +already_AddRefed<nsAtom> RemoteAccessible::DisplayStyle() const { + if (mCachedFields) { + if (auto display = + mCachedFields->GetAttribute<RefPtr<nsAtom>>(CacheKey::CSSDisplay)) { + RefPtr<nsAtom> result = *display; + return result.forget(); + } + } + return nullptr; +} + +float RemoteAccessible::Opacity() const { + if (mCachedFields) { + if (auto opacity = mCachedFields->GetAttribute<float>(CacheKey::Opacity)) { + return *opacity; + } + } + + return 1.0f; +} + +void RemoteAccessible::LiveRegionAttributes(nsAString* aLive, + nsAString* aRelevant, + Maybe<bool>* aAtomic, + nsAString* aBusy) const { + if (!mCachedFields) { + return; + } + RefPtr<const AccAttributes> attrs = GetCachedARIAAttributes(); + if (!attrs) { + return; + } + if (aLive) { + attrs->GetAttribute(nsGkAtoms::aria_live, *aLive); + } + if (aRelevant) { + attrs->GetAttribute(nsGkAtoms::aria_relevant, *aRelevant); + } + if (aAtomic) { + if (auto value = + attrs->GetAttribute<RefPtr<nsAtom>>(nsGkAtoms::aria_atomic)) { + *aAtomic = Some(*value == nsGkAtoms::_true); + } + } + if (aBusy) { + attrs->GetAttribute(nsGkAtoms::aria_busy, *aBusy); + } +} + +Maybe<bool> RemoteAccessible::ARIASelected() const { + if (mCachedFields) { + return mCachedFields->GetAttribute<bool>(CacheKey::ARIASelected); + } + return Nothing(); +} + +nsAtom* RemoteAccessible::GetPrimaryAction() const { + if (mCachedFields) { + if (auto action = mCachedFields->GetAttribute<RefPtr<nsAtom>>( + CacheKey::PrimaryAction)) { + return *action; + } + } + + return nullptr; +} + +uint8_t RemoteAccessible::ActionCount() const { + uint8_t actionCount = 0; + if (mCachedFields) { + if (HasPrimaryAction() || ActionAncestor()) { + actionCount++; + } + + if (mCachedFields->HasAttribute(CacheKey::HasLongdesc)) { + actionCount++; + } + VERIFY_CACHE(CacheDomain::Actions); + } + + return actionCount; +} + +void RemoteAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) { + if (mCachedFields) { + aName.Truncate(); + nsAtom* action = GetPrimaryAction(); + bool hasActionAncestor = !action && ActionAncestor(); + + switch (aIndex) { + case 0: + if (action) { + action->ToString(aName); + } else if (hasActionAncestor) { + aName.AssignLiteral("click ancestor"); + } else if (mCachedFields->HasAttribute(CacheKey::HasLongdesc)) { + aName.AssignLiteral("showlongdesc"); + } + break; + case 1: + if ((action || hasActionAncestor) && + mCachedFields->HasAttribute(CacheKey::HasLongdesc)) { + aName.AssignLiteral("showlongdesc"); + } + break; + default: + break; + } + } + VERIFY_CACHE(CacheDomain::Actions); +} + +bool RemoteAccessible::DoAction(uint8_t aIndex) const { + if (ActionCount() < aIndex + 1) { + return false; + } + + Unused << mDoc->SendDoActionAsync(mID, aIndex); + return true; +} + +KeyBinding RemoteAccessible::AccessKey() const { + if (mCachedFields) { + if (auto value = + mCachedFields->GetAttribute<uint64_t>(CacheKey::AccessKey)) { + return KeyBinding(*value); + } + } + return KeyBinding(); +} + +void RemoteAccessible::SelectionRanges(nsTArray<TextRange>* aRanges) const { + Document()->SelectionRanges(aRanges); +} + +bool RemoteAccessible::RemoveFromSelection(int32_t aSelectionNum) { + MOZ_ASSERT(IsHyperText()); + if (SelectionCount() <= aSelectionNum) { + return false; + } + + Unused << mDoc->SendRemoveTextSelection(mID, aSelectionNum); + + return true; +} + +void RemoteAccessible::ARIAGroupPosition(int32_t* aLevel, int32_t* aSetSize, + int32_t* aPosInSet) const { + if (!mCachedFields) { + return; + } + + if (aLevel) { + if (auto level = + mCachedFields->GetAttribute<int32_t>(nsGkAtoms::aria_level)) { + *aLevel = *level; + } + } + if (aSetSize) { + if (auto setsize = + mCachedFields->GetAttribute<int32_t>(nsGkAtoms::aria_setsize)) { + *aSetSize = *setsize; + } + } + if (aPosInSet) { + if (auto posinset = + mCachedFields->GetAttribute<int32_t>(nsGkAtoms::aria_posinset)) { + *aPosInSet = *posinset; + } + } +} + +AccGroupInfo* RemoteAccessible::GetGroupInfo() const { + if (!mCachedFields) { + return nullptr; + } + + if (auto groupInfo = mCachedFields->GetAttribute<UniquePtr<AccGroupInfo>>( + CacheKey::GroupInfo)) { + return groupInfo->get(); + } + + return nullptr; +} + +AccGroupInfo* RemoteAccessible::GetOrCreateGroupInfo() { + AccGroupInfo* groupInfo = GetGroupInfo(); + if (groupInfo) { + return groupInfo; + } + + groupInfo = AccGroupInfo::CreateGroupInfo(this); + if (groupInfo) { + if (!mCachedFields) { + mCachedFields = new AccAttributes(); + } + + mCachedFields->SetAttribute(CacheKey::GroupInfo, groupInfo); + } + + return groupInfo; +} + +void RemoteAccessible::InvalidateGroupInfo() { + if (mCachedFields) { + mCachedFields->Remove(CacheKey::GroupInfo); + } +} + +void RemoteAccessible::GetPositionAndSetSize(int32_t* aPosInSet, + int32_t* aSetSize) { + if (IsHTMLRadioButton()) { + *aSetSize = 0; + Relation rel = RelationByType(RelationType::MEMBER_OF); + while (Accessible* radio = rel.Next()) { + ++*aSetSize; + if (radio == this) { + *aPosInSet = *aSetSize; + } + } + return; + } + + Accessible::GetPositionAndSetSize(aPosInSet, aSetSize); +} + +bool RemoteAccessible::HasPrimaryAction() const { + return mCachedFields && mCachedFields->HasAttribute(CacheKey::PrimaryAction); +} + +void RemoteAccessible::TakeFocus() const { Unused << mDoc->SendTakeFocus(mID); } + +void RemoteAccessible::ScrollTo(uint32_t aHow) const { + Unused << mDoc->SendScrollTo(mID, aHow); +} + +//////////////////////////////////////////////////////////////////////////////// +// SelectAccessible + +void RemoteAccessible::SelectedItems(nsTArray<Accessible*>* aItems) { + Pivot p = Pivot(this); + PivotStateRule rule(states::SELECTED); + for (Accessible* selected = p.First(rule); selected; + selected = p.Next(selected, rule)) { + aItems->AppendElement(selected); + } +} + +uint32_t RemoteAccessible::SelectedItemCount() { + uint32_t count = 0; + Pivot p = Pivot(this); + PivotStateRule rule(states::SELECTED); + for (Accessible* selected = p.First(rule); selected; + selected = p.Next(selected, rule)) { + count++; + } + + return count; +} + +Accessible* RemoteAccessible::GetSelectedItem(uint32_t aIndex) { + uint32_t index = 0; + Accessible* selected = nullptr; + Pivot p = Pivot(this); + PivotStateRule rule(states::SELECTED); + for (selected = p.First(rule); selected && index < aIndex; + selected = p.Next(selected, rule)) { + index++; + } + + return selected; +} + +bool RemoteAccessible::IsItemSelected(uint32_t aIndex) { + uint32_t index = 0; + Accessible* selectable = nullptr; + Pivot p = Pivot(this); + PivotStateRule rule(states::SELECTABLE); + for (selectable = p.First(rule); selectable && index < aIndex; + selectable = p.Next(selectable, rule)) { + index++; + } + + return selectable && selectable->State() & states::SELECTED; +} + +bool RemoteAccessible::AddItemToSelection(uint32_t aIndex) { + uint32_t index = 0; + Accessible* selectable = nullptr; + Pivot p = Pivot(this); + PivotStateRule rule(states::SELECTABLE); + for (selectable = p.First(rule); selectable && index < aIndex; + selectable = p.Next(selectable, rule)) { + index++; + } + + if (selectable) selectable->SetSelected(true); + + return static_cast<bool>(selectable); +} + +bool RemoteAccessible::RemoveItemFromSelection(uint32_t aIndex) { + uint32_t index = 0; + Accessible* selectable = nullptr; + Pivot p = Pivot(this); + PivotStateRule rule(states::SELECTABLE); + for (selectable = p.First(rule); selectable && index < aIndex; + selectable = p.Next(selectable, rule)) { + index++; + } + + if (selectable) selectable->SetSelected(false); + + return static_cast<bool>(selectable); +} + +bool RemoteAccessible::SelectAll() { + if ((State() & states::MULTISELECTABLE) == 0) { + return false; + } + + bool success = false; + Accessible* selectable = nullptr; + Pivot p = Pivot(this); + PivotStateRule rule(states::SELECTABLE); + for (selectable = p.First(rule); selectable; + selectable = p.Next(selectable, rule)) { + success = true; + selectable->SetSelected(true); + } + return success; +} + +bool RemoteAccessible::UnselectAll() { + if ((State() & states::MULTISELECTABLE) == 0) { + return false; + } + + bool success = false; + Accessible* selectable = nullptr; + Pivot p = Pivot(this); + PivotStateRule rule(states::SELECTABLE); + for (selectable = p.First(rule); selectable; + selectable = p.Next(selectable, rule)) { + success = true; + selectable->SetSelected(false); + } + return success; +} + +void RemoteAccessible::TakeSelection() { + Unused << mDoc->SendTakeSelection(mID); +} + +void RemoteAccessible::SetSelected(bool aSelect) { + Unused << mDoc->SendSetSelected(mID, aSelect); +} + +TableAccessible* RemoteAccessible::AsTable() { + if (IsTable()) { + return CachedTableAccessible::GetFrom(this); + } + return nullptr; +} + +TableCellAccessible* RemoteAccessible::AsTableCell() { + if (IsTableCell()) { + return CachedTableCellAccessible::GetFrom(this); + } + return nullptr; +} + +bool RemoteAccessible::TableIsProbablyForLayout() { + if (mCachedFields) { + if (auto layoutGuess = + mCachedFields->GetAttribute<bool>(CacheKey::TableLayoutGuess)) { + return *layoutGuess; + } + } + return false; +} + +nsTArray<int32_t>& RemoteAccessible::GetCachedHyperTextOffsets() { + if (mCachedFields) { + if (auto offsets = mCachedFields->GetMutableAttribute<nsTArray<int32_t>>( + CacheKey::HyperTextOffsets)) { + return *offsets; + } + } + nsTArray<int32_t> newOffsets; + if (!mCachedFields) { + mCachedFields = new AccAttributes(); + } + mCachedFields->SetAttribute(CacheKey::HyperTextOffsets, + std::move(newOffsets)); + return *mCachedFields->GetMutableAttribute<nsTArray<int32_t>>( + CacheKey::HyperTextOffsets); +} + +void RemoteAccessible::SetCaretOffset(int32_t aOffset) { + Unused << mDoc->SendSetCaretOffset(mID, aOffset); +} + +Maybe<int32_t> RemoteAccessible::GetIntARIAAttr(nsAtom* aAttrName) const { + if (RefPtr<const AccAttributes> attrs = GetCachedARIAAttributes()) { + if (auto val = attrs->GetAttribute<int32_t>(aAttrName)) { + return val; + } + } + return Nothing(); +} + +void RemoteAccessible::Language(nsAString& aLocale) { + if (!IsHyperText()) { + return; + } + if (auto attrs = GetCachedTextAttributes()) { + attrs->GetAttribute(nsGkAtoms::language, aLocale); + } +} + +void RemoteAccessible::ReplaceText(const nsAString& aText) { + Unused << mDoc->SendReplaceText(mID, aText); +} + +void RemoteAccessible::InsertText(const nsAString& aText, int32_t aPosition) { + Unused << mDoc->SendInsertText(mID, aText, aPosition); +} + +void RemoteAccessible::CopyText(int32_t aStartPos, int32_t aEndPos) { + Unused << mDoc->SendCopyText(mID, aStartPos, aEndPos); +} + +void RemoteAccessible::CutText(int32_t aStartPos, int32_t aEndPos) { + Unused << mDoc->SendCutText(mID, aStartPos, aEndPos); +} + +void RemoteAccessible::DeleteText(int32_t aStartPos, int32_t aEndPos) { + Unused << mDoc->SendDeleteText(mID, aStartPos, aEndPos); +} + +void RemoteAccessible::PasteText(int32_t aPosition) { + Unused << mDoc->SendPasteText(mID, aPosition); +} + +size_t RemoteAccessible::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +size_t RemoteAccessible::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) { + size_t size = 0; + + // Count attributes. + if (mCachedFields) { + size += mCachedFields->SizeOfIncludingThis(aMallocSizeOf); + } + + // We don't recurse into mChildren because they're already counted in their + // document's mAccessibles. + size += mChildren.ShallowSizeOfExcludingThis(aMallocSizeOf); + + return size; +} + +} // namespace a11y +} // namespace mozilla diff --git a/accessible/ipc/RemoteAccessible.h b/accessible/ipc/RemoteAccessible.h new file mode 100644 index 0000000000..9215fd7bc5 --- /dev/null +++ b/accessible/ipc/RemoteAccessible.h @@ -0,0 +1,511 @@ +/* -*- 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_RemoteAccessible_h +#define mozilla_a11y_RemoteAccessible_h + +#include "mozilla/a11y/Accessible.h" +#include "mozilla/a11y/CacheConstants.h" +#include "mozilla/a11y/HyperTextAccessibleBase.h" +#include "mozilla/a11y/Role.h" +#include "AccAttributes.h" +#include "nsIAccessibleText.h" +#include "nsIAccessibleTypes.h" +#include "nsTArray.h" +#include "nsRect.h" +#include "LocalAccessible.h" + +namespace mozilla { +namespace a11y { + +class Attribute; +class DocAccessibleParent; +class RemoteAccessible; +enum class RelationType; + +/** + * The class for an accessibility tree node that originated in the parent + * process. + */ +class RemoteAccessible : public Accessible, public HyperTextAccessibleBase { + public: + virtual ~RemoteAccessible() { + MOZ_ASSERT(!mWrapper); + MOZ_COUNT_DTOR(RemoteAccessible); + } + + virtual bool IsRemote() const override { return true; } + + void AddChildAt(uint32_t aIdx, RemoteAccessible* aChild) { + mChildren.InsertElementAt(aIdx, aChild); + if (IsHyperText()) { + InvalidateCachedHyperTextOffsets(); + } + } + + virtual uint32_t ChildCount() const override { return mChildren.Length(); } + RemoteAccessible* RemoteChildAt(uint32_t aIdx) const { + return mChildren.SafeElementAt(aIdx); + } + RemoteAccessible* RemoteFirstChild() const { + return mChildren.Length() ? mChildren[0] : nullptr; + } + RemoteAccessible* RemoteLastChild() const { + return mChildren.Length() ? mChildren[mChildren.Length() - 1] : nullptr; + } + RemoteAccessible* RemotePrevSibling() const { + if (IsDoc()) { + // The normal code path doesn't work for documents because the parent + // might be a local OuterDoc, but IndexInParent() will return 1. + // A document is always a single child of an OuterDoc anyway. + return nullptr; + } + int32_t idx = IndexInParent(); + if (idx == -1) { + return nullptr; // No parent. + } + return idx > 0 ? RemoteParent()->mChildren[idx - 1] : nullptr; + } + RemoteAccessible* RemoteNextSibling() const { + if (IsDoc()) { + // The normal code path doesn't work for documents because the parent + // might be a local OuterDoc, but IndexInParent() will return 1. + // A document is always a single child of an OuterDoc anyway. + return nullptr; + } + int32_t idx = IndexInParent(); + if (idx == -1) { + return nullptr; // No parent. + } + MOZ_ASSERT(idx >= 0); + size_t newIdx = idx + 1; + return newIdx < RemoteParent()->mChildren.Length() + ? RemoteParent()->mChildren[newIdx] + : nullptr; + } + + // Accessible hierarchy method overrides + + virtual Accessible* Parent() const override { return RemoteParent(); } + + virtual Accessible* ChildAt(uint32_t aIndex) const override { + return RemoteChildAt(aIndex); + } + + virtual Accessible* NextSibling() const override { + return RemoteNextSibling(); + } + + virtual Accessible* PrevSibling() const override { + return RemotePrevSibling(); + } + + // XXX evaluate if this is fast enough. + virtual int32_t IndexInParent() const override { + RemoteAccessible* parent = RemoteParent(); + if (!parent) { + return -1; + } + return parent->mChildren.IndexOf( + static_cast<const RemoteAccessible*>(this)); + } + virtual uint32_t EmbeddedChildCount() override; + virtual int32_t IndexOfEmbeddedChild(Accessible* aChild) override; + virtual Accessible* EmbeddedChildAt(uint32_t aChildIdx) override; + + void Shutdown(); + + void SetChildDoc(DocAccessibleParent* aChildDoc); + void ClearChildDoc(DocAccessibleParent* aChildDoc); + + /** + * Remove The given child. + */ + void RemoveChild(RemoteAccessible* aChild) { + mChildren.RemoveElement(aChild); + if (IsHyperText()) { + InvalidateCachedHyperTextOffsets(); + } + } + + /** + * Return the proxy for the parent of the wrapped accessible. + */ + RemoteAccessible* RemoteParent() const; + + LocalAccessible* OuterDocOfRemoteBrowser() const; + + /** + * Get the role of the accessible we're proxying. + */ + virtual role Role() const override { return mRole; } + + /** + * Return true if this is an embedded object. + */ + bool IsEmbeddedObject() const { return !IsText(); } + + virtual bool IsLink() const override { + if (IsHTMLLink()) { + // XXX: HTML links always return true for IsLink. + return true; + } + + if (IsText()) { + return false; + } + + if (Accessible* parent = Parent()) { + return parent->IsHyperText(); + } + + return false; + } + + virtual bool HasNumericValue() const override { + // XXX: We combine the aria and native "has numeric value" field + // when we serialize the local accessible into eNumericValue. + return HasGenericType(eNumericValue); + } + + // Methods that potentially access a cache. + + virtual ENameValueFlag Name(nsString& aName) const override; + virtual void Description(nsString& aDescription) const override; + virtual void Value(nsString& aValue) const override; + + virtual double CurValue() const override; + virtual double MinValue() const override; + virtual double MaxValue() const override; + virtual double Step() const override; + virtual bool SetCurValue(double aValue) override; + + virtual Accessible* ChildAtPoint( + int32_t aX, int32_t aY, + LocalAccessible::EWhichChildAtPoint aWhichChild) override; + + virtual LayoutDeviceIntRect Bounds() const override; + + virtual nsRect BoundsInAppUnits() const override; + + virtual Relation RelationByType(RelationType aType) const override; + + virtual uint64_t State() override; + + virtual already_AddRefed<AccAttributes> Attributes() override; + + virtual nsAtom* TagName() const override; + + virtual already_AddRefed<nsAtom> InputType() const override; + + virtual already_AddRefed<nsAtom> DisplayStyle() const override; + + virtual float Opacity() const override; + + virtual void LiveRegionAttributes(nsAString* aLive, nsAString* aRelevant, + Maybe<bool>* aAtomic, + nsAString* aBusy) const override; + + virtual Maybe<bool> ARIASelected() const override; + + virtual uint8_t ActionCount() const override; + + virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override; + + virtual bool DoAction(uint8_t aIndex) const override; + + virtual KeyBinding AccessKey() const override; + + virtual void SelectionRanges(nsTArray<TextRange>* aRanges) const override; + + MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual bool RemoveFromSelection( + int32_t aSelectionNum) override; + + virtual Maybe<int32_t> GetIntARIAAttr(nsAtom* aAttrName) const override; + + virtual void Language(nsAString& aLocale) override; + + ////////////////////////////////////////////////////////////////////////////// + // EditableTextAccessible + + MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void ReplaceText( + const nsAString& aText) override; + MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void InsertText( + const nsAString& aText, int32_t aPosition) override; + MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void CopyText(int32_t aStartPos, + int32_t aEndPos) override; + MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void CutText(int32_t aStartPos, + int32_t aEndPos) override; + MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void DeleteText(int32_t aStartPos, + int32_t aEndPos) override; + MOZ_CAN_RUN_SCRIPT virtual void PasteText(int32_t aPosition) override; + + ////////////////////////////////////////////////////////////////////////////// + // SelectAccessible + + virtual void SelectedItems(nsTArray<Accessible*>* aItems) override; + + virtual uint32_t SelectedItemCount() override; + + virtual Accessible* GetSelectedItem(uint32_t aIndex) override; + + virtual bool IsItemSelected(uint32_t aIndex) override; + + virtual bool AddItemToSelection(uint32_t aIndex) override; + + virtual bool RemoveItemFromSelection(uint32_t aIndex) override; + + virtual bool SelectAll() override; + + virtual bool UnselectAll() override; + + virtual void TakeSelection() override; + + virtual void SetSelected(bool aSelect) override; + + // Methods that interact with content. + + virtual void TakeFocus() const override; + virtual void ScrollTo(uint32_t aHow) const override; + virtual void SetCaretOffset(int32_t aOffset) override; + + /** + * Allow the platform to store a pointers worth of data on us. + */ + uintptr_t GetWrapper() const { return mWrapper; } + void SetWrapper(uintptr_t aWrapper) { mWrapper = aWrapper; } + + virtual uint64_t ID() const override { return mID; } + + /** + * Return the document containing this proxy, or the proxy itself if it is a + * document. + */ + DocAccessibleParent* Document() const { return mDoc; } + + DocAccessibleParent* AsDoc() const { return IsDoc() ? mDoc : nullptr; } + + void ApplyCache(CacheUpdateType aUpdateType, AccAttributes* aFields); + + void UpdateStateCache(uint64_t aState, bool aEnabled) { + if (aState & kRemoteCalculatedStates) { + return; + } + uint64_t state = 0; + if (mCachedFields) { + if (auto oldState = + mCachedFields->GetAttribute<uint64_t>(CacheKey::State)) { + state = *oldState; + } + } else { + mCachedFields = new AccAttributes(); + } + if (aEnabled) { + state |= aState; + } else { + state &= ~aState; + } + mCachedFields->SetAttribute(CacheKey::State, state); + } + + void InvalidateGroupInfo(); + + virtual void AppendTextTo(nsAString& aText, uint32_t aStartOffset = 0, + uint32_t aLength = UINT32_MAX) override; + + virtual bool TableIsProbablyForLayout(); + + /** + * Iterates through each atom in kRelationTypeAtoms, checking to see + * if it is present in aFields. If it is present (or if aFields contains + * a DeleteEntry() for this atom) and mCachedFields is initialized, + * fetches the old rel targets and removes their existing reverse relations + * stored in mReverseRelations. + * Returns an array of bools where the ith array entry corresponds + * to whether or not the rel at the ith entry of kRelationTypeAtoms + * requires a post-processing update. + */ + nsTArray<bool> PreProcessRelations(AccAttributes* aFields); + + /** + * Takes in the array returned from PreProcessRelations. + * For each entry requiring an update, fetches the new relation + * targets stored in mCachedFields and appropriately + * updates their reverse relations in mReverseRelations. + */ + void PostProcessRelations(const nsTArray<bool>& aToUpdate); + + /** + * This method is called during shutdown, before we clear our + * reverse rel map from the document's mReverseRelations cache. + * Here, we traverse our reverse relations, removing our ID from + * the corresponding forward relation's target list. This ensures + * the stored forward relations do not reference defunct accessibles. + */ + void PruneRelationsOnShutdown(); + + uint32_t GetCachedTextLength(); + Maybe<const nsTArray<int32_t>&> GetCachedTextLines(); + nsRect GetCachedCharRect(int32_t aOffset); + RefPtr<const AccAttributes> GetCachedTextAttributes(); + RefPtr<const AccAttributes> GetCachedARIAAttributes() const; + + nsString GetCachedHTMLNameAttribute() const; + + virtual HyperTextAccessibleBase* AsHyperTextBase() override { + return IsHyperText() ? static_cast<HyperTextAccessibleBase*>(this) + : nullptr; + } + + virtual TableAccessible* AsTable() override; + virtual TableCellAccessible* AsTableCell() override; + + virtual void DOMNodeID(nsString& aID) const override; + + virtual void ScrollToPoint(uint32_t aScrollType, int32_t aX, + int32_t aY) override; + +#if !defined(XP_WIN) + void Announce(const nsString& aAnnouncement, uint16_t aPriority); +#endif // !defined(XP_WIN) + + // HyperTextAccessibleBase + virtual already_AddRefed<AccAttributes> DefaultTextAttributes() override; + + virtual void ScrollSubstringToPoint(int32_t aStartOffset, int32_t aEndOffset, + uint32_t aCoordinateType, int32_t aX, + int32_t aY) override; + + /** + * Invalidate cached HyperText offsets. This should be called whenever a + * child is added or removed or the text of a text leaf child is changed. + * Although GetChildOffset can either fully or partially invalidate the + * offsets cache, calculating which offset to invalidate is not worthwhile + * because a client might not even query offsets. This is in contrast to + * LocalAccessible, where the offsets are always needed to fire text change + * events. For RemoteAccessible, it's cheaper overall to just rebuild the + * offsets cache when a client next needs it. + */ + void InvalidateCachedHyperTextOffsets() { + if (mCachedFields) { + mCachedFields->Remove(CacheKey::HyperTextOffsets); + } + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf); + virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf); + + protected: + RemoteAccessible(uint64_t aID, DocAccessibleParent* aDoc, role aRole, + AccType aType, AccGenericType aGenericTypes, + uint8_t aRoleMapEntryIndex) + : Accessible(aType, aGenericTypes, aRoleMapEntryIndex), + mParent(kNoParent), + mDoc(aDoc), + mWrapper(0), + mID(aID), + mCachedFields(nullptr), + mRole(aRole) { + MOZ_COUNT_CTOR(RemoteAccessible); + } + + explicit RemoteAccessible(DocAccessibleParent* aThisAsDoc) + : mParent(kNoParent), + mDoc(aThisAsDoc), + mWrapper(0), + mID(0), + mCachedFields(nullptr), + mRole(roles::DOCUMENT) { + mGenericTypes = eDocument | eHyperText; + MOZ_COUNT_CTOR(RemoteAccessible); + } + + protected: + void SetParent(RemoteAccessible* aParent); + Maybe<nsRect> RetrieveCachedBounds() const; + bool ApplyTransform(nsRect& aCumulativeBounds) const; + bool ApplyScrollOffset(nsRect& aBounds) const; + void ApplyCrossDocOffset(nsRect& aBounds) const; + LayoutDeviceIntRect BoundsWithOffset( + Maybe<nsRect> aOffset, bool aBoundsAreForHittesting = false) const; + bool IsFixedPos() const; + bool IsOverflowHidden() const; + + /** + * Returns true if an accessible's frame has no scrollable overflow, and + * false otherwise. + * Does not return true for partially clipped accessibles. + */ + bool IsClipped() const; + + /** + * Checks if our hittesting match has any clipped children and, if so + * descends it and subsequent TEXT_CONTAINERs in search of a text leaf. + * We do this because some sites use clipping to hide text that is only + * visible to a11y, while displaying a visual version of the same text on + * the web page. We want a hittest of the visible text to resolve to the + * hidden, a11y-only text node. + */ + RemoteAccessible* DoFuzzyHittesting(); + + // This function is used exclusively for hit testing. + bool ContainsPoint(int32_t aX, int32_t aY); + + virtual void ARIAGroupPosition(int32_t* aLevel, int32_t* aSetSize, + int32_t* aPosInSet) const override; + + virtual AccGroupInfo* GetGroupInfo() const override; + + virtual AccGroupInfo* GetOrCreateGroupInfo() override; + + virtual void GetPositionAndSetSize(int32_t* aPosInSet, + int32_t* aSetSize) override; + + virtual bool HasPrimaryAction() const override; + + nsAtom* GetPrimaryAction() const; + + virtual nsTArray<int32_t>& GetCachedHyperTextOffsets() override; + + private: + uintptr_t mParent; + static const uintptr_t kNoParent = UINTPTR_MAX; + + friend DocAccessibleParent; + friend TextLeafPoint; + friend HyperTextAccessibleBase; + friend class xpcAccessible; + friend class CachedTableCellAccessible; +#ifdef XP_WIN + friend class sdnAccessible; +#endif + + nsTArray<RemoteAccessible*> mChildren; + DocAccessibleParent* mDoc; + uintptr_t mWrapper; + uint64_t mID; + + protected: + virtual const Accessible* Acc() const override { return this; } + + RefPtr<AccAttributes> mCachedFields; + + // XXX DocAccessibleParent gets to change this to change the role of + // documents. + role mRole : 27; +}; + +//////////////////////////////////////////////////////////////////////////////// +// RemoteAccessible downcasting method + +inline RemoteAccessible* Accessible::AsRemote() { + return IsRemote() ? static_cast<RemoteAccessible*>(this) : nullptr; +} + +} // namespace a11y +} // namespace mozilla + +#endif diff --git a/accessible/ipc/moz.build b/accessible/ipc/moz.build new file mode 100644 index 0000000000..b8ff3b9c4f --- /dev/null +++ b/accessible/ipc/moz.build @@ -0,0 +1,62 @@ +# -*- 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/. + +if CONFIG["OS_ARCH"] == "WINNT": + LOCAL_INCLUDES += [ + "/accessible/windows/ia2", + "/accessible/windows/msaa", + ] +else: + if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + LOCAL_INCLUDES += [ + "/accessible/atk", + ] + elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + LOCAL_INCLUDES += [ + "/accessible/mac", + ] + else: + LOCAL_INCLUDES += [ + "/accessible/other", + ] + +if CONFIG["ACCESSIBILITY"]: + PREPROCESSED_IPDL_SOURCES += [ + "PDocAccessible.ipdl", + ] + IPDL_SOURCES += [ + "DocAccessibleTypes.ipdlh", + ] + +EXPORTS.mozilla.a11y += [ + "IPCTypes.h", +] + +if CONFIG["ACCESSIBILITY"]: + EXPORTS.mozilla.a11y += [ + "DocAccessibleChild.h", + "DocAccessibleParent.h", + "RemoteAccessible.h", + ] + + UNIFIED_SOURCES += [ + "DocAccessibleChild.cpp", + "DocAccessibleParent.cpp", + "RemoteAccessible.cpp", + ] + + LOCAL_INCLUDES += [ + "/accessible/base", + "/accessible/generic", + "/accessible/xpcom", + ] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +# Add libFuzzer configuration directives +include("/tools/fuzzing/libfuzzer-config.mozbuild") |