910e62b5创建于 1月15日历史提交
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/views/accessibility/ax_aura_obj_cache.h"

#include <string>
#include <utility>

#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/ax_tree.h"
#include "ui/accessibility/ax_tree_serializer.h"
#include "ui/accessibility/ax_tree_source_checker.h"
#include "ui/aura/window.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/views/accessibility/ax_aura_obj_wrapper.h"
#include "ui/views/accessibility/ax_tree_source_views.h"
#include "ui/views/accessibility/ax_update_notifier.h"
#include "ui/views/accessibility/ax_update_observer.h"
#include "ui/views/accessibility/ax_virtual_view.h"
#include "ui/views/accessibility/ax_virtual_view_wrapper.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/widget/widget_delegate.h"

namespace views::test {
namespace {

bool HasNodeWithName(ui::AXNode* node, const std::string& name) {
  if (node->GetStringAttribute(ax::mojom::StringAttribute::kName) == name) {
    return true;
  }
  for (ui::AXNode* child : node->children()) {
    if (HasNodeWithName(child, name)) {
      return true;
    }
  }
  return false;
}

bool HasNodeWithName(const ui::AXTree& tree, const std::string& name) {
  return HasNodeWithName(tree.root(), name);
}

}  // namespace

class AXAuraObjCacheTest : public WidgetTest {
 public:
  AXAuraObjCacheTest() = default;
  ~AXAuraObjCacheTest() override = default;

  ui::AXNodeData GetData(AXAuraObjWrapper* wrapper) {
    ui::AXNodeData data;
    wrapper->Serialize(&data);
    return data;
  }
};

TEST_F(AXAuraObjCacheTest, TestViewRemoval) {
  AXAuraObjCache cache;
  std::unique_ptr<Widget> widget(
      CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET));
  auto* parent = widget->GetRootView()->AddChildView(std::make_unique<View>());
  auto* child = parent->AddChildView(std::make_unique<View>());

  AXAuraObjWrapper* ax_widget = cache.GetOrCreate(widget.get());
  ASSERT_NE(nullptr, ax_widget);
  AXAuraObjWrapper* ax_parent = cache.GetOrCreate(parent);
  ASSERT_NE(nullptr, ax_parent);
  AXAuraObjWrapper* ax_child = cache.GetOrCreate(child);
  ASSERT_NE(nullptr, ax_child);

  // Everything should have an ID, indicating it's in the cache.
  ASSERT_NE(cache.GetID(widget.get()), ui::kInvalidAXNodeID);
  ASSERT_NE(cache.GetID(parent), ui::kInvalidAXNodeID);
  ASSERT_NE(cache.GetID(child), ui::kInvalidAXNodeID);

  // Removing the parent view should remove both the parent and child
  // from the cache, but leave the widget.
  widget->GetRootView()->RemoveChildViewT(parent);
  ASSERT_NE(cache.GetID(widget.get()), ui::kInvalidAXNodeID);
  ASSERT_EQ(ui::kInvalidAXNodeID, cache.GetID(parent));
  ASSERT_EQ(ui::kInvalidAXNodeID, cache.GetID(child));
}

// Helper for the ViewDestruction test.
class ViewBlurObserver : public ViewObserver {
 public:
  ViewBlurObserver(AXAuraObjCache* cache, View* view) : cache_(cache) {
    observation_.Observe(view);
  }

  void OnViewBlurred(View* view) override {
    ASSERT_FALSE(was_called());
    observation_.Reset();

    // The cache entry gets deleted in
    // AXViewObjWrapper::OnViewIsDeleting which occurs later in ~View.
  }

  bool was_called() { return !observation_.IsObserving(); }

 private:
  raw_ptr<AXAuraObjCache> cache_;
  base::ScopedObservation<View, ViewObserver> observation_{this};
};

