• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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