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

#include "components/renderer_context_menu/render_view_context_menu_base.h"

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

#include "base/command_line.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/observer_list.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "components/vector_icons/vector_icons.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.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/common/buildflags.h"
#include "third_party/blink/public/mojom/context_menu/context_menu.mojom.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/models/image_model.h"
#include "ui/base/ui_base_features.h"
#include "url/origin.h"

using content::BrowserContext;
using content::GlobalRenderFrameHostId;
using content::OpenURLParams;
using content::RenderFrameHost;
using content::RenderViewHost;
using content::WebContents;

namespace {

// The (inclusive) range of command IDs reserved for content's custom menus.
int content_context_custom_first = -1;
int content_context_custom_last = -1;

bool IsCustomItemEnabledInternal(
    const std::vector<blink::mojom::CustomContextMenuItemPtr>& items,
    int id) {
  DCHECK(RenderViewContextMenuBase::IsContentCustomCommandId(id));
  for (const auto& item : items) {
    int action_id = RenderViewContextMenuBase::ConvertToContentCustomCommandId(
        item->action);
    if (action_id == id)
      return item->enabled;
    if (item->type == blink::mojom::CustomContextMenuItemType::kSubMenu) {
      if (IsCustomItemEnabledInternal(item->submenu, id))
        return true;
    }
  }
  return false;
}

bool IsCustomItemCheckedInternal(
    const std::vector<blink::mojom::CustomContextMenuItemPtr>& items,
    int id) {
  DCHECK(RenderViewContextMenuBase::IsContentCustomCommandId(id));
  for (const auto& item : items) {
    int action_id = RenderViewContextMenuBase::ConvertToContentCustomCommandId(
        item->action);
    if (action_id == id)
      return item->checked;
    if (item->type == blink::mojom::CustomContextMenuItemType::kSubMenu) {
      if (IsCustomItemCheckedInternal(item->submenu, id))
        return true;
    }
  }
  return false;
}

const size_t kMaxCustomMenuDepth = 5;
const size_t kMaxCustomMenuTotalItems = 1000;

}  // namespace

void RenderViewContextMenuBase::AddCustomItemsToMenu(
    const std::vector<blink::mojom::CustomContextMenuItemPtr>& items,
    size_t depth,
    size_t* total_items,
    std::vector<std::unique_ptr<ui::SimpleMenuModel>>* submenus,
    ui::SimpleMenuModel::Delegate* delegate,
    ui::SimpleMenuModel* menu_model) {
  if (depth > kMaxCustomMenuDepth) {
    LOG(ERROR) << "Custom menu too deeply nested.";
    return;
  }
  for (const auto& item : items) {
    int command_id = RenderViewContextMenuBase::ConvertToContentCustomCommandId(
        item->action);
    if (!RenderViewContextMenuBase::IsContentCustomCommandId(command_id)) {
      LOG(ERROR) << "Custom menu action value out of range.";
      return;
    }
    if (*total_items >= kMaxCustomMenuTotalItems) {
      LOG(ERROR) << "Custom menu too large (too many items).";
      return;
    }
    (*total_items)++;
    switch (item->type) {
      case blink::mojom::CustomContextMenuItemType::kOption:
        menu_model->AddItem(
            RenderViewContextMenuBase::ConvertToContentCustomCommandId(
                item->action),
            item->label);
        if (item->is_experimental_feature) {
          menu_model->SetMinorIcon(menu_model->GetItemCount() - 1,
                                   ui::ImageModel::FromVectorIcon(
                                       features::IsRoundedIconsEnabled()
                                           ? vector_icons::kScienceIcon
                                           : vector_icons::kScienceOldIcon));
        }
        if (!item->feature_name.empty()) {
          menu_model->SetIsNewFeatureAt(
              menu_model->GetItemCount() - 1,
              GetIsNewFeatureAtValue(base::UTF16ToUTF8(item->feature_name)));
        }
        if (item->accelerator) {
          menu_model->SetAcceleratorAt(
              menu_model->GetItemCount() - 1,
              ui::Accelerator(
                  static_cast<ui::KeyboardCode>(item->accelerator->key_code),
                  item->accelerator->modifiers));
          if (item->force_show_accelerator_for_item) {
            menu_model->SetForceShowAcceleratorForItemAt(
                menu_model->GetItemCount() - 1, true);
          }
        }
        break;
      case blink::mojom::CustomContextMenuItemType::kCheckableOption: {
        menu_model->AddCheckItem(
            RenderViewContextMenuBase::ConvertToContentCustomCommandId(
                item->action),
            item->label);
        if (item->is_experimental_feature) {
          menu_model->SetMinorIcon(menu_model->GetItemCount() - 1,
                                   ui::ImageModel::FromVectorIcon(
                                       features::IsRoundedIconsEnabled()
                                           ? vector_icons::kScienceIcon
                                           : vector_icons::kScienceOldIcon));
        }
        break;
      }
      case blink::mojom::CustomContextMenuItemType::kGroup:
        // TODO(viettrungluu): I don't know what this is supposed to do.
        NOTREACHED();
      case blink::mojom::CustomContextMenuItemType::kSeparator:
        menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
        break;
      case blink::mojom::CustomContextMenuItemType::kSubMenu: {
        ui::SimpleMenuModel* submenu = new ui::SimpleMenuModel(delegate);
        submenus->push_back(base::WrapUnique(submenu));
        AddCustomItemsToMenu(item->submenu, depth + 1, total_items, submenus,
                             delegate, submenu);
        menu_model->AddSubMenu(
            RenderViewContextMenuBase::ConvertToContentCustomCommandId(
                item->action),
            item->label, submenu);
        if (!item->feature_name.empty()) {
          menu_model->SetIsNewFeatureAt(
              menu_model->GetItemCount() - 1,
              GetIsNewFeatureAtValue(base::UTF16ToUTF8(item->feature_name)));
        }
        break;
      }
      default:
        NOTREACHED();
    }
  }
}

