// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/extensions/api/bookmark_manager_private/bookmark_manager_private_api.h"

#include <stddef.h>
#include <stdint.h>

#include <algorithm>
#include <memory>
#include <utility>
#include <vector>

#include "base/functional/bind.h"
#include "base/i18n/file_util_icu.h"
#include "base/i18n/time_formatting.h"
#include "base/lazy_instance.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/notreached.h"
#include "base/path_service.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/bookmarks/bookmark_html_writer.h"
#include "chrome/browser/bookmarks/bookmark_model_factory.h"
#include "chrome/browser/bookmarks/url_and_id.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/api/tabs/windows_util.h"
#include "chrome/browser/extensions/bookmarks/bookmarks_error_constants.h"
#include "chrome/browser/extensions/bookmarks/bookmarks_helpers.h"
#include "chrome/browser/extensions/browser_window_util.h"
#include "chrome/browser/extensions/chrome_extension_function_details.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/extensions/open_tab_helper.h"
#include "chrome/browser/importer/external_process_importer_host.h"
#include "chrome/browser/importer/importer_uma.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/renderer_host/chrome_navigation_ui_data.h"
#include "chrome/browser/ui/bookmarks/bookmark_drag_drop.h"
#include "chrome/browser/ui/bookmarks/bookmark_ui_operations_helper.h"
#include "chrome/browser/ui/bookmarks/bookmark_utils_desktop.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/navigator/browser_navigator.h"
#include "chrome/browser/ui/navigator/browser_navigator_params.h"
#include "chrome/browser/ui/select_file_policy/chrome_select_file_policy.h"
#include "chrome/browser/ui/tabs/split_tab_metrics.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/window_sizer/window_sizer.h"
#include "chrome/browser/undo/bookmark_undo_service_factory.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/extensions/api/bookmark_manager_private.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/grit/generated_resources.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node_data.h"
#include "components/bookmarks/browser/bookmark_utils.h"
#include "components/bookmarks/browser/scoped_group_bookmark_actions.h"
#include "components/bookmarks/common/bookmark_metrics.h"
#include "components/bookmarks/common/bookmark_pref_names.h"
#include "components/bookmarks/managed/managed_bookmark_service.h"
#include "components/prefs/pref_service.h"
#include "components/strings/grit/components_strings.h"
#include "components/undo/bookmark_undo_service.h"
#include "components/user_prefs/user_prefs.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui.h"
#include "extensions/browser/extension_function_dispatcher.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/view_type_utils.h"
#include "extensions/common/mojom/view_type.mojom.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom-shared.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/webui/web_ui_util.h"
#include "ui/shell_dialogs/selected_file_info.h"

using bookmarks::BookmarkModel;
using bookmarks::BookmarkNode;
using bookmarks::BookmarkNodeData;
using content::WebContents;

