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/browser/accessibility/accessibility_tree_formatter_base.h" 6 7#include "base/files/file_path.h" 8#include "base/strings/string_number_conversions.h" 9#include "base/strings/stringprintf.h" 10#include "base/strings/sys_string_conversions.h" 11#include "base/strings/utf_string_conversions.h" 12#include "base/values.h" 13#include "content/browser/accessibility/accessibility_tools_utils_mac.h" 14#include "content/browser/accessibility/accessibility_tree_formatter_blink.h" 15#include "content/browser/accessibility/accessibility_tree_formatter_utils_mac.h" 16#include "content/browser/accessibility/browser_accessibility_mac.h" 17#include "content/browser/accessibility/browser_accessibility_manager.h" 18#include "ui/accessibility/platform/inspect/property_node.h" 19 20// This file uses the deprecated NSObject accessibility interface. 21// TODO(crbug.com/948844): Migrate to the new NSAccessibility interface. 22#pragma clang diagnostic push 23#pragma clang diagnostic ignored "-Wdeprecated-declarations" 24 25using base::StringPrintf; 26using base::SysNSStringToUTF8; 27using base::SysNSStringToUTF16; 28using content::a11y::AttributeInvoker; 29using content::a11y::AttributeNamesOf; 30using content::a11y::AttributeValueOf; 31using content::a11y::ChildrenOf; 32using content::a11y::IsAXUIElement; 33using content::a11y::IsBrowserAccessibilityCocoa; 34using content::a11y::LineIndexer; 35using content::a11y::OptionalNSObject; 36using std::string; 37using ui::AXPropertyNode; 38 39namespace content { 40 41namespace { 42 43const char kPositionDictAttr[] = "position"; 44const char kXCoordDictAttr[] = "x"; 45const char kYCoordDictAttr[] = "y"; 46const char kSizeDictAttr[] = "size"; 47const char kWidthDictAttr[] = "width"; 48const char kHeightDictAttr[] = "height"; 49const char kRangeLocDictAttr[] = "loc"; 50const char kRangeLenDictAttr[] = "len"; 51 52const char kSetKeyPrefixDictAttr[] = "_setkey_"; 53const char kConstValuePrefix[] = "_const_"; 54const char kNULLValue[] = "_const_NULL"; 55const char kFailedToParseArgsError[] = "_const_ERROR:FAILED_TO_PARSE_ARGS"; 56 57} // namespace 58 59class AccessibilityTreeFormatterMac : public AccessibilityTreeFormatterBase { 60 public: 61 explicit AccessibilityTreeFormatterMac(); 62 ~AccessibilityTreeFormatterMac() override; 63 64 void AddDefaultFilters( 65 std::vector<AXPropertyFilter>* property_filters) override; 66 67 std::unique_ptr<base::DictionaryValue> BuildAccessibilityTree( 68 BrowserAccessibility* root) override; 69 base::Value BuildTreeForWindow(gfx::AcceleratedWidget widget) const override; 70 base::Value BuildTreeForSelector( 71 const AXTreeSelector& selector) const override; 72 73 private: 74 base::Value BuildTreeForAXUIElement(AXUIElementRef node) const; 75 76 void RecursiveBuildTree(const id node, 77 const LineIndexer* line_indexer, 78 base::Value* dict) const; 79 80 void AddProperties(const id node, 81 const LineIndexer* line_indexer, 82 base::Value* dict) const; 83 84 // Invokes an attributes by a property node. 85 OptionalNSObject InvokeAttributeFor( 86 const BrowserAccessibilityCocoa* cocoa_node, 87 const AXPropertyNode& property_node, 88 const LineIndexer* line_indexer) const; 89 90 base::Value PopulateSize(const BrowserAccessibilityCocoa*) const; 91 base::Value PopulatePosition(const BrowserAccessibilityCocoa*) const; 92 base::Value PopulatePoint(NSPoint) const; 93 base::Value PopulateSize(NSSize) const; 94 base::Value PopulateRect(NSRect) const; 95 base::Value PopulateRange(NSRange) const; 96 base::Value PopulateTextPosition( 97 BrowserAccessibilityPosition::AXPositionInstance::pointer, 98 const LineIndexer*) const; 99 base::Value PopulateTextMarkerRange(id, const LineIndexer*) const; 100 base::Value PopulateObject(id, const LineIndexer* line_indexer) const; 101 base::Value PopulateArray(NSArray*, const LineIndexer* line_indexer) const; 102 103 std::string NodeToLineIndex(id, const LineIndexer*) const; 104 105 std::string ProcessTreeForOutput( 106 const base::DictionaryValue& node, 107 base::DictionaryValue* filtered_dict_result = nullptr) override; 108 109 std::string FormatAttributeValue(const base::Value& value); 110}; 111 112// static 113std::unique_ptr<ui::AXTreeFormatter> AccessibilityTreeFormatter::Create() { 114 return std::make_unique<AccessibilityTreeFormatterMac>(); 115} 116 117// static 118std::vector<AccessibilityTreeFormatter::TestPass> 119AccessibilityTreeFormatter::GetTestPasses() { 120 return { 121 {"blink", &AccessibilityTreeFormatterBlink::CreateBlink}, 122 {"mac", &AccessibilityTreeFormatter::Create}, 123 }; 124} 125 126AccessibilityTreeFormatterMac::AccessibilityTreeFormatterMac() {} 127 128AccessibilityTreeFormatterMac::~AccessibilityTreeFormatterMac() {} 129 130void AccessibilityTreeFormatterMac::AddDefaultFilters( 131 std::vector<AXPropertyFilter>* property_filters) { 132 static NSArray* default_attributes = [@[ 133 @"AXAutocompleteValue=*", @"AXDescription=*", @"AXRole=*", @"AXTitle=*", 134 @"AXTitleUIElement=*", @"AXHelp=*", @"AXValue=*" 135 ] retain]; 136 137 for (NSString* attribute : default_attributes) { 138 AddPropertyFilter(property_filters, SysNSStringToUTF8(attribute)); 139 } 140 141 if (show_ids()) { 142 AddPropertyFilter(property_filters, "id"); 143 } 144} 145 146std::unique_ptr<base::DictionaryValue> 147AccessibilityTreeFormatterMac::BuildAccessibilityTree( 148 BrowserAccessibility* root) { 149 DCHECK(root); 150 BrowserAccessibilityCocoa* cocoa_root = ToBrowserAccessibilityCocoa(root); 151 LineIndexer line_indexer(cocoa_root); 152 std::unique_ptr<base::DictionaryValue> dict(new base::DictionaryValue); 153 RecursiveBuildTree(cocoa_root, &line_indexer, dict.get()); 154 return dict; 155} 156 157base::Value AccessibilityTreeFormatterMac::BuildTreeForWindow( 158 gfx::AcceleratedWidget widget) const { 159 return BuildTreeForAXUIElement(AXUIElementCreateApplication(widget)); 160} 161 162base::Value AccessibilityTreeFormatterMac::BuildTreeForSelector( 163 const AXTreeSelector& selector) const { 164 AXUIElementRef node = nil; 165 std::tie(node, std::ignore) = a11y::FindAXUIElement(selector); 166 if (node == nil) { 167 return base::Value(base::Value::Type::DICTIONARY); 168 } 169 return BuildTreeForAXUIElement(node); 170} 171 172base::Value AccessibilityTreeFormatterMac::BuildTreeForAXUIElement( 173 AXUIElementRef node) const { 174 LineIndexer line_indexer(static_cast<id>(node)); 175 176 base::Value dict(base::Value::Type::DICTIONARY); 177 RecursiveBuildTree(static_cast<id>(node), &line_indexer, &dict); 178 return dict; 179} 180 181void AccessibilityTreeFormatterMac::RecursiveBuildTree( 182 const id node, 183 const LineIndexer* line_indexer, 184 base::Value* dict) const { 185 AddProperties(node, line_indexer, dict); 186 187 NSArray* children = ChildrenOf(node); 188 base::Value child_dict_list(base::Value::Type::LIST); 189 for (id child in children) { 190 base::Value child_dict(base::Value::Type::DICTIONARY); 191 RecursiveBuildTree(child, line_indexer, &child_dict); 192 child_dict_list.Append(std::move(child_dict)); 193 } 194 dict->SetPath(kChildrenDictAttr, std::move(child_dict_list)); 195} 196 197void AccessibilityTreeFormatterMac::AddProperties( 198 const id node, 199 const LineIndexer* line_indexer, 200 base::Value* dict) const { 201 // Chromium tree special processing 202 if (IsBrowserAccessibilityCocoa(node)) { 203 BrowserAccessibilityCocoa* cocoa_node = 204 static_cast<BrowserAccessibilityCocoa*>(node); 205 206 // DOM element id 207 BrowserAccessibility* owner_node = [cocoa_node owner]; 208 dict->SetKey("id", 209 base::Value(base::NumberToString16(owner_node->GetId()))); 210 211 // Position and size 212 dict->SetPath(kPositionDictAttr, PopulatePosition(cocoa_node)); 213 dict->SetPath(kSizeDictAttr, PopulateSize(cocoa_node)); 214 } 215 216 // Dump all attributes if match-all filter is specified. 217 if (HasMatchAllPropertyFilter()) { 218 NSArray* attributes = AttributeNamesOf(node); 219 for (NSString* attribute : attributes) { 220 dict->SetPath( 221 SysNSStringToUTF8(attribute), 222 PopulateObject(AttributeValueOf(node, attribute), line_indexer)); 223 } 224 return; 225 } 226 227 // Otherwise dump attributes matching allow filters only. 228 std::string line_index = line_indexer->IndexBy(node); 229 for (const AXPropertyNode& property_node : 230 PropertyFilterNodesFor(line_index)) { 231 AttributeInvoker invoker(node, line_indexer); 232 OptionalNSObject value = invoker.Invoke(property_node); 233 if (value.IsNotApplicable()) { 234 continue; 235 } 236 if (value.IsError()) { 237 dict->SetPath(property_node.original_property, 238 base::Value(kFailedToParseArgsError)); 239 continue; 240 } 241 dict->SetPath(property_node.original_property, 242 PopulateObject(*value, line_indexer)); 243 } 244} 245 246base::Value AccessibilityTreeFormatterMac::PopulateSize( 247 const BrowserAccessibilityCocoa* cocoa_node) const { 248 base::Value size(base::Value::Type::DICTIONARY); 249 NSSize node_size = [[cocoa_node size] sizeValue]; 250 size.SetIntPath(kHeightDictAttr, static_cast<int>(node_size.height)); 251 size.SetIntPath(kWidthDictAttr, static_cast<int>(node_size.width)); 252 return size; 253} 254 255base::Value AccessibilityTreeFormatterMac::PopulatePosition( 256 const BrowserAccessibilityCocoa* cocoa_node) const { 257 BrowserAccessibility* node = [cocoa_node owner]; 258 BrowserAccessibilityManager* root_manager = node->manager()->GetRootManager(); 259 DCHECK(root_manager); 260 261 // The NSAccessibility position of an object is in global coordinates and 262 // based on the lower-left corner of the object. To make this easier and 263 // less confusing, convert it to local window coordinates using the top-left 264 // corner when dumping the position. 265 BrowserAccessibility* root = root_manager->GetRoot(); 266 BrowserAccessibilityCocoa* cocoa_root = ToBrowserAccessibilityCocoa(root); 267 NSPoint root_position = [[cocoa_root position] pointValue]; 268 NSSize root_size = [[cocoa_root size] sizeValue]; 269 int root_top = -static_cast<int>(root_position.y + root_size.height); 270 int root_left = static_cast<int>(root_position.x); 271 272 NSPoint node_position = [[cocoa_node position] pointValue]; 273 NSSize node_size = [[cocoa_node size] sizeValue]; 274 275 base::Value position(base::Value::Type::DICTIONARY); 276 position.SetIntPath(kXCoordDictAttr, 277 static_cast<int>(node_position.x - root_left)); 278 position.SetIntPath( 279 kYCoordDictAttr, 280 static_cast<int>(-node_position.y - node_size.height - root_top)); 281 return position; 282} 283 284base::Value AccessibilityTreeFormatterMac::PopulateObject( 285 id value, 286 const LineIndexer* line_indexer) const { 287 if (value == nil) { 288 return base::Value(kNULLValue); 289 } 290 291 // NSArray 292 if ([value isKindOfClass:[NSArray class]]) { 293 return PopulateArray((NSArray*)value, line_indexer); 294 } 295 296 // NSNumber 297 if ([value isKindOfClass:[NSNumber class]]) { 298 return base::Value([value intValue]); 299 } 300 301 // NSRange 302 if ([value isKindOfClass:[NSValue class]] && 303 0 == strcmp([value objCType], @encode(NSRange))) { 304 return PopulateRange([value rangeValue]); 305 } 306 307 // AXTextMarker 308 if (content::IsAXTextMarker(value)) { 309 return PopulateTextPosition(content::AXTextMarkerToPosition(value).get(), 310 line_indexer); 311 } 312 313 // AXTextMarkerRange 314 if (content::IsAXTextMarkerRange(value)) { 315 return PopulateTextMarkerRange(value, line_indexer); 316 } 317 318 // AXValue 319 if (CFGetTypeID(value) == AXValueGetTypeID()) { 320 AXValueType type = AXValueGetType(static_cast<AXValueRef>(value)); 321 switch (type) { 322 case kAXValueCGPointType: { 323 NSPoint point; 324 if (AXValueGetValue(static_cast<AXValueRef>(value), type, &point)) { 325 return PopulatePoint(point); 326 } 327 } break; 328 case kAXValueCGSizeType: { 329 NSSize size; 330 if (AXValueGetValue(static_cast<AXValueRef>(value), type, &size)) { 331 return PopulateSize(size); 332 } 333 } break; 334 case kAXValueCGRectType: { 335 NSRect rect; 336 if (AXValueGetValue(static_cast<AXValueRef>(value), type, &rect)) { 337 return PopulateRect(rect); 338 } 339 } break; 340 case kAXValueCFRangeType: { 341 NSRange range; 342 if (AXValueGetValue(static_cast<AXValueRef>(value), type, &range)) { 343 return PopulateRange(range); 344 } 345 } break; 346 default: 347 break; 348 } 349 } 350 351 // Accessible object 352 if (IsBrowserAccessibilityCocoa(value) || IsAXUIElement(value)) { 353 return base::Value(NodeToLineIndex(value, line_indexer)); 354 } 355 356 // Scalar value. 357 return base::Value( 358 SysNSStringToUTF16([NSString stringWithFormat:@"%@", value])); 359} 360 361base::Value AccessibilityTreeFormatterMac::PopulatePoint( 362 NSPoint point_value) const { 363 base::Value point(base::Value::Type::DICTIONARY); 364 point.SetIntPath("x", static_cast<int>(point_value.x)); 365 point.SetIntPath("y", static_cast<int>(point_value.y)); 366 return point; 367} 368 369base::Value AccessibilityTreeFormatterMac::PopulateSize( 370 NSSize size_value) const { 371 base::Value size(base::Value::Type::DICTIONARY); 372 size.SetIntPath("w", static_cast<int>(size_value.width)); 373 size.SetIntPath("h", static_cast<int>(size_value.height)); 374 return size; 375} 376 377base::Value AccessibilityTreeFormatterMac::PopulateRect( 378 NSRect rect_value) const { 379 base::Value rect(base::Value::Type::DICTIONARY); 380 rect.SetIntPath("x", static_cast<int>(rect_value.origin.x)); 381 rect.SetIntPath("y", static_cast<int>(rect_value.origin.y)); 382 rect.SetIntPath("w", static_cast<int>(rect_value.size.width)); 383 rect.SetIntPath("h", static_cast<int>(rect_value.size.height)); 384 return rect; 385} 386 387base::Value AccessibilityTreeFormatterMac::PopulateRange( 388 NSRange node_range) const { 389 base::Value range(base::Value::Type::DICTIONARY); 390 range.SetIntPath(kRangeLocDictAttr, static_cast<int>(node_range.location)); 391 range.SetIntPath(kRangeLenDictAttr, static_cast<int>(node_range.length)); 392 return range; 393} 394 395base::Value AccessibilityTreeFormatterMac::PopulateTextPosition( 396 BrowserAccessibilityPosition::AXPositionInstance::pointer position, 397 const LineIndexer* line_indexer) const { 398 if (position->IsNullPosition()) { 399 return base::Value(kNULLValue); 400 } 401 402 BrowserAccessibility* anchor = position->GetAnchor(); 403 BrowserAccessibilityCocoa* cocoa_anchor = ToBrowserAccessibilityCocoa(anchor); 404 405 std::string affinity; 406 switch (position->affinity()) { 407 case ax::mojom::TextAffinity::kNone: 408 affinity = "none"; 409 break; 410 case ax::mojom::TextAffinity::kDownstream: 411 affinity = "down"; 412 break; 413 case ax::mojom::TextAffinity::kUpstream: 414 affinity = "up"; 415 break; 416 } 417 418 base::Value set(base::Value::Type::DICTIONARY); 419 const std::string setkey_prefix = kSetKeyPrefixDictAttr; 420 set.SetStringPath(setkey_prefix + "index1_anchor", 421 NodeToLineIndex(cocoa_anchor, line_indexer)); 422 set.SetIntPath(setkey_prefix + "index2_offset", position->text_offset()); 423 set.SetStringPath(setkey_prefix + "index3_affinity", 424 kConstValuePrefix + affinity); 425 return set; 426} 427 428base::Value AccessibilityTreeFormatterMac::PopulateTextMarkerRange( 429 id object, 430 const LineIndexer* line_indexer) const { 431 auto range = content::AXTextMarkerRangeToRange(object); 432 if (range.IsNull()) { 433 return base::Value(kNULLValue); 434 } 435 436 base::Value dict(base::Value::Type::DICTIONARY); 437 dict.SetPath("anchor", PopulateTextPosition(range.anchor(), line_indexer)); 438 dict.SetPath("focus", PopulateTextPosition(range.focus(), line_indexer)); 439 return dict; 440} 441 442base::Value AccessibilityTreeFormatterMac::PopulateArray( 443 NSArray* node_array, 444 const LineIndexer* line_indexer) const { 445 base::Value list(base::Value::Type::LIST); 446 for (NSUInteger i = 0; i < [node_array count]; i++) 447 list.Append(PopulateObject([node_array objectAtIndex:i], line_indexer)); 448 return list; 449} 450 451std::string AccessibilityTreeFormatterMac::NodeToLineIndex( 452 id node, 453 const LineIndexer* line_indexer) const { 454 return kConstValuePrefix + line_indexer->IndexBy(node); 455} 456 457std::string AccessibilityTreeFormatterMac::ProcessTreeForOutput( 458 const base::DictionaryValue& dict, 459 base::DictionaryValue* filtered_dict_result) { 460 std::string error_value; 461 if (dict.GetString("error", &error_value)) 462 return error_value; 463 464 std::string line; 465 466 // AXRole and AXSubrole have own formatting and should be listed upfront. 467 std::string role_attr = SysNSStringToUTF8(NSAccessibilityRoleAttribute); 468 const std::string* value = dict.FindStringPath(role_attr); 469 if (value) { 470 WriteAttribute(true, *value, &line); 471 } 472 std::string subrole_attr = SysNSStringToUTF8(NSAccessibilitySubroleAttribute); 473 value = dict.FindStringPath(subrole_attr); 474 if (value) { 475 if (*value == kNULLValue) { 476 WriteAttribute(false, StringPrintf("%s=NULL", subrole_attr.c_str()), 477 &line); 478 } else { 479 WriteAttribute( 480 false, StringPrintf("%s=%s", subrole_attr.c_str(), value->c_str()), 481 &line); 482 } 483 } 484 485 // Expose all other attributes. 486 for (auto item : dict.DictItems()) { 487 if (item.second.is_string() && 488 (item.first == role_attr || item.first == subrole_attr)) { 489 continue; 490 } 491 492 // Special case: children. 493 // Children are used to generate the tree 494 // itself, thus no sense to expose them on each node. 495 if (item.first == kChildrenDictAttr) { 496 continue; 497 } 498 // Special case: position. 499 if (item.first == kPositionDictAttr) { 500 WriteAttribute(false, 501 FormatCoordinates( 502 base::Value::AsDictionaryValue(item.second), 503 kPositionDictAttr, kXCoordDictAttr, kYCoordDictAttr), 504 &line); 505 continue; 506 } 507 // Special case: size. 508 if (item.first == kSizeDictAttr) { 509 WriteAttribute( 510 false, 511 FormatCoordinates(base::Value::AsDictionaryValue(item.second), 512 kSizeDictAttr, kWidthDictAttr, kHeightDictAttr), 513 &line); 514 continue; 515 } 516 517 // Write formatted value. 518 std::string formatted_value = FormatAttributeValue(item.second); 519 WriteAttribute( 520 false, 521 StringPrintf("%s=%s", item.first.c_str(), formatted_value.c_str()), 522 &line); 523 } 524 525 return line; 526} 527 528std::string AccessibilityTreeFormatterMac::FormatAttributeValue( 529 const base::Value& value) { 530 // String. 531 if (value.is_string()) { 532 // Special handling for constants which are exposed as is, i.e. with no 533 // quotation marks. 534 std::string const_prefix = kConstValuePrefix; 535 if (base::StartsWith(value.GetString(), const_prefix, 536 base::CompareCase::SENSITIVE)) { 537 return value.GetString().substr(const_prefix.length()); 538 } 539 return "'" + value.GetString() + "'"; 540 } 541 542 // Integer. 543 if (value.is_int()) { 544 return base::NumberToString(value.GetInt()); 545 } 546 547 // List: exposed as [value1, ..., valueN]; 548 if (value.is_list()) { 549 std::string output; 550 for (const auto& item : value.GetList()) { 551 if (!output.empty()) { 552 output += ", "; 553 } 554 output += FormatAttributeValue(item); 555 } 556 return "[" + output + "]"; 557 } 558 559 // Dictionary. Exposed as {key1: value1, ..., keyN: valueN}. Set-like 560 // dictionary is exposed as {value1, ..., valueN}. 561 if (value.is_dict()) { 562 const std::string setkey_prefix(kSetKeyPrefixDictAttr); 563 std::string output; 564 for (const auto& item : value.DictItems()) { 565 if (!output.empty()) { 566 output += ", "; 567 } 568 // Special set-like dictionaries handling: keys are prefixed by 569 // "_setkey_". 570 if (base::StartsWith(item.first, setkey_prefix, 571 base::CompareCase::SENSITIVE)) { 572 output += FormatAttributeValue(item.second); 573 } else { 574 output += item.first + ": " + FormatAttributeValue(item.second); 575 } 576 } 577 return "{" + output + "}"; 578 } 579 return ""; 580} 581 582} // namespace content 583 584#pragma clang diagnostic pop 585