// static
void RenderViewContextMenuBase::SetContentCustomCommandIdRange(
    int first, int last) {
  // The range is inclusive.
  content_context_custom_first = first;
  content_context_custom_last = last;
}

// static
const size_t RenderViewContextMenuBase::kMaxSelectionTextLength = 50;

// static
int RenderViewContextMenuBase::ConvertToContentCustomCommandId(int id) {
  return content_context_custom_first + id;
}

// static
bool RenderViewContextMenuBase::IsContentCustomCommandId(int id) {
  return id >= content_context_custom_first &&
         id <= content_context_custom_last;
}

RenderViewContextMenuBase::RenderViewContextMenuBase(
    content::RenderFrameHost& render_frame_host,
    const content::ContextMenuParams& params)
    : params_(params),
      source_web_contents_(
          WebContents::FromRenderFrameHost(&render_frame_host)),
      browser_context_(source_web_contents_->GetBrowserContext()),
      menu_model_(this),
      render_frame_id_(render_frame_host.GetRoutingID()),
      render_frame_token_(render_frame_host.GetFrameToken()),
      render_process_id_(render_frame_host.GetProcess()->GetDeprecatedID()),
      site_instance_(render_frame_host.GetSiteInstance()),
      command_executed_(false) {}

RenderViewContextMenuBase::~RenderViewContextMenuBase() {
}

// Menu construction functions -------------------------------------------------

void RenderViewContextMenuBase::Init() {
  // Command id range must have been already initializerd.
  DCHECK_NE(-1, content_context_custom_first);
  DCHECK_NE(-1, content_context_custom_last);

  InitMenu();
  if (toolkit_delegate_)
    toolkit_delegate_->Init(&menu_model_);
}

void RenderViewContextMenuBase::Cancel() {
  if (toolkit_delegate_)
    toolkit_delegate_->Cancel();
}

void RenderViewContextMenuBase::InitMenu() {
  if (content_type_->SupportsGroup(ContextMenuContentType::ITEM_GROUP_CUSTOM)) {
    AppendCustomItems();

    const bool has_selection = !params_.selection_text.empty();
    if (has_selection) {
      // We will add more items if there's a selection, so add a separator.
      // TODO(lazyboy): Clean up separator logic.
      menu_model_.AddSeparator(ui::NORMAL_SEPARATOR);
    }
  }
}

void RenderViewContextMenuBase::AddMenuItem(int command_id,
                                            const std::u16string& title) {
  menu_model_.AddItem(command_id, title);
}

void RenderViewContextMenuBase::AddMenuItemWithIcon(
    int command_id,
    const std::u16string& title,
    const ui::ImageModel& icon) {
  menu_model_.AddItemWithIcon(command_id, title, icon);
}

void RenderViewContextMenuBase::AddCheckItem(int command_id,
                                             const std::u16string& title) {
  menu_model_.AddCheckItem(command_id, title);
}

void RenderViewContextMenuBase::AddSeparator() {
  menu_model_.AddSeparator(ui::NORMAL_SEPARATOR);
}

void RenderViewContextMenuBase::AddSubMenu(int command_id,
                                           const std::u16string& label,
                                           ui::MenuModel* model) {
  menu_model_.AddSubMenu(command_id, label, model);
}

void RenderViewContextMenuBase::AddSubMenuWithStringIdAndIcon(
    int command_id,
    int message_id,
    ui::MenuModel* model,
    const ui::ImageModel& icon) {
  menu_model_.AddSubMenuWithStringIdAndIcon(command_id, message_id, model,
                                            icon);
}