namespace extensions {

namespace bookmark_manager_private = api::bookmark_manager_private;
namespace CanPaste = api::bookmark_manager_private::CanPaste;
namespace IsActiveTabInSplit =
    api::bookmark_manager_private::IsActiveTabInSplit;
namespace Copy = api::bookmark_manager_private::Copy;
namespace Cut = api::bookmark_manager_private::Cut;
namespace Drop = api::bookmark_manager_private::Drop;
namespace GetSubtree = api::bookmark_manager_private::GetSubtree;
namespace Paste = api::bookmark_manager_private::Paste;
namespace RemoveTrees = api::bookmark_manager_private::RemoveTrees;
namespace SortChildren = api::bookmark_manager_private::SortChildren;
namespace StartDrag = api::bookmark_manager_private::StartDrag;
namespace OpenInNewTab = api::bookmark_manager_private::OpenInNewTab;
namespace OpenInNewWindow = api::bookmark_manager_private::OpenInNewWindow;
namespace OpenInNewTabGroup = api::bookmark_manager_private::OpenInNewTabGroup;

namespace {

constexpr char kBookmarkNodesNotFoundFromIdListError[] =
    "Could not find bookmark nodes with given ids: [*]";

constexpr char kInvalidBrowserError[] = "Can't find a valid browser";

constexpr char kDragFailedNoWebContentsError[] =
    "Drag failed: sender WebContents is gone.";
constexpr char kDropFailedNoWebContentsError[] =
    "Drop failed: sender WebContents is gone.";
constexpr char kDropFailedNoDragRouterError[] =
    "Drop failed: surface does not host a bookmark drag router.";
constexpr char kDropFailedNoDragDataError[] =
    "Drop failed: no active bookmark drag data.";

// Returns a single bookmark node from the argument ID.
// This returns nullptr in case of failure.
const BookmarkNode* GetNodeFromString(BookmarkModel* model,
                                      const std::string& id_string) {
  int64_t id;
  if (!base::StringToInt64(id_string, &id)) {
    return nullptr;
  }
  return bookmarks::GetBookmarkNodeByID(model, id);
}

// Gets a vector of bookmark nodes from the argument list of IDs.
// This returns false in the case of failure.
bool GetNodesFromVector(
    BookmarkModel* model,
    const std::vector<std::string>& id_strings,
    std::vector<raw_ptr<const BookmarkNode, VectorExperimental>>* nodes) {
  if (id_strings.empty()) {
    return false;
  }

  for (const auto& id_string : id_strings) {
    const BookmarkNode* node = GetNodeFromString(model, id_string);
    if (!node) {
      return false;
    }
    nodes->push_back(node);
  }

  return true;
}

// Recursively create a bookmark_manager_private::BookmarkNodeDataElement from
// a bookmark node. This is by used |BookmarkNodeDataToJSON| when the data comes
// from the current profile. In this case we have a BookmarkNode since we got
// the data from the current profile.
bookmark_manager_private::BookmarkNodeDataElement
CreateNodeDataElementFromBookmarkNode(const BookmarkNode& node) {
  bookmark_manager_private::BookmarkNodeDataElement element;
  // Add id and parentId so we can associate the data with existing nodes on the
  // client side.
  element.id = base::NumberToString(node.id());
  element.parent_id = base::NumberToString(node.parent()->id());

  if (node.is_url()) {
    element.url = node.url().spec();
  }

  element.title = base::UTF16ToUTF8(node.GetTitle());
  for (const auto& child : node.children()) {
    element.children.push_back(CreateNodeDataElementFromBookmarkNode(*child));
  }

  return element;
}

// Recursively create a bookmark_manager_private::BookmarkNodeDataElement from
// a BookmarkNodeData::Element. This is used by |BookmarkNodeDataToJSON| when
// the data comes from a different profile. When the data comes from a different
// profile we do not have any IDs or parent IDs.
bookmark_manager_private::BookmarkNodeDataElement CreateApiNodeDataElement(
    const BookmarkNodeData::Element& element) {
  bookmark_manager_private::BookmarkNodeDataElement node_element;

  if (element.is_url) {
    node_element.url = element.url.spec();
  }
  node_element.title = base::UTF16ToUTF8(element.title);
  for (const auto& child : element.children) {
    node_element.children.push_back(CreateApiNodeDataElement(child));
  }

  return node_element;
}

// Creates a bookmark_manager_private::BookmarkNodeData from a BookmarkNodeData.
bookmark_manager_private::BookmarkNodeData CreateApiBookmarkNodeData(
    Profile* profile,
    const BookmarkNodeData& data) {
  const base::FilePath& profile_path = profile->GetPath();

  bookmark_manager_private::BookmarkNodeData node_data;
  node_data.same_profile = data.IsFromProfilePath(profile_path);

  if (node_data.same_profile) {
    std::vector<raw_ptr<const BookmarkNode, VectorExperimental>> nodes =
        data.GetNodes(BookmarkModelFactory::GetForBrowserContext(profile),
                      profile_path);
    for (const bookmarks::BookmarkNode* node : nodes) {
      node_data.elements.push_back(
          CreateNodeDataElementFromBookmarkNode(*node));
    }
  } else {
    // We do not have a node IDs when the data comes from a different profile.
    for (size_t i = 0; i < data.size(); ++i) {
      node_data.elements.push_back(CreateApiNodeDataElement(data.elements[i]));
    }
  }
  return node_data;
}

bool HasPermanentNodes(
    const std::vector<raw_ptr<const BookmarkNode, VectorExperimental>>& list) {
  for (const BookmarkNode* node : list) {
    if (node->is_permanent_node()) {
      return true;
    }
  }
  return false;
}

// Generates a default path (including a default filename) that will be
// used for pre-populating the "Export Bookmarks" file chooser dialog box.
base::FilePath GetDefaultFilepathForBookmarkExport() {
  base::Time time = base::Time::Now();

  // Concatenate a date stamp to the filename.
  std::string filename =
      l10n_util::GetStringFUTF8(IDS_EXPORT_BOOKMARKS_DEFAULT_FILENAME,
                                base::TimeFormatShortDateNumeric(time));
  base::FilePath path = base::FilePath::FromUTF8Unsafe(filename);
  base::FilePath::StringType path_str = path.value();
  base::i18n::ReplaceIllegalCharactersInPath(&path_str, '_');
  path = base::FilePath(path_str);

  base::FilePath default_path;
  base::PathService::Get(chrome::DIR_USER_DOCUMENTS, &default_path);
  return default_path.Append(path);
}

}  // namespace

BookmarkManagerPrivateEventRouter::BookmarkManagerPrivateEventRouter(
    content::BrowserContext* browser_context,
    BookmarkModel* bookmark_model)
    : browser_context_(browser_context), bookmark_model_(bookmark_model) {
  bookmark_model_->AddObserver(this);
}

BookmarkManagerPrivateEventRouter::~BookmarkManagerPrivateEventRouter() {
  if (bookmark_model_) {
    bookmark_model_->RemoveObserver(this);
  }
}

void BookmarkManagerPrivateEventRouter::DispatchEvent(
    events::HistogramValue histogram_value,
    const std::string& event_name,
    base::ListValue event_args) {
  EventRouter::Get(browser_context_)
      ->BroadcastEvent(std::make_unique<Event>(histogram_value, event_name,
                                               std::move(event_args)));
}

void BookmarkManagerPrivateEventRouter::BookmarkModelChanged() {}

void BookmarkManagerPrivateEventRouter::BookmarkModelBeingDeleted() {
  // This codepath is unexpected because `this` is owned by a KeyedService that
  // depends on BookmarkModelFactory, which means BookmarkModel must outlive
  // `this`.
  NOTREACHED(base::NotFatalUntil::M138);
  bookmark_model_->RemoveObserver(this);
  bookmark_model_ = nullptr;
}

BookmarkManagerPrivateAPI::BookmarkManagerPrivateAPI(
    content::BrowserContext* browser_context)
    : browser_context_(browser_context) {
}

BookmarkManagerPrivateAPI::~BookmarkManagerPrivateAPI() = default;

void BookmarkManagerPrivateAPI::Shutdown() {
  EventRouter::Get(browser_context_)->UnregisterObserver(this);
}

static base::LazyInstance<
    BrowserContextKeyedAPIFactory<BookmarkManagerPrivateAPI>>::DestructorAtExit
    g_bookmark_manager_private_api_factory = LAZY_INSTANCE_INITIALIZER;

// static
BrowserContextKeyedAPIFactory<BookmarkManagerPrivateAPI>*
BookmarkManagerPrivateAPI::GetFactoryInstance() {
  return g_bookmark_manager_private_api_factory.Pointer();
}

void BookmarkManagerPrivateAPI::OnListenerAdded(
    const EventListenerInfo& details) {
  EventRouter::Get(browser_context_)->UnregisterObserver(this);
  event_router_ = std::make_unique<BookmarkManagerPrivateEventRouter>(
      browser_context_,
      BookmarkModelFactory::GetForBrowserContext(browser_context_));
}

BookmarkManagerPrivateDragEventRouter::BookmarkManagerPrivateDragEventRouter(
    content::WebContents* web_contents)
    : content::WebContentsUserData<BookmarkManagerPrivateDragEventRouter>(
          *web_contents),
      profile_(Profile::FromBrowserContext(web_contents->GetBrowserContext())) {
  // We need to guarantee the BookmarkTabHelper is created.
  BookmarkTabHelper::CreateForWebContents(web_contents);
  BookmarkTabHelper* bookmark_tab_helper =
      BookmarkTabHelper::FromWebContents(web_contents);
  bookmark_tab_helper->set_bookmark_drag_delegate(this);
}

BookmarkManagerPrivateDragEventRouter::
    ~BookmarkManagerPrivateDragEventRouter() {
  // No need to remove ourselves as the BookmarkTabHelper's delegate, since they
  // are both WebContentsUserData and will be deleted at the same time.
}

void BookmarkManagerPrivateDragEventRouter::DispatchEvent(
    events::HistogramValue histogram_value,
    const std::string& event_name,
    base::ListValue args) {
  EventRouter* event_router = EventRouter::Get(profile_);
  if (!event_router) {
    return;
  }

  std::unique_ptr<Event> event(
      new Event(histogram_value, event_name, std::move(args)));
  event_router->BroadcastEvent(std::move(event));
}

void BookmarkManagerPrivateDragEventRouter::OnDragEnter(
    const BookmarkNodeData& data) {
  if (!data.is_valid()) {
    return;
  }
  DispatchEvent(events::BOOKMARK_MANAGER_PRIVATE_ON_DRAG_ENTER,
                bookmark_manager_private::OnDragEnter::kEventName,
                bookmark_manager_private::OnDragEnter::Create(
                    CreateApiBookmarkNodeData(profile_, data)));
}

void BookmarkManagerPrivateDragEventRouter::OnDragOver(
    const BookmarkNodeData& data) {
  // Intentionally empty since these events happens too often and floods the
  // message queue. We do not need this event for the bookmark manager anyway.
}

void BookmarkManagerPrivateDragEventRouter::OnDragLeave(
    const BookmarkNodeData& data) {
  if (!data.is_valid()) {
    return;
  }
  DispatchEvent(events::BOOKMARK_MANAGER_PRIVATE_ON_DRAG_LEAVE,
                bookmark_manager_private::OnDragLeave::kEventName,
                bookmark_manager_private::OnDragLeave::Create(
                    CreateApiBookmarkNodeData(profile_, data)));
}

void BookmarkManagerPrivateDragEventRouter::OnDrop(
    const BookmarkNodeData& data) {
  if (!data.is_valid()) {
    return;
  }
  DispatchEvent(events::BOOKMARK_MANAGER_PRIVATE_ON_DROP,
                bookmark_manager_private::OnDrop::kEventName,
                bookmark_manager_private::OnDrop::Create(
                    CreateApiBookmarkNodeData(profile_, data)));

  // Make a copy that is owned by this instance.
  ClearBookmarkNodeData();
  bookmark_drag_data_ = data;
}

const BookmarkNodeData*
BookmarkManagerPrivateDragEventRouter::GetBookmarkNodeData() {
  if (bookmark_drag_data_.is_valid()) {
    return &bookmark_drag_data_;
  }
  return nullptr;
}

void BookmarkManagerPrivateDragEventRouter::ClearBookmarkNodeData() {
  bookmark_drag_data_.Clear();
}

ExtensionFunction::ResponseValue ClipboardBookmarkManagerFunction::CopyOrCut(
    bool cut,
    const std::vector<std::string>& id_list) {
  BookmarkModel* model = GetBookmarkModel();
  std::vector<raw_ptr<const BookmarkNode, VectorExperimental>> nodes;
  if (!GetNodesFromVector(model, id_list, &nodes)) {
    return Error(kBookmarkNodesNotFoundFromIdListError,
                 base::JoinString(id_list, ", "));
  }

  bookmarks::ManagedBookmarkService* managed = GetManagedBookmarkService();
  if (cut && bookmarks::HasDescendantsOf(nodes, managed->managed_node())) {
    return Error(bookmarks_errors::kModifyManagedError);
  }
  if (cut && HasPermanentNodes(nodes)) {
    return Error(bookmarks_errors::kModifySpecialError);
  }

  if (cut) {
    BookmarkUIOperationsHelperNonMergedSurfaces::CutToClipboard(
        model, nodes, bookmarks::metrics::BookmarkEditSource::kExtension,
        GetProfile()->IsOffTheRecord());
  } else {
    BookmarkUIOperationsHelperNonMergedSurfaces::CopyToClipboard(
        model, nodes, bookmarks::metrics::BookmarkEditSource::kExtension,
        GetProfile()->IsOffTheRecord());
  }
  return NoArguments();
}

ExtensionFunction::ResponseAction
BookmarkManagerPrivateCopyFunction::RunOnReady() {
  std::optional<Copy::Params> params = Copy::Params::Create(args());
  if (!params) {
    return RespondNow(BadMessage());
  }
  return RespondNow(CopyOrCut(false, params->id_list));
}

ExtensionFunction::ResponseAction
BookmarkManagerPrivateCutFunction::RunOnReady() {
  if (!EditBookmarksEnabled()) {
    return RespondNow(Error(bookmarks_errors::kEditBookmarksDisabled));
  }

  std::optional<Cut::Params> params = Cut::Params::Create(args());
  if (!params) {
    return RespondNow(BadMessage());
  }
  return RespondNow(CopyOrCut(true, params->id_list));
}

BookmarkManagerPrivatePasteFunction::BookmarkManagerPrivatePasteFunction() =
    default;

BookmarkManagerPrivatePasteFunction::~BookmarkManagerPrivatePasteFunction() =
    default;

ExtensionFunction::ResponseAction
BookmarkManagerPrivatePasteFunction::RunOnReady() {
  if (!EditBookmarksEnabled()) {
    return RespondNow(Error(bookmarks_errors::kEditBookmarksDisabled));
  }

  std::optional<Paste::Params> params = Paste::Params::Create(args());
  if (!params) {
    return RespondNow(BadMessage());
  }
  BookmarkModel* model =
      BookmarkModelFactory::GetForBrowserContext(GetProfile());
  const BookmarkNode* parent_node = GetNodeFromString(model, params->parent_id);
  std::string error;
  if (!CanBeModified(parent_node, &error)) {
    return RespondNow(Error(error));
  }
  auto helper = std::make_unique<BookmarkUIOperationsHelperNonMergedSurfaces>(
      model, parent_node);
  auto* helper_ptr = helper.get();
  helper_ptr->CanPasteFromClipboard(
      base::BindOnce(&BookmarkManagerPrivatePasteFunction::OnCanPasteFinished,
                     base::RetainedRef(this), std::move(helper),
                     params->selected_id_list, params->parent_id));
  return RespondLater();
}

void BookmarkManagerPrivatePasteFunction::OnCanPasteFinished(
    std::unique_ptr<BookmarkUIOperationsHelperNonMergedSurfaces> helper,
    std::optional<std::vector<std::string>> selected_id_list,
    const std::string& parent_id,
    bool can_paste) {
  if (!can_paste) {
    Respond(Error("Could not paste from clipboard"));
    return;
  }

  BookmarkModel* model =
      BookmarkModelFactory::GetForBrowserContext(GetProfile());
  const BookmarkNode* parent_node = GetNodeFromString(model, parent_id);
  if (!parent_node) {
    Respond(Error(bookmarks_errors::kNoParentError));
    return;
  }

  // We want to use the highest index of the selected nodes as a destination.
  std::vector<raw_ptr<const BookmarkNode, VectorExperimental>> nodes;
  // No need to test return value, if we got an empty list, we insert at end.
  if (selected_id_list) {
    GetNodesFromVector(model, *selected_id_list, &nodes);
  }
  size_t highest_index = 0;
  for (const BookmarkNode* node : nodes) {
    // Skip ids that are not direct children of `parent_node`; a compromised
    // renderer can supply such ids and `GetIndexOf` would return nullopt.
    if (std::optional<size_t> idx = parent_node->GetIndexOf(node)) {
      // + 1 so that we insert after the selection.
      highest_index = std::max(highest_index, *idx + 1);
    }
  }
  if (!highest_index) {
    highest_index = parent_node->children().size();
  }

  auto* helper_ptr = helper.get();
  helper_ptr->PasteFromClipboard(
      highest_index,
      base::BindOnce(&BookmarkManagerPrivatePasteFunction::OnPasteFinished,
                     base::RetainedRef(this), std::move(helper)));
}

void BookmarkManagerPrivatePasteFunction::OnPasteFinished(
    std::unique_ptr<BookmarkUIOperationsHelperNonMergedSurfaces> helper) {
  Respond(NoArguments());
}

ExtensionFunction::ResponseAction
BookmarkManagerPrivateCanPasteFunction::RunOnReady() {
  std::optional<CanPaste::Params> params = CanPaste::Params::Create(args());
  if (!params) {
    return RespondNow(BadMessage());
  }

  PrefService* prefs = user_prefs::UserPrefs::Get(GetProfile());
  if (!prefs->GetBoolean(bookmarks::prefs::kEditBookmarksEnabled)) {
    return RespondNow(WithArguments(false));
  }

  BookmarkModel* model =
      BookmarkModelFactory::GetForBrowserContext(GetProfile());
  const BookmarkNode* parent_node = GetNodeFromString(model, params->parent_id);
  if (!parent_node) {
    return RespondNow(Error(bookmarks_errors::kNoParentError));
  }

  auto helper = std::make_unique<BookmarkUIOperationsHelperNonMergedSurfaces>(
      model, parent_node);
  helper->CanPasteFromClipboard(base::BindOnce(
      &BookmarkManagerPrivateCanPasteFunction::OnCanPasteFinished,
      base::RetainedRef(this)));
  return RespondLater();
}

void BookmarkManagerPrivateCanPasteFunction::OnCanPasteFinished(
    bool can_paste) {
  Respond(WithArguments(can_paste));
}

ExtensionFunction::ResponseAction
BookmarkManagerPrivateIsActiveTabInSplitFunction::RunOnReady() {
  WindowController* window_controller =
      ChromeExtensionFunctionDetails(this).GetCurrentWindowController();
  if (!window_controller) {
    return RespondNow(Error(ExtensionTabUtil::kNoCurrentWindowError));
  }

  BrowserWindowInterface* browser = window_controller->GetBrowser();
  if (!browser) {
    return RespondNow(Error(kInvalidBrowserError));
  }

  const bool is_active_tab_in_split_view =
      browser->GetActiveTabInterface()->IsSplit();
  return RespondNow(WithArguments(is_active_tab_in_split_view));
}

ExtensionFunction::ResponseAction
BookmarkManagerPrivateSortChildrenFunction::RunOnReady() {
  if (!EditBookmarksEnabled()) {
    return RespondNow(Error(bookmarks_errors::kEditBookmarksDisabled));
  }

  std::optional<SortChildren::Params> params =
      SortChildren::Params::Create(args());
  if (!params) {
    return RespondNow(BadMessage());
  }

  BookmarkModel* model =
      BookmarkModelFactory::GetForBrowserContext(GetProfile());
  const BookmarkNode* parent_node = GetNodeFromString(model, params->parent_id);
  std::string error;
  if (!CanBeModified(parent_node, &error)) {
    return RespondNow(Error(error));
  }
  model->SortChildren(parent_node);
  return RespondNow(NoArguments());
}

ExtensionFunction::ResponseAction
BookmarkManagerPrivateStartDragFunction::RunOnReady() {
  if (!EditBookmarksEnabled()) {
    return RespondNow(Error(bookmarks_errors::kEditBookmarksDisabled));
  }

  std::optional<StartDrag::Params> params = StartDrag::Params::Create(args());
  if (!params) {
    return RespondNow(BadMessage());
  }

  content::WebContents* web_contents = GetSenderWebContents();
  // May be null after async BookmarkModelLoaded if the RFH is gone.
  if (!web_contents) {
    return RespondNow(Error(kDragFailedNoWebContentsError));
  }

  BookmarkModel* model =
      BookmarkModelFactory::GetForBrowserContext(GetProfile());
  std::vector<raw_ptr<const BookmarkNode, VectorExperimental>> nodes;
  if (!GetNodesFromVector(model, params->id_list, &nodes)) {
    return RespondNow(Error(kBookmarkNodesNotFoundFromIdListError,
                            base::JoinString(params->id_list, ", ")));
  }

  ui::mojom::DragEventSource source = ui::mojom::DragEventSource::kMouse;
  if (params->is_from_touch) {
    source = ui::mojom::DragEventSource::kTouch;
  }

  chrome::DragBookmarks(
      GetProfile(), {std::move(nodes), params->drag_node_index, web_contents,
                     source, gfx::Point(params->x, params->y)});

  return RespondNow(NoArguments());
}

ExtensionFunction::ResponseAction
BookmarkManagerPrivateDropFunction::RunOnReady() {
  if (!EditBookmarksEnabled()) {
    return RespondNow(Error(bookmarks_errors::kEditBookmarksDisabled));
  }

  std::optional<Drop::Params> params = Drop::Params::Create(args());
  if (!params) {
    return RespondNow(BadMessage());
  }

  BookmarkModel* model =
      BookmarkModelFactory::GetForBrowserContext(GetProfile());

  const BookmarkNode* drop_parent = GetNodeFromString(model, params->parent_id);
  std::string error;
  if (!CanBeModified(drop_parent, &error)) {
    return RespondNow(Error(error));
  }

  content::WebContents* web_contents = GetSenderWebContents();
  // May be null after async BookmarkModelLoaded if the RFH is gone.
  if (!web_contents) {
    return RespondNow(Error(kDropFailedNoWebContentsError));
  }
  size_t drop_index;
  if (params->index) {
    drop_index = static_cast<size_t>(*params->index);
    CHECK(drop_index >= 0 && drop_index <= drop_parent->children().size());
  } else {
    drop_index = drop_parent->children().size();
  }

  BookmarkManagerPrivateDragEventRouter* router =
      BookmarkManagerPrivateDragEventRouter::FromWebContents(web_contents);
  // Surfaces that do not attach a bookmark drag router can still reach
  // this sink; treat as a normal error rather than crashing.
  if (!router) {
    return RespondNow(Error(kDropFailedNoDragRouterError));
  }

  const BookmarkNodeData* drag_data = router->GetBookmarkNodeData();
  // Null if a prior Drop call already consumed and cleared the data, or
  // if the renderer invokes drop without an active drag.
  if (!drag_data) {
    return RespondNow(Error(kDropFailedNoDragDataError));
  }
  const bool copy = false;
  BookmarkUIOperationsHelperNonMergedSurfaces(model, drop_parent)
      .DropBookmarks(GetProfile(), *drag_data, drop_index, copy,
                     chrome::BookmarkReorderDropTarget::kBookmarkManagerAPI);

  router->ClearBookmarkNodeData();
  return RespondNow(NoArguments());
}

ExtensionFunction::ResponseAction
BookmarkManagerPrivateGetSubtreeFunction::RunOnReady() {
  std::optional<GetSubtree::Params> params = GetSubtree::Params::Create(args());
  if (!params) {
    return RespondNow(BadMessage());
  }

  const BookmarkNode* node = nullptr;

  if (params->id.empty()) {
    BookmarkModel* model =
        BookmarkModelFactory::GetForBrowserContext(GetProfile());
    node = model->root_node();
  } else {
    std::string error;
    node = GetBookmarkNodeFromId(params->id, &error);
    if (!node) {
      return RespondNow(Error(error));
    }
  }

  std::vector<api::bookmarks::BookmarkTreeNode> nodes;
  BookmarkModel* model =
      BookmarkModelFactory::GetForBrowserContext(GetProfile());
  bookmarks::ManagedBookmarkService* managed = GetManagedBookmarkService();
  if (params->folders_only) {
    bookmarks_helpers::AddNodeFoldersOnly(model, managed, node, &nodes, true);
  } else {
    bookmarks_helpers::AddNode(model, managed, node, &nodes, true);
  }
  return RespondNow(ArgumentList(GetSubtree::Results::Create(nodes)));
}

ExtensionFunction::ResponseAction
BookmarkManagerPrivateRemoveTreesFunction::RunOnReady() {
  if (!EditBookmarksEnabled()) {
    return RespondNow(Error(bookmarks_errors::kEditBookmarksDisabled));
  }

  std::optional<RemoveTrees::Params> params =
      RemoveTrees::Params::Create(args());
  if (!params) {
    return RespondNow(BadMessage());
  }

  BookmarkModel* model = GetBookmarkModel();
  bookmarks::ManagedBookmarkService* managed = GetManagedBookmarkService();
  bookmarks::ScopedGroupBookmarkActions group_deletes(model);
  int64_t id;
  std::string error;
  for (const std::string& id_string : params->id_list) {
    if (!base::StringToInt64(id_string, &id)) {
      return RespondNow(Error(bookmarks_errors::kInvalidIdError));
    }
    if (!bookmarks_helpers::RemoveNode(model, managed, id, true, &error)) {
      return RespondNow(Error(error));
    }
  }

  return RespondNow(NoArguments());
}

ExtensionFunction::ResponseAction
BookmarkManagerPrivateUndoFunction::RunOnReady() {
  if (!EditBookmarksEnabled()) {
    return RespondNow(Error(bookmarks_errors::kEditBookmarksDisabled));
  }

  BookmarkUndoServiceFactory::GetForProfile(GetProfile())->undo_manager()->
      Undo();
  return RespondNow(NoArguments());
}

ExtensionFunction::ResponseAction
BookmarkManagerPrivateRedoFunction::RunOnReady() {
  if (!EditBookmarksEnabled()) {
    return RespondNow(Error(bookmarks_errors::kEditBookmarksDisabled));
  }

  BookmarkUndoServiceFactory::GetForProfile(GetProfile())->undo_manager()->
      Redo();
  return RespondNow(NoArguments());
}

ExtensionFunction::ResponseAction
BookmarkManagerPrivateOpenInNewTabFunction::RunOnReady() {
  std::optional<OpenInNewTab::Params> params =
      OpenInNewTab::Params::Create(args());
  if (!params) {
    return RespondNow(BadMessage());
  }

  std::string error;
  const BookmarkNode* node = GetBookmarkNodeFromId(params->id, &error);
  if (!node) {
    return RespondNow(Error(error));
  }
  if (!node->is_url()) {
    return RespondNow(Error("Cannot open a folder in a new tab."));
  }

  OpenTabHelper::Params options;
  if (params->params) {
    options.active = params->params->active;
  }
  options.bookmark_id = node->id();

  base::expected<GURL, std::string> maybe_url =
      ExtensionTabUtil::PrepareURLForNavigation(node->url().spec(), extension(),
                                                browser_context());
  if (!maybe_url.has_value()) {
    return RespondNow(Error(maybe_url.error()));
  }
  GURL validated_url = std::move(maybe_url.value());

  base::expected<BrowserWindowInterface*, std::string> maybe_browser =
      OpenTabHelper::FindOrCreateBrowser(validated_url, *this,
                                         /*create_if_needed=*/false);
  if (!maybe_browser.has_value()) {
    return RespondNow(Error(std::move(maybe_browser.error())));
  }

  base::expected<content::WebContents*, std::string> result =
      OpenTabHelper::OpenTab(validated_url, *maybe_browser.value(), *this,
                             options);
  if (!result.has_value()) {
    return RespondNow(Error(result.error()));
  }

  content::WebContents* new_contents = result.value();

  if (params->params && params->params->split) {
    BrowserWindowInterface* browser =
        browser_window_util::GetBrowserForTabContents(*new_contents);
    if (browser) {
      TabStripModel* tab_strip = browser->GetTabStripModel();
      const int new_tab_index = tab_strip->GetIndexOfWebContents(new_contents);
      // Handle the situation where the bookmark is opened in a different window
      // (happens when opening certain internal pages in incognito mode)
      if (new_tab_index == TabStripModel::kNoTab ||
          new_tab_index == tab_strip->active_index()) {
        return RespondNow(NoArguments());
      }
      tab_strip->AddToNewSplit(
          {new_tab_index}, split_tabs::SplitTabVisualData(),
          split_tabs::SplitTabCreatedSource::kExtensionsApi);
    }
  }

  return RespondNow(NoArguments());
}

ExtensionFunction::ResponseAction
BookmarkManagerPrivateOpenInNewWindowFunction::RunOnReady() {
  std::optional<OpenInNewWindow::Params> params =
      OpenInNewWindow::Params::Create(args());
  if (!params) {
    return RespondNow(BadMessage());
  }

  Profile* calling_profile = Profile::FromBrowserContext(browser_context());

  BookmarkModel* model =
      BookmarkModelFactory::GetForBrowserContext(calling_profile);
  std::vector<raw_ptr<const BookmarkNode, VectorExperimental>> nodes;
  if (!GetNodesFromVector(model, params->id_list, &nodes)) {
    return RespondNow(Error(kBookmarkNodesNotFoundFromIdListError,
                            base::JoinString(params->id_list, ", ")));
  }

  // Validate (and possibly rewrite) every bookmark URL through the
  // extension-navigation deny-list, mirroring the sibling OpenInNewTab path, so
  // a compromised bookmarks WebUI renderer can't open a denied scheme (e.g.
  // devtools://, javascript:) as a browser-initiated top-level window.
  std::vector<UrlAndId> url_and_ids;
  url_and_ids.reserve(nodes.size());
  for (const bookmarks::BookmarkNode* node : nodes) {
    if (!node->is_url()) {
      return RespondNow(Error("Cannot open a folder in a new window."));
    }
    base::expected<GURL, std::string> maybe_url =
        ExtensionTabUtil::PrepareURLForNavigation(
            node->url().spec(), extension(), browser_context());
    if (!maybe_url.has_value()) {
      return RespondNow(Error(std::move(maybe_url.error())));
    }
    UrlAndId url_and_id;
    url_and_id.url = std::move(maybe_url.value());
    url_and_id.id = node->id();
    url_and_ids.push_back(std::move(url_and_id));
  }

  std::vector<GURL> urls;
  urls.reserve(url_and_ids.size());
  for (const UrlAndId& url_and_id : url_and_ids) {
    urls.push_back(url_and_id.url);
  }

  std::string error;
  windows_util::IncognitoResult incognito_result =
      windows_util::ShouldOpenIncognitoWindow(calling_profile,
                                              params->incognito, &urls, &error);
  if (incognito_result == windows_util::IncognitoResult::kError) {
    return RespondNow(Error(std::move(error)));
  }

  // Drop entries whose URL was filtered out by the incognito check above.
  std::erase_if(url_and_ids, [&urls](const UrlAndId& url_and_id) {
    return !std::ranges::contains(urls, url_and_id.url);
  });
  DCHECK_EQ(urls.size(), url_and_ids.size());

  DCHECK(!calling_profile->IsOffTheRecord());
  Profile* window_profile =
      incognito_result == windows_util::IncognitoResult::kIncognito
          ? calling_profile->GetPrimaryOTRProfile(/*create_if_needed=*/true)
          : calling_profile;

  bool first_tab = true;
  for (auto& url_and_id : url_and_ids) {
    NavigateParams navigate_params(window_profile, url_and_id.url,
                                   ui::PAGE_TRANSITION_LINK);
    navigate_params.window_action = NavigateParams::WindowAction::kShowWindow;
    navigate_params.disposition =
        first_tab ? WindowOpenDisposition::NEW_WINDOW
                  : WindowOpenDisposition::NEW_FOREGROUND_TAB;
    if (params->incognito) {
      navigate_params.disposition = WindowOpenDisposition::OFF_THE_RECORD;
    }
    base::WeakPtr<content::NavigationHandle> handle =
        Navigate(&navigate_params);
    if (handle) {
      ChromeNavigationUIData* ui_data =
          static_cast<ChromeNavigationUIData*>(handle->GetNavigationUIData());
      ui_data->set_bookmark_id(url_and_id.id);
    }

    first_tab = false;
  }

  return RespondNow(NoArguments());
}

ExtensionFunction::ResponseAction
BookmarkManagerPrivateOpenInNewTabGroupFunction::RunOnReady() {
  std::optional<OpenInNewTabGroup::Params> params =
      OpenInNewTabGroup::Params::Create(args());
  if (!params) {
    return RespondNow(BadMessage());
  }

  WindowController* window_controller =
      ChromeExtensionFunctionDetails(this).GetCurrentWindowController();
  if (!window_controller) {
    return RespondNow(Error(ExtensionTabUtil::kNoCurrentWindowError));
  }

  BrowserWindowInterface* browser = window_controller->GetBrowser();
  if (!browser) {
    return RespondNow(Error(kInvalidBrowserError));
  }

  BookmarkModel* model =
      BookmarkModelFactory::GetForBrowserContext(browser->GetProfile());
  std::vector<raw_ptr<const BookmarkNode, VectorExperimental>> nodes;
  if (!GetNodesFromVector(model, params->id_list, &nodes)) {
    return RespondNow(Error(kBookmarkNodesNotFoundFromIdListError,
                            base::JoinString(params->id_list, ", ")));
  }

  // OpenAllIfAllowed opens each URL node plus the immediate URL children of any
  // folder (GetURLsToOpen recurses one level). Run each through the
  // extension-navigation deny-list first, like the sibling OpenInNewTab path,
  // so a compromised renderer can't open a denied scheme (e.g. devtools://).
  auto check_url = [&](const GURL& url) -> std::optional<std::string> {
    base::expected<GURL, std::string> maybe_url =
        ExtensionTabUtil::PrepareURLForNavigation(url.spec(), extension(),
                                                  browser_context());
    if (!maybe_url.has_value()) {
      return std::move(maybe_url.error());
    }
    return std::nullopt;
  };
  for (const bookmarks::BookmarkNode* node : nodes) {
    if (node->is_url()) {
      if (auto error = check_url(node->url())) {
        return RespondNow(Error(std::move(*error)));
      }
    } else {
      for (const auto& child : node->children()) {
        if (child->is_url()) {
          if (auto error = check_url(child->url())) {
            return RespondNow(Error(std::move(*error)));
          }
        }
      }
    }
  }

  bookmarks::OpenAllIfAllowed(browser, nodes,
                              WindowOpenDisposition::NEW_BACKGROUND_TAB,
                              bookmarks::OpenAllBookmarksContext::kInGroup);

  return RespondNow(NoArguments());
}

BookmarkManagerPrivateIOFunction::BookmarkManagerPrivateIOFunction() = default;

BookmarkManagerPrivateIOFunction::~BookmarkManagerPrivateIOFunction() {
  // There may be pending file dialogs, we need to tell them that we've gone
  // away so they don't try and call back to us.
  if (select_file_dialog_.get()) {
    select_file_dialog_->ListenerDestroyed();
  }
}

void BookmarkManagerPrivateIOFunction::FileSelectionCanceled() {
  CleanupFileDialog();
}

void BookmarkManagerPrivateIOFunction::ShowSelectFileDialog(
    ui::SelectFileDialog::Type type,
    const base::FilePath& default_path) {
  if (!dispatcher()) {
    return;  // Extension was unloaded.
  }

  // Early return if the select file dialog is already active.
  if (select_file_dialog_) {
    return;
  }

  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // Balanced in one of the callbacks of SelectFileDialog:
  // either FileSelectionCanceled, or FileSelected
  AddRef();

  WebContents* web_contents = GetSenderWebContents();

  select_file_dialog_ = ui::SelectFileDialog::Create(
      this, std::make_unique<ChromeSelectFilePolicy>(web_contents));
  ui::SelectFileDialog::FileTypeInfo file_type_info;
  file_type_info.extensions.resize(1);
  file_type_info.extensions[0].push_back(FILE_PATH_LITERAL("html"));
  gfx::NativeWindow owning_window =
      web_contents ? platform_util::GetTopLevel(web_contents->GetNativeView())
                   : gfx::NativeWindow();
  // |web_contents| can be nullptr (for background pages), which is fine. In
  // such a case if file-selection dialogs are forbidden by policy, we will not
  // show an InfoBar, which is better than letting one appear out of the blue.
  select_file_dialog_->SelectFile(type, std::u16string(), default_path,
                                  &file_type_info, 0,
                                  base::FilePath::StringType(), owning_window);
}

void BookmarkManagerPrivateIOFunction::CleanupFileDialog() {
  if (select_file_dialog_) {
    select_file_dialog_->ListenerDestroyed();
  }
  select_file_dialog_.reset();
  Release();  // Balanced in ShowSelectFileDialog().
}

ExtensionFunction::ResponseAction
BookmarkManagerPrivateImportFunction::RunOnReady() {
  if (!EditBookmarksEnabled()) {
    return RespondNow(Error(bookmarks_errors::kEditBookmarksDisabled));
  }
  ShowSelectFileDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
                       base::FilePath());
  // TODO(crbug.com/40127463): This will respond before a file is selected,
  // which seems incorrect. Waiting and responding until after
  // ui::SelectFileDialog::Listener is fired should be right thing to do, but
  // that requires auditing bookmark page callsites.
  return RespondNow(NoArguments());
}

