blob: 33f02f2b6389879a8c57794b5802500cd291ef09 [file]
/*
* Copyright (C) 2024 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <WebCore/AXCoreObject.h>
#include <WebCore/AXTextMarker.h>
#include <WebCore/AccessibilityRemoteToken.h>
#include <WebCore/FrameIdentifier.h>
#include <wtf/HashMap.h>
#include <wtf/MonotonicTime.h>
#include <wtf/Vector.h>
#include <wtf/WeakPtr.h>
namespace WebCore {
DECLARE_ALLOCATOR_WITH_HEAP_IDENTIFIER(AXSearchManager);
enum class AccessibilitySearchKey : uint8_t {
AnyType = 1,
Article,
BlockquoteSameLevel,
Blockquote,
BoldFont,
Button,
Checkbox,
Control,
DifferentType,
FontChange,
FontColorChange,
Frame,
Graphic,
HeadingLevel1,
HeadingLevel2,
HeadingLevel3,
HeadingLevel4,
HeadingLevel5,
HeadingLevel6,
HeadingSameLevel,
Heading,
Highlighted,
ItalicFont,
KeyboardFocusable,
Landmark,
Link,
List,
LiveRegion,
MisspelledWord,
Outline,
PlainText,
RadioGroup,
SameType,
StaticText,
StyleChange,
TableSameLevel,
Table,
TextField,
Underline,
UnvisitedLink,
VisitedLink,
};
struct AccessibilitySearchCriteria {
// FIXME: change the object pointers to object IDs.
WeakPtr<AXCoreObject> anchorObject { nullptr };
WeakPtr<AXCoreObject> startObject { nullptr };
CharacterRange startRange;
AccessibilitySearchDirection searchDirection { AccessibilitySearchDirection::Next };
Vector<AccessibilitySearchKey> searchKeys;
String searchText;
unsigned resultsLimit { 0 };
bool visibleOnly { false };
bool immediateDescendantsOnly { false };
};
// IPC-serializable version of AccessibilitySearchCriteria for cross-process search queries.
// Excludes object pointers since the remote frame will use its own root as anchor.
struct AccessibilitySearchCriteriaIPC {
// IPC validation limits, intended to be used for defense-in-depth bounds-checking for
// data from potentially compromised processes.
static constexpr unsigned maxSearchTextLength = 5000;
AccessibilitySearchDirection searchDirection { AccessibilitySearchDirection::Next };
Vector<AccessibilitySearchKey> searchKeys;
String searchText;
unsigned resultsLimit { 0 };
bool visibleOnly { false };
bool immediateDescendantsOnly { false };
// Absolute deadline for the top-level search. Used to implement cascading timeouts
// so deeply nested frames don't each use their own full timeout budget.
std::optional<MonotonicTime> deadline;
AccessibilitySearchCriteriaIPC() = default;
// Create from a regular AccessibilitySearchCriteria for IPC transmission
explicit AccessibilitySearchCriteriaIPC(const AccessibilitySearchCriteria& criteria)
: searchDirection(criteria.searchDirection)
, searchKeys(criteria.searchKeys)
, searchText(criteria.searchText)
, resultsLimit(criteria.resultsLimit)
, visibleOnly(criteria.visibleOnly)
, immediateDescendantsOnly(criteria.immediateDescendantsOnly)
{ }
// Constructor for IPC deserialization
AccessibilitySearchCriteriaIPC(AccessibilitySearchDirection direction, Vector<AccessibilitySearchKey> keys, String text, unsigned limit, bool visible, bool immediateDescendants, std::optional<MonotonicTime> deadline)
: searchDirection(direction)
, searchKeys(WTF::move(keys))
, searchText(WTF::move(text))
, resultsLimit(limit)
, visibleOnly(visible)
, immediateDescendantsOnly(immediateDescendants)
, deadline(deadline)
{ }
// Convert back to a full AccessibilitySearchCriteria with a given anchor object
AccessibilitySearchCriteria toSearchCriteria(AXCoreObject* anchorObject) const
{
return {
anchorObject,
nullptr, // startObject - start from beginning of anchor
{ }, // startRange
searchDirection,
searchKeys,
searchText,
resultsLimit,
visibleOnly,
immediateDescendantsOnly
};
}
};
// Represents a single entry in the search result stream.
// Can be either a local result or a placeholder for a remote frame.
class SearchResultEntry {
public:
enum class Type : uint8_t { LocalResult, RemoteFrame };
static SearchResultEntry localResult(Ref<AXCoreObject> object, size_t index)
{
return { Type::LocalResult, object.ptr(), std::nullopt, index };
}
// On iOS, a RemoteFrame entry carries its AXRemoteFrame object so the result can be converted to its
// AXRemoteElement platform wrapper (see AXCoreObject::findMatchingObjectsWithin). macOS leaves it null
// and resolves the frame via IPC instead.
static SearchResultEntry remoteFrame(FrameIdentifier frameID, RefPtr<AXCoreObject> remoteFrameObject, size_t index)
{
return { Type::RemoteFrame, WTF::move(remoteFrameObject), frameID, index };
}
bool isLocalResult() const { return m_type == Type::LocalResult; }
bool isRemoteFrame() const { return m_type == Type::RemoteFrame; }
RefPtr<AXCoreObject> objectIfLocalResult() const { return isLocalResult() ? m_object : nullptr; }
// On iOS, a RemoteFrame entry carries its AXRemoteFrame object. null on macOS / when not provided.
RefPtr<AXCoreObject> remoteFrameObject() const { return isRemoteFrame() ? m_object : nullptr; }
const std::optional<FrameIdentifier>& frameID() const LIFETIME_BOUND { return m_frameID; }
size_t streamIndex() const { return m_streamIndex; }
private:
SearchResultEntry(Type type, RefPtr<AXCoreObject> object, std::optional<FrameIdentifier> frameID, size_t streamIndex)
: m_type(type)
, m_object(WTF::move(object))
, m_frameID(WTF::move(frameID))
, m_streamIndex(streamIndex)
{
// A local result must have an object. A remote frame must have a frameID and may optionally
// carry its AXRemoteFrame object (used on iOS to emit an inline AXRemoteElement placeholder).
AX_ASSERT(m_type != Type::LocalResult || m_object);
AX_ASSERT((m_type == Type::RemoteFrame) == m_frameID.has_value());
}
Type m_type { Type::LocalResult };
RefPtr<AXCoreObject> m_object;
std::optional<FrameIdentifier> m_frameID;
size_t m_streamIndex { 0 };
};
// Result of a search with entries in tree traversal order.
// This allows proper interleaving of local and remote results.
class AccessibilitySearchResultStream {
public:
// Appends a local result with automatic 1-based index assignment.
// Uses 1-based indexing because HashMap<size_t, ...> uses 0 as the empty value
// (see AXCrossProcessSearchCoordinator::m_remoteResults).
void appendLocalResult(Ref<AXCoreObject> object)
{
m_entries.append(SearchResultEntry::localResult(WTF::move(object), nextIndex()));
}
// remoteFrameObject is non-null only on iOS, where it lets callers emit an inline AXRemoteElement
// placeholder. macOS passes none and resolves the frame via IPC.
void appendRemoteFrame(FrameIdentifier frameID, RefPtr<AXCoreObject> remoteFrameObject = nullptr)
{
m_entries.append(SearchResultEntry::remoteFrame(frameID, WTF::move(remoteFrameObject), nextIndex()));
}
const Vector<SearchResultEntry>& entries() const LIFETIME_BOUND { return m_entries; }
size_t entryCount() const { return m_entries.size(); }
void setResultsLimit(unsigned limit) { m_resultsLimit = limit; }
unsigned resultsLimit() const { return m_resultsLimit; }
private:
size_t nextIndex() const { return m_entries.size() + 1; }
Vector<SearchResultEntry> m_entries;
unsigned m_resultsLimit { 0 };
};
// Represents a single search result that can be either a local accessibility object
// or a remote token from a cross-process search.
class AccessibilitySearchResult {
public:
static AccessibilitySearchResult local(Ref<AXCoreObject> object)
{
return { object.ptr(), std::nullopt };
}
static AccessibilitySearchResult remote(AccessibilityRemoteToken&& token)
{
return { nullptr, WTF::move(token) };
}
bool isLocal() const { return m_localObject != nullptr; }
bool isRemote() const { return m_remoteToken.has_value(); }
RefPtr<AXCoreObject> objectIfLocalResult() const { return isLocal() ? m_localObject : nullptr; }
const std::optional<AccessibilityRemoteToken>& remoteToken() const LIFETIME_BOUND { return m_remoteToken; }
private:
AccessibilitySearchResult(RefPtr<AXCoreObject> object, std::optional<AccessibilityRemoteToken> token)
: m_localObject(WTF::move(object))
, m_remoteToken(WTF::move(token))
{
// A result must be either local or remote, but not both, and not neither.
AX_ASSERT((m_localObject != nullptr) != m_remoteToken.has_value());
}
// For local results - the accessibility object in this process.
RefPtr<AXCoreObject> m_localObject;
// For remote results - the token to create a platform remote element.
std::optional<AccessibilityRemoteToken> m_remoteToken;
};
// Vector of search results that can contain both local and remote results in tree order.
using AccessibilitySearchResults = Vector<AccessibilitySearchResult>;
// Callback invoked when a remote frame is encountered during search.
// Parameters: frameID, streamIndex, localResultCountSoFar
// This allows callers to dispatch IPC eagerly while the local search continues.
using RemoteFrameSearchCallback = Function<void(FrameIdentifier, size_t, unsigned)>;
class AXSearchManager {
WTF_DEPRECATED_MAKE_FAST_ALLOCATED_WITH_HEAP_IDENTIFIER(AXSearchManager, AXSearchManager);
public:
// Primary search API - returns results that may include both local objects and remote tokens.
// The optional callback is invoked when a remote frame is encountered, allowing eager IPC dispatch.
// Callers should use performSearchWithCrossProcessCoordination() from AXCrossProcessSearch.h
// for automatic cross-process coordination.
WEBCORE_EXPORT AccessibilitySearchResultStream findMatchingObjectsAsStream(AccessibilitySearchCriteria&&, RemoteFrameSearchCallback&& = nullptr);
std::optional<AXTextMarkerRange> findMatchingRange(AccessibilitySearchCriteria&&);
private:
AccessibilitySearchResultStream findMatchingObjectsInternalAsStream(const AccessibilitySearchCriteria&, const RemoteFrameSearchCallback&);
bool match(Ref<AXCoreObject>, const AccessibilitySearchCriteria&);
bool matchText(Ref<AXCoreObject>, const String&);
bool matchForSearchKeyAtIndex(Ref<AXCoreObject>, const AccessibilitySearchCriteria&, size_t);
DidTimeout revealHiddenMatchWithTimeout(AXCoreObject&, Seconds);
bool lastRevealAttemptTimedOut()
{
if (isMainThread())
return false;
return m_lastRevealAttemptTimedOut;
}
void setLastRevealAttemptTimedOut(bool newValue)
{
AX_ASSERT(!isMainThread());
m_lastRevealAttemptTimedOut = newValue;
}
// Keeps the ranges of misspellings for each object.
HashMap<AXID, Vector<AXTextMarkerRange>> m_misspellingRanges;
// For certain types of searches, we may detect that an object matching the search is in a collapsed,
// but revealable / expandable container. We try to do this reveal synchronously from the accessibility thread
// to the main-thread, but with a timeout in case the main-thread is busy. If the main-thread is busy once,
// we don't want to try to synchronously reveal collapsed content again.
//
// This must only be read and written from the accessibility thread.
bool m_lastRevealAttemptTimedOut { false };
};
WTF::TextStream& operator<<(WTF::TextStream&, AccessibilitySearchKey);
WTF::TextStream& operator<<(WTF::TextStream&, const AccessibilitySearchCriteria&);
} // namespace WebCore