blob: 324696ba4872dad6ebf9729df6126a89822e2e3d [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.
#include "chrome/browser/extensions/context_menu_helpers.h"
#include <stddef.h>
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/context_menu_matcher.h"
#include "components/renderer_context_menu/render_view_context_menu_base.h"
#include "content/public/browser/context_menu_params.h"
#include "third_party/blink/public/mojom/context_menu/context_menu.mojom-shared.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/text_elider.h"
using blink::mojom::ContextMenuDataMediaType;
namespace extensions {
namespace context_menu_helpers {
namespace {
// Helper function to determine if a given URL matches a URLPatternSet.
// This is primarily a wrapper around URLPatternSet::MatchesURL but includes an
// important special case: An empty pattern set is considered a match.
// This convention is used throughout the extension system to mean that an item
// has no URL-specific restrictions.
//
// @param patterns The set of URL patterns to check against.
// @param url The URL to be tested.
// @return true if the URL is matched by the patterns or if the pattern set
// is empty, false otherwise.
bool ExtensionPatternMatch(const URLPatternSet& patterns, const GURL& url) {
// No patterns means no restriction, so that implicitly matches.
if (patterns.is_empty()) {
return true;
}
return patterns.MatchesURL(url);
}
// Escapes ampersands in a string by replacing each `&` with `&&`.
// This is necessary for strings that will be displayed in UI elements like
// menus or labels, as a single ampersand is often interpreted as a prefix for
// a mnemonic character (e.g., "S&ave" would display as "Save" with 'a'
// underlined). Doubling the ampersand displays a literal '&'. The string is
// modified in-place.
//
// @param text The string to modify.
void EscapeAmpersands(std::u16string* text) {
base::ReplaceChars(*text, u"&", u"&&", text);
}
} // namespace
const char kActionNotAllowedError[] =
"Only extensions are allowed to use action contexts";
const char kCannotFindItemError[] = "Cannot find menu item with id *";
const char kCheckedError[] =
"Only items with type \"radio\" or \"checkbox\" can be checked";
const char kDuplicateIDError[] =
"Cannot create item with duplicate id *";
const char kGeneratedIdKey[] = "generatedId";
const char kLauncherNotAllowedError[] =
"Only packaged apps are allowed to use 'launcher' context";
const char kOnclickDisallowedError[] =
"Extensions using event pages or "
"Service Workers cannot pass an onclick parameter to "
"chrome.contextMenus.create. Instead, use the "
"chrome.contextMenus.onClicked event.";
const char kParentsMustBeNormalError[] =
"Parent items must have type \"normal\"";
const char kTitleNeededError[] =
"All menu items except for separators must have a title";
const char kTooManyMenuItems[] =
"An extension can create a maximum of * menu items.";
std::string GetIDString(const MenuItem::Id& id) {
if (id.uid == 0) {
return id.string_uid;
} else {
return base::NumberToString(id.uid);
}
}
MenuItem* GetParent(MenuItem::Id parent_id,
const MenuManager* menu_manager,
std::string* error) {
MenuItem* parent = menu_manager->GetItemById(parent_id);
if (!parent) {
*error = ErrorUtils::FormatErrorMessage(
kCannotFindItemError, GetIDString(parent_id));
return nullptr;
}
if (parent->type() != MenuItem::NORMAL) {
*error = kParentsMustBeNormalError;
return nullptr;
}
return parent;
}
MenuItem::ContextList GetContexts(
const std::vector<api::context_menus::ContextType>& in_contexts) {
MenuItem::ContextList contexts;
for (auto context : in_contexts) {
switch (context) {
case api::context_menus::ContextType::kAll:
contexts.Add(MenuItem::ALL);
break;
case api::context_menus::ContextType::kPage:
contexts.Add(MenuItem::PAGE);
break;
case api::context_menus::ContextType::kSelection:
contexts.Add(MenuItem::SELECTION);
break;
case api::context_menus::ContextType::kLink:
contexts.Add(MenuItem::LINK);
break;
case api::context_menus::ContextType::kEditable:
contexts.Add(MenuItem::EDITABLE);
break;
case api::context_menus::ContextType::kImage:
contexts.Add(MenuItem::IMAGE);
break;
case api::context_menus::ContextType::kVideo:
contexts.Add(MenuItem::VIDEO);
break;
case api::context_menus::ContextType::kAudio:
contexts.Add(MenuItem::AUDIO);
break;
case api::context_menus::ContextType::kFrame:
contexts.Add(MenuItem::FRAME);
break;
case api::context_menus::ContextType::kLauncher:
// Not available for <webview>.
contexts.Add(MenuItem::LAUNCHER);
break;
case api::context_menus::ContextType::kBrowserAction:
// Not available for <webview>.
contexts.Add(MenuItem::BROWSER_ACTION);
break;
case api::context_menus::ContextType::kPageAction:
// Not available for <webview>.
contexts.Add(MenuItem::PAGE_ACTION);
break;
case api::context_menus::ContextType::kAction:
// Not available for <webview>.
contexts.Add(MenuItem::ACTION);
break;
case api::context_menus::ContextType::kNone:
NOTREACHED();
}
}
return contexts;
}
MenuItem::Type GetType(api::context_menus::ItemType type,
MenuItem::Type default_type) {
switch (type) {
case api::context_menus::ItemType::kNone:
return default_type;
case api::context_menus::ItemType::kNormal:
return MenuItem::NORMAL;
case api::context_menus::ItemType::kCheckbox:
return MenuItem::CHECKBOX;
case api::context_menus::ItemType::kRadio:
return MenuItem::RADIO;
case api::context_menus::ItemType::kSeparator:
return MenuItem::SEPARATOR;
}
return MenuItem::NORMAL;
}
bool ExtensionContextAndPatternMatch(const content::ContextMenuParams& params,
const MenuItem::ContextList& contexts,
const URLPatternSet& target_url_patterns) {
const bool has_link = !params.link_url.is_empty();
const bool has_selection = !params.selection_text.empty();
const bool in_subframe = params.is_subframe;
if (contexts.Contains(MenuItem::ALL) ||
(has_selection && contexts.Contains(MenuItem::SELECTION)) ||
(params.is_editable && contexts.Contains(MenuItem::EDITABLE)) ||
(in_subframe && contexts.Contains(MenuItem::FRAME))) {
return true;
}
if (has_link && contexts.Contains(MenuItem::LINK) &&
ExtensionPatternMatch(target_url_patterns, params.link_url)) {
return true;
}
switch (params.media_type) {
case ContextMenuDataMediaType::kImage:
if (contexts.Contains(MenuItem::IMAGE) &&
ExtensionPatternMatch(target_url_patterns, params.src_url)) {
return true;
}
break;
case ContextMenuDataMediaType::kVideo:
if (contexts.Contains(MenuItem::VIDEO) &&
ExtensionPatternMatch(target_url_patterns, params.src_url)) {
return true;
}
break;
case ContextMenuDataMediaType::kAudio:
if (contexts.Contains(MenuItem::AUDIO) &&
ExtensionPatternMatch(target_url_patterns, params.src_url)) {
return true;
}
break;
default:
break;
}
// PAGE is the least specific context, so we only examine that if none of the
// other contexts apply (except for FRAME, which is included in PAGE for
// backwards compatibility).
if (!has_link && !has_selection && !params.is_editable &&
params.media_type == ContextMenuDataMediaType::kNone &&
contexts.Contains(MenuItem::PAGE)) {
return true;
}
return false;
}
bool MenuItemMatchesParams(const content::ContextMenuParams& params,
const MenuItem* item) {
bool match = ExtensionContextAndPatternMatch(params, item->contexts(),
item->target_url_patterns());
if (!match) {
return false;
}
return ExtensionPatternMatch(item->document_url_patterns(), params.frame_url);
}
std::u16string PrintableSelectionText(const std::u16string& selection_text) {
std::u16string result = gfx::TruncateString(
selection_text, RenderViewContextMenuBase::kMaxSelectionTextLength,
gfx::WORD_BREAK);
EscapeAmpersands(&result);
return result;
}
void PopulateExtensionItems(content::BrowserContext* browser_context,
const content::ContextMenuParams& params,
ContextMenuMatcher& matcher) {
matcher.Clear();
ExtensionRegistry* registry = ExtensionRegistry::Get(browser_context);
MenuManager* menu_manager = MenuManager::Get(browser_context);
if (!menu_manager || !registry) {
return;
}
std::u16string printable_selection_text =
context_menu_helpers::PrintableSelectionText(params.selection_text);
// Get a list of extension id's that have context menu items, and sort by the
// top level context menu title of the extension.
std::vector<std::u16string> sorted_menu_titles;
std::map<std::u16string, std::vector<const Extension*>>
title_to_extensions_map;
for (const auto& id : menu_manager->ExtensionIds()) {
const Extension* extension =
registry->enabled_extensions().GetByID(id.extension_id);
// Platform apps have their context menus created directly in
// AppendPlatformAppItems.
if (extension && !extension->is_platform_app()) {
std::u16string menu_title =
matcher.GetTopLevelContextMenuTitle(id, printable_selection_text);
title_to_extensions_map[menu_title].push_back(extension);
sorted_menu_titles.push_back(menu_title);
}
}
if (sorted_menu_titles.empty()) {
return;
}
const std::string app_locale = g_browser_process->GetApplicationLocale();
l10n_util::SortStrings16(app_locale, &sorted_menu_titles);
sorted_menu_titles.erase(
std::unique(sorted_menu_titles.begin(), sorted_menu_titles.end()),
sorted_menu_titles.end());
sorted_menu_titles.erase(
std::unique(sorted_menu_titles.begin(), sorted_menu_titles.end()),
sorted_menu_titles.end());
int index = 0;
for (const auto& title : sorted_menu_titles) {
const std::vector<const Extension*>& extensions =
title_to_extensions_map[title];
for (const Extension* extension : extensions) {
MenuItem::ExtensionKey extension_key(extension->id());
matcher.AppendExtensionItems(extension_key, printable_selection_text,
&index,
/*is_action_menu=*/false);
}
}
}
} // namespace context_menu_helpers
} // namespace extensions