blob: 9231fd761d46e88f73eb800ddb8785286a2e4ba3 [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/browser/api/automation_internal/automation_internal_api.h"
#include <stdint.h>
#include <string>
#include <vector>
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/scoped_observation.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "build/chromeos_buildflags.h"
#include "content/public/browser/browser_accessibility_state.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_plugin_guest_manager.h"
#include "content/public/browser/media_player_id.h"
#include "content/public/browser/media_session.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/scoped_accessibility_mode.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/browser/web_contents_user_data.h"
#include "extensions/browser/api/automation_internal/automation_event_router.h"
#include "extensions/browser/api/automation_internal/automation_internal_api_delegate.h"
#include "extensions/browser/api/extensions_api_client.h"
#include "extensions/common/api/automation.h"
#include "extensions/common/api/automation_internal.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/manifest_handlers/automation.h"
#include "extensions/common/permissions/permissions_data.h"
#include "mojo/public/cpp/bindings/associated_remote.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_action_handler_base.h"
#include "ui/accessibility/ax_action_handler_registry.h"
#include "ui/accessibility/ax_enum_util.h"
#include "ui/accessibility/ax_location_and_scroll_updates.h"
#include "ui/accessibility/ax_updates_and_events.h"
#if defined(USE_AURA)
#include "ui/aura/env.h"
#endif
namespace extensions {
class AutomationWebContentsObserver;
} // namespace extensions
namespace extensions {
namespace {
const char kCannotRequestAutomationOnPage[] =
"Failed request of automation on a page";
// Helper function to convert extension action to ax action.
// |extension_id| can be the empty string.
// |data| is an out param.
AutomationInternalPerformActionFunction::Result ConvertToAXActionData(
const ui::AXTreeID& tree_id,
int32_t automation_node_id,
const std::string& action_type_string,
int request_id,
const base::Value::Dict& additional_properties,
const ExtensionId& extension_id,
ui::AXActionData* action) {
AutomationInternalPerformActionFunction::Result validation_error_result;
validation_error_result.validation_success = false;
AutomationInternalPerformActionFunction::Result success_result;
success_result.validation_success = true;
action->target_tree_id = tree_id;
action->source_extension_id = extension_id;
action->target_node_id = automation_node_id;
action->request_id = request_id;
api::automation::ActionType action_type =
api::automation::ParseActionType(action_type_string);
switch (action_type) {
case api::automation::ActionType::kBlur:
action->action = ax::mojom::Action::kBlur;
break;
case api::automation::ActionType::kClearAccessibilityFocus:
action->action = ax::mojom::Action::kClearAccessibilityFocus;
break;
case api::automation::ActionType::kDecrement:
action->action = ax::mojom::Action::kDecrement;
break;
case api::automation::ActionType::kDoDefault:
action->action = ax::mojom::Action::kDoDefault;
break;
case api::automation::ActionType::kIncrement:
action->action = ax::mojom::Action::kIncrement;
break;
case api::automation::ActionType::kFocus:
action->action = ax::mojom::Action::kFocus;
break;
case api::automation::ActionType::kGetImageData: {
auto get_image_data_params =
api::automation_internal::GetImageDataParams::FromValue(
additional_properties);
if (!get_image_data_params) {
return validation_error_result;
}
action->action = ax::mojom::Action::kGetImageData;
action->target_rect = gfx::Rect(0, 0, get_image_data_params->max_width,
get_image_data_params->max_height);
break;
}
case api::automation::ActionType::kHitTest: {
auto hit_test_params = api::automation_internal::HitTestParams::FromValue(
additional_properties);
if (!hit_test_params) {
return validation_error_result;
}
action->action = ax::mojom::Action::kHitTest;
action->target_point = gfx::Point(hit_test_params->x, hit_test_params->y);
action->hit_test_event_to_fire = ui::ParseAXEnum<ax::mojom::Event>(
hit_test_params->event_to_fire.c_str());
if (action->hit_test_event_to_fire == ax::mojom::Event::kNone) {
return success_result;
}
break;
}
case api::automation::ActionType::kLoadInlineTextBoxes:
action->action = ax::mojom::Action::kLoadInlineTextBoxes;
break;
case api::automation::ActionType::kSetAccessibilityFocus:
action->action = ax::mojom::Action::kSetAccessibilityFocus;
break;
case api::automation::ActionType::kScrollToMakeVisible:
action->action = ax::mojom::Action::kScrollToMakeVisible;
action->horizontal_scroll_alignment =
ax::mojom::ScrollAlignment::kScrollAlignmentCenter;
action->vertical_scroll_alignment =
ax::mojom::ScrollAlignment::kScrollAlignmentCenter;
action->scroll_behavior =
ax::mojom::ScrollBehavior::kDoNotScrollIfVisible;
break;
case api::automation::ActionType::kScrollBackward:
action->action = ax::mojom::Action::kScrollBackward;
break;
case api::automation::ActionType::kScrollForward:
action->action = ax::mojom::Action::kScrollForward;
break;
case api::automation::ActionType::kScrollUp:
action->action = ax::mojom::Action::kScrollUp;
break;
case api::automation::ActionType::kScrollDown:
action->action = ax::mojom::Action::kScrollDown;
break;
case api::automation::ActionType::kScrollLeft:
action->action = ax::mojom::Action::kScrollLeft;
break;
case api::automation::ActionType::kScrollRight:
action->action = ax::mojom::Action::kScrollRight;
break;
case api::automation::ActionType::kSetSelection: {
auto selection_params =
api::automation_internal::SetSelectionParams::FromValue(
additional_properties);
if (!selection_params) {
return validation_error_result;
}
action->anchor_node_id = automation_node_id;
action->anchor_offset = selection_params->anchor_offset;
action->focus_node_id = selection_params->focus_node_id;
action->focus_offset = selection_params->focus_offset;
action->action = ax::mojom::Action::kSetSelection;
break;
}
case api::automation::ActionType::kShowContextMenu: {
action->action = ax::mojom::Action::kShowContextMenu;
break;
}
case api::automation::ActionType::
kSetSequentialFocusNavigationStartingPoint: {
action->action =
ax::mojom::Action::kSetSequentialFocusNavigationStartingPoint;
break;
}
case api::automation::ActionType::kCustomAction: {
auto perform_custom_action_params =
api::automation_internal::PerformCustomActionParams::FromValue(
additional_properties);
if (!perform_custom_action_params) {
return validation_error_result;
}
action->action = ax::mojom::Action::kCustomAction;
action->custom_action_id = perform_custom_action_params->custom_action_id;
break;
}
case api::automation::ActionType::kReplaceSelectedText: {
auto replace_selected_text_params =
api::automation_internal::ReplaceSelectedTextParams::FromValue(
additional_properties);
if (!replace_selected_text_params) {
return validation_error_result;
}
action->action = ax::mojom::Action::kReplaceSelectedText;
action->value = replace_selected_text_params->value;
break;
}
case api::automation::ActionType::kSetValue: {
auto set_value_params =
api::automation_internal::SetValueParams::FromValue(
additional_properties);
if (!set_value_params) {
return validation_error_result;
}
action->action = ax::mojom::Action::kSetValue;
action->value = set_value_params->value;
break;
}
case api::automation::ActionType::kScrollToPoint: {
auto scroll_to_point_params =
api::automation_internal::ScrollToPointParams::FromValue(
additional_properties);
if (!scroll_to_point_params) {
return validation_error_result;
}
action->action = ax::mojom::Action::kScrollToPoint;
action->target_point =
gfx::Point(scroll_to_point_params->x, scroll_to_point_params->y);
break;
}
case api::automation::ActionType::kScrollToPositionAtRowColumn: {
auto params = api::automation_internal::
ScrollToPositionAtRowColumnParams::FromValue(additional_properties);
if (!params) {
return validation_error_result;
}
action->action = ax::mojom::Action::kScrollToPositionAtRowColumn;
action->row_column = std::pair(params->row, params->column);
break;
}
case api::automation::ActionType::kSetScrollOffset: {
auto set_scroll_offset_params =
api::automation_internal::SetScrollOffsetParams::FromValue(
additional_properties);
if (!set_scroll_offset_params) {
return validation_error_result;
}
action->action = ax::mojom::Action::kSetScrollOffset;
action->target_point =
gfx::Point(set_scroll_offset_params->x, set_scroll_offset_params->y);
break;
}
case api::automation::ActionType::kGetTextLocation: {
auto get_text_location_params =
api::automation_internal::GetTextLocationDataParams::FromValue(
additional_properties);
if (!get_text_location_params) {
return validation_error_result;
}
action->action = ax::mojom::Action::kGetTextLocation;
action->start_index = get_text_location_params->start_index;
action->end_index = get_text_location_params->end_index;
break;
}
case api::automation::ActionType::kShowTooltip:
action->action = ax::mojom::Action::kShowTooltip;
break;
case api::automation::ActionType::kHideTooltip:
action->action = ax::mojom::Action::kHideTooltip;
break;
case api::automation::ActionType::kCollapse:
action->action = ax::mojom::Action::kCollapse;
break;
case api::automation::ActionType::kExpand:
action->action = ax::mojom::Action::kExpand;
break;
case api::automation::ActionType::kResumeMedia:
action->action = ax::mojom::Action::kResumeMedia;
break;
case api::automation::ActionType::kStartDuckingMedia:
action->action = ax::mojom::Action::kStartDuckingMedia;
break;
case api::automation::ActionType::kStopDuckingMedia:
action->action = ax::mojom::Action::kStopDuckingMedia;
break;
case api::automation::ActionType::kSuspendMedia:
action->action = ax::mojom::Action::kSuspendMedia;
break;
case api::automation::ActionType::kLongClick:
action->action = ax::mojom::Action::kLongClick;
break;
case api::automation::ActionType::kAnnotatePageImages:
case api::automation::ActionType::kSignalEndOfTest:
case api::automation::ActionType::kStitchChildTree:
case api::automation::ActionType::kInternalInvalidateTree:
case api::automation::ActionType::kNone:
break; // Actions that are only internally used.
}
return success_result;
}
} // namespace
// Helper class that receives accessibility data from |WebContents|.
class AutomationWebContentsObserver
: public content::WebContentsObserver,
public content::WebContentsUserData<AutomationWebContentsObserver>,
public AutomationEventRouterObserver {
public:
AutomationWebContentsObserver(const AutomationWebContentsObserver&) = delete;
AutomationWebContentsObserver& operator=(
const AutomationWebContentsObserver&) = delete;
~AutomationWebContentsObserver() override {
automation_event_router_observer_.Reset();
}
// Set the accessibility mode for the observed WebContents so that
// accessibility events are forwarded to each WebContentsObserver. A reset is
// performed if the WebContents already has web accessibility enabled.
void EnableOrResetWebContentsAccessibility() {
// Force a reset if web accessibility is already enabled to ensure that new
// observers of accessibility events get the full accessibility tree from
// scratch.
const bool need_reset = web_contents()->GetAccessibilityMode().has_mode(
ui::AXMode::kWebContents);
if (!scoped_accessibility_mode_) {
scoped_accessibility_mode_ =
content::BrowserAccessibilityState::GetInstance()
->CreateScopedModeForWebContents(web_contents(),
ui::kAXModeWebContentsOnly);
}
if (need_reset) {
web_contents()->ResetAccessibility();
}
}
// content::WebContentsObserver overrides.
void AccessibilityEventReceived(
const ui::AXUpdatesAndEvents& content_event_bundle) override {
gfx::Point mouse_location;
#if defined(USE_AURA)
mouse_location = aura::Env::GetInstance()->last_mouse_location();
#endif
AutomationEventRouter* router = AutomationEventRouter::GetInstance();
router->DispatchAccessibilityEvents(
content_event_bundle.ax_tree_id, content_event_bundle.updates,
mouse_location, content_event_bundle.events);
}
void AccessibilityLocationChangesReceived(
const ui::AXTreeID& tree_id,
ui::AXLocationAndScrollUpdates& details) override {
AutomationEventRouter* router = AutomationEventRouter::GetInstance();
for (const auto& src : details.location_changes) {
router->DispatchAccessibilityLocationChange(tree_id, src);
}
for (const auto& change : details.scroll_changes) {
router->DispatchAccessibilityScrollChange(tree_id, change);
}
}
void MediaStartedPlaying(const MediaPlayerInfo& video_type,
const content::MediaPlayerId& id) override {
auto* render_frame_host =
content::RenderFrameHost::FromID(id.frame_routing_id);
if (!render_frame_host)
return;
ui::AXUpdatesAndEvents content_event_bundle;
content_event_bundle.ax_tree_id = render_frame_host->GetAXTreeID();
content_event_bundle.events.resize(1);
content_event_bundle.events[0].event_type =
ax::mojom::Event::kMediaStartedPlaying;
AccessibilityEventReceived(content_event_bundle);
}
void MediaStoppedPlaying(
const MediaPlayerInfo& video_type,
const content::MediaPlayerId& id,
WebContentsObserver::MediaStoppedReason reason) override {
auto* render_frame_host =
content::RenderFrameHost::FromID(id.frame_routing_id);
if (!render_frame_host)
return;
ui::AXUpdatesAndEvents content_event_bundle;
content_event_bundle.ax_tree_id = render_frame_host->GetAXTreeID();
content_event_bundle.events.resize(1);
content_event_bundle.events[0].event_type =
ax::mojom::Event::kMediaStoppedPlaying;
AccessibilityEventReceived(content_event_bundle);
}
// AutomationEventRouterObserver overrides.
void AllAutomationExtensionsGone() override {
scoped_accessibility_mode_.reset();
}
void ExtensionListenerAdded() override {
if (web_contents()) {
EnableOrResetWebContentsAccessibility();
// On ChromeOS Ash, the automation api is the native accessibility api.
// For the purposes of tracking web contents accessibility like other
// desktop platforms, record the same UMA metric as those platforms.
#if BUILDFLAG(IS_CHROMEOS)
UMA_HISTOGRAM_ENUMERATION(
"Accessibility.ModeFlag",
ui::AXMode::ModeFlagHistogramValue::UMA_AX_MODE_WEB_CONTENTS,
ui::AXMode::ModeFlagHistogramValue::UMA_AX_MODE_MAX);
UMA_HISTOGRAM_ENUMERATION(
"Accessibility.ModeFlag",
ui::AXMode::ModeFlagHistogramValue::UMA_AX_MODE_INLINE_TEXT_BOXES,
ui::AXMode::ModeFlagHistogramValue::UMA_AX_MODE_MAX);
UMA_HISTOGRAM_ENUMERATION(
"Accessibility.ModeFlag",
ui::AXMode::ModeFlagHistogramValue::UMA_AX_MODE_SCREEN_READER,
ui::AXMode::ModeFlagHistogramValue::UMA_AX_MODE_MAX);
UMA_HISTOGRAM_ENUMERATION(
"Accessibility.ModeFlag",
ui::AXMode::ModeFlagHistogramValue::UMA_AX_MODE_HTML,
ui::AXMode::ModeFlagHistogramValue::UMA_AX_MODE_MAX);
UMA_HISTOGRAM_ENUMERATION(
"Accessibility.ModeFlag",
ui::AXMode::ModeFlagHistogramValue::UMA_AX_MODE_HTML_METADATA,
ui::AXMode::ModeFlagHistogramValue::UMA_AX_MODE_MAX);
UMA_HISTOGRAM_ENUMERATION(
"Accessibility.ModeFlag",
ui::AXMode::ModeFlagHistogramValue::UMA_AX_MODE_LABEL_IMAGES,
ui::AXMode::ModeFlagHistogramValue::UMA_AX_MODE_MAX);
UMA_HISTOGRAM_ENUMERATION(
"Accessibility.ModeFlag",
ui::AXMode::ModeFlagHistogramValue::UMA_AX_MODE_PDF,
ui::AXMode::ModeFlagHistogramValue::UMA_AX_MODE_MAX);
#endif // BUILDFLAG(IS_CHROMEOS)
}
}
private:
friend class content::WebContentsUserData<AutomationWebContentsObserver>;
explicit AutomationWebContentsObserver(content::WebContents* web_contents)
: content::WebContentsObserver(web_contents),
content::WebContentsUserData<AutomationWebContentsObserver>(
*web_contents),
browser_context_(web_contents->GetBrowserContext()) {
if (web_contents->IsCurrentlyAudible()) {
content::RenderFrameHost* render_frame_host =
web_contents->GetPrimaryMainFrame();
if (!render_frame_host) {
return;
}
ui::AXUpdatesAndEvents content_event_bundle;
content_event_bundle.ax_tree_id = render_frame_host->GetAXTreeID();
content_event_bundle.events.resize(1);
content_event_bundle.events[0].event_type =
ax::mojom::Event::kMediaStartedPlaying;
AccessibilityEventReceived(content_event_bundle);
}
automation_event_router_observer_.Observe(
AutomationEventRouter::GetInstance());
}
raw_ptr<content::BrowserContext> browser_context_;
base::ScopedObservation<extensions::AutomationEventRouter,
extensions::AutomationEventRouterObserver>
automation_event_router_observer_{this};
std::unique_ptr<content::ScopedAccessibilityMode> scoped_accessibility_mode_;
WEB_CONTENTS_USER_DATA_KEY_DECL();
};
WEB_CONTENTS_USER_DATA_KEY_IMPL(AutomationWebContentsObserver);
std::optional<std::string> AutomationInternalEnableTreeFunction::EnableTree(
const ui::AXTreeID& ax_tree_id,
const ExtensionId& extension_id) {
AutomationInternalApiDelegate* automation_api_delegate =
ExtensionsAPIClient::Get()->GetAutomationInternalApiDelegate();
if (automation_api_delegate->EnableTree(ax_tree_id))
return std::nullopt;
content::RenderFrameHost* render_frame_host =
content::RenderFrameHost::FromAXTreeID(ax_tree_id);
if (!render_frame_host) {
return std::nullopt;
}
content::WebContents* contents =
content::WebContents::FromRenderFrameHost(render_frame_host);
AutomationWebContentsObserver::CreateForWebContents(contents);
// Only call this if this is the root of a frame tree, to avoid resetting
// the accessibility state multiple times.
if (render_frame_host->IsInPrimaryMainFrame()) {
AutomationWebContentsObserver::FromWebContents(contents)
->EnableOrResetWebContentsAccessibility();
}
return std::nullopt;
}
ExtensionFunction::ResponseAction AutomationInternalEnableTreeFunction::Run() {
using api::automation_internal::EnableTree::Params;
std::optional<Params> params = Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
ui::AXTreeID ax_tree_id = ui::AXTreeID::FromString(params->tree_id);
std::optional<std::string> error = EnableTree(ax_tree_id, extension_id());
if (error) {
return RespondNow(Error(error.value()));
} else {
return RespondNow(NoArguments());
}
}
AutomationInternalPerformActionFunction::Result::Result() = default;
AutomationInternalPerformActionFunction::Result::Result(const Result&) =
default;
AutomationInternalPerformActionFunction::Result::~Result() = default;
// static
AutomationInternalPerformActionFunction::Result
AutomationInternalPerformActionFunction::PerformAction(
const ui::AXActionData& data,
const Extension* extension,
const AutomationInfo* automation_info) {
Result result;
result.validation_success = true;
// The ash implementation of crosapi registers itself as an action observer.
// This allows it to forward actions in parallel to Lacros.
ui::AXActionHandlerRegistry* registry =
ui::AXActionHandlerRegistry::GetInstance();
registry->PerformAction(data);
ui::AXActionHandlerBase* action_handler =
registry->GetActionHandler(data.target_tree_id);
if (action_handler) {
// Handle an AXActionHandler with a render frame host first. Some actions
// require a render frame host -> web contents and this api requires web
// contents to perform a permissions check.
content::RenderFrameHost* render_frame_host =
content::RenderFrameHost::FromAXTreeID(data.target_tree_id);
if (render_frame_host) {
content::WebContents* contents =
content::WebContents::FromRenderFrameHost(render_frame_host);
if (extension && automation_info) {
if (!ExtensionsAPIClient::Get()
->GetAutomationInternalApiDelegate()
->CanRequestAutomation(extension, automation_info, contents)) {
result.automation_error = kCannotRequestAutomationOnPage;
return result;
}
} else {
// If |extension| is nullptr, then Lacros is receiving a crosapi request
// from ash to perform an action. We make the assumption this this is
// allowed.
// TODO(crbug.com/40753344): Confirm whether this assumption is
// valid.
}
// Handle internal actions.
content::MediaSession* session = content::MediaSession::Get(contents);
switch (data.action) {
case ax::mojom::Action::kStartDuckingMedia:
session->StartDucking();
return result;
case ax::mojom::Action::kStopDuckingMedia:
session->StopDucking();
return result;
case ax::mojom::Action::kResumeMedia:
session->Resume(content::MediaSession::SuspendType::kSystem);
return result;
case ax::mojom::Action::kSuspendMedia:
session->Suspend(content::MediaSession::SuspendType::kSystem);
return result;
default:
break;
}
}
action_handler->PerformAction(data);
}
return result;
}
ExtensionFunction::ResponseAction
AutomationInternalPerformActionFunction::Run() {
const AutomationInfo* automation_info = AutomationInfo::Get(extension());
EXTENSION_FUNCTION_VALIDATE(automation_info && automation_info->desktop);
using api::automation_internal::PerformAction::Params;
std::optional<Params> params = Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
int request_id = params->args.request_id.value_or(-1);
ui::AXActionData data;
Result result = ConvertToAXActionData(
ui::AXTreeID::FromString(params->args.tree_id),
params->args.automation_node_id, params->args.action_type, request_id,
params->opt_args.additional_properties, extension_id(), &data);
if (!result.validation_success) {
// This macro has a built in |return|.
EXTENSION_FUNCTION_VALIDATE(false);
}
result = PerformAction(data, extension(), automation_info);
if (!result.validation_success) {
// This macro has a built in |return|.
EXTENSION_FUNCTION_VALIDATE(false);
}
if (result.automation_error) {
return RespondNow(Error(result.automation_error.value()));
} else {
return RespondNow(NoArguments());
}
}
ExtensionFunction::ResponseAction
AutomationInternalEnableDesktopFunction::Run() {
#if defined(USE_AURA)
const AutomationInfo* automation_info = AutomationInfo::Get(extension());
if (!automation_info || !automation_info->desktop)
return RespondNow(Error("desktop permission must be requested"));
// This gets removed when the extension process dies.
AutomationEventRouter::GetInstance()->RegisterListenerWithDesktopPermission(
extension_id(), source_process_id(), GetSenderWebContents());
AutomationInternalApiDelegate* automation_api_delegate =
ExtensionsAPIClient::Get()->GetAutomationInternalApiDelegate();
automation_api_delegate->EnableDesktop();
ui::AXTreeID ax_tree_id = automation_api_delegate->GetAXTreeID();
return RespondNow(
ArgumentList(api::automation_internal::EnableDesktop::Results::Create(
ax_tree_id.ToString())));
#else
return RespondNow(Error("getDesktop is unsupported by this platform"));
#endif // defined(USE_AURA)
}
ExtensionFunction::ResponseAction
AutomationInternalDisableDesktopFunction::Run() {
#if defined(USE_AURA)
const AutomationInfo* automation_info = AutomationInfo::Get(extension());
if (!automation_info || !automation_info->desktop)
return RespondNow(Error("desktop permission must be requested"));
AutomationEventRouter::GetInstance()->UnregisterListenerWithDesktopPermission(
source_process_id());
return RespondNow(NoArguments());
#else
return RespondNow(Error("getDesktop is unsupported by this platform"));
#endif // defined(USE_AURA)
}
} // namespace extensions