1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "content/renderer/accessibility/accessibility_node_serializer.h"
6
7 #include <set>
8
9 #include "base/strings/string_number_conversions.h"
10 #include "base/strings/string_util.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "third_party/WebKit/public/platform/WebRect.h"
13 #include "third_party/WebKit/public/platform/WebSize.h"
14 #include "third_party/WebKit/public/platform/WebString.h"
15 #include "third_party/WebKit/public/platform/WebVector.h"
16 #include "third_party/WebKit/public/web/WebAXEnums.h"
17 #include "third_party/WebKit/public/web/WebAXObject.h"
18 #include "third_party/WebKit/public/web/WebDocument.h"
19 #include "third_party/WebKit/public/web/WebDocumentType.h"
20 #include "third_party/WebKit/public/web/WebElement.h"
21 #include "third_party/WebKit/public/web/WebFormControlElement.h"
22 #include "third_party/WebKit/public/web/WebFrame.h"
23 #include "third_party/WebKit/public/web/WebInputElement.h"
24 #include "third_party/WebKit/public/web/WebNode.h"
25
26 using blink::WebAXObject;
27 using blink::WebDocument;
28 using blink::WebDocumentType;
29 using blink::WebElement;
30 using blink::WebNode;
31 using blink::WebVector;
32
33 namespace content {
34 namespace {
35
36 // Returns true if |ancestor| is the first unignored parent of |child|,
37 // which means that when walking up the parent chain from |child|,
38 // |ancestor| is the *first* ancestor that isn't marked as
39 // accessibilityIsIgnored().
IsParentUnignoredOf(const WebAXObject & ancestor,const WebAXObject & child)40 bool IsParentUnignoredOf(const WebAXObject& ancestor,
41 const WebAXObject& child) {
42 WebAXObject parent = child.parentObject();
43 while (!parent.isDetached() && parent.accessibilityIsIgnored())
44 parent = parent.parentObject();
45 return parent.equals(ancestor);
46 }
47
IsTrue(std::string html_value)48 bool IsTrue(std::string html_value) {
49 return LowerCaseEqualsASCII(html_value, "true");
50 }
51
52 // Provides a conversion between the WebAXObject state
53 // accessors and a state bitmask that can be serialized and sent to the
54 // Browser process. Rare state are sent as boolean attributes instead.
ConvertState(const WebAXObject & o)55 uint32 ConvertState(const WebAXObject& o) {
56 uint32 state = 0;
57 if (o.isChecked())
58 state |= (1 << blink::WebAXStateChecked);
59
60 if (o.isCollapsed())
61 state |= (1 << blink::WebAXStateCollapsed);
62
63 if (o.canSetFocusAttribute())
64 state |= (1 << blink::WebAXStateFocusable);
65
66 if (o.isFocused())
67 state |= (1 << blink::WebAXStateFocused);
68
69 if (o.role() == blink::WebAXRolePopUpButton ||
70 o.ariaHasPopup()) {
71 state |= (1 << blink::WebAXStateHaspopup);
72 if (!o.isCollapsed())
73 state |= (1 << blink::WebAXStateExpanded);
74 }
75
76 if (o.isHovered())
77 state |= (1 << blink::WebAXStateHovered);
78
79 if (o.isIndeterminate())
80 state |= (1 << blink::WebAXStateIndeterminate);
81
82 if (!o.isVisible())
83 state |= (1 << blink::WebAXStateInvisible);
84
85 if (o.isLinked())
86 state |= (1 << blink::WebAXStateLinked);
87
88 if (o.isMultiSelectable())
89 state |= (1 << blink::WebAXStateMultiselectable);
90
91 if (o.isOffScreen())
92 state |= (1 << blink::WebAXStateOffscreen);
93
94 if (o.isPressed())
95 state |= (1 << blink::WebAXStatePressed);
96
97 if (o.isPasswordField())
98 state |= (1 << blink::WebAXStateProtected);
99
100 if (o.isReadOnly())
101 state |= (1 << blink::WebAXStateReadonly);
102
103 if (o.isRequired())
104 state |= (1 << blink::WebAXStateRequired);
105
106 if (o.canSetSelectedAttribute())
107 state |= (1 << blink::WebAXStateSelectable);
108
109 if (o.isSelected())
110 state |= (1 << blink::WebAXStateSelected);
111
112 if (o.isVisited())
113 state |= (1 << blink::WebAXStateVisited);
114
115 if (o.isEnabled())
116 state |= (1 << blink::WebAXStateEnabled);
117
118 if (o.isVertical())
119 state |= (1 << blink::WebAXStateVertical);
120
121 if (o.isVisited())
122 state |= (1 << blink::WebAXStateVisited);
123
124 return state;
125 }
126
127 } // Anonymous namespace
128
SerializeAccessibilityNode(const WebAXObject & src,AccessibilityNodeData * dst)129 void SerializeAccessibilityNode(
130 const WebAXObject& src,
131 AccessibilityNodeData* dst) {
132 dst->role = src.role();
133 dst->state = ConvertState(src);
134 dst->location = src.boundingBoxRect();
135 dst->id = src.axID();
136 std::string name = UTF16ToUTF8(src.title());
137
138 std::string value;
139 if (src.valueDescription().length()) {
140 dst->AddStringAttribute(dst->ATTR_VALUE,
141 UTF16ToUTF8(src.valueDescription()));
142 } else {
143 dst->AddStringAttribute(dst->ATTR_VALUE, UTF16ToUTF8(src.stringValue()));
144 }
145
146 if (dst->role == blink::WebAXRoleColorWell) {
147 int r, g, b;
148 src.colorValue(r, g, b);
149 dst->AddIntAttribute(dst->ATTR_COLOR_VALUE_RED, r);
150 dst->AddIntAttribute(dst->ATTR_COLOR_VALUE_GREEN, g);
151 dst->AddIntAttribute(dst->ATTR_COLOR_VALUE_BLUE, b);
152 }
153
154 if (dst->role == blink::WebAXRoleInlineTextBox) {
155 dst->AddIntAttribute(dst->ATTR_TEXT_DIRECTION, src.textDirection());
156
157 WebVector<int> src_character_offsets;
158 src.characterOffsets(src_character_offsets);
159 std::vector<int32> character_offsets;
160 character_offsets.reserve(src_character_offsets.size());
161 for (size_t i = 0; i < src_character_offsets.size(); ++i)
162 character_offsets.push_back(src_character_offsets[i]);
163 dst->AddIntListAttribute(dst->ATTR_CHARACTER_OFFSETS, character_offsets);
164
165 WebVector<int> src_word_starts;
166 WebVector<int> src_word_ends;
167 src.wordBoundaries(src_word_starts, src_word_ends);
168 std::vector<int32> word_starts;
169 std::vector<int32> word_ends;
170 word_starts.reserve(src_word_starts.size());
171 word_ends.reserve(src_word_starts.size());
172 for (size_t i = 0; i < src_word_starts.size(); ++i) {
173 word_starts.push_back(src_word_starts[i]);
174 word_ends.push_back(src_word_ends[i]);
175 }
176 dst->AddIntListAttribute(dst->ATTR_WORD_STARTS, word_starts);
177 dst->AddIntListAttribute(dst->ATTR_WORD_ENDS, word_ends);
178 }
179
180 if (src.accessKey().length())
181 dst->AddStringAttribute(dst->ATTR_ACCESS_KEY, UTF16ToUTF8(src.accessKey()));
182 if (src.actionVerb().length())
183 dst->AddStringAttribute(dst->ATTR_ACTION, UTF16ToUTF8(src.actionVerb()));
184 if (src.isAriaReadOnly())
185 dst->AddBoolAttribute(dst->ATTR_ARIA_READONLY, true);
186 if (src.isButtonStateMixed())
187 dst->AddBoolAttribute(dst->ATTR_BUTTON_MIXED, true);
188 if (src.canSetValueAttribute())
189 dst->AddBoolAttribute(dst->ATTR_CAN_SET_VALUE, true);
190 if (src.accessibilityDescription().length()) {
191 dst->AddStringAttribute(dst->ATTR_DESCRIPTION,
192 UTF16ToUTF8(src.accessibilityDescription()));
193 }
194 if (src.hasComputedStyle()) {
195 dst->AddStringAttribute(dst->ATTR_DISPLAY,
196 UTF16ToUTF8(src.computedStyleDisplay()));
197 }
198 if (src.helpText().length())
199 dst->AddStringAttribute(dst->ATTR_HELP, UTF16ToUTF8(src.helpText()));
200 if (src.keyboardShortcut().length()) {
201 dst->AddStringAttribute(dst->ATTR_SHORTCUT,
202 UTF16ToUTF8(src.keyboardShortcut()));
203 }
204 if (!src.titleUIElement().isDetached()) {
205 dst->AddIntAttribute(dst->ATTR_TITLE_UI_ELEMENT,
206 src.titleUIElement().axID());
207 }
208 if (!src.url().isEmpty())
209 dst->AddStringAttribute(dst->ATTR_URL, src.url().spec());
210
211 if (dst->role == blink::WebAXRoleHeading)
212 dst->AddIntAttribute(dst->ATTR_HIERARCHICAL_LEVEL, src.headingLevel());
213 else if ((dst->role == blink::WebAXRoleTreeItem ||
214 dst->role == blink::WebAXRoleRow) &&
215 src.hierarchicalLevel() > 0) {
216 dst->AddIntAttribute(dst->ATTR_HIERARCHICAL_LEVEL, src.hierarchicalLevel());
217 }
218
219 // Treat the active list box item as focused.
220 if (dst->role == blink::WebAXRoleListBoxOption &&
221 src.isSelectedOptionActive()) {
222 dst->state |= (1 << blink::WebAXStateFocused);
223 }
224
225 if (src.canvasHasFallbackContent())
226 dst->AddBoolAttribute(dst->ATTR_CANVAS_HAS_FALLBACK, true);
227
228 WebNode node = src.node();
229 bool is_iframe = false;
230 std::string live_atomic;
231 std::string live_busy;
232 std::string live_status;
233 std::string live_relevant;
234
235 if (!node.isNull() && node.isElementNode()) {
236 WebElement element = node.to<WebElement>();
237 is_iframe = (element.tagName() == ASCIIToUTF16("IFRAME"));
238
239 if (LowerCaseEqualsASCII(element.getAttribute("aria-expanded"), "true"))
240 dst->state |= (1 << blink::WebAXStateExpanded);
241
242 // TODO(ctguil): The tagName in WebKit is lower cased but
243 // HTMLElement::nodeName calls localNameUpper. Consider adding
244 // a WebElement method that returns the original lower cased tagName.
245 dst->AddStringAttribute(
246 dst->ATTR_HTML_TAG,
247 StringToLowerASCII(UTF16ToUTF8(element.tagName())));
248 for (unsigned i = 0; i < element.attributeCount(); ++i) {
249 std::string name = StringToLowerASCII(UTF16ToUTF8(
250 element.attributeLocalName(i)));
251 std::string value = UTF16ToUTF8(element.attributeValue(i));
252 dst->html_attributes.push_back(std::make_pair(name, value));
253 }
254
255 if (dst->role == blink::WebAXRoleEditableText ||
256 dst->role == blink::WebAXRoleTextArea ||
257 dst->role == blink::WebAXRoleTextField) {
258 dst->AddIntAttribute(dst->ATTR_TEXT_SEL_START, src.selectionStart());
259 dst->AddIntAttribute(dst->ATTR_TEXT_SEL_END, src.selectionEnd());
260
261 WebVector<int> src_line_breaks;
262 src.lineBreaks(src_line_breaks);
263 if (src_line_breaks.size() > 0) {
264 std::vector<int32> line_breaks;
265 line_breaks.reserve(src_line_breaks.size());
266 for (size_t i = 0; i < src_line_breaks.size(); ++i)
267 line_breaks.push_back(src_line_breaks[i]);
268 dst->AddIntListAttribute(dst->ATTR_LINE_BREAKS, line_breaks);
269 }
270 }
271
272 // ARIA role.
273 if (element.hasAttribute("role")) {
274 dst->AddStringAttribute(dst->ATTR_ROLE,
275 UTF16ToUTF8(element.getAttribute("role")));
276 }
277
278 // Live region attributes
279 live_atomic = UTF16ToUTF8(element.getAttribute("aria-atomic"));
280 live_busy = UTF16ToUTF8(element.getAttribute("aria-busy"));
281 live_status = UTF16ToUTF8(element.getAttribute("aria-live"));
282 live_relevant = UTF16ToUTF8(element.getAttribute("aria-relevant"));
283 }
284
285 // Walk up the parent chain to set live region attributes of containers
286 std::string container_live_atomic;
287 std::string container_live_busy;
288 std::string container_live_status;
289 std::string container_live_relevant;
290 WebAXObject container_accessible = src;
291 while (!container_accessible.isDetached()) {
292 WebNode container_node = container_accessible.node();
293 if (!container_node.isNull() && container_node.isElementNode()) {
294 WebElement container_elem = container_node.to<WebElement>();
295 if (container_elem.hasAttribute("aria-atomic") &&
296 container_live_atomic.empty()) {
297 container_live_atomic =
298 UTF16ToUTF8(container_elem.getAttribute("aria-atomic"));
299 }
300 if (container_elem.hasAttribute("aria-busy") &&
301 container_live_busy.empty()) {
302 container_live_busy =
303 UTF16ToUTF8(container_elem.getAttribute("aria-busy"));
304 }
305 if (container_elem.hasAttribute("aria-live") &&
306 container_live_status.empty()) {
307 container_live_status =
308 UTF16ToUTF8(container_elem.getAttribute("aria-live"));
309 }
310 if (container_elem.hasAttribute("aria-relevant") &&
311 container_live_relevant.empty()) {
312 container_live_relevant =
313 UTF16ToUTF8(container_elem.getAttribute("aria-relevant"));
314 }
315 }
316 container_accessible = container_accessible.parentObject();
317 }
318
319 if (!live_atomic.empty())
320 dst->AddBoolAttribute(dst->ATTR_LIVE_ATOMIC, IsTrue(live_atomic));
321 if (!live_busy.empty())
322 dst->AddBoolAttribute(dst->ATTR_LIVE_BUSY, IsTrue(live_busy));
323 if (!live_status.empty())
324 dst->AddStringAttribute(dst->ATTR_LIVE_STATUS, live_status);
325 if (!live_relevant.empty())
326 dst->AddStringAttribute(dst->ATTR_LIVE_RELEVANT, live_relevant);
327
328 if (!container_live_atomic.empty()) {
329 dst->AddBoolAttribute(dst->ATTR_CONTAINER_LIVE_ATOMIC,
330 IsTrue(container_live_atomic));
331 }
332 if (!container_live_busy.empty()) {
333 dst->AddBoolAttribute(dst->ATTR_CONTAINER_LIVE_BUSY,
334 IsTrue(container_live_busy));
335 }
336 if (!container_live_status.empty()) {
337 dst->AddStringAttribute(dst->ATTR_CONTAINER_LIVE_STATUS,
338 container_live_status);
339 }
340 if (!container_live_relevant.empty()) {
341 dst->AddStringAttribute(dst->ATTR_CONTAINER_LIVE_RELEVANT,
342 container_live_relevant);
343 }
344
345 if (dst->role == blink::WebAXRoleProgressIndicator ||
346 dst->role == blink::WebAXRoleScrollBar ||
347 dst->role == blink::WebAXRoleSlider ||
348 dst->role == blink::WebAXRoleSpinButton) {
349 dst->AddFloatAttribute(dst->ATTR_VALUE_FOR_RANGE, src.valueForRange());
350 dst->AddFloatAttribute(dst->ATTR_MAX_VALUE_FOR_RANGE,
351 src.maxValueForRange());
352 dst->AddFloatAttribute(dst->ATTR_MIN_VALUE_FOR_RANGE,
353 src.minValueForRange());
354 }
355
356 if (dst->role == blink::WebAXRoleDocument ||
357 dst->role == blink::WebAXRoleWebArea) {
358 dst->AddStringAttribute(dst->ATTR_HTML_TAG, "#document");
359 const WebDocument& document = src.document();
360 if (name.empty())
361 name = UTF16ToUTF8(document.title());
362 dst->AddStringAttribute(dst->ATTR_DOC_TITLE, UTF16ToUTF8(document.title()));
363 dst->AddStringAttribute(dst->ATTR_DOC_URL, document.url().spec());
364 dst->AddStringAttribute(
365 dst->ATTR_DOC_MIMETYPE,
366 document.isXHTMLDocument() ? "text/xhtml" : "text/html");
367 dst->AddBoolAttribute(dst->ATTR_DOC_LOADED, src.isLoaded());
368 dst->AddFloatAttribute(dst->ATTR_DOC_LOADING_PROGRESS,
369 src.estimatedLoadingProgress());
370
371 const WebDocumentType& doctype = document.doctype();
372 if (!doctype.isNull()) {
373 dst->AddStringAttribute(dst->ATTR_DOC_DOCTYPE,
374 UTF16ToUTF8(doctype.name()));
375 }
376
377 const gfx::Size& scroll_offset = document.frame()->scrollOffset();
378 dst->AddIntAttribute(dst->ATTR_SCROLL_X, scroll_offset.width());
379 dst->AddIntAttribute(dst->ATTR_SCROLL_Y, scroll_offset.height());
380
381 const gfx::Size& min_offset = document.frame()->minimumScrollOffset();
382 dst->AddIntAttribute(dst->ATTR_SCROLL_X_MIN, min_offset.width());
383 dst->AddIntAttribute(dst->ATTR_SCROLL_Y_MIN, min_offset.height());
384
385 const gfx::Size& max_offset = document.frame()->maximumScrollOffset();
386 dst->AddIntAttribute(dst->ATTR_SCROLL_X_MAX, max_offset.width());
387 dst->AddIntAttribute(dst->ATTR_SCROLL_Y_MAX, max_offset.height());
388 }
389
390 if (dst->role == blink::WebAXRoleTable) {
391 int column_count = src.columnCount();
392 int row_count = src.rowCount();
393 if (column_count > 0 && row_count > 0) {
394 std::set<int32> unique_cell_id_set;
395 std::vector<int32> cell_ids;
396 std::vector<int32> unique_cell_ids;
397 dst->AddIntAttribute(dst->ATTR_TABLE_COLUMN_COUNT, column_count);
398 dst->AddIntAttribute(dst->ATTR_TABLE_ROW_COUNT, row_count);
399 WebAXObject header = src.headerContainerObject();
400 if (!header.isDetached())
401 dst->AddIntAttribute(dst->ATTR_TABLE_HEADER_ID, header.axID());
402 for (int i = 0; i < column_count * row_count; ++i) {
403 WebAXObject cell = src.cellForColumnAndRow(
404 i % column_count, i / column_count);
405 int cell_id = -1;
406 if (!cell.isDetached()) {
407 cell_id = cell.axID();
408 if (unique_cell_id_set.find(cell_id) == unique_cell_id_set.end()) {
409 unique_cell_id_set.insert(cell_id);
410 unique_cell_ids.push_back(cell_id);
411 }
412 }
413 cell_ids.push_back(cell_id);
414 }
415 dst->AddIntListAttribute(dst->ATTR_CELL_IDS, cell_ids);
416 dst->AddIntListAttribute(dst->ATTR_UNIQUE_CELL_IDS, unique_cell_ids);
417 }
418 }
419
420 if (dst->role == blink::WebAXRoleRow) {
421 dst->AddIntAttribute(dst->ATTR_TABLE_ROW_INDEX, src.rowIndex());
422 WebAXObject header = src.rowHeader();
423 if (!header.isDetached())
424 dst->AddIntAttribute(dst->ATTR_TABLE_ROW_HEADER_ID, header.axID());
425 }
426
427 if (dst->role == blink::WebAXRoleColumn) {
428 dst->AddIntAttribute(dst->ATTR_TABLE_COLUMN_INDEX, src.columnIndex());
429 WebAXObject header = src.columnHeader();
430 if (!header.isDetached())
431 dst->AddIntAttribute(dst->ATTR_TABLE_COLUMN_HEADER_ID, header.axID());
432 }
433
434 if (dst->role == blink::WebAXRoleCell ||
435 dst->role == blink::WebAXRoleRowHeader ||
436 dst->role == blink::WebAXRoleColumnHeader) {
437 dst->AddIntAttribute(dst->ATTR_TABLE_CELL_COLUMN_INDEX,
438 src.cellColumnIndex());
439 dst->AddIntAttribute(dst->ATTR_TABLE_CELL_COLUMN_SPAN,
440 src.cellColumnSpan());
441 dst->AddIntAttribute(dst->ATTR_TABLE_CELL_ROW_INDEX, src.cellRowIndex());
442 dst->AddIntAttribute(dst->ATTR_TABLE_CELL_ROW_SPAN, src.cellRowSpan());
443 }
444
445 dst->AddStringAttribute(dst->ATTR_NAME, name);
446
447 // Add the ids of *indirect* children - those who are children of this node,
448 // but whose parent is *not* this node. One example is a table
449 // cell, which is a child of both a row and a column. Because the cell's
450 // parent is the row, the row adds it as a child, and the column adds it
451 // as an indirect child.
452 int child_count = src.childCount();
453 for (int i = 0; i < child_count; ++i) {
454 WebAXObject child = src.childAt(i);
455 std::vector<int32> indirect_child_ids;
456 if (!is_iframe && !child.isDetached() && !IsParentUnignoredOf(src, child))
457 indirect_child_ids.push_back(child.axID());
458 if (indirect_child_ids.size() > 0) {
459 dst->AddIntListAttribute(
460 dst->ATTR_INDIRECT_CHILD_IDS, indirect_child_ids);
461 }
462 }
463 }
464
ShouldIncludeChildNode(const WebAXObject & parent,const WebAXObject & child)465 bool ShouldIncludeChildNode(
466 const WebAXObject& parent,
467 const WebAXObject& child) {
468 switch(parent.role()) {
469 case blink::WebAXRoleSlider:
470 case blink::WebAXRoleEditableText:
471 case blink::WebAXRoleTextArea:
472 case blink::WebAXRoleTextField:
473 return false;
474 default:
475 break;
476 }
477
478 // The child may be invalid due to issues in webkit accessibility code.
479 // Don't add children that are invalid thus preventing a crash.
480 // https://bugs.webkit.org/show_bug.cgi?id=44149
481 // TODO(ctguil): We may want to remove this check as webkit stabilizes.
482 if (child.isDetached())
483 return false;
484
485 // Skip children whose parent isn't this - see indirect_child_ids, above.
486 // As an exception, include children of an iframe element.
487 bool is_iframe = false;
488 WebNode node = parent.node();
489 if (!node.isNull() && node.isElementNode()) {
490 WebElement element = node.to<WebElement>();
491 is_iframe = (element.tagName() == ASCIIToUTF16("IFRAME"));
492 }
493
494 return (is_iframe || IsParentUnignoredOf(parent, child));
495 }
496
497 } // namespace content
498