void BookmarkManagerPrivateImportFunction::FileSelected(
    const ui::SelectedFileInfo& file,
    int index) {
  // Deletes itself.
  ExternalProcessImporterHost* importer_host = new ExternalProcessImporterHost;
  user_data_importer::SourceProfile source_profile;
  source_profile.importer_type = user_data_importer::TYPE_BOOKMARKS_FILE;
  source_profile.source_path = file.path();
  importer_host->StartImportSettings(source_profile, GetProfile(),
                                     user_data_importer::FAVORITES,
                                     new ProfileWriter(GetProfile()));

  importer::LogImporterUseToMetrics("BookmarksAPI",
                                    user_data_importer::TYPE_BOOKMARKS_FILE);
  CleanupFileDialog();
}

ExtensionFunction::ResponseAction
BookmarkManagerPrivateExportFunction::RunOnReady() {
  // "bookmarks.export" is exposed to a small number of extensions. These
  // extensions use user gesture for export, so use USER_VISIBLE priority.
  // GetDefaultFilepathForBookmarkExport() might have to touch filesystem
  // (stat or access, for example), so this requires IO.
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE,
      {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
       base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
      base::BindOnce(&GetDefaultFilepathForBookmarkExport),
      base::BindOnce(&BookmarkManagerPrivateIOFunction::ShowSelectFileDialog,
                     this, ui::SelectFileDialog::SELECT_SAVEAS_FILE));
  // TODO(crbug.com/40127463): This will respond before a file is selected,
  // which seems incorrect. Waiting and responding until after
  // ui::SelectFileDialog::Listener is fired should be right thing to do, but
  // that requires auditing bookmark page callsites.
  return RespondNow(NoArguments());
}

void BookmarkManagerPrivateExportFunction::FileSelected(
    const ui::SelectedFileInfo& file,
    int index) {
  bookmark_html_writer::WriteBookmarks(GetProfile(), file.path(),
                                       base::DoNothing());
  CleanupFileDialog();
}

WEB_CONTENTS_USER_DATA_KEY_IMPL(BookmarkManagerPrivateDragEventRouter);

}  // namespace extensions
