|  | // 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 |