// Test that stale cache entries are not left behind if a cache entry is
// re-created during View destruction.
TEST_F(AXAuraObjCacheTest, ViewDestruction) {
  AXAuraObjCache cache;

  std::unique_ptr<Widget> widget(
      CreateTopLevelPlatformWidget(Widget::InitParams::CLIENT_OWNS_WIDGET));
  auto button =
      std::make_unique<LabelButton>(Button::PressedCallback(), u"button");
  auto* button_ptr = button.get();
  widget->GetRootView()->AddChildView(std::move(button));
  widget->Activate();
  button_ptr->RequestFocus();
  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(button_ptr->HasFocus());

  cache.GetOrCreate(widget.get());
  cache.GetOrCreate(button_ptr);
  // Everything should have an ID, indicating it's in the cache.
  EXPECT_NE(cache.GetID(widget.get()), ui::kInvalidAXNodeID);
  EXPECT_NE(cache.GetID(button_ptr), ui::kInvalidAXNodeID);

  ViewBlurObserver observer(&cache, button_ptr);
  widget->GetRootView()->RemoveChildViewT(button_ptr);

  // The button object is destroyed, so there should be no stale cache entries.
  EXPECT_NE(button_ptr, nullptr);
  EXPECT_EQ(ui::kInvalidAXNodeID, cache.GetID(button_ptr));
  EXPECT_TRUE(observer.was_called());
}

TEST_F(AXAuraObjCacheTest, CacheDestructionUAF) {
  // This test ensures that a UAF is not possible during cache destruction.
  // Two top-level widgets need to be created and inserted into |root_windows_|.
  // This test uses manual memory management, rather than managed helpers
  // that other tests are using. Ensure there is not a UAF crash when deleting
  // the cache.
  auto cache = std::make_unique<AXAuraObjCache>();

  auto widget = std::make_unique<Widget>();
  Widget::InitParams params = CreateParams(
      Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW);
  params.bounds = gfx::Rect(0, 0, 200, 200);
  widget->Init(std::move(params));
  cache->OnRootWindowObjCreated(widget->GetNativeWindow());

  widget->Activate();
  base::RunLoop().RunUntilIdle();

  cache->GetOrCreate(widget.get());

  // Everything should have an ID, indicating it's in the cache.
  EXPECT_NE(cache->GetID(widget.get()), ui::kInvalidAXNodeID);

  // Create a second top-level widget to ensure |root_windows_| isn't empty.
  auto widget2 = std::make_unique<Widget>();
  Widget::InitParams params2 = CreateParams(
      Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW);
  params2.bounds = gfx::Rect(0, 0, 200, 200);
  widget2->Init(std::move(params2));
  cache->OnRootWindowObjCreated(widget2->GetNativeWindow());

  cache->GetOrCreate(widget2.get());
  widget2->Activate();
  base::RunLoop().RunUntilIdle();

  // Everything should have an ID, indicating it's in the cache.
  EXPECT_NE(cache->GetID(widget2.get()), ui::kInvalidAXNodeID);

  // Delete the first widget, then delete the cache.
  cache->OnRootWindowObjDestroyed(widget->GetNativeWindow());
  widget.reset();
  cache.reset();
}

TEST_F(AXAuraObjCacheTest, ValidTree) {
  // Create a parent window.
  auto parent_widget = std::make_unique<Widget>();
  Widget::InitParams params = CreateParams(
      Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW);
  params.bounds = gfx::Rect(0, 0, 200, 200);
  parent_widget->Init(std::move(params));
  parent_widget->GetNativeWindow()->SetTitle(u"ParentWindow");
  parent_widget->Show();

  // Create a child window.
  auto child_widget = std::make_unique<Widget>();  // Owned by parent_widget.
  params = CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET,
                        Widget::InitParams::TYPE_BUBBLE);
  params.parent = parent_widget->GetNativeWindow();
  params.child = true;
  params.bounds = gfx::Rect(100, 100, 200, 200);
  child_widget->Init(std::move(params));
  child_widget->GetNativeWindow()->SetTitle(u"ChildWindow");
  child_widget->Show();

  // Create a child view.
  auto button =
      std::make_unique<LabelButton>(Button::PressedCallback(), u"ChildButton");
  button->SetSize(gfx::Size(20, 20));
  child_widget->GetContentsView()->AddChildView(std::move(button));

  // Use AXAuraObjCache to serialize the node tree.
  AXAuraObjCache cache;
  ui::AXTreeID tree_id = ui::AXTreeID::CreateNewAXTreeID();
  AXTreeSourceViews tree_source(
      cache.GetOrCreate(parent_widget->GetNativeWindow())->GetUniqueId(),
      tree_id, &cache);
  ui::AXTreeSerializer<AXAuraObjWrapper*, std::vector<AXAuraObjWrapper*>,
                       ui::AXTreeUpdate*, ui::AXTreeData*, ui::AXNodeData>
      serializer(&tree_source);
  ui::AXTreeUpdate serialized_tree;
  serializer.SerializeChanges(tree_source.GetRoot(), &serialized_tree);

  // Verify tree is valid.
  ui::AXTreeSourceChecker<AXAuraObjWrapper*> checker(&tree_source);
  std::string error_string;
  EXPECT_TRUE(checker.CheckAndGetErrorString(&error_string)) << error_string;
  ui::AXTree ax_tree(serialized_tree);
  EXPECT_TRUE(HasNodeWithName(ax_tree, "ParentWindow"));
  EXPECT_TRUE(HasNodeWithName(ax_tree, "ChildWindow"));
  EXPECT_TRUE(HasNodeWithName(ax_tree, "ChildButton"));
}

