// Copyright 2020 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_inspect_utils_mac.h"
#include <CoreGraphics/CoreGraphics.h>
#include <ostream>
#include "base/apple/bridging.h"
#include "base/apple/foundation_util.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/memory/scoped_policy.h"
#include "base/strings/pattern.h"
#include "base/strings/sys_string_conversions.h"
#include "ui/accessibility/platform/ax_private_attributes_mac.h"
#include "ui/accessibility/platform/inspect/ax_element_wrapper_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 {
namespace {
const char kChromeTitle[] = "Google Chrome";
const char kChromiumTitle[] = "Chromium";
const char kFirefoxTitle[] = "Firefox";
const char kSafariTitle[] = "Safari";
NSArray* AXChildrenOf(const id node) {
return AXElementWrapper(node).Children();
}
} // namespace
bool IsValidAXAttribute(const std::string& attribute) {
static NSSet<NSString*>* valid_attributes = [NSSet setWithArray:@[
NSAccessibilityAccessKeyAttribute,
NSAccessibilityARIAAtomicAttribute,
NSAccessibilityARIABusyAttribute,
NSAccessibilityARIAColumnCountAttribute,
NSAccessibilityARIAColumnIndexAttribute,
NSAccessibilityARIACurrentAttribute,
NSAccessibilityARIALiveAttribute,
NSAccessibilityARIAPosInSetAttribute,
NSAccessibilityARIARelevantAttribute,
NSAccessibilityARIARowCountAttribute,
NSAccessibilityARIARowIndexAttribute,
NSAccessibilityARIASetSizeAttribute,
NSAccessibilityAutocompleteValueAttribute,
NSAccessibilityBlockQuoteLevelAttribute,
NSAccessibilityBrailleLabelAttribute,
NSAccessibilityBrailleRoleDescription,
NSAccessibilityChromeAXNodeIdAttribute,
NSAccessibilityColumnHeaderUIElementsAttribute,
NSAccessibilityDescriptionAttribute,
NSAccessibilityDetailsElementsAttribute,
NSAccessibilityDOMClassList,
NSAccessibilityDropEffectsAttribute,
NSAccessibilityElementBusyAttribute,
NSAccessibilityFocusableAncestorAttribute,
NSAccessibilityGrabbedAttribute,
NSAccessibilityHasPopupAttribute,
NSAccessibilityInvalidAttribute,
NSAccessibilityIsMultiSelectable,
NSAccessibilityKeyShortcutsValueAttribute,
NSAccessibilityLoadedAttribute,
NSAccessibilityLoadingProgressAttribute,
NSAccessibilityMathFractionNumeratorAttribute,
NSAccessibilityMathFractionDenominatorAttribute,
NSAccessibilityMathRootRadicandAttribute,
NSAccessibilityMathRootIndexAttribute,
NSAccessibilityMathBaseAttribute,
NSAccessibilityMathSubscriptAttribute,
NSAccessibilityMathSuperscriptAttribute,
NSAccessibilityMathUnderAttribute,
NSAccessibilityMathOverAttribute,
NSAccessibilityMathPostscriptsAttribute,
NSAccessibilityMathPrescriptsAttribute,
NSAccessibilityOwnsAttribute,
NSAccessibilityPopupValueAttribute,
NSAccessibilityRequiredAttribute,
NSAccessibilityRoleDescriptionAttribute,
NSAccessibilitySelectedAttribute,
NSAccessibilitySizeAttribute,
NSAccessibilityTitleAttribute,
NSAccessibilityTitleUIElementAttribute,
NSAccessibilityURLAttribute,
NSAccessibilityVisitedAttribute,
]];
return [valid_attributes containsObject:base::SysUTF8ToNSString(attribute)];
}
base::apple::ScopedCFTypeRef<AXUIElementRef> FindAXUIElement(
const AXUIElementRef node,
const AXFindCriteria& criteria) {
if (criteria.Run(node)) {
return base::apple::ScopedCFTypeRef<AXUIElementRef>(
node, base::scoped_policy::RETAIN);
}
NSArray* children = AXChildrenOf((__bridge id)node);
for (id child in children) {
base::apple::ScopedCFTypeRef<AXUIElementRef> found =
FindAXUIElement((__bridge AXUIElementRef)child, criteria);
if (found) {
return found;
}
}
return base::apple::ScopedCFTypeRef<AXUIElementRef>();
}
std::pair<base::apple::ScopedCFTypeRef<AXUIElementRef>, int> FindAXUIElement(
const AXTreeSelector& selector) {
if (selector.widget) {
return {base::apple::ScopedCFTypeRef<AXUIElementRef>(
AXUIElementCreateApplication(selector.widget)),
selector.widget};
}
std::string title;
if (selector.types & AXTreeSelector::Chrome)
title = kChromeTitle;
else if (selector.types & AXTreeSelector::Chromium)
title = kChromiumTitle;
else if (selector.types & AXTreeSelector::Firefox)
title = kFirefoxTitle;
else if (selector.types & AXTreeSelector::Safari)
title = kSafariTitle;
else
return {base::apple::ScopedCFTypeRef<AXUIElementRef>(), 0};
NSArray* windows =
base::apple::CFToNSOwnershipCast(CGWindowListCopyWindowInfo(
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
kCGNullWindowID));
for (NSDictionary* window_info in windows) {
int pid = base::apple::ObjCCast<NSNumber>(window_info[@"kCGWindowOwnerPID"])
.intValue;
std::string window_name = base::SysNSStringToUTF8(
base::apple::ObjCCast<NSString>(window_info[@"kCGWindowOwnerName"]));
base::apple::ScopedCFTypeRef<AXUIElementRef> node;
// Application pre-defined selectors match or application title exact match.
bool app_title_match = window_name == selector.pattern;
if (window_name == title || app_title_match) {
node.reset(AXUIElementCreateApplication(pid));
}
// Window title match. Application contain an AXWindow accessible object as
// a first child, which accessible name contain a window title. For example:
// 'Inbox (2) - [email protected] - Gmail'.
if (!selector.pattern.empty() && !app_title_match) {
if (!node) {
node.reset(AXUIElementCreateApplication(pid));
}
base::apple::ScopedCFTypeRef<AXUIElementRef> window =
FindAXWindowChild(node.get(), selector.pattern);
if (window) {
node = window;
}
}
// ActiveTab selector.
if (node && selector.types & AXTreeSelector::ActiveTab) {
node = FindAXUIElement(
node.get(), base::BindRepeating([](const AXUIElementRef node) {
// Only active tab in exposed in browsers, thus find first
// AXWebArea role.
AXElementWrapper ax_node((__bridge id)node);
NSString* role =
*ax_node.GetAttributeValue(NSAccessibilityRoleAttribute);
return base::SysNSStringToUTF8(role) == "AXWebArea";
}));
}
// Found a match.
if (node)
return {node, pid};
}
return {base::apple::ScopedCFTypeRef<AXUIElementRef>(), 0};
}
base::apple::ScopedCFTypeRef<AXUIElementRef> FindAXWindowChild(
AXUIElementRef parent,
const std::string& pattern) {
NSArray* children = AXChildrenOf((__bridge id)parent);
if (children.count == 0) {
return base::apple::ScopedCFTypeRef<AXUIElementRef>();
}
id window = children.firstObject;
AXElementWrapper ax_window(window);
NSString* role = *ax_window.GetAttributeValue(NSAccessibilityRoleAttribute);
if (base::SysNSStringToUTF8(role) != "AXWindow") {
return base::apple::ScopedCFTypeRef<AXUIElementRef>();
}
NSString* window_title =
*ax_window.GetAttributeValue(NSAccessibilityTitleAttribute);
if (base::MatchPattern(base::SysNSStringToUTF8(window_title), pattern)) {
return base::apple::ScopedCFTypeRef<AXUIElementRef>(
(__bridge AXUIElementRef)window, base::scoped_policy::RETAIN);
}
return base::apple::ScopedCFTypeRef<AXUIElementRef>();
}
} // namespace ui
#pragma clang diagnostic pop