// Copyright 2022 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/widget/drop_helper.h"

#include <memory>
#include <set>
#include <utility>

#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/dragdrop/drop_target_event.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h"
#include "ui/base/dragdrop/os_exchange_data.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer_tree_owner.h"
#include "ui/gfx/geometry/point.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"

namespace views {
namespace {

// A view implementation for validating drop location.
class TestDropTargetView : public views::View {
  METADATA_HEADER(TestDropTargetView, View)

 public:
  TestDropTargetView() : drop_location_(-1, -1) {}

  bool GetDropFormats(
      int* formats,
      std::set<ui::ClipboardFormatType>* format_types) override {
    *formats = ui::OSExchangeData::STRING;
    return true;
  }

  bool CanDrop(const OSExchangeData& data) override { return true; }

  int OnDragUpdated(const ui::DropTargetEvent& event) override {
    return static_cast<int>(ui::mojom::DragOperation::kCopy);
  }

  gfx::Point drop_location() { return drop_location_; }

  DropCallback GetDropCallback(const ui::DropTargetEvent& event) override {
    return base::BindOnce(&TestDropTargetView::PerformDrop,
                          base::Unretained(this));
  }

 private:
  gfx::Point drop_location_;

  void PerformDrop(const ui::DropTargetEvent& event,
                   ui::mojom::DragOperation& output_drag_op,
                   std::unique_ptr<ui::LayerTreeOwner> drag_image_layer_owner) {
    drop_location_ = event.location();
    output_drag_op = ui::mojom::DragOperation::kCopy;
  }
};

BEGIN_METADATA(TestDropTargetView)
END_METADATA

class DropHelperTest : public ViewsTestBase {
 public:
  DropHelperTest() = default;
  ~DropHelperTest() override = default;
};

// Verifies that drop coordinates are dispatched in View coordinates.
TEST_F(DropHelperTest, DropCoordinates) {
  // Create widget
  Widget widget;
  Widget::InitParams init_params(
      CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS));
  init_params.ownership = Widget::InitParams::CLIENT_OWNS_WIDGET;
  widget.Init(std::move(init_params));

  // Setup a widget with a view that isn't aligned with the corner. In screen
  // coordinates, the target view starts at (120, 80) and ends at (220, 130).
  gfx::Rect bounds(100, 50, 200, 100);
  widget.SetBounds(bounds);
  views::View* container =
      widget.SetContentsView(std::make_unique<views::View>());
  TestDropTargetView* drop_target =
      container->AddChildView(std::make_unique<TestDropTargetView>());
  drop_target->SetBounds(20, 30, 100, 50);
  widget.Show();

  auto drop_helper = std::make_unique<DropHelper>(widget.GetRootView());

  // Construct drag data
  auto data = std::make_unique<ui::OSExchangeData>();
  data->SetString(u"Drag and drop is cool");
  int drag_operation = static_cast<int>(ui::mojom::DragOperation::kCopy);
  gfx::Point enter_location = {10, 0};

  // Enter parent view
  drop_helper->OnDragOver(*data, enter_location, drag_operation);

  // Location of center of target view in root.
  gfx::Point target = {70, 55};
  // Enter target view
  drop_helper->OnDragOver(*data, target, drag_operation);

  // Start drop.
  DropHelper::DropCallback callback =
      drop_helper->GetDropCallback(*data, target, drag_operation);
  ASSERT_TRUE(callback);

  // Perform the drop.
  ui::mojom::DragOperation output_op = ui::mojom::DragOperation::kNone;
  std::move(callback).Run(std::move(data), output_op,
                          /*drag_image_layer_owner=*/nullptr);

  // The test view always executes a copy operation.
  EXPECT_EQ(output_op, ui::mojom::DragOperation::kCopy);
  // Verify the location of the drop is centered in the target view.
  EXPECT_EQ(drop_target->drop_location(), gfx::Point(50, 25));
}

// A drop-target view whose OnDragExited() handler synchronously removes and
// deletes a sibling view from the hierarchy. Used to demonstrate the UAF in
// DropHelper::OnDragOver when the old target's OnDragExited deletes the view
// that was just selected as the new target.
class DragExitDeletesSibling : public views::View {
  METADATA_HEADER(DragExitDeletesSibling, View)