TEST_F(AXAuraObjCacheTest, GetFocusIsUnignoredAncestor) {
  AXAuraObjCache cache;
  auto widget = std::make_unique<Widget>();
  Widget::InitParams params = CreateParams(
      Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW);
  params.bounds = gfx::Rect(0, 0, 200, 200);
  params.activatable = views::Widget::InitParams::Activatable::kYes;
  widget->Init(std::move(params));
  widget->Show();

  // Note that AXAuraObjCache::GetFocusedView has some logic to force focus on
  // the first child of the client view when one cannot be found from the
  // FocusManager if it has a child tree id.
  ClientView* client = widget->non_client_view()->client_view();
  ASSERT_NE(nullptr, client);
  View* client_child = client->children().front();
  ASSERT_NE(nullptr, client_child);
  client_child->GetViewAccessibility().SetRole(ax::mojom::Role::kDialog);
  client_child->GetViewAccessibility().SetChildTreeID(
      ui::AXTreeID::CreateNewAXTreeID());

  auto* parent = widget->GetRootView()->AddChildView(std::make_unique<View>());
  parent->GetViewAccessibility().SetRole(ax::mojom::Role::kTextField);
  parent->SetFocusBehavior(View::FocusBehavior::ALWAYS);

  auto* child = parent->AddChildView(std::make_unique<View>());
  child->GetViewAccessibility().SetRole(ax::mojom::Role::kGroup);
  child->SetFocusBehavior(View::FocusBehavior::ALWAYS);

  AXAuraObjWrapper* ax_widget = cache.GetOrCreate(widget.get());
  ASSERT_NE(nullptr, ax_widget);
  AXAuraObjWrapper* ax_client_child = cache.GetOrCreate(client_child);
  ASSERT_NE(nullptr, ax_client_child);
  AXAuraObjWrapper* ax_parent = cache.GetOrCreate(parent);
  ASSERT_NE(nullptr, ax_parent);
  AXAuraObjWrapper* ax_child = cache.GetOrCreate(child);
  ASSERT_NE(nullptr, ax_child);

  ASSERT_EQ(nullptr, cache.GetFocus());
  cache.OnRootWindowObjCreated(widget->GetNativeWindow());
  ASSERT_EQ(ax::mojom::Role::kDialog, GetData(cache.GetFocus()).role);
  ASSERT_EQ(ax_client_child, cache.GetFocus());

  parent->RequestFocus();
  ASSERT_EQ(ax::mojom::Role::kTextField, GetData(cache.GetFocus()).role);
  ASSERT_EQ(ax_parent, cache.GetFocus());

  child->RequestFocus();
  ASSERT_EQ(ax::mojom::Role::kGroup, GetData(cache.GetFocus()).role);
  ASSERT_EQ(ax_child, cache.GetFocus());

  // Ignore should cause focus to move upwards.
  child->GetViewAccessibility().SetIsIgnored(true);
  ASSERT_EQ(ax::mojom::Role::kTextField, GetData(cache.GetFocus()).role);
  ASSERT_EQ(ax_parent, cache.GetFocus());

  // Propagate focus to ancestor should also cause focus to move upward.
  parent->GetViewAccessibility().set_propagate_focus_to_ancestor(true);
  ASSERT_EQ(ax::mojom::Role::kWindow, GetData(cache.GetFocus()).role);
  ASSERT_EQ(cache.GetOrCreate(widget->GetRootView()), cache.GetFocus());

  cache.OnRootWindowObjDestroyed(widget->GetNativeWindow());
}

