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