blob: d278dc034f4e5cf3b52dd8d6ea33133adf9ba38b [file] [log] [blame]
// 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.
// Definition of helper functions for the ContextMenus API.
#ifndef CHROME_BROWSER_EXTENSIONS_CONTEXT_MENU_HELPERS_H_
#define CHROME_BROWSER_EXTENSIONS_CONTEXT_MENU_HELPERS_H_
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/types/optional_util.h"
#include "chrome/browser/extensions/menu_manager.h"
#include "chrome/common/extensions/api/context_menus.h"
#include "content/public/browser/browser_context.h"
#include "extensions/common/error_utils.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/manifest_handlers/background_info.h"
#include "extensions/common/utils/extension_utils.h"
namespace content {
struct ContextMenuParams;
} // namespace content
namespace extensions {
class ContextMenuMatcher;
} // namespace extensions
namespace extensions::context_menu_helpers {
namespace {
template <typename PropertyWithEnumT>
std::unique_ptr<MenuItem::Id> GetParentId(const PropertyWithEnumT& property,
bool is_off_the_record,
const MenuItem::ExtensionKey& key) {
if (!property.parent_id)
return nullptr;
std::unique_ptr<MenuItem::Id> parent_id(
new MenuItem::Id(is_off_the_record, key));
if (property.parent_id->as_integer) {
parent_id->uid = *property.parent_id->as_integer;
} else if (property.parent_id->as_string) {
parent_id->string_uid = *property.parent_id->as_string;
} else {
NOTREACHED();
}
return parent_id;
}
} // namespace
extern const char kActionNotAllowedError[];
extern const char kCannotFindItemError[];
extern const char kCheckedError[];
extern const char kDuplicateIDError[];
extern const char kGeneratedIdKey[];
extern const char kLauncherNotAllowedError[];
extern const char kOnclickDisallowedError[];
extern const char kParentsMustBeNormalError[];
extern const char kTitleNeededError[];
extern const char kTooManyMenuItems[];
std::string GetIDString(const MenuItem::Id& id);
MenuItem* GetParent(MenuItem::Id parent_id,
const MenuManager* menu_manager,
std::string* error);
MenuItem::ContextList GetContexts(
const std::vector<api::context_menus::ContextType>& in_contexts);
MenuItem::Type GetType(api::context_menus::ItemType type,
MenuItem::Type default_type);
// Determines if a context menu item should be shown for a given click context.
// This checks if the properties of a right-click (the `params`) match the
// requirements of an extension's context menu item, which are defined by its
// allowed `contexts` and `target_url_patterns`.
//
// params The properties of the context menu click, such as the link
// URL, selected text, and media type.
// contexts The set of contexts the menu item is registered for (e.g.,
// `MenuItem::IMAGE`, `MenuItem::LINK`).
// target_url_patterns The set of URL patterns to match against for
// applicable contexts like links and media.
// Returns whether the menu item is a match for the given context and should be
// shown.
bool ExtensionContextAndPatternMatch(const content::ContextMenuParams& params,
const MenuItem::ContextList& contexts,
const URLPatternSet& target_url_patterns);
// Determines if a given MenuItem should be shown for a context menu click,
// based on the context (e.g., link, image, or selection) and URL.
//
// params The properties of the context menu click.
// item The extension menu item to be evaluated.
// Returns whether the menu item should be displayed in the context menu.
bool MenuItemMatchesParams(const content::ContextMenuParams& params,
const MenuItem* item);
// Prepares user-selected text for display in a context menu item, by truncating
// the string to a maximum length (`kMaxSelectionTextLength`) and escaping
// ampersands to prevent them from being interpreted as UI mnemonic character
// shortcuts.
//
// selection_text The raw text selected by the user.
// Returns a truncated and escaped version of the input string suitable for
// display.
std::u16string PrintableSelectionText(const std::u16string& selection_text);
// Populates a ContextMenuMatcher with all relevant context menu items from
// enabled extensions, sorted and grouped appropriately.
//
// params The parameters of the context menu click. This is used to get
// the selected text for menu items that include it (e.g., "Search for %s").
// matcher The `ContextMenuMatcher` that will be cleared and then
// populated with the extension menu items.
void PopulateExtensionItems(content::BrowserContext* browser_context,
const content::ContextMenuParams& params,
ContextMenuMatcher& matcher);
// Creates and adds a menu item from `create_properties`.
template <typename PropertyWithEnumT>
bool CreateMenuItem(const PropertyWithEnumT& create_properties,
content::BrowserContext* browser_context,
const Extension* extension,
const MenuItem::Id& item_id,
std::string* error) {
bool is_webview = item_id.extension_key.webview_instance_id != 0;
MenuManager* menu_manager = MenuManager::Get(browser_context);
if (menu_manager->MenuItemsSize(item_id.extension_key) >=
MenuManager::kMaxItemsPerExtension) {
*error = ErrorUtils::FormatErrorMessage(
kTooManyMenuItems,
base::NumberToString(MenuManager::kMaxItemsPerExtension));
return false;
}
if (menu_manager->GetItemById(item_id)) {
*error =
ErrorUtils::FormatErrorMessage(kDuplicateIDError, GetIDString(item_id));
return false;
}
if (!is_webview && BackgroundInfo::HasLazyContext(extension) &&
create_properties.onclick) {
*error = kOnclickDisallowedError;
return false;
}
// Contexts.
MenuItem::ContextList contexts;
if (create_properties.contexts)
contexts = GetContexts(*create_properties.contexts);
else
contexts.Add(MenuItem::PAGE);
if (contexts.Contains(MenuItem::LAUNCHER)) {
// Launcher item is not allowed for <webview>.
if (is_webview || !extension->is_platform_app()) {
*error = kLauncherNotAllowedError;
return false;
}
}
if (contexts.Contains(MenuItem::BROWSER_ACTION) ||
contexts.Contains(MenuItem::PAGE_ACTION) ||
contexts.Contains(MenuItem::ACTION)) {
// Action items are not allowed for <webview>.
if (is_webview || !extension->is_extension()) {
*error = kActionNotAllowedError;
return false;
}
}
// Title.
std::string title;
if (create_properties.title)
title = *create_properties.title;
MenuItem::Type type = GetType(create_properties.type, MenuItem::NORMAL);
if (title.empty() && type != MenuItem::SEPARATOR) {
*error = kTitleNeededError;
return false;
}
// Visibility state.
bool visible = create_properties.visible.value_or(true);
// Checked state.
bool checked = create_properties.checked.value_or(false);
// Enabled.
bool enabled = create_properties.enabled.value_or(true);
std::unique_ptr<MenuItem> item(
new MenuItem(item_id, title, checked, visible, enabled, type, contexts));
// URL Patterns.
if (!item->PopulateURLPatterns(
base::OptionalToPtr(create_properties.document_url_patterns),
base::OptionalToPtr(create_properties.target_url_patterns), error)) {
return false;
}
// Parent id.
bool success = true;
std::unique_ptr<MenuItem::Id> parent_id(
GetParentId(create_properties, browser_context->IsOffTheRecord(),
item_id.extension_key));
if (parent_id.get()) {
MenuItem* parent = GetParent(*parent_id, menu_manager, error);
if (!parent)
return false;
success = menu_manager->AddChildItem(parent->id(), std::move(item));
} else {
success = menu_manager->AddContextItem(extension, std::move(item));
}
if (!success)
return false;
menu_manager->WriteToStorage(extension, item_id.extension_key);
return true;
}
// Updates a menu item from `update_properties`.
template <typename PropertyWithEnumT>
bool UpdateMenuItem(const PropertyWithEnumT& update_properties,
content::BrowserContext* browser_context,
const Extension* extension,
const MenuItem::Id& item_id,
std::string* error) {
bool radio_item_updated = false;
bool is_webview = item_id.extension_key.webview_instance_id != 0;
MenuManager* menu_manager = MenuManager::Get(browser_context);
MenuItem* item = menu_manager->GetItemById(item_id);
const ExtensionId& extension_id = MaybeGetExtensionId(extension);
if (!item || item->extension_id() != extension_id) {
*error = ErrorUtils::FormatErrorMessage(kCannotFindItemError,
GetIDString(item_id));
return false;
}
// Type.
MenuItem::Type type = GetType(update_properties.type, item->type());
if (type != item->type()) {
if (type == MenuItem::RADIO || item->type() == MenuItem::RADIO) {
radio_item_updated = true;
}
item->set_type(type);
}
// Title.
if (update_properties.title) {
std::string title(*update_properties.title);
if (title.empty() && item->type() != MenuItem::SEPARATOR) {
*error = kTitleNeededError;
return false;
}
item->set_title(title);
}
// Checked state.
if (update_properties.checked) {
bool checked = *update_properties.checked;
if (checked && item->type() != MenuItem::CHECKBOX &&
item->type() != MenuItem::RADIO) {
*error = kCheckedError;
return false;
}
const bool should_toggle_checked =
// If radio item was unchecked nothing should happen. The radio item
// should remain checked because there should always be one item checked
// in the radio list.
(item->type() == MenuItem::RADIO && checked) ||
// Checkboxes are always updated.
item->type() == MenuItem::CHECKBOX;
if (should_toggle_checked) {
if (!item->SetChecked(checked)) {
*error = kCheckedError;
return false;
}
radio_item_updated = true;
}
}
// Visibility state.
if (update_properties.visible)
item->set_visible(*update_properties.visible);
// Enabled.
if (update_properties.enabled)
item->set_enabled(*update_properties.enabled);
// Contexts.
MenuItem::ContextList contexts;
if (update_properties.contexts) {
contexts = GetContexts(*update_properties.contexts);
if (contexts.Contains(MenuItem::LAUNCHER)) {
// Launcher item is not allowed for <webview>.
if (is_webview || !extension->is_platform_app()) {
*error = kLauncherNotAllowedError;
return false;
}
}
if (contexts != item->contexts())
item->set_contexts(contexts);
}
// Parent id.
std::unique_ptr<MenuItem::Id> parent_id(
GetParentId(update_properties, browser_context->IsOffTheRecord(),
item_id.extension_key));
if (parent_id.get()) {
MenuItem* parent = GetParent(*parent_id, menu_manager, error);
if (!parent || !menu_manager->ChangeParent(item->id(), &parent->id()))
return false;
}
// URL Patterns.
if (!item->PopulateURLPatterns(
base::OptionalToPtr(update_properties.document_url_patterns),
base::OptionalToPtr(update_properties.target_url_patterns), error)) {
return false;
}
// There is no need to call ItemUpdated if ChangeParent is called because
// all sanitation is taken care of in ChangeParent.
if (radio_item_updated && !menu_manager->ItemUpdated(item->id()))
return false;
menu_manager->WriteToStorage(extension, item_id.extension_key);
return true;
}
} // namespace extensions::context_menu_helpers
#endif // CHROME_BROWSER_EXTENSIONS_CONTEXT_MENU_HELPERS_H_