class TestingWidgetDelegateView : public WidgetDelegateView {
 public:
  explicit TestingWidgetDelegateView(base::RunLoop* run_loop)
      : run_loop_(run_loop) {}
  ~TestingWidgetDelegateView() override {
    NotifyAccessibilityEventDeprecated(ax::mojom::Event::kChildrenChanged,
                                       false);
    run_loop_->QuitWhenIdle();
  }
  TestingWidgetDelegateView(const TestingWidgetDelegateView&) = delete;
  TestingWidgetDelegateView& operator=(const TestingWidgetDelegateView&) =
      delete;

 private:
  raw_ptr<base::RunLoop> run_loop_;
};

class TestingAXEventObserver : public AXUpdateObserver {
 public:
  explicit TestingAXEventObserver(AXAuraObjCache* cache) : cache_(cache) {
    observation_.Observe(AXUpdateNotifier::Get());
  }
  ~TestingAXEventObserver() override = default;
  TestingAXEventObserver(const TestingAXEventObserver&) = delete;
  TestingAXEventObserver& operator=(const TestingAXEventObserver&) = delete;

 private:
  void OnViewEvent(View* view, ax::mojom::Event event_type) override {
    AXAuraObjWrapper* ax_view = cache_->GetOrCreate(view);
    while (ax_view != nullptr) {
      ax_view = ax_view->GetParent();
    }
  }

  raw_ptr<AXAuraObjCache> cache_;
  base::ScopedObservation<AXUpdateNotifier, AXUpdateObserver> observation_{
      this};
};

TEST_F(AXAuraObjCacheTest, DoNotCreateWidgetWrapperOnDestroyed) {
  AXAuraObjCache cache;
  TestingAXEventObserver observer(&cache);
  auto widget = std::make_unique<Widget>();

  base::RunLoop run_loop;
  auto delegate = std::make_unique<TestingWidgetDelegateView>(&run_loop);

  Widget::InitParams params = CreateParams(
      Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW);
  params.bounds = gfx::Rect(0, 0, 200, 200);
  params.activatable = views::Widget::InitParams::Activatable::kYes;
  params.delegate = delegate.release();
  widget->Init(std::move(params));
  widget->Show();
  auto* widget_ptr = widget.get();

  EXPECT_NE(ui::kInvalidAXNodeID, cache.GetID(widget_ptr));

  // NativeWidget is closed asynchronously.
  widget.reset();
  run_loop.Run();

  EXPECT_EQ(ui::kInvalidAXNodeID, cache.GetID(widget_ptr));
}

TEST_F(AXAuraObjCacheTest, VirtualViews) {
  AXAuraObjCache cache;
  auto widget = std::make_unique<Widget>();
  Widget::InitParams params = CreateParams(
      Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW);
  params.bounds = gfx::Rect(0, 0, 200, 200);
  params.activatable = views::Widget::InitParams::Activatable::kYes;
  widget->Init(std::move(params));
  widget->Show();

  auto* parent = widget->GetRootView()->AddChildView(std::make_unique<View>());
  auto virtual_label = std::make_unique<AXVirtualView>();
  auto* virtual_label_ptr = virtual_label.get();
  virtual_label->SetRole(ax::mojom::Role::kStaticText);
  virtual_label->SetName("Label");
  parent->GetViewAccessibility().AddVirtualChildView(std::move(virtual_label));

  AXVirtualViewWrapper* wrapper = virtual_label_ptr->GetOrCreateWrapper(&cache);
  ui::AXNodeID id = wrapper->GetUniqueId();
  AXAuraObjWrapper* wrapper2 = cache.Get(id);
  EXPECT_EQ(wrapper, wrapper2);

  parent->GetViewAccessibility().RemoveVirtualChildView(virtual_label_ptr);
  EXPECT_EQ(nullptr, cache.Get(id));
}

}  // namespace views::test