blob: 481c92bd331f0deee4c05a9cac95355ff0f66f06 [file] [log] [blame]
// 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/menu_manager.h"
#include <memory>
#include <tuple>
#include <utility>
#include "base/bind.h"
#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/json/json_writer.h"
#include "base/notreached.h"
#include "base/observer_list.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/extensions/menu_manager_factory.h"
#include "chrome/browser/extensions/tab_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/extensions/api/chrome_web_view_internal.h"
#include "chrome/common/extensions/api/context_menus.h"
#include "chrome/common/extensions/api/url_handlers/url_handlers_parser.h"
#include "content/public/browser/context_menu_params.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/child_process_host.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_api_frame_id_map.h"
#include "extensions/browser/guest_view/web_view/web_view_guest.h"
#include "extensions/browser/state_store.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest_handlers/background_info.h"
#include "extensions/common/mojom/event_dispatcher.mojom.h"
#include "third_party/blink/public/mojom/context_menu/context_menu.mojom.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/text_elider.h"
using content::ChildProcessHost;
using content::WebContents;
using guest_view::kInstanceIDNone;
namespace extensions {
namespace {
// Keys for serialization to and from Value to store in the preferences.
const char kContextMenusKey[] = "context_menus";
const char kCheckedKey[] = "checked";
const char kContextsKey[] = "contexts";
const char kDocumentURLPatternsKey[] = "document_url_patterns";
const char kEnabledKey[] = "enabled";
const char kMenuManagerIncognitoKey[] = "incognito";
const char kParentUIDKey[] = "parent_uid";
const char kStringUIDKey[] = "string_uid";
const char kTargetURLPatternsKey[] = "target_url_patterns";
const char kTitleKey[] = "title";
const char kMenuManagerTypeKey[] = "type";
const char kVisibleKey[] = "visible";
void SetIdKeyValue(base::Value::Dict& properties,
const char* key,
const MenuItem::Id& id) {
if (id.uid == 0)
properties.Set(key, id.string_uid);
else
properties.Set(key, id.uid);
}
MenuItem::OwnedList MenuItemsFromValue(
const std::string& extension_id,
const absl::optional<base::Value>& value) {
MenuItem::OwnedList items;
if (!value || !value->is_list())
return items;
for (const base::Value& elem : value->GetList()) {
if (!elem.is_dict())
continue;
std::unique_ptr<MenuItem> item =
MenuItem::Populate(extension_id, elem.GetDict(), nullptr);
if (!item)
continue;
items.push_back(std::move(item));
}
return items;
}
base::Value::List MenuItemsToValue(const MenuItem::List& items) {
base::Value::List list;
for (const auto* item : items)
list.Append(item->ToValue());
return list;
}
bool GetStringList(const base::Value::Dict& dict,
const std::string& key,
std::vector<std::string>* out) {
const base::Value* value = dict.Find(key);
if (!value)
return true;
if (!value->is_list())
return false;
const base::Value::List& list = value->GetList();
for (const auto& pattern : list) {
if (!pattern.is_string())
return false;
out->push_back(pattern.GetString());
}
return true;
}
} // namespace
MenuItem::MenuItem(const Id& id,
const std::string& title,
bool checked,
bool visible,
bool enabled,
Type type,
const ContextList& contexts)
: id_(id),
title_(title),
type_(type),
checked_(checked),
visible_(visible),
enabled_(enabled),
contexts_(contexts) {}
MenuItem::~MenuItem() {
}
std::unique_ptr<MenuItem> MenuItem::ReleaseChild(const Id& child_id,
bool recursive) {
for (auto i = children_.begin(); i != children_.end(); ++i) {
std::unique_ptr<MenuItem> child;
if ((*i)->id() == child_id) {
child = std::move(*i);
children_.erase(i);
return child;
}
if (recursive) {
child = (*i)->ReleaseChild(child_id, recursive);
if (child)
return child;
}
}
return nullptr;
}
void MenuItem::GetFlattenedSubtree(MenuItem::List* list) {
list->push_back(this);
for (const auto& child : children_)
child->GetFlattenedSubtree(list);
}
std::set<MenuItem::Id> MenuItem::RemoveAllDescendants() {
std::set<Id> result;
for (const auto& child : children_) {
result.insert(child->id());
std::set<Id> removed = child->RemoveAllDescendants();
result.insert(removed.begin(), removed.end());
}
children_.clear();
return result;
}
std::u16string MenuItem::TitleWithReplacement(const std::u16string& selection,
size_t max_length) const {
std::u16string result = base::UTF8ToUTF16(title_);
// TODO(asargent) - Change this to properly handle %% escaping so you can
// put "%s" in titles that won't get substituted.
base::ReplaceSubstringsAfterOffset(&result, 0, u"%s", selection);
if (result.length() > max_length)
result = gfx::TruncateString(result, max_length, gfx::WORD_BREAK);
return result;
}
bool MenuItem::SetChecked(bool checked) {
if (type_ != CHECKBOX && type_ != RADIO)
return false;
checked_ = checked;
return true;
}
void MenuItem::AddChild(std::unique_ptr<MenuItem> item) {
item->parent_id_ = std::make_unique<Id>(id_);
children_.push_back(std::move(item));
}
base::Value::Dict MenuItem::ToValue() const {
base::Value::Dict value;
// Should only be called for extensions with event pages, which only have
// string IDs for items.
DCHECK_EQ(0, id_.uid);
value.Set(kStringUIDKey, id_.string_uid);
value.Set(kMenuManagerIncognitoKey, id_.incognito);
value.Set(kMenuManagerTypeKey, type_);
if (type_ != SEPARATOR)
value.Set(kTitleKey, title_);
if (type_ == CHECKBOX || type_ == RADIO)
value.Set(kCheckedKey, checked_);
value.Set(kEnabledKey, enabled_);
value.Set(kVisibleKey, visible_);
value.Set(kContextsKey, contexts_.ToValue());
if (parent_id_) {
DCHECK_EQ(0, parent_id_->uid);
value.Set(kParentUIDKey, parent_id_->string_uid);
}
value.Set(kDocumentURLPatternsKey, document_url_patterns_.ToValue());
value.Set(kTargetURLPatternsKey, target_url_patterns_.ToValue());
return value;
}
// static
std::unique_ptr<MenuItem> MenuItem::Populate(const std::string& extension_id,
const base::Value::Dict& value,
std::string* error) {
absl::optional<bool> incognito = value.FindBool(kMenuManagerIncognitoKey);
if (!incognito.has_value())
return nullptr;
Id id(incognito.value(), MenuItem::ExtensionKey(extension_id));
const std::string* string_uid = value.FindString(kStringUIDKey);
if (!string_uid)
return nullptr;
id.string_uid = *string_uid;
absl::optional<int> type_int = value.FindInt(kMenuManagerTypeKey);
if (!type_int.has_value())
return nullptr;
Type type = static_cast<Type>(type_int.value());
std::string title;
if (type != SEPARATOR) {
const std::string* specified_title = value.FindString(kTitleKey);
if (!specified_title)
return nullptr;
title = *specified_title;
}
bool checked = false;
if (type == CHECKBOX || type == RADIO) {
absl::optional<bool> specified_checked = value.FindBool(kCheckedKey);
if (!specified_checked)
return nullptr;
checked = specified_checked.value();
}
// The ability to toggle a menu item's visibility was introduced in M62, so it
// is expected that the kVisibleKey will not be present in older menu items in
// storage. Thus, we do not return nullptr if the kVisibleKey is not found.
// TODO(catmullings): Remove this in M65 when all prefs should be migrated.
bool visible = value.FindBool(kVisibleKey).value_or(true);
absl::optional<bool> specified_enabled = value.FindBool(kEnabledKey);
if (!specified_enabled.has_value())
return nullptr;
bool enabled = specified_enabled.value();
ContextList contexts;
const base::Value* contexts_value = value.Find(kContextsKey);
if (!contexts_value)
return nullptr;
if (!contexts.Populate(*contexts_value))
return nullptr;
std::unique_ptr<MenuItem> result = std::make_unique<MenuItem>(
id, title, checked, visible, enabled, type, contexts);
std::vector<std::string> document_url_patterns;
if (!GetStringList(value, kDocumentURLPatternsKey, &document_url_patterns))
return nullptr;
std::vector<std::string> target_url_patterns;
if (!GetStringList(value, kTargetURLPatternsKey, &target_url_patterns))
return nullptr;
if (!result->PopulateURLPatterns(&document_url_patterns, &target_url_patterns,
error)) {
return nullptr;
}
// parent_id is filled in from the value, but it might not be valid. It's left
// to be validated upon being added (via AddChildItem) to the menu manager.
std::unique_ptr<Id> parent_id = std::make_unique<Id>(
incognito.value(), MenuItem::ExtensionKey(extension_id));
const base::Value* parent = value.Find(kParentUIDKey);
if (parent) {
if (!parent->is_string())
return nullptr;
parent_id->string_uid = parent->GetString();
result->parent_id_.swap(parent_id);
}
return result;
}
bool MenuItem::PopulateURLPatterns(
const std::vector<std::string>* document_url_patterns,
const std::vector<std::string>* target_url_patterns,
std::string* error) {
if (document_url_patterns) {
if (!document_url_patterns_.Populate(
*document_url_patterns, URLPattern::SCHEME_ALL, true, error)) {
return false;
}
}
if (target_url_patterns) {
if (!target_url_patterns_.Populate(
*target_url_patterns, URLPattern::SCHEME_ALL, true, error)) {
return false;
}
}
return true;
}
// static
const char MenuManager::kOnContextMenus[] = "contextMenus";
const char MenuManager::kOnWebviewContextMenus[] =
"webViewInternal.contextMenus";
MenuManager::MenuManager(content::BrowserContext* context, StateStore* store)
: browser_context_(context), store_(store) {
extension_registry_observation_.Observe(
ExtensionRegistry::Get(browser_context_));
Profile* profile = Profile::FromBrowserContext(context);
observed_profiles_.AddObservation(profile);
if (profile->HasPrimaryOTRProfile())
observed_profiles_.AddObservation(
profile->GetPrimaryOTRProfile(/*create_if_needed=*/true));
if (store_)
store_->RegisterKey(kContextMenusKey);
}
MenuManager::~MenuManager() = default;
// static
MenuManager* MenuManager::Get(content::BrowserContext* context) {
return MenuManagerFactory::GetForBrowserContext(context);
}
std::set<MenuItem::ExtensionKey> MenuManager::ExtensionIds() {
std::set<MenuItem::ExtensionKey> id_set;
for (auto i = context_items_.begin(); i != context_items_.end(); ++i) {
id_set.insert(i->first);
}
return id_set;
}
const MenuItem::OwnedList* MenuManager::MenuItems(
const MenuItem::ExtensionKey& key) {
auto i = context_items_.find(key);
if (i != context_items_.end()) {
return &i->second;
}
return nullptr;
}
bool MenuManager::AddContextItem(const Extension* extension,
std::unique_ptr<MenuItem> item) {
MenuItem* item_ptr = item.get();
const MenuItem::ExtensionKey& key = item->id().extension_key;
// The item must have a non-empty key, and not have already been added.
if (key.empty() || base::Contains(items_by_id_, item->id()))
return false;
DCHECK_EQ(extension->id(), key.extension_id);
bool first_item = !base::Contains(context_items_, key);
context_items_[key].push_back(std::move(item));
items_by_id_[item_ptr->id()] = item_ptr;
if (item_ptr->type() == MenuItem::RADIO) {
if (item_ptr->checked())
RadioItemSelected(item_ptr);
else
SanitizeRadioListsInMenu(context_items_[key]);
}
// If this is the first item for this extension, start loading its icon.
if (first_item)
icon_manager_.LoadIcon(browser_context_, extension);
return true;
}
bool MenuManager::AddChildItem(const MenuItem::Id& parent_id,
std::unique_ptr<MenuItem> child) {
MenuItem* parent = GetItemById(parent_id);
if (!parent || parent->type() != MenuItem::NORMAL ||
parent->incognito() != child->incognito() ||
parent->extension_id() != child->extension_id() ||
base::Contains(items_by_id_, child->id()))
return false;
MenuItem* child_ptr = child.get();
parent->AddChild(std::move(child));
items_by_id_[child_ptr->id()] = child_ptr;
if (child_ptr->type() == MenuItem::RADIO)
SanitizeRadioListsInMenu(parent->children());
return true;
}
bool MenuManager::DescendantOf(MenuItem* item,
const MenuItem::Id& ancestor_id) {
// Work our way up the tree until we find the ancestor or null.
MenuItem::Id* id = item->parent_id();
while (id != nullptr) {
DCHECK(*id != item->id()); // Catch circular graphs.
if (*id == ancestor_id)
return true;
MenuItem* next = GetItemById(*id);
if (!next) {
NOTREACHED();
return false;
}
id = next->parent_id();
}
return false;
}
bool MenuManager::ChangeParent(const MenuItem::Id& child_id,
const MenuItem::Id* parent_id) {
MenuItem* child_ptr = GetItemById(child_id);
std::unique_ptr<MenuItem> child;
MenuItem* new_parent = parent_id ? GetItemById(*parent_id) : nullptr;
if ((parent_id && (child_id == *parent_id)) || !child_ptr ||
(!new_parent && parent_id != nullptr) ||
(new_parent && (DescendantOf(new_parent, child_id) ||
child_ptr->incognito() != new_parent->incognito() ||
child_ptr->extension_id() != new_parent->extension_id())))
return false;
MenuItem::Id* old_parent_id = child_ptr->parent_id();
if (old_parent_id != nullptr) {
MenuItem* old_parent = GetItemById(*old_parent_id);
if (!old_parent) {
NOTREACHED();
return false;
}
child = old_parent->ReleaseChild(child_id, false /* non-recursive search*/);
DCHECK(child.get() == child_ptr);
SanitizeRadioListsInMenu(old_parent->children());
} else {
// This is a top-level item, so we need to pull it out of our list of
// top-level items.
const MenuItem::ExtensionKey& child_key = child_ptr->id().extension_key;
auto i = context_items_.find(child_key);
if (i == context_items_.end()) {
NOTREACHED();
return false;
}
MenuItem::OwnedList& list = i->second;
auto j =
base::ranges::find(list, child_ptr, &std::unique_ptr<MenuItem>::get);
if (j == list.end()) {
NOTREACHED();
return false;
}
child = std::move(*j);
list.erase(j);
SanitizeRadioListsInMenu(list);
}
if (new_parent) {
new_parent->AddChild(std::move(child));
SanitizeRadioListsInMenu(new_parent->children());
} else {
const MenuItem::ExtensionKey& child_key = child_ptr->id().extension_key;
context_items_[child_key].push_back(std::move(child));
child_ptr->parent_id_.reset(nullptr);
SanitizeRadioListsInMenu(context_items_[child_key]);
}
return true;
}
bool MenuManager::RemoveContextMenuItem(const MenuItem::Id& id) {
if (!base::Contains(items_by_id_, id))
return false;
MenuItem* menu_item = GetItemById(id);
DCHECK(menu_item);
const MenuItem::ExtensionKey extension_key = id.extension_key;
auto i = context_items_.find(extension_key);
if (i == context_items_.end()) {
NOTREACHED();
return false;
}
bool result = false;
std::set<MenuItem::Id> items_removed;
MenuItem::OwnedList& list = i->second;
for (auto j = list.begin(); j < list.end(); ++j) {
// See if the current top-level item is a match.
if ((*j)->id() == id) {
items_removed = (*j)->RemoveAllDescendants();
items_removed.insert(id);
list.erase(j);
result = true;
SanitizeRadioListsInMenu(list);
break;
} else {
// See if the item to remove was found as a descendant of the current
// top-level item.
std::unique_ptr<MenuItem> child =
(*j)->ReleaseChild(id, true /* recursive */);
if (child) {
items_removed = child->RemoveAllDescendants();
items_removed.insert(id);
SanitizeRadioListsInMenu(GetItemById(*child->parent_id())->children());
result = true;
break;
}
}
}
DCHECK(result); // The check at the very top should have prevented this.
// Clear entries from the items_by_id_ map.
for (auto removed_iter = items_removed.begin();
removed_iter != items_removed.end(); ++removed_iter) {
items_by_id_.erase(*removed_iter);
}
if (list.empty()) {
context_items_.erase(extension_key);
icon_manager_.RemoveIcon(extension_key.extension_id);
}
return result;
}
void MenuManager::RemoveAllContextItems(
const MenuItem::ExtensionKey& extension_key) {
auto it = context_items_.find(extension_key);
if (it == context_items_.end())
return;
// We use the |extension_id| from the stored ExtensionKey, since the provided
// |extension_key| may leave it empty (if matching solely based on the
// webview IDs).
// TODO(paulmeyer): We can get rid of this hack if/when we reliably track
// extension IDs at WebView cleanup.
std::string extension_id = it->first.extension_id;
MenuItem::OwnedList& context_items_for_key = it->second;
for (const auto& item : context_items_for_key) {
items_by_id_.erase(item->id());
// Remove descendants from this item and erase them from the lookup cache.
std::set<MenuItem::Id> removed_ids = item->RemoveAllDescendants();
for (auto j = removed_ids.begin(); j != removed_ids.end(); ++j) {
items_by_id_.erase(*j);
}
}
context_items_.erase(extension_key);
icon_manager_.RemoveIcon(extension_id);
}
MenuItem* MenuManager::GetItemById(const MenuItem::Id& id) const {
auto i = items_by_id_.find(id);
return i != items_by_id_.end() ? i->second : nullptr;
}
void MenuManager::RadioItemSelected(MenuItem* item) {
// If this is a child item, we need to get a handle to the list from its
// parent. Otherwise get a handle to the top-level list.
const MenuItem::OwnedList* list = nullptr;
if (item->parent_id()) {
MenuItem* parent = GetItemById(*item->parent_id());
if (!parent) {
NOTREACHED();
return;
}
list = &(parent->children());
} else {
const MenuItem::ExtensionKey& key = item->id().extension_key;
if (context_items_.find(key) == context_items_.end()) {
NOTREACHED();
return;
}
list = &context_items_[key];
}
// Find where |item| is in the list.
MenuItem::OwnedList::const_iterator item_location;
for (item_location = list->begin(); item_location != list->end();
++item_location) {
if (item_location->get() == item)
break;
}
if (item_location == list->end()) {
NOTREACHED(); // We should have found the item.
return;
}
// Iterate backwards from |item| and uncheck any adjacent radio items.
MenuItem::OwnedList::const_iterator i;
if (item_location != list->begin()) {
i = item_location;
do {
--i;
if ((*i)->type() != MenuItem::RADIO)
break;
(*i)->SetChecked(false);
} while (i != list->begin());
}
// Now iterate forwards from |item| and uncheck any adjacent radio items.
for (i = item_location + 1; i != list->end(); ++i) {
if ((*i)->type() != MenuItem::RADIO)
break;
(*i)->SetChecked(false);
}
}
static void AddURLProperty(base::Value::Dict& dictionary,
const std::string& key,
const GURL& url) {
if (!url.is_empty())
dictionary.Set(key, url.possibly_invalid_spec());
}
void MenuManager::ExecuteCommand(content::BrowserContext* context,
WebContents* web_contents,
content::RenderFrameHost* render_frame_host,
const content::ContextMenuParams& params,
const MenuItem::Id& menu_item_id) {
EventRouter* event_router = EventRouter::Get(context);
if (!event_router)
return;
MenuItem* item = GetItemById(menu_item_id);
if (!item)
return;
ExtensionRegistry* registry = ExtensionRegistry::Get(browser_context_);
const Extension* extension =
registry->enabled_extensions().GetByID(item->extension_id());
if (item->type() == MenuItem::RADIO)
RadioItemSelected(item);
base::Value::Dict properties;
SetIdKeyValue(properties, "menuItemId", item->id());
if (item->parent_id())
SetIdKeyValue(properties, "parentMenuItemId", *item->parent_id());
switch (params.media_type) {
case blink::mojom::ContextMenuDataMediaType::kImage:
properties.Set("mediaType", "image");
break;
case blink::mojom::ContextMenuDataMediaType::kVideo:
properties.Set("mediaType", "video");
break;
case blink::mojom::ContextMenuDataMediaType::kAudio:
properties.Set("mediaType", "audio");
break;
default: {} // Do nothing.
}
AddURLProperty(properties, "linkUrl", params.unfiltered_link_url);
AddURLProperty(properties, "srcUrl", params.src_url);
AddURLProperty(properties, "pageUrl", params.page_url);
AddURLProperty(properties, "frameUrl", params.frame_url);
if (params.selection_text.length() > 0)
properties.Set("selectionText", params.selection_text);
properties.Set("editable", params.is_editable);
WebViewGuest* webview_guest =
WebViewGuest::FromRenderFrameHost(render_frame_host);
if (webview_guest) {
// This is used in web_view_internalcustom_bindings.js.
// The property is not exposed to developer API.
properties.Set("webviewInstanceId", webview_guest->view_instance_id());
}
base::Value::List args;
args.Append(std::move(properties));
// Add the tab info to the argument list.
// No tab info in a platform app.
if (!extension || !extension->is_platform_app()) {
// Note: web_contents are null in unit tests :(
if (web_contents) {
int frame_id = ExtensionApiFrameIdMap::GetFrameId(render_frame_host);
if (frame_id != ExtensionApiFrameIdMap::kInvalidFrameId)
args[0].GetDict().Set("frameId", frame_id);
// We intentionally don't scrub the tab data here, since the user chose to
// invoke the extension on the page.
// TODO(tjudkins) Potentially use GetScrubTabBehavior here to gate based
// on permissions.
ExtensionTabUtil::ScrubTabBehavior scrub_tab_behavior = {
ExtensionTabUtil::kDontScrubTab, ExtensionTabUtil::kDontScrubTab};
args.Append(ExtensionTabUtil::CreateTabObject(
web_contents, scrub_tab_behavior, extension)
.ToValue());
} else {
args.Append(base::Value(base::Value::Type::DICTIONARY));
}
}
if (item->type() == MenuItem::CHECKBOX ||
item->type() == MenuItem::RADIO) {
bool was_checked = item->checked();
args[0].GetDict().Set("wasChecked", was_checked);
// RADIO items always get set to true when you click on them, but CHECKBOX
// items get their state toggled.
bool checked = item->type() == MenuItem::RADIO || !was_checked;
item->SetChecked(checked);
args[0].GetDict().Set("checked", item->checked());
if (extension)
WriteToStorage(extension, item->id().extension_key);
}
// Note: web_contents are null in unit tests :(
if (web_contents && TabHelper::FromWebContents(web_contents)) {
TabHelper::FromWebContents(web_contents)
->active_tab_permission_granter()
->GrantIfRequested(extension);
}
{
// Dispatch to menu item's .onclick handler (this is the legacy API, from
// before chrome.contextMenus.onClicked existed).
auto event = std::make_unique<Event>(
webview_guest ? events::WEB_VIEW_INTERNAL_CONTEXT_MENUS
: events::CONTEXT_MENUS,
webview_guest ? kOnWebviewContextMenus : kOnContextMenus, args.Clone(),
context);
event->user_gesture = EventRouter::USER_GESTURE_ENABLED;
event_router->DispatchEventToExtension(item->extension_id(),
std::move(event));
}
{
// Dispatch to .contextMenus.onClicked handler.
auto event = std::make_unique<Event>(
webview_guest ? events::CHROME_WEB_VIEW_INTERNAL_ON_CLICKED
: events::CONTEXT_MENUS_ON_CLICKED,
webview_guest ? api::chrome_web_view_internal::OnClicked::kEventName
: api::context_menus::OnClicked::kEventName,
std::move(args), context);
event->user_gesture = EventRouter::USER_GESTURE_ENABLED;
if (webview_guest) {
event->filter_info->has_instance_id = true;
event->filter_info->instance_id = webview_guest->view_instance_id();
}
event_router->DispatchEventToExtension(item->extension_id(),
std::move(event));
}
}
void MenuManager::SanitizeRadioListsInMenu(
const MenuItem::OwnedList& item_list) {
auto i = item_list.begin();
while (i != item_list.end()) {
if ((*i)->type() != MenuItem::RADIO) {
++i;
// Move on to sanitize the next radio list, if any.
continue;
}
// Uncheck any checked radio items in the run, and at the end reset
// the appropriate one to checked. If no check radio items were found,
// then check the first radio item in the run.
auto last_checked = item_list.end();
MenuItem::OwnedList::const_iterator radio_run_iter;
for (radio_run_iter = i; radio_run_iter != item_list.end();
++radio_run_iter) {
if ((*radio_run_iter)->type() != MenuItem::RADIO) {
break;
}
if ((*radio_run_iter)->checked()) {
last_checked = radio_run_iter;
(*radio_run_iter)->SetChecked(false);
}
}
if (last_checked != item_list.end())
(*last_checked)->SetChecked(true);
else
(*i)->SetChecked(true);
i = radio_run_iter;
}
}
bool MenuManager::ItemUpdated(const MenuItem::Id& id) {
if (!base::Contains(items_by_id_, id))
return false;
MenuItem* menu_item = GetItemById(id);
DCHECK(menu_item);
if (!menu_item->parent_id()) {
auto i = context_items_.find(menu_item->id().extension_key);
if (i == context_items_.end()) {
NOTREACHED();
return false;
}
}
// If we selected a radio item, unselect all other items in its group.
if (menu_item->type() == MenuItem::RADIO && menu_item->checked())
RadioItemSelected(menu_item);
return true;
}
void MenuManager::WriteToStorage(const Extension* extension,
const MenuItem::ExtensionKey& extension_key) {
if (!BackgroundInfo::HasLazyContext(extension))
return;
// <webview> menu items are transient and not stored in storage.
if (extension_key.webview_instance_id)
return;
const MenuItem::OwnedList* top_items = MenuItems(extension_key);
MenuItem::List all_items;
if (top_items) {
for (auto i = top_items->begin(); i != top_items->end(); ++i) {
DCHECK(!(*i)->id().extension_key.webview_instance_id);
(*i)->GetFlattenedSubtree(&all_items);
}
}
for (TestObserver& observer : observers_)
observer.WillWriteToStorage(extension->id());
if (store_) {
store_->SetExtensionValue(extension->id(), kContextMenusKey,
base::Value(MenuItemsToValue(all_items)));
}
}
void MenuManager::ReadFromStorage(const std::string& extension_id,
absl::optional<base::Value> value) {
const Extension* extension = ExtensionRegistry::Get(browser_context_)
->enabled_extensions()
.GetByID(extension_id);
if (!extension)
return;
MenuItem::OwnedList items = MenuItemsFromValue(extension_id, value);
for (auto& item : items) {
if (item->parent_id()) {
// Parent IDs are stored in the parent_id field for convenience, but
// they have not yet been validated. Separate them out here.
// Because of the order in which we store items in the prefs, parents will
// precede children, so we should already know about any parent items.
std::unique_ptr<MenuItem::Id> parent_id;
parent_id.swap(item->parent_id_);
AddChildItem(*parent_id, std::move(item));
} else {
AddContextItem(extension, std::move(item));
}
}
for (TestObserver& observer : observers_)
observer.DidReadFromStorage(extension_id);
}
void MenuManager::OnExtensionLoaded(content::BrowserContext* browser_context,
const Extension* extension) {
if (store_ && BackgroundInfo::HasLazyContext(extension)) {
store_->GetExtensionValue(
extension->id(), kContextMenusKey,
base::BindOnce(&MenuManager::ReadFromStorage,
weak_ptr_factory_.GetWeakPtr(), extension->id()));
}
}
void MenuManager::OnExtensionUnloaded(content::BrowserContext* browser_context,
const Extension* extension,
UnloadedExtensionReason reason) {
MenuItem::ExtensionKey extension_key(extension->id());
if (base::Contains(context_items_, extension_key)) {
RemoveAllContextItems(extension_key);
}
}
void MenuManager::OnOffTheRecordProfileCreated(Profile* off_the_record) {
observed_profiles_.AddObservation(off_the_record);
}
void MenuManager::OnProfileWillBeDestroyed(Profile* profile) {
observed_profiles_.RemoveObservation(profile);
if (profile->IsOffTheRecord())
RemoveAllIncognitoContextItems();
}
gfx::Image MenuManager::GetIconForExtension(const std::string& extension_id) {
return icon_manager_.GetIcon(extension_id);
}
void MenuManager::RemoveAllIncognitoContextItems() {
// Get all context menu items with "incognito" set to "split".
std::set<MenuItem::Id> items_to_remove;
for (auto iter = items_by_id_.begin(); iter != items_by_id_.end(); ++iter) {
if (iter->first.incognito)
items_to_remove.insert(iter->first);
}
for (auto remove_iter = items_to_remove.begin();
remove_iter != items_to_remove.end(); ++remove_iter)
RemoveContextMenuItem(*remove_iter);
}
void MenuManager::AddObserver(TestObserver* observer) {
observers_.AddObserver(observer);
}
void MenuManager::RemoveObserver(TestObserver* observer) {
observers_.RemoveObserver(observer);
}
MenuItem::ExtensionKey::ExtensionKey()
: webview_embedder_process_id(ChildProcessHost::kInvalidUniqueID),
webview_instance_id(kInstanceIDNone) {}
MenuItem::ExtensionKey::ExtensionKey(const std::string& extension_id)
: extension_id(extension_id),
webview_embedder_process_id(ChildProcessHost::kInvalidUniqueID),
webview_instance_id(kInstanceIDNone) {
DCHECK(!extension_id.empty());
}
MenuItem::ExtensionKey::ExtensionKey(const std::string& extension_id,
int webview_embedder_process_id,
int webview_instance_id)
: extension_id(extension_id),
webview_embedder_process_id(webview_embedder_process_id),
webview_instance_id(webview_instance_id) {
DCHECK(webview_embedder_process_id != ChildProcessHost::kInvalidUniqueID &&
webview_instance_id != kInstanceIDNone);
}
bool MenuItem::ExtensionKey::operator==(const ExtensionKey& other) const {
bool webview_ids_match = webview_instance_id == other.webview_instance_id &&
webview_embedder_process_id == other.webview_embedder_process_id;
// If either extension ID is empty, then these ExtensionKeys will be matched
// only based on the other IDs.
if (extension_id.empty() || other.extension_id.empty())
return webview_ids_match;
return extension_id == other.extension_id && webview_ids_match;
}
bool MenuItem::ExtensionKey::operator<(const ExtensionKey& other) const {
if (webview_embedder_process_id != other.webview_embedder_process_id)
return webview_embedder_process_id < other.webview_embedder_process_id;
if (webview_instance_id != other.webview_instance_id)
return webview_instance_id < other.webview_instance_id;
// If either extension ID is empty, then these ExtensionKeys will be compared
// only based on the other IDs.
if (extension_id.empty() || other.extension_id.empty())
return false;
return extension_id < other.extension_id;
}
bool MenuItem::ExtensionKey::operator!=(const ExtensionKey& other) const {
return !(*this == other);
}
bool MenuItem::ExtensionKey::empty() const {
return extension_id.empty() &&
webview_embedder_process_id == ChildProcessHost::kInvalidUniqueID &&
webview_instance_id == kInstanceIDNone;
}
MenuItem::Id::Id() : incognito(false), uid(0) {}
MenuItem::Id::Id(bool incognito, const MenuItem::ExtensionKey& extension_key)
: incognito(incognito), extension_key(extension_key), uid(0) {}
MenuItem::Id::~Id() {
}
bool MenuItem::Id::operator==(const Id& other) const {
return (incognito == other.incognito &&
extension_key == other.extension_key && uid == other.uid &&
string_uid == other.string_uid);
}
bool MenuItem::Id::operator!=(const Id& other) const {
return !(*this == other);
}
bool MenuItem::Id::operator<(const Id& other) const {
return std::tie(incognito, extension_key, uid, string_uid) <
std::tie(other.incognito, other.extension_key, other.uid, other.string_uid);
}
} // namespace extensions