chromium/ui/accessibility/platform/inspect/ax_tree_formatter_mac.mm

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/accessibility/platform/inspect/ax_tree_formatter_mac.h"

#include <string>

#include "base/files/file_path.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "ui/accessibility/platform/ax_platform_node_cocoa.h"
#include "ui/accessibility/platform/inspect/ax_element_wrapper_mac.h"
#include "ui/accessibility/platform/inspect/ax_inspect_scenario.h"
#include "ui/accessibility/platform/inspect/ax_inspect_utils.h"
#include "ui/accessibility/platform/inspect/ax_inspect_utils_mac.h"
#include "ui/accessibility/platform/inspect/ax_property_node.h"
#include "ui/accessibility/platform/inspect/ax_script_instruction.h"
#include "ui/accessibility/platform/inspect/ax_transform_mac.h"

// This file uses the deprecated NSObject accessibility interface.
// TODO(crbug.com/41450813): Migrate to the new NSAccessibility interface.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"

namespace ui {

namespace {

constexpr char kLocalPositionDictAttr[] = "LocalPosition";

constexpr char kFailedPrefix[] = "_const_ERROR:";
constexpr char kFailedToParseError[] = "_const_ERROR:FAILED_TO_PARSE";
constexpr char kNotApplicable[] = "_const_n/a";

}  // namespace

AXTreeFormatterMac::AXTreeFormatterMac() = default;

AXTreeFormatterMac::~AXTreeFormatterMac() = default;

void AXTreeFormatterMac::AddDefaultFilters(
    std::vector<AXPropertyFilter>* property_filters) {
  static NSArray* default_attributes = @[
    @"AXAutocompleteValue", @"AXDescription", @"AXRole", @"AXSubrole",
    @"AXTitle", @"AXTitleUIElement", @"AXValue"
  ];

  for (NSString* attribute : default_attributes) {
    AddPropertyFilter(property_filters, base::SysNSStringToUTF8(attribute));
  }

  if (show_ids()) {
    AddPropertyFilter(property_filters, "ChromeAXNodeId");
  }
}

base::Value::Dict AXTreeFormatterMac::BuildTree(
    AXPlatformNodeDelegate* root) const {
  DCHECK(root);
  return BuildTree(root->GetNativeViewAccessible());
}

base::Value::Dict AXTreeFormatterMac::BuildTreeForSelector(
    const AXTreeSelector& selector) const {
  base::apple::ScopedCFTypeRef<AXUIElementRef> node;
  std::tie(node, std::ignore) = FindAXUIElement(selector);
  if (!node) {
    return base::Value::Dict();
  }
  return BuildTreeForAXUIElement(node.get());
}

base::Value::Dict AXTreeFormatterMac::BuildTreeForAXUIElement(
    AXUIElementRef node) const {
  return BuildTree((__bridge id)node);
}

base::Value::Dict AXTreeFormatterMac::BuildTree(const id root) const {
  DCHECK(root);

  AXTreeIndexerMac indexer(root);
  base::Value::Dict dict;

  AXElementWrapper ax_element(root);
  NSPoint position = ax_element.Position();
  NSSize size = ax_element.Size();
  NSRect rect = NSMakeRect(position.x, position.y, size.width, size.height);

  RecursiveBuildTree(ax_element, rect, &indexer, &dict);

  return dict;
}

std::string AXTreeFormatterMac::EvaluateScript(
    const AXTreeSelector& selector,
    const AXInspectScenario& scenario) const {
  base::apple::ScopedCFTypeRef<AXUIElementRef> root;
  std::tie(root, std::ignore) = FindAXUIElement(selector);
  if (!root)
    return "";

  std::string result =
      EvaluateScript((__bridge id)root.get(), scenario.script_instructions, 0,
                     scenario.script_instructions.size());

  return result;
}

std::string AXTreeFormatterMac::EvaluateScript(
    AXPlatformNodeDelegate* root,
    const std::vector<AXScriptInstruction>& instructions,
    size_t start_index,
    size_t end_index) const {
  return EvaluateScript(root->GetNativeViewAccessible(), instructions,
                        start_index, end_index);
}

std::string AXTreeFormatterMac::EvaluateScript(
    id platform_root,
    const std::vector<AXScriptInstruction>& instructions,
    size_t start_index,
    size_t end_index) const {
  base::Value::List scripts;
  AXTreeIndexerMac indexer(platform_root);
  std::map<std::string, id> storage;
  AXCallStatementInvoker invoker(&indexer, &storage);
  for (size_t index = start_index; index < end_index; index++) {
    if (instructions[index].IsComment()) {
      scripts.Append(instructions[index].AsComment());
      continue;
    }

    DCHECK(instructions[index].IsScript());
    const AXPropertyNode& property_node = instructions[index].AsScript();
    AXOptionalNSObject value = invoker.Invoke(property_node);
    if (value.IsUnsupported()) {
      continue;
    }

    base::Value result;
    if (value.IsError()) {
      result = value.HasStateText()
                   ? base::Value(kFailedPrefix + value.StateText())
                   : base::Value(kFailedToParseError);
    } else if (value.IsNotApplicable()) {
      result = base::Value(kNotApplicable);
    } else {
      result = AXNSObjectToBaseValue(*value, &indexer);
    }

    scripts.Append(property_node.ToString() + "=" + AXFormatValue(result));
  }

  std::string contents;
  for (const base::Value& script : scripts) {
    std::string line;
    WriteAttribute(true, script.GetString(), &line);
    contents += line + "\n";
  }
  return contents;
}

base::Value::Dict AXTreeFormatterMac::BuildNode(
    AXPlatformNodeDelegate* node) const {
  DCHECK(node);
  return BuildNode(node->GetNativeViewAccessible());
}

base::Value::Dict AXTreeFormatterMac::BuildNode(const id node) const {
  DCHECK(node);

  AXTreeIndexerMac indexer(node);
  base::Value::Dict dict;

  AXElementWrapper ax_element(node);
  NSPoint position = ax_element.Position();
  NSSize size = ax_element.Size();
  NSRect rect = NSMakeRect(position.x, position.y, size.width, size.height);

  AddProperties(ax_element, rect, &indexer, &dict);
  return dict;
}

void AXTreeFormatterMac::RecursiveBuildTree(const AXElementWrapper& ax_element,
                                            const NSRect& root_rect,
                                            const AXTreeIndexerMac* indexer,
                                            base::Value::Dict* dict) const {
  AXPlatformNodeDelegate* platform_node = ax_element.IsNSAccessibilityElement()
                                              ? [ax_element.AsId() nodeDelegate]
                                              : nullptr;

  if (platform_node && !ShouldDumpNode(*platform_node))
    return;

  AddProperties(ax_element, root_rect, indexer, dict);
  if (platform_node && !ShouldDumpChildren(*platform_node))
    return;

  NSArray* children = ax_element.Children();
  base::Value::List child_dict_list;
  for (id child in children) {
    base::Value::Dict child_dict;
    RecursiveBuildTree(AXElementWrapper{child}, root_rect, indexer,
                       &child_dict);
    child_dict_list.Append(std::move(child_dict));
  }
  dict->Set(kChildrenDictAttr, std::move(child_dict_list));
}

void AXTreeFormatterMac::AddProperties(const AXElementWrapper& ax_element,
                                       const NSRect& root_rect,
                                       const AXTreeIndexerMac* indexer,
                                       base::Value::Dict* dict) const {
  // Chromium special attributes.
  dict->Set(kLocalPositionDictAttr,
            PopulateLocalPosition(ax_element, root_rect));

  // Dump all attributes if match-all filter is specified.
  if (HasMatchAllPropertyFilter()) {
    NSArray* attributes = ax_element.AttributeNames();
    for (NSString* attribute : attributes) {
      dict->SetByDottedPath(
          base::SysNSStringToUTF8(attribute),
          AXNSObjectToBaseValue(*ax_element.GetAttributeValue(attribute),
                                indexer));
    }
    return;
  }

  // Otherwise dump attributes matching allow filters only.
  std::string line_index = indexer->IndexBy(ax_element.AsId());
  for (const AXPropertyNode& property_node :
       PropertyFilterNodesFor(line_index)) {
    AXCallStatementInvoker invoker(ax_element.AsId(), indexer);
    AXOptionalNSObject value = invoker.Invoke(property_node);
    if (value.IsNotApplicable() || value.IsUnsupported()) {
      continue;
    }
    if (value.IsError()) {
      dict->SetByDottedPath(property_node.original_property,
                            base::Value(kFailedToParseError));
      continue;
    }
    dict->SetByDottedPath(property_node.original_property,
                          AXNSObjectToBaseValue(*value, indexer));
  }
}

base::Value::Dict AXTreeFormatterMac::PopulateLocalPosition(
    const AXElementWrapper& ax_element,
    const NSRect& root_rect) const {
  // The NSAccessibility position of an object is in global coordinates and
  // based on the lower-left corner of the object. To make this easier and
  // less confusing, convert it to local window coordinates using the top-left
  // corner when dumping the position.
  int root_top = -static_cast<int>(root_rect.origin.y + root_rect.size.height);
  int root_left = static_cast<int>(root_rect.origin.x);

  NSPoint node_position = ax_element.Position();
  NSSize node_size = ax_element.Size();

  return AXNSPointToBaseValue(NSMakePoint(
      static_cast<int>(node_position.x - root_left),
      static_cast<int>(-node_position.y - node_size.height - root_top)));
}

std::string AXTreeFormatterMac::ProcessTreeForOutput(
    const base::Value::Dict& dict) const {
  const std::string* error_value = dict.FindString("error");
  if (error_value)
    return *error_value;

  std::string line;

  // AXRole and AXSubrole have own formatting and should be listed upfront.
  std::string role_attr = base::SysNSStringToUTF8(NSAccessibilityRoleAttribute);
  const std::string* value = dict.FindString(role_attr);
  if (value) {
    WriteAttribute(true, *value, &line);
  }
  std::string subrole_attr =
      base::SysNSStringToUTF8(NSAccessibilitySubroleAttribute);
  value = dict.FindString(subrole_attr);
  if (value) {
    WriteAttribute(
        false,
        base::StringPrintf("%s=%s", subrole_attr.c_str(), value->c_str()),
        &line);
  }

  // Expose all other attributes.
  for (auto item : dict) {
    if (item.second.is_string() &&
        (item.first == role_attr || item.first == subrole_attr)) {
      continue;
    }

    // Special case: children.
    // Children are used to generate the tree
    // itself, thus no sense to expose them on each node.
    if (item.first == kChildrenDictAttr) {
      continue;
    }

    // Write formatted value.
    std::string formatted_value = AXFormatValue(item.second);
    WriteAttribute(false,
                   base::StringPrintf("%s=%s", item.first.c_str(),
                                      formatted_value.c_str()),
                   &line);
  }

  return line;
}

}  // namespace ui

#pragma clang diagnostic pop