#include "content/renderer/accessibility/render_accessibility_impl.h"
#include <stddef.h>
#include <stdint.h>
#include <string>
#include <utility>
#include "base/debug/crash_logging.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/task/common/task_annotator.h"
#include "base/timer/elapsed_timer.h"
#include "build/build_config.h"
#include "content/public/renderer/render_thread.h"
#include "content/renderer/accessibility/annotations/ax_annotators_manager.h"
#include "content/renderer/accessibility/ax_action_target_factory.h"
#include "content/renderer/accessibility/blink_ax_action_target.h"
#include "content/renderer/accessibility/render_accessibility_manager.h"
#include "content/renderer/render_frame_impl.h"
#include "services/metrics/public/cpp/mojo_ukm_recorder.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/public/web/web_disallow_transition_scope.h"
#include "third_party/blink/public/web/web_document.h"
#include "third_party/blink/public/web/web_page_popup.h"
#include "third_party/blink/public/web/web_settings.h"
#include "third_party/blink/public/web/web_view.h"
#include "ui/accessibility/ax_event_intent.h"
#include "ui/accessibility/ax_mode_histogram_logger.h"
#include "ui/accessibility/ax_tree_id.h"
#include "ui/gfx/geometry/vector2d_conversions.h"
using blink::WebAXContext;
using blink::WebAXObject;
using blink::WebDocument;
using blink::WebView;
namespace {
constexpr base::TimeDelta kMinSerializationTimeToSend = base::Milliseconds(100);
constexpr base::TimeDelta kMinUKMDelay = base::Seconds(300);
void SetAccessibilityCrashKey(ui::AXMode mode) {
static auto* const ax_mode_crash_key = base::debug::AllocateCrashKeyString(
"ax_mode", base::debug::CrashKeySize::Size64);
base::debug::SetCrashKeyString(ax_mode_crash_key, mode.ToString());
}
}
namespace content {
RenderAccessibilityImpl::RenderAccessibilityImpl(
RenderAccessibilityManager* const render_accessibility_manager,
RenderFrameImpl* const render_frame)
: RenderFrameObserver(render_frame),
render_accessibility_manager_(render_accessibility_manager),
render_frame_(render_frame),
ukm_timer_(std::make_unique<base::ElapsedTimer>()),
last_ukm_source_id_(ukm::kInvalidSourceId) {
mojo::Remote<ukm::mojom::UkmRecorderFactory> factory;
content::RenderThread::Get()->BindHostReceiver(
factory.BindNewPipeAndPassReceiver());
ukm_recorder_ = ukm::MojoUkmRecorder::Create(*factory);
#if BUILDFLAG(IS_ANDROID)
render_frame_->GetWebView()
->GetSettings()
->SetAccessibilityPasswordValuesEnabled(true);
#endif
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_ANDROID)
render_frame_->GetWebView()->GetSettings()->SetAriaModalPrunesAXTree(true);
#endif
#if BUILDFLAG(IS_CHROMEOS)
render_frame_->GetWebView()
->GetSettings()
->SetAccessibilityIncludeSvgGElement(true);
#endif
ax_annotators_manager_ = std::make_unique<AXAnnotatorsManager>(this);
}
RenderAccessibilityImpl::~RenderAccessibilityImpl() {
if (ax_context_) {
ax_context_->ResetSerializer();
}
}
void RenderAccessibilityImpl::DidCreateNewDocument() {
const WebDocument& document = GetMainDocument();
DCHECK(!document.IsNull());
ax_context_ = std::make_unique<WebAXContext>(document, accessibility_mode_);
ax_context_->SetSerializationResetToken(*reset_token_);
ScheduleImmediateAXUpdate();
}
void RenderAccessibilityImpl::DidCommitProvisionalLoad(
ui::PageTransition transition) {
MaybeSendUKM();
slowest_serialization_time_ = base::TimeDelta();
ukm_timer_ = std::make_unique<base::ElapsedTimer>();
ax_annotators_manager_->CancelAnnotations();
page_language_.clear();
ax_context_->OnSerializationCancelled();
weak_factory_for_pending_events_.InvalidateWeakPtrs();
loading_stage_ = LoadingStage::kPreload;
}
void RenderAccessibilityImpl::NotifyAccessibilityModeChange(
const ui::AXMode& mode) {
CHECK(reset_token_);
ui::AXMode old_mode = accessibility_mode_;
DCHECK(!mode.is_mode_off())
<< "Should not be reached when turning a11y off; rather, the "
"RenderAccessibilityImpl should be destroyed.";
if (old_mode == mode) {
DCHECK(ax_context_);
NOTREACHED() << "Do not call AccessibilityModeChanged unless it changes.";
}
accessibility_mode_ = mode;
bool was_on = !old_mode.is_mode_off();
DCHECK_EQ(was_on, !!ax_context_);
SetAccessibilityCrashKey(mode);
if (ax_context_) {
ax_context_->SetAXMode(mode);
} else {
DidCreateNewDocument();
}
DCHECK(ax_context_);
DCHECK_EQ(accessibility_mode_, ax_context_->GetAXMode());
ui::RecordAccessibilityModeHistograms(ui::AXHistogramPrefix::kBlink,
accessibility_mode_, old_mode);
if (was_on) {
ax_context_->MarkDocumentDirty();
}
ax_annotators_manager_->AccessibilityModeChanged(old_mode, mode);
FireLoadCompleteIfLoaded();
}
void RenderAccessibilityImpl::set_reset_token(uint32_t reset_token) {
CHECK(reset_token);
reset_token_ = reset_token;
if (ax_context_) {
ax_context_->SetSerializationResetToken(reset_token);
}
}
#if BUILDFLAG(IS_CHROMEOS)
void RenderAccessibilityImpl::FireLayoutComplete() {
if (ax_context_) {
ax_context_->AddEventToSerializationQueue(
ui::AXEvent(ComputeRoot().AxID(), ax::mojom::Event::kLayoutComplete),
true);
}
}
#endif
void RenderAccessibilityImpl::FireLoadCompleteIfLoaded() {
#if BUILDFLAG(ARKWEB_ACCESSIBILITY)
if (GetMainDocument().GetFrame()->GetEmbeddingToken()) {
#else
if (GetMainDocument().IsLoaded() &&
GetMainDocument().GetFrame()->GetEmbeddingToken()) {
#endif
DCHECK(ax_context_);
ax_context_->FireLoadCompleteIfLoaded();
}
}
void RenderAccessibilityImpl::HitTest(
const gfx::Point& point,
ax::mojom::Event event_to_fire,
int request_id,
blink::mojom::RenderAccessibility::HitTestCallback callback) {
const WebDocument& document = GetMainDocument();
DCHECK(!document.IsNull());
DCHECK(ax_context_);
ax_context_->UpdateAXForAllDocuments();
WebAXObject ax_object;
auto root_obj = WebAXObject::FromWebDocument(document);
ax_object = root_obj.HitTest(point);
if (ax_object.IsDetached()) {
std::move(callback).Run(nullptr);
return;
}
DCHECK(ax_object.IsIncludedInTree());
ui::AXNodeData data;
ax_object.Serialize(&data, ax_context_->GetAXMode());
std::optional<ui::AXTreeID> child_tree_id = data.GetChildTreeID();
gfx::Point transformed_point = point;
if (child_tree_id) {
blink::WebFrame* child_frame =
blink::WebFrame::FromFrameOwnerElement(ax_object.GetNode());
if (!child_frame || child_frame->IsWebRemoteFrame()) {
gfx::Rect rect = ax_object.GetBoundsInFrameCoordinates();
WebView* web_view = render_frame_->GetWebView();
gfx::PointF viewport_offset = web_view->VisualViewportOffset();
transformed_point +=
gfx::Vector2d(viewport_offset.x(), viewport_offset.y()) -
rect.OffsetFromOrigin();
}
if (child_frame) {
std::move(callback).Run(blink::mojom::HitTestResponse::New(
ui::AXTreeIDUnknown(), child_frame->GetFrameToken(),
transformed_point, ax_object.AxID()));
return;
}
} else {
if (event_to_fire != ax::mojom::Event::kNone) {
const std::vector<ui::AXEventIntent> intents;
MarkWebAXObjectDirty(ax_object);
HandleAXEvent(ui::AXEvent(
ax_object.AxID(), event_to_fire, ax::mojom::EventFrom::kAction,
ax::mojom::Action::kHitTest, intents, request_id));
}
}
const auto& frame_token = render_frame_->GetWebFrame()->GetFrameToken();
std::move(callback).Run(blink::mojom::HitTestResponse::New(
child_tree_id.value_or(ui::AXTreeIDUnknown()), frame_token,
transformed_point, ax_object.AxID()));
}
void RenderAccessibilityImpl::PerformAction(const ui::AXActionData& data) {
if (!ax_context_) {
return;
}
ax_context_->UpdateAXForAllDocuments();
WebDocument document = GetMainDocument();
if (document.IsNull()) {
return;
}
ScheduleImmediateAXUpdate();
std::unique_ptr<ui::AXActionTarget> target =
AXActionTargetFactory::CreateFromNodeIdOrRole(
document, plugin_action_target_adapter_, data.target_node_id,
data.target_role);
std::unique_ptr<ui::AXActionTarget> anchor =
AXActionTargetFactory::CreateFromNodeIdOrRole(
document, plugin_action_target_adapter_, data.anchor_node_id);
std::unique_ptr<ui::AXActionTarget> focus =
AXActionTargetFactory::CreateFromNodeIdOrRole(
document, plugin_action_target_adapter_, data.focus_node_id);
#if BUILDFLAG(ARKWEB_MEDIA_POLICY)
if (target->GetType() == ui::AXActionTarget::Type::kNull) {
blink::WebFrame* curFrame = render_frame_->GetWebFrame()->FirstChild();
blink::WebFrame* middleFrame = nullptr;
int ilayers = 1;
const int kLayersMax = 10;
while (curFrame && ilayers < kLayersMax) {
middleFrame = curFrame;
if (middleFrame->ToWebLocalFrame()) {
document = middleFrame->ToWebLocalFrame()->GetDocument();
target = AXActionTargetFactory::CreateFromNodeIdOrRole(
document, plugin_action_target_adapter_, data.target_node_id);
anchor = AXActionTargetFactory::CreateFromNodeIdOrRole(
document, plugin_action_target_adapter_, data.anchor_node_id);
focus = AXActionTargetFactory::CreateFromNodeIdOrRole(
document, plugin_action_target_adapter_, data.focus_node_id);
} else {
break;
}
if (target->GetType() == ui::AXActionTarget::Type::kNull) {
curFrame = middleFrame->FirstChild();
} else {
break;
}
ilayers = ilayers + 1;
}
}
#endif
ax_annotators_manager_->PerformAction(data.action);
switch (data.action) {
case ax::mojom::Action::kGetImageData:
OnGetImageData(target.get(), data.target_rect.size());
break;
case ax::mojom::Action::kLoadInlineTextBoxes:
OnLoadInlineTextBoxes(target.get());
break;
case ax::mojom::Action::kSetSelection:
anchor->SetSelection(anchor.get(), data.anchor_offset, focus.get(),
data.focus_offset);
break;
case ax::mojom::Action::kScrollToMakeVisible:
target->ScrollToMakeVisibleWithSubFocus(
data.target_rect, data.horizontal_scroll_alignment,
data.vertical_scroll_alignment, data.scroll_behavior);
break;
case ax::mojom::Action::kBlur:
case ax::mojom::Action::kClearAccessibilityFocus:
case ax::mojom::Action::kCollapse:
case ax::mojom::Action::kDecrement:
case ax::mojom::Action::kDoDefault:
case ax::mojom::Action::kExpand:
case ax::mojom::Action::kIncrement:
case ax::mojom::Action::kRequestLayoutBasedAction:
case ax::mojom::Action::kScrollToPoint:
case ax::mojom::Action::kScrollToPositionAtRowColumn:
case ax::mojom::Action::kFocus:
case ax::mojom::Action::kSetAccessibilityFocus:
case ax::mojom::Action::kSetScrollOffset:
case ax::mojom::Action::kSetSequentialFocusNavigationStartingPoint:
case ax::mojom::Action::kSetValue:
case ax::mojom::Action::kShowContextMenu:
case ax::mojom::Action::kScrollBackward:
case ax::mojom::Action::kScrollForward:
case ax::mojom::Action::kScrollUp:
case ax::mojom::Action::kScrollDown:
case ax::mojom::Action::kScrollLeft:
case ax::mojom::Action::kScrollRight:
case ax::mojom::Action::kStitchChildTree:
target->PerformAction(data);
break;
case ax::mojom::Action::kCustomAction:
case ax::mojom::Action::kHitTest:
case ax::mojom::Action::kReplaceSelectedText:
case ax::mojom::Action::kNone:
NOTREACHED();
case ax::mojom::Action::kGetTextLocation:
break;
case ax::mojom::Action::kAnnotatePageImages:
break;
case ax::mojom::Action::kSignalEndOfTest:
HandleAXEvent(
ui::AXEvent(ComputeRoot().AxID(), ax::mojom::Event::kEndOfTest));
break;
case ax::mojom::Action::kShowTooltip:
case ax::mojom::Action::kHideTooltip:
case ax::mojom::Action::kInternalInvalidateTree:
case ax::mojom::Action::kResumeMedia:
case ax::mojom::Action::kStartDuckingMedia:
case ax::mojom::Action::kStopDuckingMedia:
case ax::mojom::Action::kSuspendMedia:
case ax::mojom::Action::kLongClick:
break;
}
}
void RenderAccessibilityImpl::Reset(uint32_t reset_token) {
DCHECK(ax_context_);
DCHECK(!accessibility_mode_.is_mode_off());
ax_context_->ResetSerializer();
set_reset_token(reset_token);
FireLoadCompleteIfLoaded();
}
void RenderAccessibilityImpl::MarkWebAXObjectDirty(
const WebAXObject& obj,
ax::mojom::EventFrom event_from,
ax::mojom::Action event_from_action,
std::vector<ui::AXEventIntent> event_intents,
ax::mojom::Event event_type) {
DCHECK(obj.IsIncludedInTree())
<< "Cannot serialize unincluded object: " << obj.ToString().Utf8();
obj.AddDirtyObjectToSerializationQueue(event_from, event_from_action,
event_intents);
}
void RenderAccessibilityImpl::HandleAXEvent(const ui::AXEvent& event) {
DCHECK(ax_context_);
ax_context_->AddEventToSerializationQueue(event, true);
}
void RenderAccessibilityImpl::ScheduleImmediateAXUpdate() {
DCHECK(ax_context_);
ax_context_->ScheduleImmediateSerialization();
}
bool RenderAccessibilityImpl::HasActiveDocument() const {
DCHECK(ax_context_);
return ax_context_->HasActiveDocument();
}
ui::AXMode RenderAccessibilityImpl::GetAXMode() const {
return accessibility_mode_;
}
WebDocument RenderAccessibilityImpl::GetMainDocument() const {
if (render_frame_ && render_frame_->GetWebFrame())
return render_frame_->GetWebFrame()->GetDocument();
return WebDocument();
}
std::string RenderAccessibilityImpl::GetLanguage() {
return page_language_;
}
bool RenderAccessibilityImpl::SendAccessibilitySerialization(
std::vector<ui::AXTreeUpdate> updates,
std::vector<ui::AXEvent> events,
ui::AXLocationAndScrollUpdates location_and_scroll_updates,
bool had_load_complete_messages) {
if (had_load_complete_messages) {
loading_stage_ = LoadingStage::kLoadCompleted;
}
TRACE_EVENT0(
"accessibility",
loading_stage_ == LoadingStage::kPostLoad
? "RenderAccessibilityImpl::SendPendingAccessibilityEvents"
: "RenderAccessibilityImpl::SendPendingAccessibilityEventsLoading");
base::ElapsedTimer timer;
DCHECK(!accessibility_mode_.is_mode_off());
DCHECK(ax_context_);
DCHECK(ax_context_->IsSerializationInFlight());
DCHECK(ax_context_->HasActiveDocument());
CHECK(render_frame_);
CHECK(render_frame_->in_frame_tree());
DCHECK(render_frame_->GetWebFrame()->GetAXTreeID().token());
WebDocument document = GetMainDocument();
CHECK(!document.IsNull());
DCHECK(document.GetFrame()->GetEmbeddingToken());
WebAXObject root = ComputeRoot();
#if DCHECK_IS_ON()
std::unique_ptr<blink::WebDisallowTransitionScope> disallow;
if (!image_annotation_debugging_) {
disallow = std::make_unique<blink::WebDisallowTransitionScope>(&document);
}
#endif
page_language_ = root.Language().Utf8();
#if DCHECK_IS_ON()
WebDocument popup_document = GetPopupDocument();
std::optional<blink::WebDisallowTransitionScope> disallow2;
if (!popup_document.IsNull()) {
disallow2.emplace(&popup_document);
}
#endif
ui::AXUpdatesAndEvents updates_and_events;
updates_and_events.updates = std::move(updates);
updates_and_events.events = std::move(events);
for (auto& update : updates_and_events.updates) {
ax_annotators_manager_->Annotate(document, &update,
had_load_complete_messages);
}
ax_annotators_manager_->AddDebuggingAttributes(updates_and_events.updates);
CHECK(!weak_factory_for_pending_events_.HasWeakPtrs());
CHECK(reset_token_);
render_accessibility_manager_->HandleAXEvents(
updates_and_events, location_and_scroll_updates, *reset_token_,
base::BindOnce(&RenderAccessibilityImpl::OnSerializationReceived,
weak_factory_for_pending_events_.GetWeakPtr()));
base::TimeDelta elapsed_time_ms = timer.Elapsed();
if (elapsed_time_ms > slowest_serialization_time_) {
last_ukm_source_id_ = document.GetUkmSourceId();
slowest_serialization_time_ = elapsed_time_ms;
}
UMA_HISTOGRAM_CUSTOM_TIMES(
"Accessibility.Performance.SendPendingAccessibilityEvents2",
elapsed_time_ms, base::Microseconds(1), base::Seconds(1), 50);
if (loading_stage_ == LoadingStage::kPostLoad) {
UMA_HISTOGRAM_CUSTOM_TIMES(
"Accessibility.Performance.SendPendingAccessibilityEvents.PostLoad2",
elapsed_time_ms, base::Microseconds(1), base::Seconds(1), 50);
}
if (loading_stage_ == LoadingStage::kLoadCompleted) {
loading_stage_ = LoadingStage::kPostLoad;
}
if (ukm_timer_->Elapsed() >= kMinUKMDelay) {
MaybeSendUKM();
}
return true;
}
void RenderAccessibilityImpl::OnSerializationReceived() {
DCHECK(ax_context_);
ax_context_->OnSerializationReceived();
}
void RenderAccessibilityImpl::OnLoadInlineTextBoxes(
const ui::AXActionTarget* target) {
const BlinkAXActionTarget* blink_target =
BlinkAXActionTarget::FromAXActionTarget(target);
if (!blink_target) {
return;
}
const WebAXObject& obj = blink_target->WebAXObject();
DCHECK(ax_context_);
obj.OnLoadInlineTextBoxes();
HandleAXEvent(ui::AXEvent(obj.AxID(), ax::mojom::Event::kTreeChanged));
}
void RenderAccessibilityImpl::OnGetImageData(const ui::AXActionTarget* target,
const gfx::Size& max_size) {
const BlinkAXActionTarget* blink_target =
BlinkAXActionTarget::FromAXActionTarget(target);
if (!blink_target) {
return;
}
const WebAXObject& obj = blink_target->WebAXObject();
obj.SetImageAsDataNodeId(max_size);
const WebDocument& document = GetMainDocument();
if (document.IsNull()) {
return;
}
MarkWebAXObjectDirty(obj);
HandleAXEvent(ui::AXEvent(obj.AxID(), ax::mojom::Event::kImageFrameUpdated,
ax::mojom::EventFrom::kAction,
ax::mojom::Action::kGetImageData));
}
void RenderAccessibilityImpl::OnDestruct() {
render_frame_ = nullptr;
delete this;
}
blink::WebDocument RenderAccessibilityImpl::GetPopupDocument() {
blink::WebPagePopup* popup = render_frame_->GetWebView()->GetPagePopup();
if (popup)
return popup->GetDocument();
return WebDocument();
}
WebAXObject RenderAccessibilityImpl::ComputeRoot() {
DCHECK(render_frame_);
DCHECK(render_frame_->GetWebFrame());
return WebAXObject::FromWebDocument(GetMainDocument());
}
void RenderAccessibilityImpl::ConnectionClosed() {
ax_context_->OnSerializationCancelled();
}
void RenderAccessibilityImpl::SetPluginAXTreeActionTargetAdapter(
PluginAXTreeActionTargetAdapter* adapter) {
plugin_action_target_adapter_ = adapter;
}
void RenderAccessibilityImpl::MaybeSendUKM() {
if (slowest_serialization_time_ < kMinSerializationTimeToSend)
return;
ukm::builders::Accessibility_Renderer(last_ukm_source_id_)
.SetCpuTime_SendPendingAccessibilityEvents(
slowest_serialization_time_.InMilliseconds())
.Record(ukm_recorder_.get());
ResetUKMData();
}
void RenderAccessibilityImpl::ResetUKMData() {
slowest_serialization_time_ = base::TimeDelta();
ukm_timer_ = std::make_unique<base::ElapsedTimer>();
last_ukm_source_id_ = ukm::kInvalidSourceId;
}
}