 public:
  DragExitDeletesSibling() = default;

  void set_sibling_to_delete(View* sibling) { sibling_to_delete_ = sibling; }

  // Make this view a valid drop target so DropHelper selects it.
  bool GetDropFormats(
      int* formats,
      std::set<ui::ClipboardFormatType>* format_types) override {
    *formats = ui::OSExchangeData::STRING;
    return true;
  }
  bool CanDrop(const OSExchangeData& data) override { return true; }
  int OnDragUpdated(const ui::DropTargetEvent& event) override {
    return static_cast<int>(ui::mojom::DragOperation::kCopy);
  }

  // The critical override: when the drag leaves this view, synchronously
  // destroy the sibling (which DropHelper has just chosen as the *new*
  // target and is holding in a plain View* local).
  void OnDragExited() override {
    if (sibling_to_delete_) {
      View* victim = sibling_to_delete_.get();
      // Release our raw_ptr ref before deletion so BRP refcount on the victim
      // is zero at delete-time -- matching the production scenario where
      // ResetTargetViewIfEquals clears deepest_view_ before ~View runs.
      sibling_to_delete_ = nullptr;
      victim->parent()->RemoveChildViewT(victim);
    }
  }

 private:
  raw_ptr<View> sibling_to_delete_ = nullptr;
};

BEGIN_METADATA(DragExitDeletesSibling)
END_METADATA

// Regression / PoC test for the DropHelper::OnDragOver UAF.
//
// Sequence:
//   1. Drag over view A  -> target_view_ = A.
//   2. Drag over view B  -> CalculateTargetViewImpl returns B into a *plain*
//      View* local; NotifyDragExit() runs A->OnDragExited(), which deletes B;
//      target_view_ = (freed) B; NotifyDragEntered() dereferences freed B via
//      ConvertPointToTarget and a virtual OnDragEntered() call.
//
// Under ASAN this produces heap-use-after-free in NotifyDragEntered.
TEST_F(DropHelperTest, OnDragExitedDeletesNewTarget) {
  Widget widget;
  Widget::InitParams init_params(
      CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS));
  init_params.ownership = Widget::InitParams::CLIENT_OWNS_WIDGET;
  widget.Init(std::move(init_params));
  widget.SetBounds(gfx::Rect(0, 0, 500, 500));

  views::View* container =
      widget.SetContentsView(std::make_unique<views::View>());

  // View A: the "old" drop target whose OnDragExited deletes B.
  DragExitDeletesSibling* view_a =
      container->AddChildView(std::make_unique<DragExitDeletesSibling>());
  view_a->SetBounds(10, 10, 100, 100);

  // View B: the "new" drop target that will be freed mid-OnDragOver.
  TestDropTargetView* view_b =
      container->AddChildView(std::make_unique<TestDropTargetView>());
  view_b->SetBounds(200, 10, 100, 100);

  view_a->set_sibling_to_delete(view_b);
  widget.Show();

  auto drop_helper = std::make_unique<DropHelper>(widget.GetRootView());

  auto data = std::make_unique<ui::OSExchangeData>();
  data->SetString(u"payload");
  const int drag_op = static_cast<int>(ui::mojom::DragOperation::kCopy);

  // Step 1: drag over A. target_view_ becomes A.
  drop_helper->OnDragOver(*data, gfx::Point(60, 60), drag_op);

  // Step 2: drag over B. Inside this call:
  //   - local `view` = B
  //   - NotifyDragExit() -> A->OnDragExited() -> deletes B
  //   - target_view_ = (freed) B
  //   - NotifyDragEntered() -> UAF on B
  drop_helper->OnDragOver(*data, gfx::Point(250, 60), drag_op);

  // If we get here without ASAN tripping, the fix is in place. B was deleted,
  // so only A should remain.
  EXPECT_EQ(1u, container->children().size());

  // Avoid dangling raw_ptr<View> deepest_view_ at DropHelper teardown.
  drop_helper->OnDragExit();
}

}  // namespace
}  // namespace views