ui::IsNewFeatureAtValue RenderViewContextMenuBase::GetIsNewFeatureAtValue(
    const std::string& feature_name) const {
  return ui::IsNewFeatureAtValue();
}

void RenderViewContextMenuBase::UpdateMenuItem(int command_id,
                                               bool enabled,
                                               bool hidden,
                                               const std::u16string& label) {
  std::optional<size_t> index = menu_model_.GetIndexOfCommandId(command_id);
  if (!index.has_value())
    return;

  menu_model_.SetLabel(index.value(), label);
  menu_model_.SetEnabledAt(index.value(), enabled);
  menu_model_.SetVisibleAt(index.value(), !hidden);
  if (toolkit_delegate_) {
#if BUILDFLAG(IS_MAC)
    toolkit_delegate_->UpdateMenuItem(command_id, enabled, hidden, label);
#else
    toolkit_delegate_->RebuildMenu();
#endif
  }
}

void RenderViewContextMenuBase::UpdateMenuIcon(int command_id,
                                               const ui::ImageModel& icon) {
  std::optional<size_t> index = menu_model_.GetIndexOfCommandId(command_id);
  if (!index.has_value())
    return;

  menu_model_.SetIcon(index.value(), icon);
#if BUILDFLAG(IS_CHROMEOS)
  if (toolkit_delegate_)
    toolkit_delegate_->RebuildMenu();
#endif
}

void RenderViewContextMenuBase::RemoveMenuItem(int command_id) {
  std::optional<size_t> index = menu_model_.GetIndexOfCommandId(command_id);
  if (!index.has_value())
    return;

  menu_model_.RemoveItemAt(index.value());
  if (toolkit_delegate_)
    toolkit_delegate_->RebuildMenu();
}

// Removes separators so that if there are two separators next to each other,
// only one of them remains.
void RenderViewContextMenuBase::RemoveAdjacentSeparators() {
  size_t num_items = menu_model_.GetItemCount();
  for (size_t index = num_items; index > 1; --index) {
    ui::MenuModel::ItemType curr_type = menu_model_.GetTypeAt(index - 1);
    ui::MenuModel::ItemType prev_type = menu_model_.GetTypeAt(index - 2);

    if (curr_type == ui::MenuModel::ItemType::TYPE_SEPARATOR &&
        prev_type == ui::MenuModel::ItemType::TYPE_SEPARATOR) {
      // We found adjacent separators, remove the one at the bottom.
      menu_model_.RemoveItemAt(index - 1);
    }
  }

  if (toolkit_delegate_)
    toolkit_delegate_->RebuildMenu();
}

void RenderViewContextMenuBase::RemoveSeparatorBeforeMenuItem(int command_id) {
  std::optional<size_t> index = menu_model_.GetIndexOfCommandId(command_id);
  // Ignore if command not found or if it's the first menu item.
  if (!index.has_value() || index == size_t{0})
    return;

  ui::MenuModel::ItemType prev_type = menu_model_.GetTypeAt(index.value() - 1);
  if (prev_type != ui::MenuModel::ItemType::TYPE_SEPARATOR)
    return;

  menu_model_.RemoveItemAt(index.value() - 1);

  if (toolkit_delegate_)
    toolkit_delegate_->RebuildMenu();
}

WebContents* RenderViewContextMenuBase::GetWebContents() const {
  return source_web_contents_;
}

BrowserContext* RenderViewContextMenuBase::GetBrowserContext() const {
  return browser_context_;
}

bool RenderViewContextMenuBase::AppendCustomItems() {
  size_t total_items = 0;
  AddCustomItemsToMenu(params_.custom_items, 0, &total_items, &custom_submenus_,
                       this, &menu_model_);
  return total_items > 0;
}

bool RenderViewContextMenuBase::IsCommandIdKnown(
    int id,
    bool* enabled) const {
  // If this command is added by one of our observers, we dispatch
  // it to the observer.
  for (auto& observer : observers_) {
    if (observer.IsCommandIdSupported(id)) {
      *enabled = observer.IsCommandIdEnabled(id);
      return true;
    }
  }

  // Custom items.
  if (IsContentCustomCommandId(id)) {
    *enabled = IsCustomItemEnabled(id);
    return true;
  }

  return false;
}

// Menu delegate functions -----------------------------------------------------

bool RenderViewContextMenuBase::IsCommandIdChecked(int id) const {
  // If this command is is added by one of our observers, we dispatch it to the
  // observer.
  for (auto& observer : observers_) {
    if (observer.IsCommandIdSupported(id))
      return observer.IsCommandIdChecked(id);
  }

  // Custom items.
  if (IsContentCustomCommandId(id))
    return IsCustomItemChecked(id);

  return false;
}

