chromium/ui/accessibility/platform/inspect/ax_tree_formatter_win.cc

// 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.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

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

#include <math.h>
#include <stddef.h>
#include <stdint.h>

#include <algorithm>
#include <string>
#include <utility>

#include "base/files/file_path.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "base/win/scoped_bstr.h"
#include "base/win/scoped_variant.h"
#include "third_party/iaccessible2/ia2_api_all.h"
#include "ui/accessibility/platform/ax_platform_node_base.h"
#include "ui/accessibility/platform/ax_platform_tree_manager.h"
#include "ui/accessibility/platform/inspect/ax_call_statement_invoker_win.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_win.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_tree_indexer_win.h"
#include "ui/base/win/atl_module.h"
#include "ui/gfx/win/hwnd_util.h"

namespace ui {

void AXTreeFormatterWin::AddDefaultFilters(
    std::vector<AXPropertyFilter>* property_filters) {
  // Too noisy: HOTTRACKED, LINKED, SELECTABLE, IA2_STATE_EDITABLE,
  //            IA2_STATE_OPAQUE, IA2_STATE_SELECTAbLE_TEXT,
  //            IA2_STATE_SINGLE_LINE, IA2_STATE_VERTICAL.
  // Too unpredictiable: OFFSCREEN
  // Windows states to log by default:
  AddPropertyFilter(property_filters, "ALERT*");
  AddPropertyFilter(property_filters, "ANIMATED*");
  AddPropertyFilter(property_filters, "BUSY");
  AddPropertyFilter(property_filters, "CHECKED");
  AddPropertyFilter(property_filters, "COLLAPSED");
  AddPropertyFilter(property_filters, "EXPANDED");
  AddPropertyFilter(property_filters, "FLOATING");
  AddPropertyFilter(property_filters, "FOCUSABLE");
  AddPropertyFilter(property_filters, "HASPOPUP");
  AddPropertyFilter(property_filters, "INVISIBLE");
  AddPropertyFilter(property_filters, "MARQUEED");
  AddPropertyFilter(property_filters, "MIXED");
  AddPropertyFilter(property_filters, "MOVEABLE");
  AddPropertyFilter(property_filters, "MULTISELECTABLE");
  AddPropertyFilter(property_filters, "PRESSED");
  AddPropertyFilter(property_filters, "PROTECTED");
  AddPropertyFilter(property_filters, "READONLY");
  AddPropertyFilter(property_filters, "SELECTED");
  AddPropertyFilter(property_filters, "SIZEABLE");
  AddPropertyFilter(property_filters, "TRAVERSED");
  AddPropertyFilter(property_filters, "UNAVAILABLE");
  AddPropertyFilter(property_filters, "IA2_STATE_ACTIVE");
  AddPropertyFilter(property_filters, "IA2_STATE_ARMED");
  AddPropertyFilter(property_filters, "IA2_STATE_CHECKABLE");
  AddPropertyFilter(property_filters, "IA2_STATE_DEFUNCT");
  AddPropertyFilter(property_filters, "IA2_STATE_HORIZONTAL");
  AddPropertyFilter(property_filters, "IA2_STATE_ICONIFIED");
  AddPropertyFilter(property_filters, "IA2_STATE_INVALID_ENTRY");
  AddPropertyFilter(property_filters, "IA2_STATE_MODAL");
  AddPropertyFilter(property_filters, "IA2_STATE_MULTI_LINE");
  AddPropertyFilter(property_filters, "IA2_STATE_PINNED");
  AddPropertyFilter(property_filters, "IA2_STATE_REQUIRED");
  AddPropertyFilter(property_filters, "IA2_STATE_STALE");
  AddPropertyFilter(property_filters, "IA2_STATE_TRANSIENT");
  // Reduce flakiness.
  AddPropertyFilter(property_filters, "FOCUSED", AXPropertyFilter::DENY);
  AddPropertyFilter(property_filters, "HOTTRACKED", AXPropertyFilter::DENY);
  AddPropertyFilter(property_filters, "OFFSCREEN", AXPropertyFilter::DENY);
}

AXTreeFormatterWin::AXTreeFormatterWin() {
  win::CreateATLModuleIfNeeded();
}

AXTreeFormatterWin::~AXTreeFormatterWin() {}

Microsoft::WRL::ComPtr<IAccessible> GetIAObject(AXPlatformNodeDelegate* node,
                                                LONG& root_x,
                                                LONG& root_y) {
  DCHECK(node);
  // If dumping when the page or iframe is reloading, the
  // tree manager may have been removed.
  AXTreeManager* manager = node->GetTreeManager();
  if (!manager) {
    return nullptr;
  }
  AXTreeManager* root_manager = manager->GetRootManager();
  if (!root_manager) {
    return nullptr;
  }

  base::win::ScopedVariant variant_self(CHILDID_SELF);
  LONG root_width, root_height;

  HRESULT hr = static_cast<AXPlatformTreeManager*>(root_manager)
                   ->RootDelegate()
                   ->GetNativeViewAccessible()
                   ->accLocation(&root_x, &root_y, &root_width, &root_height,
                                 variant_self);
  DCHECK(SUCCEEDED(hr));

  return node->GetNativeViewAccessible();
}

base::Value::Dict AXTreeFormatterWin::BuildNode(
    AXPlatformNodeDelegate* node) const {
  LONG root_x = 0, root_y = 0;
  Microsoft::WRL::ComPtr<IAccessible> node_ia =
      GetIAObject(node, root_x, root_y);
  base::Value::Dict dict;
  if (!node_ia) {
    return dict;
  }
  AddProperties(node_ia, &dict, root_x, root_y);
  return dict;
}

base::Value::Dict AXTreeFormatterWin::BuildTree(
    AXPlatformNodeDelegate* start) const {
  LONG root_x = 0, root_y = 0;
  Microsoft::WRL::ComPtr<IAccessible> start_ia =
      GetIAObject(start, root_x, root_y);
  base::Value::Dict dict;
  if (!start_ia) {
    return dict;
  }
  RecursiveBuildTree(start_ia, &dict, root_x, root_y);
  return dict;
}

Microsoft::WRL::ComPtr<IAccessible> AXTreeFormatterWin::FindAccessibleRoot(
    const AXTreeSelector& selector) const {
  HWND hwnd = GetHWNDBySelector(selector);
  if (!hwnd)
    return nullptr;

  Microsoft::WRL::ComPtr<IAccessible> root;
  HRESULT hr =
      ::AccessibleObjectFromWindow(hwnd, OBJID_CLIENT, IID_PPV_ARGS(&root));
  if (FAILED(hr))
    return nullptr;

  if (selector.types & AXTreeSelector::ActiveTab) {
    root = FindActiveDocument(root.Get());
    if (!root)
      return nullptr;
  }
  return root.Get();
}

base::Value::Dict AXTreeFormatterWin::BuildTreeForSelector(
    const AXTreeSelector& selector) const {
  base::Value::Dict dict;

  Microsoft::WRL::ComPtr<IAccessible> root = FindAccessibleRoot(selector);
  if (!root) {
    return dict;
  }

  RecursiveBuildTree(root, &dict, 0, 0);
  return dict;
}

std::string AXTreeFormatterWin::EvaluateScript(
    const AXTreeSelector& selector,
    const AXInspectScenario& scenario) const {
  Microsoft::WRL::ComPtr<IAccessible> root = FindAccessibleRoot(selector);
  return EvaluateScript(root, scenario.script_instructions, 0 /* start_index */,
                        scenario.script_instructions.size());
}

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

std::string AXTreeFormatterWin::EvaluateScript(
    Microsoft::WRL::ComPtr<IAccessible> root,
    const std::vector<AXScriptInstruction>& instructions,
    size_t start_index,
    size_t end_index) const {
  if (!root)
    return "error no accessibility tree found";

  base::Value::List scripts;
  AXTreeIndexerWin indexer(root);
  std::map<std::string, AXTargetWin> storage;
  AXCallStatementInvokerWin 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();

    AXOptionalObject value = invoker.Invoke(property_node);
    if (value.IsUnsupported()) {
      continue;
    }

    scripts.Append(property_node.ToString() + "=" +
                   AXCallStatementInvokerWin::ToString(value));
  }

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

void AXTreeFormatterWin::RecursiveBuildTree(
    const Microsoft::WRL::ComPtr<IAccessible> node,
    base::Value::Dict* dict,
    LONG root_x,
    LONG root_y) const {
  AXPlatformNode* platform_node =
      AXPlatformNode::FromNativeViewAccessible(node.Get());

  bool skipChildren = false;
  if (platform_node) {
    AXPlatformNodeDelegate* delegate = platform_node->GetDelegate();
    DCHECK(delegate);

    if (!ShouldDumpNode(*delegate))
      return;

    if (!ShouldDumpChildren(*delegate))
      skipChildren = true;
  }

  AddProperties(node, dict, root_x, root_y);
  if (skipChildren)
    return;

  base::Value::List child_list;
  for (const MSAAChild& msaa_child : MSAAChildren(node)) {
    base::Value::Dict child_dict;

    Microsoft::WRL::ComPtr<IAccessible> child = msaa_child.AsIAccessible();
    if (child) {
      RecursiveBuildTree(child, &child_dict, root_x, root_y);
    } else {
      const base::win::ScopedVariant& child_variant = msaa_child.AsVariant();
      if (child_variant.type() == VT_EMPTY ||
          child_variant.type() == VT_DISPATCH) {
        child_dict.Set("error", "[Error retrieving child]");
      } else if (child_variant.type() == VT_I4) {
        // Partial child does not have its own object.
        // Add minimal info -- role and name.
        base::win::ScopedVariant role_variant;
        if (SUCCEEDED(
                node->get_accRole(child_variant, role_variant.Receive()))) {
          if (role_variant.type() == VT_I4) {
            child_dict.Set("role", " [partial child]");
          }
        }
        base::win::ScopedBstr name;
        if (S_OK == node->get_accName(child_variant, name.Receive())) {
          child_dict.Set("name", base::WideToUTF8(name.Get()));
        }
      } else {
        child_dict.Set("error", "[Unknown child type]");
      }
    }
    child_list.Append(std::move(child_dict));
  }
  dict->Set(kChildrenDictAttr, std::move(child_list));
}

const char* const ALL_ATTRIBUTES[] = {
    "name",
    "parent",
    "window_class",
    "value",
    "states",
    "attributes",
    "text_attributes",
    "ia2_hypertext",
    "currentValue",
    "minimumValue",
    "maximumValue",
    "description",
    "default_action",
    "action_name",
    "keyboard_shortcut",
    "location",
    "size",
    "index_in_parent",
    "n_relations",
    "group_level",
    "similar_items_in_group",
    "position_in_group",
    "table_rows",
    "table_columns",
    "row_index",
    "column_index",
    "row_headers",
    "column_headers",
    "n_characters",
    "caret_offset",
    "n_selections",
    "selection_start",
    "selection_end",
    "localized_extended_role",
    "inner_html",
    "ia2_table_cell_column_index",
    "ia2_table_cell_row_index",
    // IAccessibleRelation constants.
    // https://accessibility.linuxfoundation.org/a11yspecs/ia2/docs/html/group__grp_relations.html
    // Omitted label/description relations as we can already test those with
    // `IAccessible2::get_accName()` and `IAccessible2::get_accDescription()`.
    "containingApplication",
    "containingDocument",
    "containingTabPane",
    "containingWindow",
    "controlledBy",
    "controllerFor",
    // Note that the `details-roles:` attribute found in IA2 aria-details tests
    // isn't necessarily duplicative of `IA2_RELATION_DETAILS` (the `details`
    // string below). The former does additional processing, see
    // `AXPlatformNodeBase::ComputeDetailsRoles()`.
    "details",
    "detailsFor",
    "embeddedBy",
    "embeds",
    "error",
    "errorFor",
    "flowsFrom",
    "flowsTo",
    "memberOf",
    "nextTabbable",
    "nodeChildOf",
    "nodeParentOf",
    "parentWindowOf",
    "popupFor",
    "previousTabbable",
    "subwindowOf",
};

void AXTreeFormatterWin::AddProperties(
    const Microsoft::WRL::ComPtr<IAccessible> node,
    base::Value::Dict* dict,
    LONG root_x,
    LONG root_y) const {
  AddMSAAProperties(node, dict, root_x, root_y);
  AddSimpleDOMNodeProperties(node, dict);
  if (AddIA2Properties(node, dict)) {
    AddIA2ActionProperties(node, dict);
    AddIA2HypertextProperties(node, dict);
    AddIA2RelationProperties(node, dict);
    AddIA2TableProperties(node, dict);
    AddIA2TableCellProperties(node, dict);
    AddIA2TextProperties(node, dict);
    AddIA2ValueProperties(node, dict);
  }
}

void AXTreeFormatterWin::AddMSAAProperties(
    const Microsoft::WRL::ComPtr<IAccessible> node,
    base::Value::Dict* dict,
    LONG root_x,
    LONG root_y) const {
  base::win::ScopedVariant variant_self(CHILDID_SELF);
  base::win::ScopedBstr bstr;
  base::win::ScopedVariant ia_role_variant;
  if (SUCCEEDED(node->get_accRole(variant_self, ia_role_variant.Receive()))) {
    dict->Set("role", RoleVariantToString(ia_role_variant));
  }

  // If S_FALSE it means there is no name
  if (S_OK == node->get_accName(variant_self, bstr.Receive())) {
    dict->Set("name", base::WideToUTF8(bstr.Get()));
  }
  bstr.Reset();

  Microsoft::WRL::ComPtr<IDispatch> parent_dispatch;
  if (SUCCEEDED(node->get_accParent(&parent_dispatch))) {
    Microsoft::WRL::ComPtr<IAccessible> parent_accessible;
    if (!parent_dispatch) {
      dict->Set("parent", "[null]");
    } else if (SUCCEEDED(parent_dispatch.As(&parent_accessible))) {
      base::win::ScopedVariant parent_ia_role_variant;
      if (SUCCEEDED(parent_accessible->get_accRole(
              variant_self, parent_ia_role_variant.Receive())))
        dict->Set("parent", RoleVariantToString(parent_ia_role_variant));
      else
        dict->Set("parent", "[Error retrieving role from parent]");
    } else {
      dict->Set("parent", "[Error getting IAccessible* for parent]");
    }
  } else {
    dict->Set("parent", "[Error retrieving parent]");
  }

  HWND hwnd;
  if (SUCCEEDED(::WindowFromAccessibleObject(node.Get(), &hwnd)) && hwnd) {
    dict->Set("window_class", base::WideToUTF16(gfx::GetClassName(hwnd)));
  } else {
    // This method is implemented by oleacc.dll and uses get_accParent,
    // therefore it Will fail if get_accParent from root fails.
    dict->Set("window_class", "[Error]");
  }

  if (SUCCEEDED(node->get_accValue(variant_self, bstr.Receive())) && bstr.Get())
    dict->Set("value", base::WideToUTF8(bstr.Get()));
  bstr.Reset();

  int32_t ia_state = 0;
  base::win::ScopedVariant ia_state_variant;
  if (node->get_accState(variant_self, ia_state_variant.Receive()) == S_OK &&
      ia_state_variant.type() == VT_I4) {
    ia_state = ia_state_variant.ptr()->intVal;
    std::vector<std::wstring> state_strings;
    IAccessibleStateToStringVector(ia_state, &state_strings);

    base::Value::List states;
    states.reserve(state_strings.size());
    for (const auto& str : state_strings)
      states.Append(base::WideToUTF8(str));
    dict->Set("states", std::move(states));
  }

  if (S_OK == node->get_accDescription(variant_self, bstr.Receive())) {
    dict->Set("description", base::WideToUTF8(bstr.Get()));
  }
  bstr.Reset();

  // |get_accDefaultAction| returns a localized string.
  if (S_OK == node->get_accDefaultAction(variant_self, bstr.Receive())) {
    dict->Set("default_action", base::WideToUTF8(bstr.Get()));
  }
  bstr.Reset();

  if (S_OK == node->get_accKeyboardShortcut(variant_self, bstr.Receive())) {
    dict->Set("keyboard_shortcut", base::WideToUTF8(bstr.Get()));
  }
  bstr.Reset();

  if (S_OK == node->get_accHelp(variant_self, bstr.Receive())) {
    dict->Set("help", base::WideToUTF8(bstr.Get()));
  }
  bstr.Reset();

  LONG x, y, width, height;
  if (SUCCEEDED(node->accLocation(&x, &y, &width, &height, variant_self))) {
    base::Value::Dict location;
    location.Set("x", static_cast<int>(x - root_x));
    location.Set("y", static_cast<int>(y - root_y));
    dict->Set("location", std::move(location));

    base::Value::Dict size;
    size.Set("width", static_cast<int>(width));
    size.Set("height", static_cast<int>(height));
    dict->Set("size", std::move(size));
  }
}

void AXTreeFormatterWin::AddSimpleDOMNodeProperties(
    const Microsoft::WRL::ComPtr<IAccessible> node,
    base::Value::Dict* dict) const {
  Microsoft::WRL::ComPtr<ISimpleDOMNode> simple_dom_node;

  if (S_OK != IA2QueryInterface<ISimpleDOMNode>(node.Get(), &simple_dom_node))
    return;

  base::win::ScopedBstr bstr;
  if (S_OK == simple_dom_node->get_innerHTML(bstr.Receive())) {
    dict->Set("inner_html", base::WideToUTF8(bstr.Get()));
  }
  bstr.Reset();
}

bool AXTreeFormatterWin::AddIA2Properties(
    const Microsoft::WRL::ComPtr<IAccessible> node,
    base::Value::Dict* dict) const {
  Microsoft::WRL::ComPtr<IAccessible2> ia2;
  if (S_OK != IA2QueryInterface<IAccessible2>(node.Get(), &ia2))
    return false;

  LONG ia2_role = 0;
  if (SUCCEEDED(ia2->role(&ia2_role))) {
    const std::string* legacy_role = dict->FindString("role");
    if (legacy_role)
      dict->Set("msaa_legacy_role", *legacy_role);
    // Overwrite MSAA role which is more limited.
    dict->Set("role", base::WideToUTF8(IAccessible2RoleToString(ia2_role)));
  }

  std::vector<std::wstring> state_strings;
  AccessibleStates states;
  if (ia2->get_states(&states) == S_OK) {
    IAccessible2StateToStringVector(states, &state_strings);
    // Append IA2 state list to MSAA state
    base::Value::List* states_list = dict->FindList("states");
    if (states_list) {
      for (const auto& str : state_strings)
        states_list->Append(base::WideToUTF8(str));
    }
  }

  base::win::ScopedBstr bstr;

  if (ia2->get_attributes(bstr.Receive()) == S_OK) {
    // get_attributes() returns a semicolon delimited string. Turn it into a
    // Value::List

    std::vector<std::u16string> ia2_attributes =
        base::SplitString(base::WideToUTF16(bstr.Get()), std::u16string(1, ';'),
                          base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);

    base::Value::List attributes;
    attributes.reserve(ia2_attributes.size());
    for (const auto& str : ia2_attributes)
      attributes.Append(str);
    dict->Set("attributes", std::move(attributes));
  }
  bstr.Reset();

  LONG index_in_parent;
  if (SUCCEEDED(ia2->get_indexInParent(&index_in_parent)))
    dict->Set("index_in_parent", static_cast<int>(index_in_parent));

  LONG n_relations;
  if (SUCCEEDED(ia2->get_nRelations(&n_relations)))
    dict->Set("n_relations", static_cast<int>(n_relations));

  LONG group_level, similar_items_in_group, position_in_group;
  // |GetGroupPosition| returns S_FALSE when no grouping information is
  // available so avoid using |SUCCEEDED|.
  if (ia2->get_groupPosition(&group_level, &similar_items_in_group,
                             &position_in_group) == S_OK) {
    dict->Set("group_level", static_cast<int>(group_level));
    dict->Set("similar_items_in_group",
              static_cast<int>(similar_items_in_group));
    dict->Set("position_in_group", static_cast<int>(position_in_group));
  }

  if (SUCCEEDED(ia2->get_localizedExtendedRole(bstr.Receive())) && bstr.Get()) {
    dict->Set("localized_extended_role", base::WideToUTF8(bstr.Get()));
  }
  bstr.Reset();

  return true;
}

void AXTreeFormatterWin::AddIA2ActionProperties(
    const Microsoft::WRL::ComPtr<IAccessible> node,
    base::Value::Dict* dict) const {
  Microsoft::WRL::ComPtr<IAccessibleAction> ia2action;
  if (S_OK != IA2QueryInterface<IAccessibleAction>(node.Get(), &ia2action))
    return;

  // |IAccessibleAction::get_name| returns a localized string.
  base::win::ScopedBstr name;
  if (SUCCEEDED(ia2action->get_name(0 /* action_index */, name.Receive())) &&
      name.Get()) {
    dict->Set("action_name", base::WideToUTF8(name.Get()));
  }
}

void AXTreeFormatterWin::AddIA2HypertextProperties(
    Microsoft::WRL::ComPtr<IAccessible> node,
    base::Value::Dict* dict) const {
  Microsoft::WRL::ComPtr<IAccessibleHypertext> ia2hyper;
  if (S_OK != IA2QueryInterface<IAccessibleHypertext>(node.Get(), &ia2hyper))
    return;

  base::win::ScopedBstr text_bstr;
  HRESULT hr;

  hr = ia2hyper->get_text(0, IA2_TEXT_OFFSET_LENGTH, text_bstr.Receive());
  if (FAILED(hr))
    return;

  std::wstring ia2_hypertext(text_bstr.Get(), text_bstr.Length());
  // IA2 Spec calls embedded objects hyperlinks. We stick to embeds for clarity.
  LONG number_of_embeds;
  hr = ia2hyper->get_nHyperlinks(&number_of_embeds);
  if (SUCCEEDED(hr) && number_of_embeds > 0) {
    // Replace all embedded characters with the child indices of the
    // accessibility objects they refer to.
    std::wstring embedded_character = base::UTF16ToWide(
        std::u16string(1, AXPlatformNodeBase::kEmbeddedCharacter));
    size_t character_index = 0;
    size_t hypertext_index = 0;
    while (hypertext_index < ia2_hypertext.length()) {
      if (ia2_hypertext[hypertext_index] !=
          AXPlatformNodeBase::kEmbeddedCharacter) {
        ++character_index;
        ++hypertext_index;
        continue;
      }

      LONG index_of_embed;
      hr = ia2hyper->get_hyperlinkIndex(character_index, &index_of_embed);
      // S_FALSE will be returned if no embedded object is found at the given
      // embedded character offset. Exclude child index from such cases.
      LONG child_index = -1;
      if (hr == S_OK) {
        DCHECK_GE(index_of_embed, 0);
        Microsoft::WRL::ComPtr<IAccessibleHyperlink> embedded_object;
        hr = ia2hyper->get_hyperlink(index_of_embed, &embedded_object);
        DCHECK(SUCCEEDED(hr));
        Microsoft::WRL::ComPtr<IAccessible2> ax_embed;
        hr = embedded_object.As(&ax_embed);
        DCHECK(SUCCEEDED(hr));
        hr = ax_embed->get_indexInParent(&child_index);
        DCHECK(SUCCEEDED(hr));
      }

      std::wstring child_index_str =
          (child_index >= 0)
              ? base::StrCat(
                    {L"<obj", base::NumberToWString(child_index), L">"})
              : std::wstring(L"<obj>");
      base::ReplaceFirstSubstringAfterOffset(
          &ia2_hypertext, hypertext_index, embedded_character, child_index_str);
      ++character_index;
      hypertext_index += child_index_str.length();
      --number_of_embeds;
    }
  }
  DCHECK_EQ(number_of_embeds, 0);

  dict->Set("ia2_hypertext", base::WideToUTF16(ia2_hypertext));
}

void AXTreeFormatterWin::AddIA2RelationProperties(
    const Microsoft::WRL::ComPtr<IAccessible> node,
    base::Value::Dict* dict) const {
  Microsoft::WRL::ComPtr<IAccessible2> ia2;
  if (S_OK != IA2QueryInterface<IAccessible2>(node.Get(), &ia2)) {
    return;
  }

  LONG n_relations;
  if (!SUCCEEDED(ia2->get_nRelations(&n_relations)) || n_relations <= 0) {
    // If we don't have n_relations, we certainly won't get any
    // more information.
    return;
  }

  LONG ignored;
  IAccessibleRelation** relations = new IAccessibleRelation*[n_relations]();
  // We need to do an if check here because failure is possible if the relation
  // points to nodes that are hidden, even if n_relations > 0.
  if (SUCCEEDED(ia2->get_relations(n_relations, relations, &ignored))) {
    for (int i = 0; i < n_relations; i++) {
      AddIA2RelationProperty(relations[i], dict);
    }
  }
  delete[] relations;
}

void AXTreeFormatterWin::AddIA2RelationProperty(
    const Microsoft::WRL::ComPtr<IAccessibleRelation> relation,
    base::Value::Dict* dict) const {
  // Since we already verified a relation exists and points to some
  // non-hidden node, all of these checks should work.
  LONG n_targets;
  relation->get_nTargets(&n_targets);
  DCHECK(n_targets != NULL);
  DCHECK(n_targets > 0);

  base::win::ScopedBstr name;
  relation->get_relationType(name.Receive());
  DCHECK(name.Get() != NULL);

  LONG ignored;
  IUnknown** targets = new IUnknown*[n_targets]();
  HRESULT hr = relation->get_targets(n_targets, targets, &ignored);
  DCHECK(SUCCEEDED(hr));
  std::string targetsString;
  for (int i = 0; i < n_targets; i++) {
    Microsoft::WRL::ComPtr<IAccessible2> ia2;
    if (S_OK != IA2QueryInterface<IAccessible2>(targets[i], &ia2)) {
      continue;
    }
    LONG role = 0;
    if (SUCCEEDED(ia2->role(&role))) {
      if (targetsString.size() > 0) {
        targetsString.append(",");
      }
      targetsString.append(base::WideToUTF8(IAccessible2RoleToString(role)));
    }
  }
  delete[] targets;

  dict->Set(base::WideToUTF8(name.Get()), targetsString);
}

void AXTreeFormatterWin::AddIA2TableProperties(
    const Microsoft::WRL::ComPtr<IAccessible> node,
    base::Value::Dict* dict) const {
  Microsoft::WRL::ComPtr<IAccessibleTable> ia2table;
  if (S_OK != IA2QueryInterface<IAccessibleTable>(node.Get(), &ia2table))
    return;  // No IA2Text, we are finished with this node.

  LONG table_rows;
  if (SUCCEEDED(ia2table->get_nRows(&table_rows)))
    dict->Set("table_rows", static_cast<int>(table_rows));

  LONG table_columns;
  if (SUCCEEDED(ia2table->get_nColumns(&table_columns)))
    dict->Set("table_columns", static_cast<int>(table_columns));
}

static std::u16string ProcessAccessiblesArray(IUnknown** accessibles,
                                              LONG num_accessibles) {
  std::u16string related_accessibles_string;
  if (num_accessibles <= 0)
    return related_accessibles_string;

  base::win::ScopedVariant variant_self(CHILDID_SELF);
  for (int index = 0; index < num_accessibles; index++) {
    related_accessibles_string += (index > 0) ? u"," : u"<";
    Microsoft::WRL::ComPtr<IUnknown> unknown = accessibles[index];
    Microsoft::WRL::ComPtr<IAccessible> accessible;
    if (SUCCEEDED(unknown.As(&accessible))) {
      base::win::ScopedBstr name;
      if (S_OK == accessible->get_accName(variant_self, name.Receive()))
        related_accessibles_string += base::WideToUTF16(name.Get());
      else
        related_accessibles_string += u"no name";
    }
  }

  return related_accessibles_string + u">";
}

void AXTreeFormatterWin::AddIA2TableCellProperties(
    const Microsoft::WRL::ComPtr<IAccessible> node,
    base::Value::Dict* dict) const {
  Microsoft::WRL::ComPtr<IAccessibleTableCell> ia2cell;
  if (S_OK != IA2QueryInterface<IAccessibleTableCell>(node.Get(), &ia2cell))
    return;  // No IA2Text, we are finished with this node.

  LONG column_index;
  if (SUCCEEDED(ia2cell->get_columnIndex(&column_index))) {
    dict->Set("ia2_table_cell_column_index", static_cast<int>(column_index));
  }

  LONG row_index;
  if (SUCCEEDED(ia2cell->get_rowIndex(&row_index))) {
    dict->Set("ia2_table_cell_row_index", static_cast<int>(row_index));
  }

  LONG n_row_header_cells;
  IUnknown** row_headers;
  if (SUCCEEDED(
          ia2cell->get_rowHeaderCells(&row_headers, &n_row_header_cells)) &&
      n_row_header_cells > 0) {
    std::u16string accessibles_desc =
        ProcessAccessiblesArray(row_headers, n_row_header_cells);
    CoTaskMemFree(row_headers);  // Free the array manually.
    dict->Set("row_headers", accessibles_desc);
  }

  LONG n_column_header_cells;
  IUnknown** column_headers;
  if (SUCCEEDED(ia2cell->get_columnHeaderCells(&column_headers,
                                               &n_column_header_cells)) &&
      n_column_header_cells > 0) {
    std::u16string accessibles_desc =
        ProcessAccessiblesArray(column_headers, n_column_header_cells);
    CoTaskMemFree(column_headers);  // Free the array manually.
    dict->Set("column_headers", accessibles_desc);
  }
}

void AXTreeFormatterWin::AddIA2TextProperties(
    const Microsoft::WRL::ComPtr<IAccessible> node,
    base::Value::Dict* dict) const {
  Microsoft::WRL::ComPtr<IAccessibleText> ia2text;

  if (S_OK != IA2QueryInterface<IAccessibleText>(node.Get(), &ia2text))
    return;

  LONG n_characters;
  if (SUCCEEDED(ia2text->get_nCharacters(&n_characters)))
    dict->Set("n_characters", static_cast<int>(n_characters));

  LONG caret_offset;
  if (ia2text->get_caretOffset(&caret_offset) == S_OK)
    dict->Set("caret_offset", static_cast<int>(caret_offset));

  LONG n_selections;
  if (SUCCEEDED(ia2text->get_nSelections(&n_selections))) {
    dict->Set("n_selections", static_cast<int>(n_selections));
    if (n_selections > 0) {
      LONG start, end;
      if (SUCCEEDED(ia2text->get_selection(0, &start, &end))) {
        dict->Set("selection_start", static_cast<int>(start));
        dict->Set("selection_end", static_cast<int>(end));
      }
    }
  }

  // Handle IA2 text attributes, adding them as a list.
  // IA2 text attributes comes formatted as a single string, as follows:
  // https://wiki.linuxfoundation.org/accessibility/iaccessible2/textattributes
  base::Value::List text_attributes;
  LONG current_offset = 0, start_offset, end_offset;
  while (current_offset < n_characters) {
    // TODO(aleventhal) n_characters is not actually useful for ending the
    // loop, because it counts embedded object characters as more than 1,
    // meaning that it counts all the text in the subtree. However, the
    // offsets used in other IAText methods only count the embedded object
    // characters as 1.
    base::win::ScopedBstr temp_bstr;
    HRESULT hr = ia2text->get_attributes(current_offset, &start_offset,
                                         &end_offset, temp_bstr.Receive());
    // The below start_offset < current_offset check is needed because
    // nCharacters is not helpful as described above.
    // When asking for a range past the end of the string, this will occur,
    // although it's not clear whether that's desired or whether
    // S_FALSE or an error should be returned when the offset is out of range.
    if (FAILED(hr) || start_offset < current_offset)
      break;
    // DCHECK(start_offset == current_offset);  // Always at text range start.
    if (hr == S_OK && temp_bstr.Get() && wcslen(temp_bstr.Get())) {
      // Append offset:<number>.
      std::u16string offset_str =
          u"offset:" + base::NumberToString16(start_offset);
      text_attributes.Append(offset_str);
      // Append name:value pairs.
      std::vector<std::wstring> name_val_pairs =
          SplitString(std::wstring(temp_bstr.Get()), L";",
                      base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
      for (const auto& name_val_pair : name_val_pairs)
        text_attributes.Append(base::WideToUTF16(name_val_pair));
    }
    current_offset = end_offset;
  }

  dict->Set("text_attributes", std::move(text_attributes));
}

void AXTreeFormatterWin::AddIA2ValueProperties(
    const Microsoft::WRL::ComPtr<IAccessible> node,
    base::Value::Dict* dict) const {
  Microsoft::WRL::ComPtr<IAccessibleValue> ia2value;
  if (S_OK != IA2QueryInterface<IAccessibleValue>(node.Get(), &ia2value))
    return;  // No IA2Value, we are finished with this node.

  base::win::ScopedVariant current_value;
  if (ia2value->get_currentValue(current_value.Receive()) == S_OK &&
      isfinite(V_R8(current_value.ptr()))) {
    dict->Set("currentValue", V_R8(current_value.ptr()));
  }

  base::win::ScopedVariant minimum_value;
  if (ia2value->get_minimumValue(minimum_value.Receive()) == S_OK &&
      isfinite(V_R8(minimum_value.ptr()))) {
    dict->Set("minimumValue", V_R8(minimum_value.ptr()));
  }

  base::win::ScopedVariant maximum_value;
  if (ia2value->get_maximumValue(maximum_value.Receive()) == S_OK &&
      isfinite(V_R8(maximum_value.ptr()))) {
    dict->Set("maximumValue", V_R8(maximum_value.ptr()));
  }
}

std::string AXTreeFormatterWin::ProcessTreeForOutput(
    const base::Value::Dict& dict) const {
  std::string line;

  // Always show role, and show it first.
  const std::string* role_value = dict.FindString("role");
  if (role_value) {
    WriteAttribute(true, *role_value, &line);
  }

  for (const char* attribute_name : ALL_ATTRIBUTES) {
    const base::Value* value = dict.Find(attribute_name);
    if (!value)
      continue;

    switch (value->type()) {
      case base::Value::Type::LIST: {
        for (const auto& entry : value->GetList()) {
          std::string string_value;
          if (entry.is_string())
            WriteAttribute(false, entry.GetString(), &line);
        }
        break;
      }
      case base::Value::Type::DICT: {
        // Currently all dictionary values are coordinates.
        // Revisit this if that changes.
        const base::Value::Dict& dict_value = value->GetDict();
        if (strcmp(attribute_name, "size") == 0) {
          WriteAttribute(
              false, FormatCoordinates(dict_value, "size", "width", "height"),
              &line);
        } else if (strcmp(attribute_name, "location") == 0) {
          WriteAttribute(false,
                         FormatCoordinates(dict_value, "location", "x", "y"),
                         &line);
        }
        break;
      }
      default:
        WriteAttribute(
            false, base::StrCat({attribute_name, "=", AXFormatValue(*value)}),
            &line);
        break;
    }
  }

  return line;
}

Microsoft::WRL::ComPtr<IAccessible> AXTreeFormatterWin::FindActiveDocument(
    IAccessible* root) const {
  for (const MSAAChild& child : MSAAChildren(root)) {
    IAccessible* ia = child.AsIAccessible();
    if (!ia)
      continue;

    Microsoft::WRL::ComPtr<IAccessible2> ia2;
    if (FAILED(IA2QueryInterface<IAccessible2>(ia, &ia2)))
      continue;  // No IA2, we are finished with this node.

    LONG role = 0;
    if (FAILED(ia2->role(&role)))
      continue;

    // Firefox browser exposes documents for all tabs, grab one that doesn't
    // have OFFSCREEN state.
    if (role == IA2_ROLE_INTERNAL_FRAME) {
      base::win::ScopedVariant state_variant;
      if (SUCCEEDED(ia->get_accState(base::win::ScopedVariant(CHILDID_SELF),
                                     state_variant.Receive())) &&
          state_variant.type() == VT_I4) {
        int32_t state = V_I4(state_variant.ptr());
        if (!(state & STATE_SYSTEM_OFFSCREEN))
          return ia;
      }
      continue;
    }

    // Chrome-based browsers expose active tab document only.
    if (role == ROLE_SYSTEM_DOCUMENT)
      return ia;

    Microsoft::WRL::ComPtr<IAccessible> active_document =
        FindActiveDocument(ia);
    if (active_document)
      return active_document;
  }

  return nullptr;
}

}  // namespace ui