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(). 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 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. 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 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 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