void RenderViewContextMenuBase::ExecuteCommand(int id, int event_flags) {
  command_executed_ = true;
  RecordUsedItem(id);

  // Notify all observers the command to be executed.
  for (auto& observer : observers_)
    observer.CommandWillBeExecuted(id);

  // If this command is is added by one of our observers, we dispatch
  // it to the observer.
  for (auto& observer : observers_) {
    if (observer.IsCommandIdSupported(id))
      return observer.ExecuteCommand(id);
  }

  // Process custom actions range.
  if (IsContentCustomCommandId(id)) {
    unsigned action = id - content_context_custom_first;
    const GURL& link_followed = params_.link_followed;
#if BUILDFLAG(ENABLE_PLUGINS)
    HandleAuthorizeAllPlugins();
#endif
    source_web_contents_->ExecuteCustomContextMenuCommand(action,
                                                          link_followed);
    return;
  }
  command_executed_ = false;
}

void RenderViewContextMenuBase::OnMenuWillShow(ui::SimpleMenuModel* source) {
  for (size_t i = 0; i < source->GetItemCount(); ++i) {
    if (source->IsVisibleAt(i) &&
        source->GetTypeAt(i) != ui::MenuModel::TYPE_SEPARATOR) {
      RecordShownItem(source->GetCommandIdAt(i),
                      source->GetTypeAt(i) == ui::MenuModel::TYPE_SUBMENU);
    }
  }

  // Ignore notifications from submenus.
  if (source != &menu_model_)
    return;

  source_web_contents_->SetShowingContextMenu(true);

  NotifyMenuShown();
}

void RenderViewContextMenuBase::MenuClosed(ui::SimpleMenuModel* source) {
  // Ignore notifications from submenus.
  if (source != &menu_model_)
    return;

  source_web_contents_->SetShowingContextMenu(false);
  source_web_contents_->NotifyContextMenuClosed(params_.link_followed);
  for (auto& observer : observers_) {
    observer.OnMenuClosed();
  }
}

RenderFrameHost* RenderViewContextMenuBase::GetRenderFrameHost() const {
  return RenderFrameHost::FromID(render_process_id_, render_frame_id_);
}

// Controller functions --------------------------------------------------------

void RenderViewContextMenuBase::OpenURL(const GURL& url,
                                        const GURL& referring_url,
                                        const url::Origin& initiator,
                                        WindowOpenDisposition disposition,
                                        ui::PageTransition transition) {
  OpenURLWithExtraHeaders(url, referring_url, initiator, disposition,
                          transition, "" /* extra_headers */,
                          true /* started_from_context_menu */);
}

void RenderViewContextMenuBase::OpenURLWithExtraHeaders(
    const GURL& url,
    const GURL& referring_url,
    const url::Origin& initiator,
    WindowOpenDisposition disposition,
    ui::PageTransition transition,
    const std::string& extra_headers,
    bool started_from_context_menu) {
  content::OpenURLParams open_url_params = GetOpenURLParamsWithExtraHeaders(
      url, referring_url, initiator, disposition, transition, extra_headers,
      started_from_context_menu);

  source_web_contents_->OpenURL(open_url_params,
                                /*navigation_handle_callback=*/{});
}

content::OpenURLParams
RenderViewContextMenuBase::GetOpenURLParamsWithExtraHeaders(
    const GURL& url,
    const GURL& referring_url,
    const url::Origin& initiator,
    WindowOpenDisposition disposition,
    ui::PageTransition transition,
    const std::string& extra_headers,
    bool started_from_context_menu) {
  // Do not send the referrer url to OTR windows. We still need the
  // |referring_url| to populate the |initiator_origin| below for browser UI.
  GURL referrer_url;
  if (disposition != WindowOpenDisposition::OFF_THE_RECORD) {
    referrer_url = referring_url.GetAsReferrer();
  }

  content::Referrer referrer = content::Referrer::SanitizeForRequest(
      url, content::Referrer(referrer_url, params_.referrer_policy));

  if (params_.link_url == url &&
      disposition != WindowOpenDisposition::OFF_THE_RECORD) {
    params_.link_followed = url;
  }

  OpenURLParams open_url_params(url, referrer, disposition, transition, false,
                                started_from_context_menu);
  if (!extra_headers.empty())
    open_url_params.extra_headers = extra_headers;

  open_url_params.source_render_process_id = render_process_id_;
  open_url_params.source_render_frame_id = render_frame_id_;

  open_url_params.initiator_frame_token = render_frame_token_;
  open_url_params.initiator_process_id = render_process_id_;
  open_url_params.initiator_origin = initiator;

  open_url_params.source_site_instance = site_instance_;

  return open_url_params;
}

bool RenderViewContextMenuBase::IsCustomItemChecked(int id) const {
  return IsCustomItemCheckedInternal(params_.custom_items, id);
}

bool RenderViewContextMenuBase::IsCustomItemEnabled(int id) const {
  return IsCustomItemEnabledInternal(params_.custom_items, id);
}
