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