1// Copyright 2020 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_utils_mac.h" 6 7#include "base/strings/sys_string_conversions.h" 8#include "content/browser/accessibility/accessibility_tools_utils_mac.h" 9#include "content/browser/accessibility/browser_accessibility_mac.h" 10#include "ui/accessibility/platform/inspect/property_node.h" 11 12using ui::AXPropertyNode; 13 14namespace content { 15namespace a11y { 16 17namespace { 18 19#define INT_FAIL(property_node, msg) \ 20 LOG(ERROR) << "Failed to parse " << property_node.name_or_value \ 21 << " to Int: " << msg; \ 22 return nil; 23 24#define INTARRAY_FAIL(property_node, msg) \ 25 LOG(ERROR) << "Failed to parse " << property_node.name_or_value \ 26 << " to IntArray: " << msg; \ 27 return nil; 28 29#define NSRANGE_FAIL(property_node, msg) \ 30 LOG(ERROR) << "Failed to parse " << property_node.name_or_value \ 31 << " to NSRange: " << msg; \ 32 return nil; 33 34#define UIELEMENT_FAIL(property_node, msg) \ 35 LOG(ERROR) << "Failed to parse " << property_node.name_or_value \ 36 << " to UIElement: " << msg; \ 37 return nil; 38 39#define TEXTMARKER_FAIL(property_node, msg) \ 40 LOG(ERROR) << "Failed to parse " << property_node.name_or_value \ 41 << " to AXTextMarker: " << msg \ 42 << ". Expected format: {anchor, offset, affinity}, where anchor " \ 43 "is :line_num, offset is integer, affinity is either down, " \ 44 "up or none"; \ 45 return nil; 46 47} // namespace 48 49// Line indexers 50 51LineIndexer::LineIndexer(const gfx::NativeViewAccessible node) { 52 int counter = 0; 53 Build(node, &counter); 54} 55 56LineIndexer::~LineIndexer() {} 57 58std::string LineIndexer::IndexBy(const gfx::NativeViewAccessible node) const { 59 std::string line_index = ":unknown"; 60 if (IsBrowserAccessibilityCocoa(node)) { 61 auto iter = map.find(node); 62 if (iter != map.end()) { 63 line_index = iter->second; 64 } 65 } else if (IsAXUIElement(node)) { 66 for (auto& iter : map) { 67 if (CFEqual(iter.first, node)) { 68 line_index = iter.second; 69 break; 70 } 71 } 72 } 73 return line_index; 74} 75 76gfx::NativeViewAccessible LineIndexer::NodeBy( 77 const std::string& line_index) const { 78 for (std::pair<const gfx::NativeViewAccessible, std::string> item : map) { 79 if (item.second == line_index) { 80 return item.first; 81 } 82 } 83 return nil; 84} 85 86void LineIndexer::Build(const gfx::NativeViewAccessible node, int* counter) { 87 const std::string line_index = 88 std::string(1, ':') + base::NumberToString(++(*counter)); 89 map.insert({node, line_index}); 90 NSArray* children = ChildrenOf(node); 91 for (gfx::NativeViewAccessible child in children) { 92 Build(child, counter); 93 } 94} 95 96// OptionalNSObject 97 98std::string OptionalNSObject::ToString() const { 99 if (IsNotApplicable()) { 100 return "<n/a>"; 101 } else if (IsError()) { 102 return "<error>"; 103 } else if (value == nil) { 104 return "<nil>"; 105 } else { 106 return base::SysNSStringToUTF8([NSString stringWithFormat:@"%@", value]); 107 } 108} 109 110// AttributeInvoker 111 112AttributeInvoker::AttributeInvoker(const id node, 113 const LineIndexer* line_indexer) 114 : node(node), line_indexer(line_indexer) { 115 attributes = AttributeNamesOf(node); 116 parameterized_attributes = ParameterizedAttributeNamesOf(node); 117} 118 119OptionalNSObject AttributeInvoker::Invoke( 120 const AXPropertyNode& property_node) const { 121 // Attributes 122 for (NSString* attribute : attributes) { 123 if (property_node.IsMatching(base::SysNSStringToUTF8(attribute))) { 124 return OptionalNSObject::NotNullOrNotApplicable( 125 AttributeValueOf(node, attribute)); 126 } 127 } 128 129 // Parameterized attributes 130 for (NSString* attribute : parameterized_attributes) { 131 if (property_node.IsMatching(base::SysNSStringToUTF8(attribute))) { 132 OptionalNSObject param = ParamByPropertyNode(property_node); 133 if (param.IsNotNil()) { 134 return OptionalNSObject( 135 ParameterizedAttributeValueOf(node, attribute, *param)); 136 } 137 return param; 138 } 139 } 140 141 return OptionalNSObject::NotApplicable(); 142} 143 144OptionalNSObject AttributeInvoker::GetValue( 145 const std::string& property_name, 146 const OptionalNSObject& param) const { 147 NSString* attribute = base::SysUTF8ToNSString(property_name); 148 if ([parameterized_attributes containsObject:attribute]) { 149 if (param.IsNotNil()) { 150 return OptionalNSObject( 151 ParameterizedAttributeValueOf(node, attribute, *param)); 152 } else { 153 return param; 154 } 155 } 156 return OptionalNSObject::NotApplicable(); 157} 158 159OptionalNSObject AttributeInvoker::GetValue( 160 const std::string& property_name) const { 161 NSString* attribute = base::SysUTF8ToNSString(property_name); 162 if ([attributes containsObject:attribute]) { 163 return OptionalNSObject::NotNullOrNotApplicable( 164 AttributeValueOf(node, attribute)); 165 } 166 return OptionalNSObject::NotApplicable(); 167} 168 169void AttributeInvoker::SetValue(const std::string& property_name, 170 const OptionalNSObject& value) const { 171 NSString* attribute = base::SysUTF8ToNSString(property_name); 172 if ([attributes containsObject:attribute] && 173 IsAttributeSettable(node, attribute)) { 174 SetAttributeValueOf(node, attribute, *value); 175 } 176} 177 178OptionalNSObject AttributeInvoker::ParamByPropertyNode( 179 const AXPropertyNode& property_node) const { 180 // NSAccessibility attributes always take a single parameter. 181 if (property_node.parameters.size() != 1) { 182 LOG(ERROR) << "Failed to parse " << property_node.original_property 183 << ": single parameter is expected"; 184 return OptionalNSObject::Error(); 185 } 186 187 // Nested attribute case: attempt to invoke an attribute for an argument node. 188 const AXPropertyNode& arg_node = property_node.parameters[0]; 189 OptionalNSObject subvalue = Invoke(arg_node); 190 if (!subvalue.IsNotApplicable()) { 191 return subvalue; 192 } 193 194 // Otherwise parse argument node value. 195 const std::string& property_name = property_node.name_or_value; 196 if (property_name == "AXLineForIndex" || 197 property_name == "AXTextMarkerForIndex") { // Int 198 return OptionalNSObject::NotNilOrError(PropertyNodeToInt(arg_node)); 199 } 200 if (property_name == "AXCellForColumnAndRow") { // IntArray 201 return OptionalNSObject::NotNilOrError(PropertyNodeToIntArray(arg_node)); 202 } 203 if (property_name == "AXStringForRange") { // NSRange 204 return OptionalNSObject::NotNilOrError(PropertyNodeToRange(arg_node)); 205 } 206 if (property_name == "AXIndexForChildUIElement") { // UIElement 207 return OptionalNSObject::NotNilOrError(PropertyNodeToUIElement(arg_node)); 208 } 209 if (property_name == "AXIndexForTextMarker") { // TextMarker 210 return OptionalNSObject::NotNilOrError(PropertyNodeToTextMarker(arg_node)); 211 } 212 if (property_name == "AXStringForTextMarkerRange") { // TextMarkerRange 213 return OptionalNSObject::NotNilOrError( 214 PropertyNodeToTextMarkerRange(arg_node)); 215 } 216 217 return OptionalNSObject::NotApplicable(); 218} 219 220// NSNumber. Format: integer. 221NSNumber* AttributeInvoker::PropertyNodeToInt( 222 const AXPropertyNode& intnode) const { 223 base::Optional<int> param = intnode.AsInt(); 224 if (!param) { 225 INT_FAIL(intnode, "not a number") 226 } 227 return [NSNumber numberWithInt:*param]; 228} 229 230// NSArray of two NSNumber. Format: [integer, integer]. 231NSArray* AttributeInvoker::PropertyNodeToIntArray( 232 const AXPropertyNode& arraynode) const { 233 if (arraynode.name_or_value != "[]") { 234 INTARRAY_FAIL(arraynode, "not array") 235 } 236 237 NSMutableArray* array = 238 [[NSMutableArray alloc] initWithCapacity:arraynode.parameters.size()]; 239 for (const auto& paramnode : arraynode.parameters) { 240 base::Optional<int> param = paramnode.AsInt(); 241 if (!param) { 242 INTARRAY_FAIL(arraynode, paramnode.name_or_value + " is not a number") 243 } 244 [array addObject:@(*param)]; 245 } 246 return array; 247} 248 249// NSRange. Format: {loc: integer, len: integer}. 250NSValue* AttributeInvoker::PropertyNodeToRange( 251 const AXPropertyNode& dictnode) const { 252 if (!dictnode.IsDict()) { 253 NSRANGE_FAIL(dictnode, "dictionary is expected") 254 } 255 256 base::Optional<int> loc = dictnode.FindIntKey("loc"); 257 if (!loc) { 258 NSRANGE_FAIL(dictnode, "no loc or loc is not a number") 259 } 260 261 base::Optional<int> len = dictnode.FindIntKey("len"); 262 if (!len) { 263 NSRANGE_FAIL(dictnode, "no len or len is not a number") 264 } 265 266 return [NSValue valueWithRange:NSMakeRange(*loc, *len)]; 267} 268 269// UIElement. Format: :line_num. 270gfx::NativeViewAccessible AttributeInvoker::PropertyNodeToUIElement( 271 const AXPropertyNode& uielement_node) const { 272 gfx::NativeViewAccessible uielement = 273 line_indexer->NodeBy(uielement_node.name_or_value); 274 if (!uielement) { 275 UIELEMENT_FAIL(uielement_node, 276 "no corresponding UIElement was found in the tree") 277 } 278 return uielement; 279} 280 281id AttributeInvoker::DictNodeToTextMarker( 282 const AXPropertyNode& dictnode) const { 283 if (!dictnode.IsDict()) { 284 TEXTMARKER_FAIL(dictnode, "dictionary is expected") 285 } 286 if (dictnode.parameters.size() != 3) { 287 TEXTMARKER_FAIL(dictnode, "wrong number of dictionary elements") 288 } 289 290 BrowserAccessibilityCocoa* anchor_cocoa = 291 line_indexer->NodeBy(dictnode.parameters[0].name_or_value); 292 if (!anchor_cocoa) { 293 TEXTMARKER_FAIL(dictnode, "1st argument: wrong anchor") 294 } 295 296 base::Optional<int> offset = dictnode.parameters[1].AsInt(); 297 if (!offset) { 298 TEXTMARKER_FAIL(dictnode, "2nd argument: wrong offset") 299 } 300 301 ax::mojom::TextAffinity affinity; 302 const std::string& affinity_str = dictnode.parameters[2].name_or_value; 303 if (affinity_str == "none") { 304 affinity = ax::mojom::TextAffinity::kNone; 305 } else if (affinity_str == "down") { 306 affinity = ax::mojom::TextAffinity::kDownstream; 307 } else if (affinity_str == "up") { 308 affinity = ax::mojom::TextAffinity::kUpstream; 309 } else { 310 TEXTMARKER_FAIL(dictnode, "3rd argument: wrong affinity") 311 } 312 313 return content::AXTextMarkerFrom(anchor_cocoa, *offset, affinity); 314} 315 316id AttributeInvoker::PropertyNodeToTextMarker( 317 const AXPropertyNode& dictnode) const { 318 return DictNodeToTextMarker(dictnode); 319} 320 321id AttributeInvoker::PropertyNodeToTextMarkerRange( 322 const AXPropertyNode& rangenode) const { 323 if (!rangenode.IsDict()) { 324 TEXTMARKER_FAIL(rangenode, "dictionary is expected") 325 } 326 327 const AXPropertyNode* anchornode = rangenode.FindKey("anchor"); 328 if (!anchornode) { 329 TEXTMARKER_FAIL(rangenode, "no anchor") 330 } 331 332 id anchor_textmarker = DictNodeToTextMarker(*anchornode); 333 if (!anchor_textmarker) { 334 TEXTMARKER_FAIL(rangenode, "failed to parse anchor") 335 } 336 337 const AXPropertyNode* focusnode = rangenode.FindKey("focus"); 338 if (!focusnode) { 339 TEXTMARKER_FAIL(rangenode, "no focus") 340 } 341 342 id focus_textmarker = DictNodeToTextMarker(*focusnode); 343 if (!focus_textmarker) { 344 TEXTMARKER_FAIL(rangenode, "failed to parse focus") 345 } 346 347 return content::AXTextMarkerRangeFrom(anchor_textmarker, focus_textmarker); 348} 349 350OptionalNSObject TextMarkerRangeGetStartMarker(const OptionalNSObject& obj) { 351 if (!IsAXTextMarkerRange(*obj)) 352 return OptionalNSObject::NotApplicable(); 353 354 BrowserAccessibilityPosition::AXRangeType range = 355 AXTextMarkerRangeToRange(*obj); 356 if (range.IsNull()) 357 return OptionalNSObject::Error(); 358 359 BrowserAccessibilityPosition::AXPositionInstance::pointer position = 360 range.anchor(); 361 const BrowserAccessibility* node = position->GetAnchor(); 362 const BrowserAccessibilityCocoa* cocoa_node = 363 ToBrowserAccessibilityCocoa(node); 364 return OptionalNSObject::NotNilOrError(content::AXTextMarkerFrom( 365 cocoa_node, position->text_offset(), position->affinity())); 366} 367 368OptionalNSObject TextMarkerRangeGetEndMarker(const OptionalNSObject& obj) { 369 if (!IsAXTextMarkerRange(*obj)) 370 return OptionalNSObject::NotApplicable(); 371 372 BrowserAccessibilityPosition::AXRangeType range = 373 AXTextMarkerRangeToRange(*obj); 374 if (range.IsNull()) 375 return OptionalNSObject::Error(); 376 377 BrowserAccessibilityPosition::AXPositionInstance::pointer position = 378 range.focus(); 379 const BrowserAccessibility* node = position->GetAnchor(); 380 const BrowserAccessibilityCocoa* cocoa_node = 381 ToBrowserAccessibilityCocoa(node); 382 return OptionalNSObject::NotNilOrError(content::AXTextMarkerFrom( 383 cocoa_node, position->text_offset(), position->affinity())); 384} 385 386OptionalNSObject MakePairArray(const OptionalNSObject& obj1, 387 const OptionalNSObject& obj2) { 388 if (!obj1.IsNotNil() || !obj2.IsNotNil()) 389 return OptionalNSObject::Error(); 390 return OptionalNSObject::NotNilOrError( 391 [NSArray arrayWithObjects:*obj1, *obj2, nil]); 392} 393 394} // namespace a11y 395} // namespace content 396