blob: 1e3da03cd774fc3c867fdb9a4d0b95fb1145024d [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/renderer/accessibility/blink_ax_tree_source.h"
#include <stddef.h>
#include <algorithm>
#include <set>
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "content/common/ax_serialization_utils.h"
#include "content/public/common/content_features.h"
#include "content/renderer/accessibility/ax_image_annotator.h"
#include "content/renderer/accessibility/render_accessibility_impl.h"
#include "content/renderer/render_frame_impl.h"
#include "content/renderer/render_frame_proxy.h"
#include "content/renderer/render_view_impl.h"
#include "third_party/blink/public/platform/web_string.h"
#include "third_party/blink/public/platform/web_vector.h"
#include "third_party/blink/public/web/web_ax_enums.h"
#include "third_party/blink/public/web/web_ax_object.h"
#include "third_party/blink/public/web/web_disallow_transition_scope.h"
#include "third_party/blink/public/web/web_document.h"
#include "third_party/blink/public/web/web_element.h"
#include "third_party/blink/public/web/web_form_control_element.h"
#include "third_party/blink/public/web/web_frame.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_node.h"
#include "third_party/blink/public/web/web_plugin.h"
#include "third_party/blink/public/web/web_plugin_container.h"
#include "third_party/blink/public/web/web_view.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/accessibility_switches.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/vector2d_f.h"
#include "url/gurl.h"
#include "url/url_constants.h"
using base::ASCIIToUTF16;
using base::UTF16ToUTF8;
using blink::WebAXObject;
using blink::WebDocument;
using blink::WebElement;
using blink::WebFrame;
using blink::WebLocalFrame;
using blink::WebNode;
using blink::WebPlugin;
using blink::WebPluginContainer;
using blink::WebVector;
using blink::WebView;
namespace content {
namespace {
// Images smaller than this number, in CSS pixels, will never get annotated.
// Note that OCR works on pretty small images, so this shouldn't be too large.
const int kMinImageAnnotationWidth = 16;
const int kMinImageAnnotationHeight = 16;
void AddIntListAttributeFromWebObjects(ax::mojom::IntListAttribute attr,
const WebVector<WebAXObject>& objects,
ui::AXNodeData* dst) {
std::vector<int32_t> ids;
for (size_t i = 0; i < objects.size(); i++)
ids.push_back(objects[i].AxID());
if (!ids.empty())
dst->AddIntListAttribute(attr, ids);
}
#if DCHECK_IS_ON()
WebAXObject ParentObjectUnignored(WebAXObject child) {
WebAXObject parent = child.ParentObject();
while (!parent.IsDetached() && !parent.AccessibilityIsIncludedInTree())
parent = parent.ParentObject();
return parent;
}
// Check that |parent| is the first unignored parent of |child|.
void CheckParentUnignoredOf(WebAXObject parent, WebAXObject child) {
WebAXObject preexisting_parent = ParentObjectUnignored(child);
DCHECK(preexisting_parent.Equals(parent))
<< "Child thinks it has a different preexisting parent:"
<< "\nChild: " << child.ToString(true).Utf8()
<< "\nPassed-in parent: " << parent.ToString(true).Utf8()
<< "\nPreexisting parent: " << preexisting_parent.ToString(true).Utf8();
}
#endif
// Helper function that searches in the subtree of |obj| to a max
// depth of |max_depth| for an image.
//
// Returns true on success, or false if it finds more than one image,
// or any node with a name, or anything deeper than |max_depth|.
bool SearchForExactlyOneInnerImage(WebAXObject obj,
WebAXObject* inner_image,
int max_depth) {
DCHECK(inner_image);
// If it's the first image, set |inner_image|. If we already
// found an image, fail.
if (ui::IsImage(obj.Role())) {
if (!inner_image->IsDetached())
return false;
*inner_image = obj;
} else {
// If we found something else with a name, fail.
if (!ui::IsPlatformDocument(obj.Role()) && !ui::IsLink(obj.Role())) {
blink::WebString web_name = obj.GetName();
if (!base::ContainsOnlyChars(web_name.Utf8(), base::kWhitespaceASCII)) {
return false;
}
}
}
// Fail if we recursed to |max_depth| and there's more of a subtree.
if (max_depth == 0 && obj.ChildCount())
return false;
// Don't count ignored nodes toward depth.
int next_depth = obj.AccessibilityIsIgnored() ? max_depth : max_depth - 1;
// Recurse.
for (unsigned int i = 0; i < obj.ChildCount(); i++) {
if (!SearchForExactlyOneInnerImage(obj.ChildAt(i), inner_image, next_depth))
return false;
}
return !inner_image->IsDetached();
}
// Return true if the subtree of |obj|, to a max depth of 3, contains
// exactly one image. Return that image in |inner_image|.
bool FindExactlyOneInnerImageInMaxDepthThree(WebAXObject obj,
WebAXObject* inner_image) {
DCHECK(inner_image);
return SearchForExactlyOneInnerImage(obj, inner_image, /* max_depth = */ 3);
}
// Ignore code that limits based on the protocol (like https, file, etc.)
// to enable tests to run.
bool g_ignore_protocol_checks_for_testing;
} // namespace
ScopedFreezeBlinkAXTreeSource::ScopedFreezeBlinkAXTreeSource(
BlinkAXTreeSource* tree_source)
: tree_source_(tree_source) {
tree_source_->Freeze();
}
ScopedFreezeBlinkAXTreeSource::~ScopedFreezeBlinkAXTreeSource() {
tree_source_->Thaw();
}
BlinkAXTreeSource::BlinkAXTreeSource(RenderFrameImpl* render_frame,
ui::AXMode mode)
: render_frame_(render_frame), accessibility_mode_(mode), frozen_(false) {
image_annotation_debugging_ =
base::CommandLine::ForCurrentProcess()->HasSwitch(
::switches::kEnableExperimentalAccessibilityLabelsDebugging);
}
BlinkAXTreeSource::~BlinkAXTreeSource() {}
void BlinkAXTreeSource::Freeze() {
CHECK(!frozen_);
frozen_ = true;
if (render_frame_ && render_frame_->GetWebFrame())
document_ = render_frame_->GetWebFrame()->GetDocument();
else
document_ = WebDocument();
root_ = ComputeRoot();
if (!document_.IsNull())
focus_ = WebAXObject::FromWebDocumentFocused(document_);
else
focus_ = WebAXObject();
WebAXObject::Freeze(document_);
}
void BlinkAXTreeSource::Thaw() {
CHECK(frozen_);
WebAXObject::Thaw(document_);
document_ = WebDocument();
focus_ = WebAXObject();
root_ = WebAXObject();
frozen_ = false;
}
void BlinkAXTreeSource::SetRoot(WebAXObject root) {
CHECK(!frozen_);
explicit_root_ = root;
}
#if defined(AX_FAIL_FAST_BUILD)
// TODO(accessibility) Remove once it's clear this never triggers.
bool BlinkAXTreeSource::IsInTree(WebAXObject node) const {
CHECK(frozen_);
while (IsValid(node)) {
if (node.Equals(root()))
return true;
node = GetParent(node);
}
return false;
}
#endif
void BlinkAXTreeSource::SetAccessibilityMode(ui::AXMode new_mode) {
if (accessibility_mode_ == new_mode)
return;
accessibility_mode_ = new_mode;
}
bool BlinkAXTreeSource::ShouldLoadInlineTextBoxes(
const blink::WebAXObject& obj) const {
#if !defined(OS_ANDROID)
// If inline text boxes are enabled globally, no need to explicitly load them.
if (accessibility_mode_.has_mode(ui::AXMode::kInlineTextBoxes))
return false;
#endif
// On some platforms, like Android, we only load inline text boxes for
// a subset of nodes:
//
// Within the subtree of a focused editable text area.
// When specifically enabled for a subtree via |load_inline_text_boxes_ids_|.
int32_t focus_id = focus().AxID();
WebAXObject ancestor = obj;
while (!ancestor.IsDetached()) {
int32_t ancestor_id = ancestor.AxID();
if (base::Contains(load_inline_text_boxes_ids_, ancestor_id) ||
(ancestor_id == focus_id && ancestor.IsEditable())) {
return true;
}
ancestor = ancestor.ParentObject();
}
return false;
}
void BlinkAXTreeSource::SetLoadInlineTextBoxesForId(int32_t id) {
// Keeping stale IDs in the set is harmless but we don't want it to keep
// growing without bound, so clear out any unnecessary IDs whenever this
// method is called.
for (auto iter = load_inline_text_boxes_ids_.begin();
iter != load_inline_text_boxes_ids_.end();) {
if (GetFromId(*iter).IsDetached())
iter = load_inline_text_boxes_ids_.erase(iter);
else
++iter;
}
load_inline_text_boxes_ids_.insert(id);
}
void BlinkAXTreeSource::PopulateAXRelativeBounds(WebAXObject obj,
ui::AXRelativeBounds* bounds,
bool* clips_children) const {
WebAXObject offset_container;
gfx::RectF bounds_in_container;
skia::Matrix44 web_container_transform;
obj.GetRelativeBounds(offset_container, bounds_in_container,
web_container_transform, clips_children);
bounds->bounds = bounds_in_container;
if (!offset_container.IsDetached())
bounds->offset_container_id = offset_container.AxID();
if (content::AXShouldIncludePageScaleFactorInRoot() && obj.Equals(root())) {
const WebView* web_view = render_frame_->GetWebView();
std::unique_ptr<gfx::Transform> container_transform =
std::make_unique<gfx::Transform>(web_container_transform);
container_transform->Scale(web_view->PageScaleFactor(),
web_view->PageScaleFactor());
container_transform->Translate(
-web_view->VisualViewportOffset().OffsetFromOrigin());
if (!container_transform->IsIdentity())
bounds->transform = std::move(container_transform);
} else if (!web_container_transform.isIdentity()) {
bounds->transform =
base::WrapUnique(new gfx::Transform(web_container_transform));
}
}
bool BlinkAXTreeSource::HasCachedBoundingBox(int32_t id) const {
return base::Contains(cached_bounding_boxes_, id);
}
const ui::AXRelativeBounds& BlinkAXTreeSource::GetCachedBoundingBox(
int32_t id) const {
auto iter = cached_bounding_boxes_.find(id);
DCHECK(iter != cached_bounding_boxes_.end());
return iter->second;
}
void BlinkAXTreeSource::SetCachedBoundingBox(
int32_t id,
const ui::AXRelativeBounds& bounds) {
cached_bounding_boxes_[id] = bounds;
}
size_t BlinkAXTreeSource::GetCachedBoundingBoxCount() const {
return cached_bounding_boxes_.size();
}
bool BlinkAXTreeSource::GetTreeData(ui::AXTreeData* tree_data) const {
CHECK(frozen_);
tree_data->doctype = "html";
tree_data->loaded = root().IsLoaded();
tree_data->loading_progress = root().EstimatedLoadingProgress();
tree_data->mimetype =
document().IsXHTMLDocument() ? "text/xhtml" : "text/html";
tree_data->title = document().Title().Utf8();
tree_data->url = document().Url().GetString().Utf8();
if (!focus().IsNull())
tree_data->focus_id = focus().AxID();
bool is_selection_backward = false;
WebAXObject anchor_object, focus_object;
int anchor_offset, focus_offset;
ax::mojom::TextAffinity anchor_affinity, focus_affinity;
root().Selection(is_selection_backward, anchor_object, anchor_offset,
anchor_affinity, focus_object, focus_offset, focus_affinity);
if (!anchor_object.IsNull() && !focus_object.IsNull() && anchor_offset >= 0 &&
focus_offset >= 0) {
int32_t anchor_id = anchor_object.AxID();
int32_t focus_id = focus_object.AxID();
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 (WebLocalFrame* web_frame = document().GetFrame())
tree_data->tree_id = web_frame->GetAXTreeID();
tree_data->root_scroller_id = root().RootScroller().AxID();
if (accessibility_mode_.has_mode(ui::AXMode::kHTMLMetadata)) {
WebElement head = GetMainDocument().Head();
for (WebNode child = head.FirstChild(); !child.IsNull();
child = child.NextSibling()) {
if (!child.IsElementNode())
continue;
WebElement elem = child.To<WebElement>();
if (elem.HasHTMLTagName("SCRIPT")) {
if (elem.GetAttribute("type") != "application/ld+json")
continue;
} else if (!elem.HasHTMLTagName("LINK") &&
!elem.HasHTMLTagName("TITLE") &&
!elem.HasHTMLTagName("META")) {
continue;
}
std::string tag = base::ToLowerASCII(elem.TagName().Utf8());
std::string html = "<" + tag;
for (unsigned i = 0; i < elem.AttributeCount(); i++) {
html += " " + elem.AttributeLocalName(i).Utf8() + "=\"" +
elem.AttributeValue(i).Utf8() + "\"";
}
html += ">" + elem.InnerHTML().Utf8() + "</" + tag + ">";
tree_data->metadata.push_back(html);
}
}
return true;
}
WebAXObject BlinkAXTreeSource::GetRoot() const {
if (frozen_)
return root_;
else
return ComputeRoot();
}
WebAXObject BlinkAXTreeSource::GetFromId(int32_t id) const {
return WebAXObject::FromWebDocumentByID(GetMainDocument(), id);
}
int32_t BlinkAXTreeSource::GetId(WebAXObject node) const {
return node.AxID();
}
void BlinkAXTreeSource::GetChildren(
WebAXObject parent,
std::vector<WebAXObject>* out_children) const {
CHECK(frozen_);
if (ui::CanHaveInlineTextBoxChildren(parent.Role()) &&
ShouldLoadInlineTextBoxes(parent)) {
parent.LoadInlineTextBoxes();
}
bool is_iframe = false;
WebNode node = parent.GetNode();
if (!node.IsNull() && node.IsElementNode())
is_iframe = node.To<WebElement>().HasHTMLTagName("iframe");
for (unsigned i = 0; i < parent.ChildCount(); i++) {
WebAXObject child = parent.ChildAt(i);
// The child may be invalid due to issues in blink accessibility code.
if (child.IsDetached()) {
NOTREACHED() << "Should not try to serialize an invalid child:"
<< "\nParent: " << parent.ToString(true).Utf8()
<< "\nChild: " << child.ToString(true).Utf8();
continue;
}
if (!child.AccessibilityIsIncludedInTree()) {
NOTREACHED() << "Should not receive unincluded child."
<< "\nChild: " << child.ToString(true).Utf8()
<< "\nParent: " << parent.ToString(true).Utf8();
continue;
}
#if DCHECK_IS_ON()
CheckParentUnignoredOf(parent, child);
#endif
// These should not be produced by Blink. They are only needed on Mac and
// handled in AXTableInfo on the browser side.
DCHECK_NE(child.Role(), ax::mojom::Role::kColumn);
DCHECK_NE(child.Role(), ax::mojom::Role::kTableHeaderContainer);
// If an optional exclude_offscreen flag is set (only intended to be
// used for a one-time snapshot of the accessibility tree), prune any
// node that's entirely offscreen from the tree.
if (exclude_offscreen() && child.IsOffScreen())
continue;
out_children->push_back(child);
}
}
WebAXObject BlinkAXTreeSource::GetParent(WebAXObject node) const {
CHECK(frozen_);
// 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.Equals(root()))
return WebAXObject();
node = node.ParentObject();
} while (!node.IsDetached() && !node.AccessibilityIsIncludedInTree());
return node;
}
bool BlinkAXTreeSource::IsIgnored(WebAXObject node) const {
return node.AccessibilityIsIgnored();
}
bool BlinkAXTreeSource::IsValid(WebAXObject node) const {
return !node.IsDetached(); // This also checks if it's null.
}
bool BlinkAXTreeSource::IsEqual(WebAXObject node1, WebAXObject node2) const {
return node1.Equals(node2);
}
WebAXObject BlinkAXTreeSource::GetNull() const {
return WebAXObject();
}
std::string BlinkAXTreeSource::GetDebugString(blink::WebAXObject node) const {
return node.ToString(true).Utf8();
}
void BlinkAXTreeSource::SerializerClearedNode(int32_t node_id) {
cached_bounding_boxes_.erase(node_id);
}
void BlinkAXTreeSource::SerializeNode(WebAXObject 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.
WebDocument document = GetMainDocument();
blink::WebDisallowTransitionScope disallow(&document);
#endif
dst->id = src.AxID();
dst->role = src.Role();
if (src.IsDetached() || !src.AccessibilityIsIncludedInTree()) {
dst->AddState(ax::mojom::State::kIgnored);
NOTREACHED();
return;
}
// TODO(crbug.com/1068668): AX onion soup - finish migrating the rest of
// this function inside of AXObject::Serialize and removing
// unneeded WebAXObject interfaces.
src.Serialize(dst, accessibility_mode_);
TRACE_EVENT2("accessibility", "BlinkAXTreeSource::SerializeNode", "role",
ui::ToString(dst->role), "id", dst->id);
if (accessibility_mode_.has_mode(ui::AXMode::kPDF)) {
SerializeNameAndDescriptionAttributes(src, dst);
// Return early. None of the following attributes are needed for PDFs.
return;
}
// Bounding boxes are needed on all nodes, including ignored, for hit testing.
SerializeBoundingBoxAttributes(src, dst);
cached_bounding_boxes_[dst->id] = dst->relative_bounds;
// Return early. The following attributes are unnecessary for ignored nodes.
// Exception: focusable ignored nodes are fully serialized, so that reasonable
// verbalizations can be made if they actually receive focus.
if (src.AccessibilityIsIgnored() &&
!dst->HasState(ax::mojom::State::kFocusable)) {
// The name is important for exposing the selection around ignored nodes.
// TODO(accessibility) Remove this and still pass this content_browsertest:
// All/DumpAccessibilityTreeTest.AccessibilityIgnoredSelection/blink
if (src.Role() == ax::mojom::Role::kStaticText)
SerializeNameAndDescriptionAttributes(src, dst);
return;
}
SerializeNameAndDescriptionAttributes(src, dst);
if (accessibility_mode_.has_mode(ui::AXMode::kScreenReader)) {
if (src.IsInLiveRegion())
SerializeLiveRegionAttributes(src, dst);
SerializeOtherScreenReaderAttributes(src, dst);
}
WebNode node = src.GetNode();
bool is_iframe = false;
if (!node.IsNull() && node.IsElementNode()) {
WebElement element = node.To<WebElement>();
is_iframe = element.HasHTMLTagName("iframe");
}
if (dst->id == image_data_node_id_) {
// In general, string attributes should be truncated using
// TruncateAndAddStringAttribute, but ImageDataUrl contains a data url
// representing an image, so add it directly using AddStringAttribute.
dst->AddStringAttribute(ax::mojom::StringAttribute::kImageDataUrl,
src.ImageDataUrl(max_image_data_size_).Utf8());
}
}
void BlinkAXTreeSource::SerializeBoundingBoxAttributes(
WebAXObject src,
ui::AXNodeData* dst) const {
bool clips_children = false;
PopulateAXRelativeBounds(src, &dst->relative_bounds, &clips_children);
if (clips_children)
dst->AddBoolAttribute(ax::mojom::BoolAttribute::kClipsChildren, true);
if (src.IsLineBreakingObject()) {
dst->AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
true);
}
}
void BlinkAXTreeSource::SerializeNameAndDescriptionAttributes(
WebAXObject src,
ui::AXNodeData* dst) const {
ax::mojom::NameFrom name_from;
blink::WebVector<WebAXObject> name_objects;
blink::WebString web_name = src.GetName(name_from, name_objects);
if ((!web_name.IsEmpty() && !web_name.IsNull()) ||
name_from == ax::mojom::NameFrom::kAttributeExplicitlyEmpty) {
int max_length = dst->role == ax::mojom::Role::kStaticText
? kMaxStaticTextLength
: kMaxStringAttributeLength;
TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kName,
web_name.Utf8(), max_length);
dst->SetNameFrom(name_from);
AddIntListAttributeFromWebObjects(
ax::mojom::IntListAttribute::kLabelledbyIds, name_objects, dst);
}
ax::mojom::DescriptionFrom description_from;
blink::WebVector<WebAXObject> description_objects;
blink::WebString web_description =
src.Description(name_from, description_from, description_objects);
if (!web_description.IsEmpty()) {
TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kDescription,
web_description.Utf8());
dst->SetDescriptionFrom(description_from);
AddIntListAttributeFromWebObjects(
ax::mojom::IntListAttribute::kDescribedbyIds, description_objects, dst);
}
blink::WebString web_title = src.Title(name_from);
if (!web_title.IsEmpty()) {
TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kTooltip,
web_title.Utf8());
}
if (accessibility_mode_.has_mode(ui::AXMode::kScreenReader)) {
blink::WebString web_placeholder = src.Placeholder(name_from);
if (!web_placeholder.IsEmpty())
TruncateAndAddStringAttribute(dst,
ax::mojom::StringAttribute::kPlaceholder,
web_placeholder.Utf8());
}
}
void BlinkAXTreeSource::SerializeInlineTextBoxAttributes(
WebAXObject src,
ui::AXNodeData* dst) const {
DCHECK_EQ(ax::mojom::Role::kInlineTextBox, dst->role);
WebVector<int> src_character_offsets;
src.CharacterOffsets(src_character_offsets);
dst->AddIntListAttribute(ax::mojom::IntListAttribute::kCharacterOffsets,
src_character_offsets.ReleaseVector());
WebVector<int> src_word_starts;
WebVector<int> src_word_ends;
src.GetWordBoundaries(src_word_starts, src_word_ends);
dst->AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts,
src_word_starts.ReleaseVector());
dst->AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds,
src_word_ends.ReleaseVector());
}
void BlinkAXTreeSource::SerializeLiveRegionAttributes(
WebAXObject src,
ui::AXNodeData* dst) const {
DCHECK(src.IsInLiveRegion());
dst->AddBoolAttribute(ax::mojom::BoolAttribute::kLiveAtomic,
src.LiveRegionAtomic());
if (!src.LiveRegionStatus().IsEmpty()) {
TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kLiveStatus,
src.LiveRegionStatus().Utf8());
}
TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kLiveRelevant,
src.LiveRegionRelevant().Utf8());
// If we are not at the root of an atomic live region.
if (src.ContainerLiveRegionAtomic() && !src.LiveRegionRoot().IsDetached() &&
!src.LiveRegionAtomic()) {
dst->AddIntAttribute(ax::mojom::IntAttribute::kMemberOfId,
src.LiveRegionRoot().AxID());
}
dst->AddBoolAttribute(ax::mojom::BoolAttribute::kContainerLiveAtomic,
src.ContainerLiveRegionAtomic());
dst->AddBoolAttribute(ax::mojom::BoolAttribute::kContainerLiveBusy,
src.ContainerLiveRegionBusy());
TruncateAndAddStringAttribute(
dst, ax::mojom::StringAttribute::kContainerLiveStatus,
src.ContainerLiveRegionStatus().Utf8());
TruncateAndAddStringAttribute(
dst, ax::mojom::StringAttribute::kContainerLiveRelevant,
src.ContainerLiveRegionRelevant().Utf8());
}
void BlinkAXTreeSource::SerializeOtherScreenReaderAttributes(
WebAXObject src,
ui::AXNodeData* dst) const {
if (dst->role == ax::mojom::Role::kColorWell)
dst->AddIntAttribute(ax::mojom::IntAttribute::kColorValue,
src.ColorValue());
if (dst->role == ax::mojom::Role::kLink) {
WebAXObject target = src.InPageLinkTarget();
if (!target.IsNull()) {
int32_t target_id = target.AxID();
dst->AddIntAttribute(ax::mojom::IntAttribute::kInPageLinkTargetId,
target_id);
}
}
if (dst->role == ax::mojom::Role::kRadioButton) {
AddIntListAttributeFromWebObjects(
ax::mojom::IntListAttribute::kRadioGroupIds, src.RadioButtonsInGroup(),
dst);
}
if (src.AriaCurrentState() != ax::mojom::AriaCurrentState::kNone) {
dst->AddIntAttribute(ax::mojom::IntAttribute::kAriaCurrentState,
static_cast<int32_t>(src.AriaCurrentState()));
}
if (src.InvalidState() != ax::mojom::InvalidState::kNone)
dst->SetInvalidState(src.InvalidState());
if (src.InvalidState() == ax::mojom::InvalidState::kOther &&
src.AriaInvalidValue().length()) {
TruncateAndAddStringAttribute(dst,
ax::mojom::StringAttribute::kAriaInvalidValue,
src.AriaInvalidValue().Utf8());
}
if (src.CheckedState() != ax::mojom::CheckedState::kNone) {
dst->SetCheckedState(src.CheckedState());
}
if (dst->role == ax::mojom::Role::kInlineTextBox) {
SerializeInlineTextBoxAttributes(src, dst);
}
if (src.AccessKey().length()) {
TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kAccessKey,
src.AccessKey().Utf8());
}
if (src.AutoComplete().length()) {
TruncateAndAddStringAttribute(dst,
ax::mojom::StringAttribute::kAutoComplete,
src.AutoComplete().Utf8());
}
if (src.Action() != ax::mojom::DefaultActionVerb::kNone) {
dst->SetDefaultActionVerb(src.Action());
}
blink::WebString display_style = src.ComputedStyleDisplay();
if (!display_style.IsEmpty()) {
TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kDisplay,
display_style.Utf8());
}
if (src.KeyboardShortcut().length() &&
!dst->HasStringAttribute(ax::mojom::StringAttribute::kKeyShortcuts)) {
TruncateAndAddStringAttribute(dst,
ax::mojom::StringAttribute::kKeyShortcuts,
src.KeyboardShortcut().Utf8());
}
if (!src.NextOnLine().IsDetached()) {
dst->AddIntAttribute(ax::mojom::IntAttribute::kNextOnLineId,
src.NextOnLine().AxID());
}
if (!src.PreviousOnLine().IsDetached()) {
dst->AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId,
src.PreviousOnLine().AxID());
}
if (!src.AriaActiveDescendant().IsDetached()) {
dst->AddIntAttribute(ax::mojom::IntAttribute::kActivedescendantId,
src.AriaActiveDescendant().AxID());
}
if (!src.ErrorMessage().IsDetached()) {
dst->AddIntAttribute(ax::mojom::IntAttribute::kErrormessageId,
src.ErrorMessage().AxID());
}
if (ui::SupportsHierarchicalLevel(dst->role) && src.HierarchicalLevel()) {
dst->AddIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel,
src.HierarchicalLevel());
}
if (src.CanvasHasFallbackContent())
dst->AddBoolAttribute(ax::mojom::BoolAttribute::kCanvasHasFallback, true);
if (dst->role == ax::mojom::Role::kProgressIndicator ||
dst->role == ax::mojom::Role::kMeter ||
dst->role == ax::mojom::Role::kScrollBar ||
dst->role == ax::mojom::Role::kSlider ||
dst->role == ax::mojom::Role::kSpinButton ||
(dst->role == ax::mojom::Role::kSplitter &&
dst->HasState(ax::mojom::State::kFocusable))) {
float value;
if (src.ValueForRange(&value))
dst->AddFloatAttribute(ax::mojom::FloatAttribute::kValueForRange, value);
float max_value;
if (src.MaxValueForRange(&max_value)) {
dst->AddFloatAttribute(ax::mojom::FloatAttribute::kMaxValueForRange,
max_value);
}
float min_value;
if (src.MinValueForRange(&min_value)) {
dst->AddFloatAttribute(ax::mojom::FloatAttribute::kMinValueForRange,
min_value);
}
float step_value;
if (src.StepValueForRange(&step_value)) {
dst->AddFloatAttribute(ax::mojom::FloatAttribute::kStepValueForRange,
step_value);
}
}
if (ui::IsDialog(dst->role)) {
dst->AddBoolAttribute(ax::mojom::BoolAttribute::kModal, src.IsModal());
}
if (ui::IsImage(dst->role))
AddImageAnnotations(src, dst);
// If a link or web area isn't otherwise labeled and contains exactly one
// image (searching only to a max depth of 2), and the link doesn't have
// accessible text from an attribute like aria-label, then annotate the
// link/web area with the image's annotation, too.
if ((ui::IsLink(dst->role) || ui::IsPlatformDocument(dst->role)) &&
dst->GetNameFrom() != ax::mojom::NameFrom::kAttribute) {
WebAXObject inner_image;
if (FindExactlyOneInnerImageInMaxDepthThree(src, &inner_image))
AddImageAnnotations(inner_image, dst);
}
WebNode node = src.GetNode();
if (!node.IsNull() && node.IsElementNode()) {
WebElement element = node.To<WebElement>();
if (element.HasHTMLTagName("input") && element.HasAttribute("type")) {
TruncateAndAddStringAttribute(dst, ax::mojom::StringAttribute::kInputType,
element.GetAttribute("type").Utf8());
}
}
// aria-dropeffect is deprecated in WAI-ARIA 1.1.
WebVector<ax::mojom::Dropeffect> src_dropeffects;
src.Dropeffects(src_dropeffects);
if (!src_dropeffects.empty()) {
for (auto&& dropeffect : src_dropeffects) {
dst->AddDropeffect(dropeffect);
}
}
}
blink::WebDocument BlinkAXTreeSource::GetMainDocument() const {
CHECK(frozen_);
return document_;
}
// static
void BlinkAXTreeSource::IgnoreProtocolChecksForTesting() {
g_ignore_protocol_checks_for_testing = true;
}
WebAXObject BlinkAXTreeSource::ComputeRoot() const {
if (!explicit_root_.IsNull())
return explicit_root_;
if (!render_frame_ || !render_frame_->GetWebFrame())
return WebAXObject();
WebDocument document = render_frame_->GetWebFrame()->GetDocument();
if (!document.IsNull())
return WebAXObject::FromWebDocument(document);
return WebAXObject();
}
void BlinkAXTreeSource::TruncateAndAddStringAttribute(
ui::AXNodeData* dst,
ax::mojom::StringAttribute attribute,
const std::string& value,
uint32_t max_len) const {
if (value.size() > max_len) {
std::string truncated;
base::TruncateUTF8ToByteSize(value, max_len, &truncated);
dst->AddStringAttribute(attribute, truncated);
} else {
dst->AddStringAttribute(attribute, value);
}
}
void BlinkAXTreeSource::AddImageAnnotations(blink::WebAXObject& src,
ui::AXNodeData* dst) const {
if (!base::FeatureList::IsEnabled(features::kExperimentalAccessibilityLabels))
return;
// Reject ignored objects
if (src.AccessibilityIsIgnored()) {
return;
}
// Reject images that are explicitly empty, or that have a
// meaningful name already.
ax::mojom::NameFrom name_from;
blink::WebVector<WebAXObject> name_objects;
blink::WebString web_name = src.GetName(name_from, name_objects);
// If an image has a nonempty name, compute whether we should add an
// image annotation or not.
bool should_annotate_image_with_nonempty_name = false;
// When visual debugging is enabled, the "title" attribute is set to a
// string beginning with a "%". If the name comes from that string we
// can ignore it, and treat the name as empty.
if (image_annotation_debugging_ &&
base::StartsWith(web_name.Utf8(), "%", base::CompareCase::SENSITIVE))
should_annotate_image_with_nonempty_name = true;
if (features::IsAugmentExistingImageLabelsEnabled()) {
// If the name consists of mostly stopwords, we can add an image
// annotations. See ax_image_stopwords.h for details.
if (image_annotator_->ImageNameHasMostlyStopwords(web_name.Utf8()))
should_annotate_image_with_nonempty_name = true;
}
// If the image's name is explicitly empty, or if it has a name (and
// we're not treating the name as empty), then it's ineligible for
// an annotation.
if ((name_from == ax::mojom::NameFrom::kAttributeExplicitlyEmpty ||
!web_name.IsEmpty()) &&
!should_annotate_image_with_nonempty_name) {
dst->SetImageAnnotationStatus(
ax::mojom::ImageAnnotationStatus::kIneligibleForAnnotation);
return;
}
// If the name of a document (root web area) starts with the filename,
// it probably means the user opened an image in a new tab.
// If so, we can treat the name as empty and give it an annotation.
std::string dst_name =
dst->GetStringAttribute(ax::mojom::StringAttribute::kName);
if (ui::IsPlatformDocument(dst->role)) {
std::string filename = GURL(document().Url()).ExtractFileName();
if (base::StartsWith(dst_name, filename, base::CompareCase::SENSITIVE))
should_annotate_image_with_nonempty_name = true;
}
// |dst| may be a document or link containing an image. Skip annotating
// it if it already has text other than whitespace.
if (!base::ContainsOnlyChars(dst_name, base::kWhitespaceASCII) &&
!should_annotate_image_with_nonempty_name) {
dst->SetImageAnnotationStatus(
ax::mojom::ImageAnnotationStatus::kIneligibleForAnnotation);
return;
}
// Skip images that are too small to label. This also catches
// unloaded images where the size is unknown.
WebAXObject offset_container;
gfx::RectF bounds;
skia::Matrix44 container_transform;
bool clips_children = false;
src.GetRelativeBounds(offset_container, bounds, container_transform,
&clips_children);
if (bounds.width() < kMinImageAnnotationWidth ||
bounds.height() < kMinImageAnnotationHeight) {
dst->SetImageAnnotationStatus(
ax::mojom::ImageAnnotationStatus::kIneligibleForAnnotation);
return;
}
// Skip images in documents which are not http, https, file and data schemes.
blink::WebString protocol = document().GetSecurityOrigin().Protocol();
if (!g_ignore_protocol_checks_for_testing && protocol != url::kHttpScheme &&
protocol != url::kHttpsScheme && protocol != url::kFileScheme &&
protocol != url::kDataScheme) {
dst->SetImageAnnotationStatus(
ax::mojom::ImageAnnotationStatus::kWillNotAnnotateDueToScheme);
return;
}
if (!image_annotator_) {
if (!first_unlabeled_image_id_.has_value() ||
first_unlabeled_image_id_.value() == src.AxID()) {
dst->SetImageAnnotationStatus(
ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation);
first_unlabeled_image_id_ = src.AxID();
} else {
dst->SetImageAnnotationStatus(
ax::mojom::ImageAnnotationStatus::kSilentlyEligibleForAnnotation);
}
return;
}
if (image_annotator_->HasAnnotationInCache(src)) {
dst->AddStringAttribute(ax::mojom::StringAttribute::kImageAnnotation,
image_annotator_->GetImageAnnotation(src));
dst->SetImageAnnotationStatus(
image_annotator_->GetImageAnnotationStatus(src));
} else if (image_annotator_->HasImageInCache(src)) {
image_annotator_->OnImageUpdated(src);
dst->SetImageAnnotationStatus(
ax::mojom::ImageAnnotationStatus::kAnnotationPending);
} else if (!image_annotator_->HasImageInCache(src)) {
image_annotator_->OnImageAdded(src);
dst->SetImageAnnotationStatus(
ax::mojom::ImageAnnotationStatus::kAnnotationPending);
}
}
} // namespace content