blob: c78f0d1382acbc51a6e91645626baad9c15c5739 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/renderer/extensions/automation_internal_custom_bindings.h"
#include <stddef.h>
#include <stdint.h>
#include <memory>
#include "base/bind.h"
#include "base/macros.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/values.h"
#include "chrome/common/extensions/chrome_extension_messages.h"
#include "chrome/common/extensions/manifest_handlers/automation.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_thread.h"
#include "content/public/renderer/render_view.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest.h"
#include "extensions/renderer/script_context.h"
#include "ipc/message_filter.h"
#include "ui/accessibility/ax_enums.h"
#include "ui/accessibility/ax_node.h"
#include "ui/gfx/geometry/rect_conversions.h"
namespace extensions {
namespace {
// Helper to convert an enum to a V8 object.
template <typename EnumType>
v8::Local<v8::Object> ToEnumObject(v8::Isolate* isolate,
EnumType start_after,
EnumType end_at) {
v8::Local<v8::Object> object = v8::Object::New(isolate);
for (int i = start_after + 1; i <= end_at; ++i) {
v8::Local<v8::String> value = v8::String::NewFromUtf8(
isolate, ui::ToString(static_cast<EnumType>(i)).c_str());
object->Set(value, value);
}
return object;
}
void ThrowInvalidArgumentsException(
AutomationInternalCustomBindings* automation_bindings) {
v8::Isolate* isolate = automation_bindings->GetIsolate();
automation_bindings->GetIsolate()->ThrowException(
v8::String::NewFromUtf8(
isolate,
"Invalid arguments to AutomationInternalCustomBindings function",
v8::NewStringType::kNormal)
.ToLocalChecked());
LOG(FATAL) << "Invalid arguments to AutomationInternalCustomBindings function"
<< automation_bindings->context()->GetStackTraceAsString();
}
v8::Local<v8::Value> CreateV8String(v8::Isolate* isolate, const char* str) {
return v8::String::NewFromUtf8(isolate, str, v8::String::kNormalString,
strlen(str));
}
v8::Local<v8::Value> CreateV8String(v8::Isolate* isolate,
const std::string& str) {
return v8::String::NewFromUtf8(isolate, str.c_str(),
v8::String::kNormalString, str.length());
}
v8::Local<v8::Object> RectToV8Object(v8::Isolate* isolate,
const gfx::Rect& rect) {
v8::Local<v8::Object> result(v8::Object::New(isolate));
result->Set(CreateV8String(isolate, "left"),
v8::Integer::New(isolate, rect.x()));
result->Set(CreateV8String(isolate, "top"),
v8::Integer::New(isolate, rect.y()));
result->Set(CreateV8String(isolate, "width"),
v8::Integer::New(isolate, rect.width()));
result->Set(CreateV8String(isolate, "height"),
v8::Integer::New(isolate, rect.height()));
return result;
}
// Compute the bounding box of a node, fixing nodes with empty bounds by
// unioning the bounds of their children.
static gfx::RectF ComputeLocalNodeBounds(TreeCache* cache, ui::AXNode* node) {
gfx::RectF bounds = node->data().location;
if (bounds.width() > 0 && bounds.height() > 0)
return bounds;
// Compute the bounds of each child.
for (size_t i = 0; i < node->children().size(); i++) {
ui::AXNode* child = node->children()[i];
gfx::RectF child_bounds = ComputeLocalNodeBounds(cache, child);
// Ignore children that don't have valid bounds themselves.
if (child_bounds.width() == 0 || child_bounds.height() == 0)
continue;
// For the first valid child, just set the bounds to that child's bounds.
if (bounds.width() == 0 || bounds.height() == 0) {
bounds = child_bounds;
continue;
}
// Union each additional child's bounds.
bounds.Union(child_bounds);
}
return bounds;
}
// Compute the bounding box of a node in global coordinates, walking up the
// parent hierarchy to offset by frame offsets and scroll offsets.
static gfx::Rect ComputeGlobalNodeBounds(TreeCache* cache, ui::AXNode* node) {
gfx::RectF bounds = ComputeLocalNodeBounds(cache, node);
while (node) {
if (node->data().transform)
node->data().transform->TransformRect(&bounds);
ui::AXNode* container =
cache->tree.GetFromId(node->data().offset_container_id);
if (!container) {
if (node == cache->tree.root()) {
container = cache->owner->GetParent(node, &cache);
} else {
container = cache->tree.root();
}
}
if (!container || container == node)
break;
gfx::RectF container_bounds = container->data().location;
bounds.Offset(container_bounds.x(), container_bounds.y());
int scroll_x = 0;
int scroll_y = 0;
if (container->data().GetIntAttribute(ui::AX_ATTR_SCROLL_X, &scroll_x) &&
container->data().GetIntAttribute(ui::AX_ATTR_SCROLL_Y, &scroll_y)) {
bounds.Offset(-scroll_x, -scroll_y);
}
node = container;
}
return gfx::ToEnclosingRect(bounds);
}
ui::AXNode* FindNodeWithChildTreeId(ui::AXNode* node, int child_tree_id) {
if (child_tree_id == node->data().GetIntAttribute(ui::AX_ATTR_CHILD_TREE_ID))
return node;
for (int i = 0; i < node->child_count(); ++i) {
ui::AXNode* result =
FindNodeWithChildTreeId(node->ChildAtIndex(i), child_tree_id);
if (result)
return result;
}
return nullptr;
}
//
// Helper class that helps implement bindings for a JavaScript function
// that takes a single input argument consisting of a Tree ID. Looks up
// the TreeCache and passes it to the function passed to the constructor.
//
typedef void (*TreeIDFunction)(v8::Isolate* isolate,
v8::ReturnValue<v8::Value> result,
TreeCache* cache);
class TreeIDWrapper : public base::RefCountedThreadSafe<TreeIDWrapper> {
public:
TreeIDWrapper(AutomationInternalCustomBindings* automation_bindings,
TreeIDFunction function)
: automation_bindings_(automation_bindings), function_(function) {}
void Run(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = automation_bindings_->GetIsolate();
if (args.Length() != 1 || !args[0]->IsNumber())
ThrowInvalidArgumentsException(automation_bindings_);
int tree_id = args[0]->Int32Value();
TreeCache* cache = automation_bindings_->GetTreeCacheFromTreeID(tree_id);
if (!cache)
return;
// The root can be null if this is called from an onTreeChange callback.
if (!cache->tree.root())
return;
function_(isolate, args.GetReturnValue(), cache);
}
private:
virtual ~TreeIDWrapper() {}
friend class base::RefCountedThreadSafe<TreeIDWrapper>;
AutomationInternalCustomBindings* automation_bindings_;
TreeIDFunction function_;
};
//
// Helper class that helps implement bindings for a JavaScript function
// that takes two input arguments: a tree ID and node ID. Looks up the
// TreeCache and the AXNode and passes them to the function passed to
// the constructor.
//
typedef void (*NodeIDFunction)(v8::Isolate* isolate,
v8::ReturnValue<v8::Value> result,
TreeCache* cache,
ui::AXNode* node);
class NodeIDWrapper : public base::RefCountedThreadSafe<NodeIDWrapper> {
public:
NodeIDWrapper(AutomationInternalCustomBindings* automation_bindings,
NodeIDFunction function)
: automation_bindings_(automation_bindings), function_(function) {}
void Run(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = automation_bindings_->GetIsolate();
if (args.Length() < 2 || !args[0]->IsNumber() || !args[1]->IsNumber())
ThrowInvalidArgumentsException(automation_bindings_);
int tree_id = args[0]->Int32Value();
int node_id = args[1]->Int32Value();
TreeCache* cache = automation_bindings_->GetTreeCacheFromTreeID(tree_id);
if (!cache)
return;
ui::AXNode* node = cache->tree.GetFromId(node_id);
if (!node)
return;
function_(isolate, args.GetReturnValue(), cache, node);
}
private:
virtual ~NodeIDWrapper() {}
friend class base::RefCountedThreadSafe<NodeIDWrapper>;
AutomationInternalCustomBindings* automation_bindings_;
NodeIDFunction function_;
};
//
// Helper class that helps implement bindings for a JavaScript function
// that takes three input arguments: a tree ID, node ID, and string
// argument. Looks up the TreeCache and the AXNode and passes them to the
// function passed to the constructor.
//
typedef void (*NodeIDPlusAttributeFunction)(v8::Isolate* isolate,
v8::ReturnValue<v8::Value> result,
ui::AXNode* node,
const std::string& attribute);
class NodeIDPlusAttributeWrapper
: public base::RefCountedThreadSafe<NodeIDPlusAttributeWrapper> {
public:
NodeIDPlusAttributeWrapper(
AutomationInternalCustomBindings* automation_bindings,
NodeIDPlusAttributeFunction function)
: automation_bindings_(automation_bindings), function_(function) {}
void Run(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = automation_bindings_->GetIsolate();
if (args.Length() < 3 || !args[0]->IsNumber() || !args[1]->IsNumber() ||
!args[2]->IsString()) {
ThrowInvalidArgumentsException(automation_bindings_);
}
int tree_id = args[0]->Int32Value();
int node_id = args[1]->Int32Value();
std::string attribute = *v8::String::Utf8Value(args[2]);
TreeCache* cache = automation_bindings_->GetTreeCacheFromTreeID(tree_id);
if (!cache)
return;
ui::AXNode* node = cache->tree.GetFromId(node_id);
if (!node)
return;
function_(isolate, args.GetReturnValue(), node, attribute);
}
private:
virtual ~NodeIDPlusAttributeWrapper() {}
friend class base::RefCountedThreadSafe<NodeIDPlusAttributeWrapper>;
AutomationInternalCustomBindings* automation_bindings_;
NodeIDPlusAttributeFunction function_;
};
//
// Helper class that helps implement bindings for a JavaScript function
// that takes four input arguments: a tree ID, node ID, and integer start
// and end indices. Looks up the TreeCache and the AXNode and passes them
// to the function passed to the constructor.
//
typedef void (*NodeIDPlusRangeFunction)(v8::Isolate* isolate,
v8::ReturnValue<v8::Value> result,
TreeCache* cache,
ui::AXNode* node,
int start,
int end);
class NodeIDPlusRangeWrapper
: public base::RefCountedThreadSafe<NodeIDPlusRangeWrapper> {
public:
NodeIDPlusRangeWrapper(AutomationInternalCustomBindings* automation_bindings,
NodeIDPlusRangeFunction function)
: automation_bindings_(automation_bindings), function_(function) {}
void Run(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = automation_bindings_->GetIsolate();
if (args.Length() < 4 || !args[0]->IsNumber() || !args[1]->IsNumber() ||
!args[2]->IsNumber() || !args[3]->IsNumber()) {
ThrowInvalidArgumentsException(automation_bindings_);
}
int tree_id = args[0]->Int32Value();
int node_id = args[1]->Int32Value();
int start = args[2]->Int32Value();
int end = args[3]->Int32Value();
TreeCache* cache = automation_bindings_->GetTreeCacheFromTreeID(tree_id);
if (!cache)
return;
ui::AXNode* node = cache->tree.GetFromId(node_id);
if (!node)
return;
function_(isolate, args.GetReturnValue(), cache, node, start, end);
}
private:
virtual ~NodeIDPlusRangeWrapper() {}
friend class base::RefCountedThreadSafe<NodeIDPlusRangeWrapper>;
AutomationInternalCustomBindings* automation_bindings_;
NodeIDPlusRangeFunction function_;
};
} // namespace
TreeCache::TreeCache() {}
TreeCache::~TreeCache() {}
class AutomationMessageFilter : public IPC::MessageFilter {
public:
explicit AutomationMessageFilter(AutomationInternalCustomBindings* owner)
: owner_(owner),
removed_(false) {
DCHECK(owner);
content::RenderThread::Get()->AddFilter(this);
task_runner_ = base::ThreadTaskRunnerHandle::Get();
}
void Detach() {
owner_ = nullptr;
Remove();
}
// IPC::MessageFilter
bool OnMessageReceived(const IPC::Message& message) override {
task_runner_->PostTask(
FROM_HERE,
base::Bind(
&AutomationMessageFilter::OnMessageReceivedOnRenderThread,
this, message));
// Always return false in case there are multiple
// AutomationInternalCustomBindings instances attached to the same thread.
return false;
}
void OnFilterRemoved() override {
removed_ = true;
}
private:
void OnMessageReceivedOnRenderThread(const IPC::Message& message) {
if (owner_)
owner_->OnMessageReceived(message);
}
~AutomationMessageFilter() override {
Remove();
}
void Remove() {
if (!removed_) {
removed_ = true;
content::RenderThread::Get()->RemoveFilter(this);
}
}
AutomationInternalCustomBindings* owner_;
bool removed_;
scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
DISALLOW_COPY_AND_ASSIGN(AutomationMessageFilter);
};
AutomationInternalCustomBindings::AutomationInternalCustomBindings(
ScriptContext* context)
: ObjectBackedNativeHandler(context),
is_active_profile_(true),
tree_change_observer_overall_filter_(0) {
// It's safe to use base::Unretained(this) here because these bindings
// will only be called on a valid AutomationInternalCustomBindings instance
// and none of the functions have any side effects.
#define ROUTE_FUNCTION(FN) \
RouteFunction(#FN, "automation", \
base::Bind(&AutomationInternalCustomBindings::FN, \
base::Unretained(this)))
ROUTE_FUNCTION(IsInteractPermitted);
ROUTE_FUNCTION(GetSchemaAdditions);
ROUTE_FUNCTION(GetRoutingID);
ROUTE_FUNCTION(StartCachingAccessibilityTrees);
ROUTE_FUNCTION(DestroyAccessibilityTree);
ROUTE_FUNCTION(AddTreeChangeObserver);
ROUTE_FUNCTION(RemoveTreeChangeObserver);
ROUTE_FUNCTION(GetChildIDAtIndex);
ROUTE_FUNCTION(GetFocus);
ROUTE_FUNCTION(GetHtmlAttributes);
ROUTE_FUNCTION(GetState);
#undef ROUTE_FUNCTION
// Bindings that take a Tree ID and return a property of the tree.
RouteTreeIDFunction(
"GetRootID", [](v8::Isolate* isolate, v8::ReturnValue<v8::Value> result,
TreeCache* cache) {
result.Set(v8::Integer::New(isolate, cache->tree.root()->id()));
});
RouteTreeIDFunction(
"GetDocURL", [](v8::Isolate* isolate, v8::ReturnValue<v8::Value> result,
TreeCache* cache) {
result.Set(
v8::String::NewFromUtf8(isolate, cache->tree.data().url.c_str()));
});
RouteTreeIDFunction(
"GetDocTitle", [](v8::Isolate* isolate, v8::ReturnValue<v8::Value> result,
TreeCache* cache) {
result.Set(
v8::String::NewFromUtf8(isolate, cache->tree.data().title.c_str()));
});
RouteTreeIDFunction(
"GetDocLoaded", [](v8::Isolate* isolate,
v8::ReturnValue<v8::Value> result, TreeCache* cache) {
result.Set(v8::Boolean::New(isolate, cache->tree.data().loaded));
});
RouteTreeIDFunction("GetDocLoadingProgress",
[](v8::Isolate* isolate,
v8::ReturnValue<v8::Value> result, TreeCache* cache) {
result.Set(v8::Number::New(
isolate, cache->tree.data().loading_progress));
});
RouteTreeIDFunction("GetAnchorObjectID",
[](v8::Isolate* isolate,
v8::ReturnValue<v8::Value> result, TreeCache* cache) {
result.Set(v8::Number::New(
isolate, cache->tree.data().sel_anchor_object_id));
});
RouteTreeIDFunction("GetAnchorOffset", [](v8::Isolate* isolate,
v8::ReturnValue<v8::Value> result,
TreeCache* cache) {
result.Set(v8::Number::New(isolate, cache->tree.data().sel_anchor_offset));
});
RouteTreeIDFunction("GetFocusObjectID",
[](v8::Isolate* isolate,
v8::ReturnValue<v8::Value> result, TreeCache* cache) {
result.Set(v8::Number::New(
isolate, cache->tree.data().sel_focus_object_id));
});
RouteTreeIDFunction("GetFocusOffset", [](v8::Isolate* isolate,
v8::ReturnValue<v8::Value> result,
TreeCache* cache) {
result.Set(v8::Number::New(isolate, cache->tree.data().sel_focus_offset));
});
// Bindings that take a Tree ID and Node ID and return a property of the node.
RouteNodeIDFunction(
"GetParentID", [](v8::Isolate* isolate, v8::ReturnValue<v8::Value> result,
TreeCache* cache, ui::AXNode* node) {
if (node->parent())
result.Set(v8::Integer::New(isolate, node->parent()->id()));
});
RouteNodeIDFunction("GetChildCount", [](v8::Isolate* isolate,
v8::ReturnValue<v8::Value> result,
TreeCache* cache, ui::AXNode* node) {
result.Set(v8::Integer::New(isolate, node->child_count()));
});
RouteNodeIDFunction(
"GetIndexInParent",
[](v8::Isolate* isolate, v8::ReturnValue<v8::Value> result,
TreeCache* cache, ui::AXNode* node) {
result.Set(v8::Integer::New(isolate, node->index_in_parent()));
});
RouteNodeIDFunction(
"GetRole", [](v8::Isolate* isolate, v8::ReturnValue<v8::Value> result,
TreeCache* cache, ui::AXNode* node) {
std::string role_name = ui::ToString(node->data().role);
result.Set(v8::String::NewFromUtf8(isolate, role_name.c_str()));
});
RouteNodeIDFunction(
"GetLocation", [](v8::Isolate* isolate, v8::ReturnValue<v8::Value> result,
TreeCache* cache, ui::AXNode* node) {
gfx::Rect location = ComputeGlobalNodeBounds(cache, node);
location.Offset(cache->location_offset);
result.Set(RectToV8Object(isolate, location));
});
RouteNodeIDFunction("GetChildIDs", [](v8::Isolate* isolate,
v8::ReturnValue<v8::Value> result,
TreeCache* cache, ui::AXNode* node) {
const std::vector<ui::AXNode*>& children = node->children();
v8::Local<v8::Array> array_result(v8::Array::New(isolate, children.size()));
for (size_t i = 0; i < children.size(); ++i) {
array_result->Set(static_cast<uint32_t>(i),
v8::Integer::New(isolate, children[i]->id()));
}
result.Set(array_result);
});
// Bindings that take a Tree ID and Node ID and string attribute name
// and return a property of the node.
RouteNodeIDPlusRangeFunction(
"GetBoundsForRange",
[](v8::Isolate* isolate, v8::ReturnValue<v8::Value> result,
TreeCache* cache, ui::AXNode* node, int start, int end) {
gfx::Rect location = ComputeGlobalNodeBounds(cache, node);
location.Offset(cache->location_offset);
if (node->data().role == ui::AX_ROLE_INLINE_TEXT_BOX) {
std::string name = node->data().GetStringAttribute(ui::AX_ATTR_NAME);
std::vector<int> character_offsets =
node->data().GetIntListAttribute(ui::AX_ATTR_CHARACTER_OFFSETS);
int len =
static_cast<int>(std::min(name.size(), character_offsets.size()));
if (start >= 0 && start <= end && end <= len) {
int start_offset = start > 0 ? character_offsets[start - 1] : 0;
int end_offset = end > 0 ? character_offsets[end - 1] : 0;
switch (node->data().GetIntAttribute(ui::AX_ATTR_TEXT_DIRECTION)) {
case ui::AX_TEXT_DIRECTION_LTR:
default:
location.set_x(location.x() + start_offset);
location.set_width(end_offset - start_offset);
break;
case ui::AX_TEXT_DIRECTION_RTL:
location.set_x(location.x() + location.width() - end_offset);
location.set_width(end_offset - start_offset);
break;
case ui::AX_TEXT_DIRECTION_TTB:
location.set_y(location.y() + start_offset);
location.set_height(end_offset - start_offset);
break;
case ui::AX_TEXT_DIRECTION_BTT:
location.set_y(location.y() + location.height() - end_offset);
location.set_height(end_offset - start_offset);
break;
}
}
}
result.Set(RectToV8Object(isolate, location));
});
// Bindings that take a Tree ID and Node ID and string attribute name
// and return a property of the node.
RouteNodeIDPlusAttributeFunction(
"GetStringAttribute",
[](v8::Isolate* isolate, v8::ReturnValue<v8::Value> result,
ui::AXNode* node, const std::string& attribute_name) {
ui::AXStringAttribute attribute =
ui::ParseAXStringAttribute(attribute_name);
std::string attr_value;
if (!node->data().GetStringAttribute(attribute, &attr_value))
return;
result.Set(v8::String::NewFromUtf8(isolate, attr_value.c_str()));
});
RouteNodeIDPlusAttributeFunction(
"GetBoolAttribute",
[](v8::Isolate* isolate, v8::ReturnValue<v8::Value> result,
ui::AXNode* node, const std::string& attribute_name) {
ui::AXBoolAttribute attribute =
ui::ParseAXBoolAttribute(attribute_name);
bool attr_value;
if (!node->data().GetBoolAttribute(attribute, &attr_value))
return;
result.Set(v8::Boolean::New(isolate, attr_value));
});
RouteNodeIDPlusAttributeFunction(
"GetIntAttribute",
[](v8::Isolate* isolate, v8::ReturnValue<v8::Value> result,
ui::AXNode* node, const std::string& attribute_name) {
ui::AXIntAttribute attribute = ui::ParseAXIntAttribute(attribute_name);
int attr_value;
if (!node->data().GetIntAttribute(attribute, &attr_value))
return;
result.Set(v8::Integer::New(isolate, attr_value));
});
RouteNodeIDPlusAttributeFunction(
"GetFloatAttribute",
[](v8::Isolate* isolate, v8::ReturnValue<v8::Value> result,
ui::AXNode* node, const std::string& attribute_name) {
ui::AXFloatAttribute attribute =
ui::ParseAXFloatAttribute(attribute_name);
float attr_value;
if (!node->data().GetFloatAttribute(attribute, &attr_value))
return;
result.Set(v8::Number::New(isolate, attr_value));
});
RouteNodeIDPlusAttributeFunction(
"GetIntListAttribute",
[](v8::Isolate* isolate, v8::ReturnValue<v8::Value> result,
ui::AXNode* node, const std::string& attribute_name) {
ui::AXIntListAttribute attribute =
ui::ParseAXIntListAttribute(attribute_name);
if (!node->data().HasIntListAttribute(attribute))
return;
const std::vector<int32_t>& attr_value =
node->data().GetIntListAttribute(attribute);
v8::Local<v8::Array> array_result(
v8::Array::New(isolate, attr_value.size()));
for (size_t i = 0; i < attr_value.size(); ++i)
array_result->Set(static_cast<uint32_t>(i),
v8::Integer::New(isolate, attr_value[i]));
result.Set(array_result);
});
RouteNodeIDPlusAttributeFunction(
"GetHtmlAttribute",
[](v8::Isolate* isolate, v8::ReturnValue<v8::Value> result,
ui::AXNode* node, const std::string& attribute_name) {
std::string attr_value;
if (!node->data().GetHtmlAttribute(attribute_name.c_str(), &attr_value))
return;
result.Set(v8::String::NewFromUtf8(isolate, attr_value.c_str()));
});
}
AutomationInternalCustomBindings::~AutomationInternalCustomBindings() {}
void AutomationInternalCustomBindings::Invalidate() {
ObjectBackedNativeHandler::Invalidate();
if (message_filter_)
message_filter_->Detach();
// Delete the TreeCaches quickly by first clearing their delegates so
// we don't get a callback for every node being deleted.
for (auto iter : tree_id_to_tree_cache_map_) {
TreeCache* cache = iter.second;
cache->tree.SetDelegate(nullptr);
delete cache;
}
tree_id_to_tree_cache_map_.clear();
}
void AutomationInternalCustomBindings::OnMessageReceived(
const IPC::Message& message) {
IPC_BEGIN_MESSAGE_MAP(AutomationInternalCustomBindings, message)
IPC_MESSAGE_HANDLER(ExtensionMsg_AccessibilityEvent, OnAccessibilityEvent)
IPC_MESSAGE_HANDLER(ExtensionMsg_AccessibilityLocationChange,
OnAccessibilityLocationChange)
IPC_END_MESSAGE_MAP()
}
TreeCache* AutomationInternalCustomBindings::GetTreeCacheFromTreeID(
int tree_id) {
const auto iter = tree_id_to_tree_cache_map_.find(tree_id);
if (iter == tree_id_to_tree_cache_map_.end())
return nullptr;
return iter->second;
}
void AutomationInternalCustomBindings::IsInteractPermitted(
const v8::FunctionCallbackInfo<v8::Value>& args) {
const Extension* extension = context()->extension();
CHECK(extension);
const AutomationInfo* automation_info = AutomationInfo::Get(extension);
CHECK(automation_info);
args.GetReturnValue().Set(
v8::Boolean::New(GetIsolate(), automation_info->interact));
}
void AutomationInternalCustomBindings::GetRoutingID(
const v8::FunctionCallbackInfo<v8::Value>& args) {
int routing_id = context()->GetRenderFrame()->GetRenderView()->GetRoutingID();
args.GetReturnValue().Set(v8::Integer::New(GetIsolate(), routing_id));
}
void AutomationInternalCustomBindings::StartCachingAccessibilityTrees(
const v8::FunctionCallbackInfo<v8::Value>& args) {
if (!message_filter_)
message_filter_ = new AutomationMessageFilter(this);
}
void AutomationInternalCustomBindings::GetSchemaAdditions(
const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Local<v8::Object> additions = v8::Object::New(GetIsolate());
additions->Set(
v8::String::NewFromUtf8(GetIsolate(), "EventType"),
ToEnumObject(GetIsolate(), ui::AX_EVENT_NONE, ui::AX_EVENT_LAST));
additions->Set(
v8::String::NewFromUtf8(GetIsolate(), "RoleType"),
ToEnumObject(GetIsolate(), ui::AX_ROLE_NONE, ui::AX_ROLE_LAST));
additions->Set(
v8::String::NewFromUtf8(GetIsolate(), "StateType"),
ToEnumObject(GetIsolate(), ui::AX_STATE_NONE, ui::AX_STATE_LAST));
additions->Set(
v8::String::NewFromUtf8(GetIsolate(), "TreeChangeType"),
ToEnumObject(GetIsolate(), ui::AX_MUTATION_NONE, ui::AX_MUTATION_LAST));
v8::Local<v8::Object> name_from_type(v8::Object::New(GetIsolate()));
for (int i = ui::AX_NAME_FROM_NONE; i <= ui::AX_NAME_FROM_LAST; ++i) {
name_from_type->Set(
v8::Integer::New(GetIsolate(), i),
CreateV8String(GetIsolate(),
ui::ToString(static_cast<ui::AXNameFrom>(i))));
}
additions->Set(v8::String::NewFromUtf8(GetIsolate(), "NameFromType"),
name_from_type);
v8::Local<v8::Object> description_from_type(v8::Object::New(GetIsolate()));
for (int i = ui::AX_DESCRIPTION_FROM_NONE; i <= ui::AX_DESCRIPTION_FROM_LAST;
++i) {
description_from_type->Set(
v8::Integer::New(GetIsolate(), i),
CreateV8String(GetIsolate(),
ui::ToString(static_cast<ui::AXDescriptionFrom>(i))));
}
additions->Set(v8::String::NewFromUtf8(GetIsolate(), "DescriptionFromType"),
description_from_type);
args.GetReturnValue().Set(additions);
}
void AutomationInternalCustomBindings::DestroyAccessibilityTree(
const v8::FunctionCallbackInfo<v8::Value>& args) {
if (args.Length() != 1 || !args[0]->IsNumber()) {
ThrowInvalidArgumentsException(this);
return;
}
int tree_id = args[0]->Int32Value();
auto iter = tree_id_to_tree_cache_map_.find(tree_id);
if (iter == tree_id_to_tree_cache_map_.end())
return;
TreeCache* cache = iter->second;
tree_id_to_tree_cache_map_.erase(tree_id);
axtree_to_tree_cache_map_.erase(&cache->tree);
delete cache;
}
void AutomationInternalCustomBindings::AddTreeChangeObserver(
const v8::FunctionCallbackInfo<v8::Value>& args) {
if (args.Length() != 2 || !args[0]->IsNumber() || !args[1]->IsString()) {
ThrowInvalidArgumentsException(this);
return;
}
TreeChangeObserver observer;
observer.id = args[0]->Int32Value();
std::string filter_str = *v8::String::Utf8Value(args[1]);
observer.filter = api::automation::ParseTreeChangeObserverFilter(filter_str);
tree_change_observers_.push_back(observer);
UpdateOverallTreeChangeObserverFilter();
}
void AutomationInternalCustomBindings::RemoveTreeChangeObserver(
const v8::FunctionCallbackInfo<v8::Value>& args) {
// The argument is an integer key for an object which is automatically
// converted to a string.
if (args.Length() != 1 || !args[0]->IsString()) {
ThrowInvalidArgumentsException(this);
return;
}
int observer_id = args[0]->Int32Value();
for (auto iter = tree_change_observers_.begin();
iter != tree_change_observers_.end(); ++iter) {
if (iter->id == observer_id) {
tree_change_observers_.erase(iter);
break;
}
}
UpdateOverallTreeChangeObserverFilter();
}
bool AutomationInternalCustomBindings::GetFocusInternal(TreeCache* cache,
TreeCache** out_cache,
ui::AXNode** out_node) {
int focus_id = cache->tree.data().focus_id;
ui::AXNode* focus = cache->tree.GetFromId(focus_id);
if (!focus)
return false;
// If the focused node is the owner of a child tree, that indicates
// a node within the child tree is the one that actually has focus.
while (focus->data().HasIntAttribute(ui::AX_ATTR_CHILD_TREE_ID)) {
// Try to keep following focus recursively, by letting |tree_id| be the
// new subtree to search in, while keeping |focus_tree_id| set to the tree
// where we know we found a focused node.
int child_tree_id =
focus->data().GetIntAttribute(ui::AX_ATTR_CHILD_TREE_ID);
TreeCache* child_cache = GetTreeCacheFromTreeID(child_tree_id);
if (!child_cache)
break;
// If the child cache is a frame tree that indicates a focused frame,
// jump to that frame if possible.
if (child_cache->tree.data().focused_tree_id > 0) {
TreeCache* focused_cache =
GetTreeCacheFromTreeID(child_cache->tree.data().focused_tree_id);
if (focused_cache)
child_cache = focused_cache;
}
int child_focus_id = child_cache->tree.data().focus_id;
ui::AXNode* child_focus = child_cache->tree.GetFromId(child_focus_id);
if (!child_focus)
break;
focus = child_focus;
cache = child_cache;
}
*out_cache = cache;
*out_node = focus;
return true;
}
void AutomationInternalCustomBindings::GetFocus(
const v8::FunctionCallbackInfo<v8::Value>& args) {
if (args.Length() != 1 || !args[0]->IsNumber()) {
ThrowInvalidArgumentsException(this);
return;
}
int tree_id = args[0]->Int32Value();
TreeCache* cache = GetTreeCacheFromTreeID(tree_id);
if (!cache)
return;
TreeCache* focused_tree_cache = nullptr;
ui::AXNode* focused_node = nullptr;
if (!GetFocusInternal(cache, &focused_tree_cache, &focused_node))
return;
v8::Isolate* isolate = GetIsolate();
v8::Local<v8::Object> result(v8::Object::New(isolate));
result->Set(CreateV8String(isolate, "treeId"),
v8::Integer::New(isolate, focused_tree_cache->tree_id));
result->Set(CreateV8String(isolate, "nodeId"),
v8::Integer::New(isolate, focused_node->id()));
args.GetReturnValue().Set(result);
}
void AutomationInternalCustomBindings::GetHtmlAttributes(
const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = GetIsolate();
if (args.Length() < 2 || !args[0]->IsNumber() || !args[1]->IsNumber())
ThrowInvalidArgumentsException(this);
int tree_id = args[0]->Int32Value();
int node_id = args[1]->Int32Value();
TreeCache* cache = GetTreeCacheFromTreeID(tree_id);
if (!cache)
return;
ui::AXNode* node = cache->tree.GetFromId(node_id);
if (!node)
return;
v8::Local<v8::Object> dst(v8::Object::New(isolate));
base::StringPairs src = node->data().html_attributes;
for (size_t i = 0; i < src.size(); i++) {
std::string& key = src[i].first;
std::string& value = src[i].second;
dst->Set(CreateV8String(isolate, key), CreateV8String(isolate, value));
}
args.GetReturnValue().Set(dst);
}
void AutomationInternalCustomBindings::GetState(
const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = GetIsolate();
if (args.Length() < 2 || !args[0]->IsNumber() || !args[1]->IsNumber())
ThrowInvalidArgumentsException(this);
int tree_id = args[0]->Int32Value();
int node_id = args[1]->Int32Value();
TreeCache* cache = GetTreeCacheFromTreeID(tree_id);
if (!cache)
return;
ui::AXNode* node = cache->tree.GetFromId(node_id);
if (!node)
return;
v8::Local<v8::Object> state(v8::Object::New(isolate));
uint32_t state_pos = 0, state_shifter = node->data().state;
while (state_shifter) {
if (state_shifter & 1) {
std::string key = ToString(static_cast<ui::AXState>(state_pos));
state->Set(CreateV8String(isolate, key), v8::Boolean::New(isolate, true));
}
state_shifter = state_shifter >> 1;
state_pos++;
}
TreeCache* top_cache = GetTreeCacheFromTreeID(0);
if (!top_cache)
top_cache = cache;
TreeCache* focused_cache = nullptr;
ui::AXNode* focused_node = nullptr;
if (GetFocusInternal(top_cache, &focused_cache, &focused_node)) {
if (focused_cache == cache && focused_node == node) {
state->Set(CreateV8String(isolate, "focused"),
v8::Boolean::New(isolate, true));
}
}
if (cache->tree.data().focus_id == node->id()) {
state->Set(CreateV8String(isolate, "focused"),
v8::Boolean::New(isolate, true));
}
args.GetReturnValue().Set(state);
}
void AutomationInternalCustomBindings::UpdateOverallTreeChangeObserverFilter() {
tree_change_observer_overall_filter_ = 0;
for (const auto& observer : tree_change_observers_)
tree_change_observer_overall_filter_ |= 1 << observer.filter;
}
ui::AXNode* AutomationInternalCustomBindings::GetParent(
ui::AXNode* node,
TreeCache** in_out_cache) {
if (node->parent())
return node->parent();
int parent_tree_id = (*in_out_cache)->tree.data().parent_tree_id;
if (parent_tree_id < 0)
return nullptr;
TreeCache* parent_cache = GetTreeCacheFromTreeID(parent_tree_id);
if (!parent_cache)
return nullptr;
// Try to use the cached parent node from the most recent time this
// was called.
if (parent_cache->parent_node_id_from_parent_tree > 0) {
ui::AXNode* parent = parent_cache->tree.GetFromId(
parent_cache->parent_node_id_from_parent_tree);
if (parent) {
int parent_child_tree_id =
parent->data().GetIntAttribute(ui::AX_ATTR_CHILD_TREE_ID);
if (parent_child_tree_id == (*in_out_cache)->tree_id) {
*in_out_cache = parent_cache;
return parent;
}
}
}
// If that fails, search for it and cache it for next time.
ui::AXNode* parent = FindNodeWithChildTreeId(parent_cache->tree.root(),
(*in_out_cache)->tree_id);
if (parent) {
(*in_out_cache)->parent_node_id_from_parent_tree = parent->id();
*in_out_cache = parent_cache;
return parent;
}
return nullptr;
}
void AutomationInternalCustomBindings::RouteTreeIDFunction(
const std::string& name,
TreeIDFunction callback) {
scoped_refptr<TreeIDWrapper> wrapper = new TreeIDWrapper(this, callback);
RouteFunction(name, base::Bind(&TreeIDWrapper::Run, wrapper));
}
void AutomationInternalCustomBindings::RouteNodeIDFunction(
const std::string& name,
NodeIDFunction callback) {
scoped_refptr<NodeIDWrapper> wrapper = new NodeIDWrapper(this, callback);
RouteFunction(name, base::Bind(&NodeIDWrapper::Run, wrapper));
}
void AutomationInternalCustomBindings::RouteNodeIDPlusAttributeFunction(
const std::string& name,
NodeIDPlusAttributeFunction callback) {
scoped_refptr<NodeIDPlusAttributeWrapper> wrapper =
new NodeIDPlusAttributeWrapper(this, callback);
RouteFunction(name, base::Bind(&NodeIDPlusAttributeWrapper::Run, wrapper));
}
void AutomationInternalCustomBindings::RouteNodeIDPlusRangeFunction(
const std::string& name,
NodeIDPlusRangeFunction callback) {
scoped_refptr<NodeIDPlusRangeWrapper> wrapper =
new NodeIDPlusRangeWrapper(this, callback);
RouteFunction(name, base::Bind(&NodeIDPlusRangeWrapper::Run, wrapper));
}
void AutomationInternalCustomBindings::GetChildIDAtIndex(
const v8::FunctionCallbackInfo<v8::Value>& args) {
if (args.Length() < 3 || !args[2]->IsNumber()) {
ThrowInvalidArgumentsException(this);
return;
}
int tree_id = args[0]->Int32Value();
int node_id = args[1]->Int32Value();
const auto iter = tree_id_to_tree_cache_map_.find(tree_id);
if (iter == tree_id_to_tree_cache_map_.end())
return;
TreeCache* cache = iter->second;
if (!cache)
return;
ui::AXNode* node = cache->tree.GetFromId(node_id);
if (!node)
return;
int index = args[2]->Int32Value();
if (index < 0 || index >= node->child_count())
return;
int child_id = node->children()[index]->id();
args.GetReturnValue().Set(v8::Integer::New(GetIsolate(), child_id));
}
//
// Handle accessibility events from the browser process.
//
void AutomationInternalCustomBindings::OnAccessibilityEvent(
const ExtensionMsg_AccessibilityEventParams& params,
bool is_active_profile) {
is_active_profile_ = is_active_profile;
int tree_id = params.tree_id;
TreeCache* cache;
auto iter = tree_id_to_tree_cache_map_.find(tree_id);
if (iter == tree_id_to_tree_cache_map_.end()) {
cache = new TreeCache();
cache->tab_id = -1;
cache->tree_id = params.tree_id;
cache->parent_node_id_from_parent_tree = -1;
cache->tree.SetDelegate(this);
cache->owner = this;
tree_id_to_tree_cache_map_.insert(std::make_pair(tree_id, cache));
axtree_to_tree_cache_map_.insert(std::make_pair(&cache->tree, cache));
} else {
cache = iter->second;
}
// Update the internal state whether it's the active profile or not.
cache->location_offset = params.location_offset;
deleted_node_ids_.clear();
if (!cache->tree.Unserialize(params.update)) {
LOG(ERROR) << cache->tree.error();
return;
}
// Don't send any events if it's not the active profile.
if (!is_active_profile)
return;
SendNodesRemovedEvent(&cache->tree, deleted_node_ids_);
deleted_node_ids_.clear();
v8::Isolate* isolate = GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::Context::Scope context_scope(context()->v8_context());
v8::Local<v8::Array> args(v8::Array::New(GetIsolate(), 1U));
v8::Local<v8::Object> event_params(v8::Object::New(GetIsolate()));
event_params->Set(CreateV8String(isolate, "treeID"),
v8::Integer::New(GetIsolate(), params.tree_id));
event_params->Set(CreateV8String(isolate, "targetID"),
v8::Integer::New(GetIsolate(), params.id));
event_params->Set(CreateV8String(isolate, "eventType"),
CreateV8String(isolate, ToString(params.event_type)));
event_params->Set(CreateV8String(isolate, "eventFrom"),
CreateV8String(isolate, ToString(params.event_from)));
args->Set(0U, event_params);
context()->DispatchEvent("automationInternal.onAccessibilityEvent", args);
}
void AutomationInternalCustomBindings::OnAccessibilityLocationChange(
const ExtensionMsg_AccessibilityLocationChangeParams& params) {
int tree_id = params.tree_id;
auto iter = tree_id_to_tree_cache_map_.find(tree_id);
if (iter == tree_id_to_tree_cache_map_.end())
return;
TreeCache* cache = iter->second;
ui::AXNode* node = cache->tree.GetFromId(params.id);
if (!node)
return;
node->SetLocation(params.new_location.offset_container_id,
params.new_location.bounds,
params.new_location.transform.get());
}
void AutomationInternalCustomBindings::OnNodeDataWillChange(
ui::AXTree* tree,
const ui::AXNodeData& old_node_data,
const ui::AXNodeData& new_node_data) {
if (old_node_data.GetStringAttribute(ui::AX_ATTR_NAME) !=
new_node_data.GetStringAttribute(ui::AX_ATTR_NAME))
text_changed_node_ids_.push_back(new_node_data.id);
}
void AutomationInternalCustomBindings::OnTreeDataChanged(ui::AXTree* tree) {}
void AutomationInternalCustomBindings::OnNodeWillBeDeleted(ui::AXTree* tree,
ui::AXNode* node) {
SendTreeChangeEvent(
api::automation::TREE_CHANGE_TYPE_NODEREMOVED,
tree, node);
deleted_node_ids_.push_back(node->id());
}
void AutomationInternalCustomBindings::OnSubtreeWillBeDeleted(
ui::AXTree* tree,
ui::AXNode* node) {
// This isn't strictly needed, as OnNodeWillBeDeleted will already be
// called. We could send a JS event for this only if it turns out to
// be needed for something.
}
void AutomationInternalCustomBindings::OnNodeWillBeReparented(
ui::AXTree* tree,
ui::AXNode* node) {
// Don't do anything here since the node will soon go away and be re-created.
}
void AutomationInternalCustomBindings::OnSubtreeWillBeReparented(
ui::AXTree* tree,
ui::AXNode* node) {
// Don't do anything here since the node will soon go away and be re-created.
}
void AutomationInternalCustomBindings::OnNodeCreated(ui::AXTree* tree,
ui::AXNode* node) {
// Not needed, this is called in the middle of an update so it's not
// safe to trigger JS from here. Wait for the notification in
// OnAtomicUpdateFinished instead.
}
void AutomationInternalCustomBindings::OnNodeReparented(ui::AXTree* tree,
ui::AXNode* node) {
// Not needed, this is called in the middle of an update so it's not
// safe to trigger JS from here. Wait for the notification in
// OnAtomicUpdateFinished instead.
}
void AutomationInternalCustomBindings::OnNodeChanged(ui::AXTree* tree,
ui::AXNode* node) {
// Not needed, this is called in the middle of an update so it's not
// safe to trigger JS from here. Wait for the notification in
// OnAtomicUpdateFinished instead.
}
void AutomationInternalCustomBindings::OnAtomicUpdateFinished(
ui::AXTree* tree,
bool root_changed,
const std::vector<ui::AXTreeDelegate::Change>& changes) {
auto iter = axtree_to_tree_cache_map_.find(tree);
if (iter == axtree_to_tree_cache_map_.end())
return;
for (const auto change : changes) {
ui::AXNode* node = change.node;
switch (change.type) {
case NODE_CREATED:
SendTreeChangeEvent(
api::automation::TREE_CHANGE_TYPE_NODECREATED,
tree, node);
break;
case SUBTREE_CREATED:
SendTreeChangeEvent(
api::automation::TREE_CHANGE_TYPE_SUBTREECREATED,
tree, node);
break;
case NODE_CHANGED:
SendTreeChangeEvent(
api::automation::TREE_CHANGE_TYPE_NODECHANGED,
tree, node);
break;
// Unhandled.
case NODE_REPARENTED:
case SUBTREE_REPARENTED:
break;
}
}
for (int id : text_changed_node_ids_) {
SendTreeChangeEvent(api::automation::TREE_CHANGE_TYPE_TEXTCHANGED, tree,
tree->GetFromId(id));
}
text_changed_node_ids_.clear();
}
void AutomationInternalCustomBindings::SendTreeChangeEvent(
api::automation::TreeChangeType change_type,
ui::AXTree* tree,
ui::AXNode* node) {
// Don't send tree change events when it's not the active profile.
if (!is_active_profile_)
return;
// Always notify the custom bindings when there's a node with a child tree
// ID that might need to be loaded.
if (node->data().HasIntAttribute(ui::AX_ATTR_CHILD_TREE_ID))
SendChildTreeIDEvent(tree, node);
bool has_filter = false;
if (tree_change_observer_overall_filter_ &
(1 <<
api::automation::TREE_CHANGE_OBSERVER_FILTER_LIVEREGIONTREECHANGES)) {
if (node->data().HasStringAttribute(ui::AX_ATTR_CONTAINER_LIVE_STATUS) ||
node->data().role == ui::AX_ROLE_ALERT) {
has_filter = true;
}
}
if (tree_change_observer_overall_filter_ &
(1 << api::automation::TREE_CHANGE_OBSERVER_FILTER_TEXTMARKERCHANGES)) {
if (node->data().HasIntListAttribute(ui::AX_ATTR_MARKER_TYPES))
has_filter = true;
}
if (tree_change_observer_overall_filter_ &
(1 << api::automation::TREE_CHANGE_OBSERVER_FILTER_ALLTREECHANGES))
has_filter = true;
if (!has_filter)
return;
auto iter = axtree_to_tree_cache_map_.find(tree);
if (iter == axtree_to_tree_cache_map_.end())
return;
int tree_id = iter->second->tree_id;
v8::Isolate* isolate = GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::Context::Scope context_scope(context()->v8_context());
for (const auto& observer : tree_change_observers_) {
switch (observer.filter) {
case api::automation::TREE_CHANGE_OBSERVER_FILTER_NOTREECHANGES:
default:
continue;
case api::automation::TREE_CHANGE_OBSERVER_FILTER_LIVEREGIONTREECHANGES:
if (!node->data().HasStringAttribute(
ui::AX_ATTR_CONTAINER_LIVE_STATUS) &&
node->data().role != ui::AX_ROLE_ALERT) {
continue;
}
break;
case api::automation::TREE_CHANGE_OBSERVER_FILTER_TEXTMARKERCHANGES:
if (!node->data().HasIntListAttribute(ui::AX_ATTR_MARKER_TYPES))
continue;
break;
case api::automation::TREE_CHANGE_OBSERVER_FILTER_ALLTREECHANGES:
break;
}
v8::Local<v8::Array> args(v8::Array::New(GetIsolate(), 4U));
args->Set(0U, v8::Integer::New(GetIsolate(), observer.id));
args->Set(1U, v8::Integer::New(GetIsolate(), tree_id));
args->Set(2U, v8::Integer::New(GetIsolate(), node->id()));
args->Set(3U, CreateV8String(isolate, ToString(change_type)));
context()->DispatchEvent("automationInternal.onTreeChange", args);
}
}
void AutomationInternalCustomBindings::SendChildTreeIDEvent(ui::AXTree* tree,
ui::AXNode* node) {
auto iter = axtree_to_tree_cache_map_.find(tree);
if (iter == axtree_to_tree_cache_map_.end())
return;
int tree_id = iter->second->tree_id;
v8::Isolate* isolate = GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::Context::Scope context_scope(context()->v8_context());
v8::Local<v8::Array> args(v8::Array::New(GetIsolate(), 2U));
args->Set(0U, v8::Integer::New(GetIsolate(), tree_id));
args->Set(1U, v8::Integer::New(GetIsolate(), node->id()));
context()->DispatchEvent("automationInternal.onChildTreeID", args);
}
void AutomationInternalCustomBindings::SendNodesRemovedEvent(
ui::AXTree* tree,
const std::vector<int>& ids) {
auto iter = axtree_to_tree_cache_map_.find(tree);
if (iter == axtree_to_tree_cache_map_.end())
return;
int tree_id = iter->second->tree_id;
v8::Isolate* isolate = GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::Context::Scope context_scope(context()->v8_context());
v8::Local<v8::Array> args(v8::Array::New(GetIsolate(), 2U));
args->Set(0U, v8::Integer::New(GetIsolate(), tree_id));
v8::Local<v8::Array> nodes(v8::Array::New(GetIsolate(), ids.size()));
args->Set(1U, nodes);
for (size_t i = 0; i < ids.size(); ++i)
nodes->Set(i, v8::Integer::New(GetIsolate(), ids[i]));
context()->DispatchEvent("automationInternal.onNodesRemoved", args);
}
} // namespace extensions