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

// Copyright 2022 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_element_wrapper_mac.h"

#include <CoreFoundation/CoreFoundation.h>
#include <Foundation/Foundation.h>

#include <ostream>

#include "base/apple/bridging.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/containers/fixed_flat_set.h"
#include "base/debug/stack_trace.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/strings/pattern.h"
#include "base/strings/sys_string_conversions.h"
#include "ui/accessibility/platform/ax_private_attributes_mac.h"

// error: 'accessibilityAttributeNames' is deprecated: first deprecated in
// macOS 10.10 - Use the NSAccessibility protocol methods instead (see
// NSAccessibilityProtocols.h
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"

namespace ui {

constexpr char kUnsupportedObject[] =
    "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";

// static
bool AXElementWrapper::IsValidElement(const id node) {
  return AXElementWrapper(node).IsValidElement();
}

// static
bool AXElementWrapper::IsNSAccessibilityElement(const id node) {
  return AXElementWrapper(node).IsNSAccessibilityElement();
}

// static
bool AXElementWrapper::IsAXUIElement(const id node) {
  return AXElementWrapper(node).IsAXUIElement();
}

// static
NSArray* AXElementWrapper::ChildrenOf(const id node) {
  return AXElementWrapper(node).Children();
}

// Returns DOM id of a given node (either AXUIElement or
// BrowserAccessibilityCocoa).
// static
std::string AXElementWrapper::DOMIdOf(const id node) {
  return AXElementWrapper(node).DOMId();
}

bool AXElementWrapper::IsValidElement() const {
  return IsNSAccessibilityElement() || IsAXUIElement();
}

bool AXElementWrapper::IsNSAccessibilityElement() const {
  return [node_ isKindOfClass:[NSAccessibilityElement class]];
}

bool AXElementWrapper::IsAXUIElement() const {
  return CFGetTypeID((__bridge CFTypeRef)node_) == AXUIElementGetTypeID();
}

id AXElementWrapper::AsId() const {
  return node_;
}

std::string AXElementWrapper::DOMId() const {
  const id domid_value = *GetAttributeValue(@"AXDOMIdentifier");
  return base::SysNSStringToUTF8(static_cast<NSString*>(domid_value));
}

NSArray* AXElementWrapper::Children() const {
  if (IsNSAccessibilityElement())
    return [node_ children];

  if (IsAXUIElement()) {
    base::apple::ScopedCFTypeRef<CFTypeRef> children_ref;
    if ((AXUIElementCopyAttributeValue(
            (__bridge AXUIElementRef)node_, kAXChildrenAttribute,
            children_ref.InitializeInto())) == kAXErrorSuccess) {
      return base::apple::CFToNSOwnershipCast(
          (CFArrayRef)children_ref.release());
    }
    return nil;
  }

  NOTREACHED_IN_MIGRATION()
      << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
  return nil;
}

NSSize AXElementWrapper::Size() const {
  if (IsNSAccessibilityElement()) {
    return [node_ accessibilityFrame].size;
  }

  if (!IsAXUIElement()) {
    NOTREACHED_IN_MIGRATION()
        << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
    return NSMakeSize(0, 0);
  }

  id value = *GetAttributeValue(NSAccessibilitySizeAttribute);
  if (value && CFGetTypeID((__bridge CFTypeRef)value) == AXValueGetTypeID()) {
    AXValueType type = AXValueGetType((__bridge AXValueRef)value);
    if (type == kAXValueCGSizeType) {
      NSSize size;
      if (AXValueGetValue((__bridge AXValueRef)value, type, &size)) {
        return size;
      }
    }
  }
  return NSMakeSize(0, 0);
}

NSPoint AXElementWrapper::Position() const {
  if (IsNSAccessibilityElement()) {
    return [node_ accessibilityFrame].origin;
  }

  if (IsAXUIElement()) {
    id value = *GetAttributeValue(NSAccessibilityPositionAttribute);
    if (value && CFGetTypeID((__bridge CFTypeRef)value) == AXValueGetTypeID()) {
      AXValueType type = AXValueGetType((__bridge AXValueRef)value);
      if (type == kAXValueCGPointType) {
        NSPoint point;
        if (AXValueGetValue((__bridge AXValueRef)value, type, &point)) {
          return point;
        }
      }
    }
  }

  NOTREACHED_IN_MIGRATION()
      << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
  return NSMakePoint(0, 0);
}

NSArray* AXElementWrapper::AttributeNames() const {
  if (IsNSAccessibilityElement()) {
    return [node_ accessibilityAttributeNames];
  }

  if (IsAXUIElement()) {
    base::apple::ScopedCFTypeRef<CFArrayRef> attributes_ref;
    AXError result = AXUIElementCopyAttributeNames(
        (__bridge AXUIElementRef)node_, attributes_ref.InitializeInto());
    if (AXSuccess(result, "AXAttributeNamesOf")) {
      return base::apple::CFToNSOwnershipCast(attributes_ref.release());
    }
    return nil;
  }

  NOTREACHED_IN_MIGRATION()
      << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
  return nil;
}

NSArray* AXElementWrapper::ParameterizedAttributeNames() const {
  if (IsNSAccessibilityElement()) {
    return [node_ accessibilityParameterizedAttributeNames];
  }

  if (IsAXUIElement()) {
    base::apple::ScopedCFTypeRef<CFArrayRef> attributes_ref;
    AXError result = AXUIElementCopyParameterizedAttributeNames(
        (__bridge AXUIElementRef)node_, attributes_ref.InitializeInto());
    if (AXSuccess(result, "AXParameterizedAttributeNamesOf")) {
      return base::apple::CFToNSOwnershipCast(attributes_ref.release());
    }
    return nil;
  }

  NOTREACHED_IN_MIGRATION()
      << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
  return nil;
}

AXOptionalNSObject AXElementWrapper::GetAttributeValue(
    NSString* attribute) const {
  if (IsNSAccessibilityElement()) {
    return AXOptionalNSObject([node_ accessibilityAttributeValue:attribute]);
  }

  if (IsAXUIElement()) {
    base::apple::ScopedCFTypeRef<CFTypeRef> value_ref;
    AXError result = AXUIElementCopyAttributeValue(
        (__bridge AXUIElementRef)node_, (__bridge CFStringRef)attribute,
        value_ref.InitializeInto());
    return ToOptional(
        (__bridge id)value_ref.get(), result,
        "AXGetAttributeValue(" + base::SysNSStringToUTF8(attribute) + ")");
  }

  return AXOptionalNSObject::Error(kUnsupportedObject);
}

AXOptionalNSObject AXElementWrapper::GetParameterizedAttributeValue(
    NSString* attribute,
    id parameter) const {
  if (IsNSAccessibilityElement())
    return AXOptionalNSObject([node_ accessibilityAttributeValue:attribute
                                                    forParameter:parameter]);

  if (IsAXUIElement()) {
    base::apple::ScopedCFTypeRef<CFTypeRef> parameter_ref(
        CFBridgingRetain(parameter));
    if ([parameter isKindOfClass:[NSValue class]] &&
        !strcmp([parameter objCType], @encode(NSRange))) {
      NSRange range = [parameter rangeValue];
      parameter_ref.reset(AXValueCreate(kAXValueTypeCFRange, &range));
    }

    // Get value.
    base::apple::ScopedCFTypeRef<CFTypeRef> value_ref;
    AXError result = AXUIElementCopyParameterizedAttributeValue(
        (__bridge AXUIElementRef)node_, (__bridge CFStringRef)attribute,
        parameter_ref.get(), value_ref.InitializeInto());

    return ToOptional((__bridge id)value_ref.get(), result,
                      "GetParameterizedAttributeValue(" +
                          base::SysNSStringToUTF8(attribute) + ")");
  }

  return AXOptionalNSObject::Error(kUnsupportedObject);
}

std::optional<id> AXElementWrapper::PerformSelector(
    const std::string& selector_string) const {
  if (![node_ conformsToProtocol:@protocol(NSAccessibility)])
    return std::nullopt;

  NSString* selector_nsstring = base::SysUTF8ToNSString(selector_string);
  SEL selector = NSSelectorFromString(selector_nsstring);

  if ([node_ respondsToSelector:selector])
    return [node_ valueForKey:selector_nsstring];
  return std::nullopt;
}

std::optional<id> AXElementWrapper::PerformSelector(
    const std::string& selector_string,
    const std::string& argument_string) const {
  if (![node_ conformsToProtocol:@protocol(NSAccessibility)])
    return std::nullopt;

  SEL selector =
      NSSelectorFromString(base::SysUTF8ToNSString(selector_string + ":"));
  NSString* argument = base::SysUTF8ToNSString(argument_string);

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  if ([node_ respondsToSelector:selector])
    return [node_ performSelector:selector withObject:argument];
#pragma clang diagnostic pop
  return std::nullopt;
}

void AXElementWrapper::SetAttributeValue(NSString* attribute, id value) const {
  if (IsNSAccessibilityElement()) {
    [node_ accessibilitySetValue:value forAttribute:attribute];
    return;
  }

  if (IsAXUIElement()) {
    AXUIElementSetAttributeValue((__bridge AXUIElementRef)node_,
                                 (__bridge CFStringRef)attribute,
                                 (__bridge CFTypeRef)value);
    return;
  }

  NOTREACHED_IN_MIGRATION()
      << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
}

NSArray* AXElementWrapper::ActionNames() const {
  if (IsNSAccessibilityElement())
    return [node_ accessibilityActionNames];

  if (IsAXUIElement()) {
    base::apple::ScopedCFTypeRef<CFArrayRef> attributes_ref;
    if ((AXUIElementCopyActionNames((__bridge AXUIElementRef)node_,
                                    attributes_ref.InitializeInto())) ==
        kAXErrorSuccess) {
      return base::apple::CFToNSOwnershipCast(attributes_ref.release());
    }
    return nil;
  }

  NOTREACHED_IN_MIGRATION()
      << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
  return nil;
}

void AXElementWrapper::PerformAction(NSString* action) const {
  if (IsNSAccessibilityElement()) {
    [node_ accessibilityPerformAction:action];
    return;
  }

  if (IsAXUIElement()) {
    AXUIElementPerformAction((__bridge AXUIElementRef)node_,
                             (__bridge CFStringRef)action);
    return;
  }

  NOTREACHED_IN_MIGRATION()
      << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported.";
}

std::string AXElementWrapper::AXErrorMessage(AXError result,
                                             const std::string& message) const {
  if (result == kAXErrorSuccess) {
    return {};
  }

  std::string error;
  switch (result) {
    case kAXErrorAPIDisabled:
      error = "API disabled; you may need to add terminal and/or this binary "
              "to System Settings -> Privacy & Security -> Accessibility";
      break;
    case kAXErrorActionUnsupported:
      error = "action unsupported";
      break;
    case kAXErrorAttributeUnsupported:
      error = "attribute unsupported";
      break;
    case kAXErrorCannotComplete:
      error = "cannot complete";
      break;
    case kAXErrorFailure:
      error = "failure";
      break;
    case kAXErrorIllegalArgument:
      error = "illegal argument";
      break;
    case kAXErrorInvalidUIElement:
      error = "invalid UI element";
      break;
    case kAXErrorInvalidUIElementObserver:
      error = "illegal UI element observer";
      break;
    case kAXErrorNoValue:
      error = "no value";
      break;
    case kAXErrorNotEnoughPrecision:
      error = "not enough precision";
      break;
    case kAXErrorNotImplemented:
      error = "not implemented";
      break;
    case kAXErrorNotificationAlreadyRegistered:
      error = "notification already registered";
      break;
    case kAXErrorNotificationNotRegistered:
      error = "notification not registered";
      break;
    case kAXErrorNotificationUnsupported:
      error = "notification unsupported";
      break;
    case kAXErrorParameterizedAttributeUnsupported:
      error = "parameterized attribute unsupported";
      break;
    default:
      error = "unknown error";
      break;
  }
  return {message + ": " + error};
}

bool AXElementWrapper::AXSuccess(AXError result,
                                 const std::string& message) const {
  std::string message_text = AXErrorMessage(result, message);
  if (message_text.empty())
    return true;

  LOG(WARNING) << message_text;
  return false;
}

AXOptionalNSObject AXElementWrapper::ToOptional(
    id value,
    AXError result,
    const std::string& message) const {
  if (result == kAXErrorSuccess)
    return AXOptionalNSObject(value);

  return AXOptionalNSObject::Error(AXErrorMessage(result, message));
}

}  // namespace ui

#pragma clang diagnostic pop