| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/modules/accessibility/blink_ax_tree_source.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <set> |
| |
| #include "base/containers/contains.h" |
| #include "base/memory/ptr_util.h" |
| #include "build/build_config.h" |
| #include "build/chromeos_buildflags.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/html/html_head_element.h" |
| #include "third_party/blink/renderer/modules/accessibility/ax_object.h" |
| #include "third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h" |
| #include "third_party/blink/renderer/modules/accessibility/ax_selection.h" |
| #include "third_party/blink/renderer/platform/heap/collection_support/heap_deque.h" |
| #include "ui/accessibility/ax_common.h" |
| #include "ui/accessibility/ax_enum_util.h" |
| #include "ui/accessibility/ax_role_properties.h" |
| #include "ui/accessibility/ax_tree_id.h" |
| #include "ui/gfx/geometry/transform.h" |
| #include "ui/gfx/geometry/vector2d_f.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| #if DCHECK_IS_ON() |
| AXObject* ParentObjectUnignored(AXObject* child) { |
| if (!child || child->IsDetached()) |
| return nullptr; |
| AXObject* parent = child->ParentObjectIncludedInTree(); |
| while (parent && !parent->IsDetached() && |
| !parent->AccessibilityIsIncludedInTree()) |
| parent = parent->ParentObjectIncludedInTree(); |
| return parent; |
| } |
| |
| // Check that |parent| is the first unignored parent of |child|. |
| void CheckParentUnignoredOf(AXObject* parent, AXObject* child) { |
| AXObject* preexisting_parent = ParentObjectUnignored(child); |
| DCHECK(preexisting_parent == parent) |
| << "Child thinks it has a different preexisting parent:" |
| << "\nChild: " << child << "\nPassed-in parent: " << parent |
| << "\nPreexisting parent: " << preexisting_parent; |
| } |
| #endif |
| |
| } // namespace |
| |
| BlinkAXTreeSource::BlinkAXTreeSource(AXObjectCacheImpl& ax_object_cache, |
| bool truncate_inline_textboxes) |
| : ax_object_cache_(ax_object_cache), |
| truncate_inline_textboxes_(truncate_inline_textboxes) {} |
| |
| BlinkAXTreeSource::~BlinkAXTreeSource() = default; |
| |
| static ax::mojom::blink::TextAffinity ToAXAffinity(TextAffinity affinity) { |
| switch (affinity) { |
| case TextAffinity::kUpstream: |
| return ax::mojom::blink::TextAffinity::kUpstream; |
| case TextAffinity::kDownstream: |
| return ax::mojom::blink::TextAffinity::kDownstream; |
| default: |
| NOTREACHED(); |
| return ax::mojom::blink::TextAffinity::kDownstream; |
| } |
| } |
| |
| void BlinkAXTreeSource::Selection( |
| const AXObject* obj, |
| bool& is_selection_backward, |
| AXObject** anchor_object, |
| int& anchor_offset, |
| ax::mojom::blink::TextAffinity& anchor_affinity, |
| AXObject** focus_object, |
| int& focus_offset, |
| ax::mojom::blink::TextAffinity& focus_affinity) const { |
| is_selection_backward = false; |
| *anchor_object = nullptr; |
| anchor_offset = -1; |
| anchor_affinity = ax::mojom::blink::TextAffinity::kDownstream; |
| *focus_object = nullptr; |
| focus_offset = -1; |
| focus_affinity = ax::mojom::blink::TextAffinity::kDownstream; |
| |
| if (!obj || obj->IsDetached()) |
| return; |
| |
| AXObject* focus = GetFocusedObject(); |
| if (!focus || focus->IsDetached()) |
| return; |
| |
| const auto ax_selection = |
| focus->IsAtomicTextField() |
| ? AXSelection::FromCurrentSelection(ToTextControl(*focus->GetNode())) |
| : AXSelection::FromCurrentSelection(*focus->GetDocument()); |
| if (!ax_selection) |
| return; |
| |
| const AXPosition base = ax_selection.Anchor(); |
| *anchor_object = const_cast<AXObject*>(base.ContainerObject()); |
| const AXPosition extent = ax_selection.Focus(); |
| *focus_object = const_cast<AXObject*>(extent.ContainerObject()); |
| |
| is_selection_backward = base > extent; |
| if (base.IsTextPosition()) { |
| anchor_offset = base.TextOffset(); |
| anchor_affinity = ToAXAffinity(base.Affinity()); |
| } else { |
| anchor_offset = base.ChildIndex(); |
| } |
| |
| if (extent.IsTextPosition()) { |
| focus_offset = extent.TextOffset(); |
| focus_affinity = ToAXAffinity(extent.Affinity()); |
| } else { |
| focus_offset = extent.ChildIndex(); |
| } |
| } |
| |
| static ui::AXTreeID GetAXTreeID(LocalFrame* local_frame) { |
| const std::optional<base::UnguessableToken>& embedding_token = |
| local_frame->GetEmbeddingToken(); |
| if (embedding_token && !embedding_token->is_empty()) |
| return ui::AXTreeID::FromToken(embedding_token.value()); |
| return ui::AXTreeIDUnknown(); |
| } |
| |
| bool BlinkAXTreeSource::GetTreeData(ui::AXTreeData* tree_data) const { |
| CHECK(frozen_); |
| AXObject* root = GetRoot(); |
| tree_data->doctype = "html"; |
| tree_data->loaded = root->IsLoaded(); |
| tree_data->loading_progress = root->EstimatedLoadingProgress(); |
| const Document& document = ax_object_cache_->GetDocument(); |
| tree_data->mimetype = document.IsXHTMLDocument() ? "text/xhtml" : "text/html"; |
| tree_data->title = document.title().Utf8(); |
| tree_data->url = document.Url().GetString().Utf8(); |
| |
| if (AXObject* focus = GetFocusedObject()) |
| tree_data->focus_id = focus->AXObjectID(); |
| |
| bool is_selection_backward = false; |
| AXObject *anchor_object, *focus_object; |
| int anchor_offset, focus_offset; |
| ax::mojom::blink::TextAffinity anchor_affinity, focus_affinity; |
| Selection(root, is_selection_backward, &anchor_object, anchor_offset, |
| anchor_affinity, &focus_object, focus_offset, focus_affinity); |
| if (anchor_object && focus_object && anchor_offset >= 0 && |
| focus_offset >= 0) { |
| int32_t anchor_id = anchor_object->AXObjectID(); |
| int32_t focus_id = focus_object->AXObjectID(); |
| tree_data->sel_is_backward = is_selection_backward; |
| tree_data->sel_anchor_object_id = anchor_id; |
| tree_data->sel_anchor_offset = anchor_offset; |
| tree_data->sel_focus_object_id = focus_id; |
| tree_data->sel_focus_offset = focus_offset; |
| tree_data->sel_anchor_affinity = anchor_affinity; |
| tree_data->sel_focus_affinity = focus_affinity; |
| } |
| |
| // Get the tree ID for this frame. |
| if (LocalFrame* local_frame = document.GetFrame()) |
| tree_data->tree_id = GetAXTreeID(local_frame); |
| |
| if (auto* root_scroller = root->RootScroller()) |
| tree_data->root_scroller_id = root_scroller->AXObjectID(); |
| else |
| tree_data->root_scroller_id = 0; |
| |
| if (ax_object_cache_->GetAXMode().has_mode(ui::AXMode::kHTMLMetadata)) { |
| if (HTMLHeadElement* head = ax_object_cache_->GetDocument().head()) { |
| for (Node* child = head->firstChild(); child; |
| child = child->nextSibling()) { |
| const Element* elem = DynamicTo<Element>(*child); |
| if (!elem) { |
| continue; |
| } |
| if (IsA<HTMLScriptElement>(*elem)) { |
| if (elem->getAttribute(html_names::kTypeAttr) != |
| "application/ld+json") { |
| continue; |
| } |
| } else if (!IsA<HTMLLinkElement>(*elem) && |
| !IsA<HTMLTitleElement>(*elem) && |
| !IsA<HTMLMetaElement>(*elem)) { |
| continue; |
| } |
| // TODO(chrishtr): replace the below with elem->outerHTML(). |
| String tag = elem->tagName().LowerASCII(); |
| String html = "<" + tag; |
| for (unsigned i = 0; i < elem->Attributes().size(); i++) { |
| html = html + String(" ") + elem->Attributes().at(i).LocalName() + |
| String("=\"") + elem->Attributes().at(i).Value() + "\""; |
| } |
| html = html + String(">") + elem->innerHTML() + String("</") + tag + |
| String(">"); |
| tree_data->metadata.push_back(html.Utf8()); |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| void BlinkAXTreeSource::Freeze() { |
| CHECK(!frozen_); |
| frozen_ = true; |
| |
| // The root cannot be null. |
| root_ = ax_object_cache_->Root(); |
| CHECK(root_); |
| focus_ = ax_object_cache_->FocusedObject(); |
| CHECK(focus_); |
| } |
| |
| void BlinkAXTreeSource::Thaw() { |
| CHECK(frozen_); |
| frozen_ = false; |
| root_ = nullptr; |
| focus_ = nullptr; |
| } |
| |
| AXObject* BlinkAXTreeSource::GetRoot() const { |
| CHECK(frozen_); |
| CHECK(root_); |
| return root_.Get(); |
| } |
| |
| AXObject* BlinkAXTreeSource::GetFocusedObject() const { |
| CHECK(frozen_); |
| CHECK(focus_); |
| return focus_.Get(); |
| } |
| |
| AXObject* BlinkAXTreeSource::GetFromId(int32_t id) const { |
| AXObject* result = ax_object_cache_->ObjectFromAXID(id); |
| if (result && !result->AccessibilityIsIncludedInTree()) { |
| DCHECK(false) << "Should not serialize an unincluded object:" |
| << "\nChild: " << result->ToString(true).Utf8(); |
| return nullptr; |
| } |
| return result; |
| } |
| |
| int32_t BlinkAXTreeSource::GetId(AXObject* node) const { |
| return node->AXObjectID(); |
| } |
| |
| size_t BlinkAXTreeSource::GetChildCount(AXObject* node) const { |
| if (truncate_inline_textboxes_ && |
| ui::CanHaveInlineTextBoxChildren(node->RoleValue())) { |
| return 0; |
| } |
| return node->ChildCountIncludingIgnored(); |
| } |
| |
| AXObject* BlinkAXTreeSource::ChildAt(AXObject* node, size_t index) const { |
| if (truncate_inline_textboxes_) { |
| CHECK(!ui::CanHaveInlineTextBoxChildren(node->RoleValue())); |
| } |
| auto* child = node->ChildAtIncludingIgnored(static_cast<int>(index)); |
| |
| // The child may be invalid due to issues in blink accessibility code. |
| CHECK(child); |
| if (child->IsDetached()) { |
| DCHECK(false) << "Should not try to serialize an invalid child:" |
| << "\nParent: " << node->ToString(true).Utf8() |
| << "\nChild: " << child->ToString(true).Utf8(); |
| return nullptr; |
| } |
| |
| if (!child->AccessibilityIsIncludedInTree()) { |
| // TODO(https://crbug.com/1407396) resolve and restore to NOTREACHED(). |
| DCHECK(false) << "Should not receive unincluded child." |
| << "\nChild: " << child->ToString(true).Utf8() |
| << "\nParent: " << node->ToString(true).Utf8(); |
| return nullptr; |
| } |
| |
| // These should not be produced by Blink. They are only needed on Mac and |
| // handled in AXTableInfo on the browser side. |
| DCHECK_NE(child->RoleValue(), ax::mojom::blink::Role::kColumn); |
| DCHECK_NE(child->RoleValue(), ax::mojom::blink::Role::kTableHeaderContainer); |
| |
| #if DCHECK_IS_ON() |
| CheckParentUnignoredOf(node, child); |
| #endif |
| |
| return child; |
| } |
| |
| AXObject* BlinkAXTreeSource::GetParent(AXObject* node) const { |
| // Blink returns ignored objects when walking up the parent chain, |
| // we have to skip those here. Also, stop when we get to the root |
| // element. |
| do { |
| if (node == GetRoot()) |
| return nullptr; |
| node = node->ParentObject(); |
| } while (node && !node->IsDetached() && |
| !node->AccessibilityIsIncludedInTree()); |
| |
| return node; |
| } |
| |
| bool BlinkAXTreeSource::IsIgnored(AXObject* node) const { |
| if (!node || node->IsDetached()) |
| return false; |
| return node->AccessibilityIsIgnored(); |
| } |
| |
| bool BlinkAXTreeSource::IsEqual(AXObject* node1, AXObject* node2) const { |
| return node1 == node2; |
| } |
| |
| AXObject* BlinkAXTreeSource::GetNull() const { |
| return nullptr; |
| } |
| |
| std::string BlinkAXTreeSource::GetDebugString(AXObject* node) const { |
| if (!node || node->IsDetached()) |
| return ""; |
| return node->ToString(true).Utf8(); |
| } |
| |
| void BlinkAXTreeSource::SerializeNode(AXObject* src, |
| ui::AXNodeData* dst) const { |
| #if DCHECK_IS_ON() |
| // Never causes a document lifecycle change during serialization, |
| // because the assumption is that layout is in a safe, stable state. |
| DocumentLifecycle::DisallowTransitionScope disallow( |
| ax_object_cache_->GetDocument().Lifecycle()); |
| #endif |
| |
| if (!src || src->IsDetached() || !src->AccessibilityIsIncludedInTree()) { |
| dst->AddState(ax::mojom::blink::State::kIgnored); |
| dst->id = -1; |
| dst->role = ax::mojom::blink::Role::kUnknown; |
| NOTREACHED(); |
| return; |
| } |
| |
| src->Serialize(dst, ax_object_cache_->GetAXMode()); |
| } |
| |
| void BlinkAXTreeSource::Trace(Visitor* visitor) const { |
| visitor->Trace(ax_object_cache_); |
| visitor->Trace(root_); |
| visitor->Trace(focus_); |
| } |
| |
| } // namespace blink |