// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/extensions/extension_menu_manager.h"
#include <algorithm>
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/stl_util-inl.h"
#include "base/string_util.h"
#include "base/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_event_router.h"
#include "chrome/browser/extensions/extension_tabs_module.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/extensions/extension.h"
#include "content/common/notification_service.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/favicon_size.h"
#include "webkit/glue/context_menu.h"
ExtensionMenuItem::ExtensionMenuItem(const Id& id,
const std::string& title,
bool checked,
Type type,
const ContextList& contexts)
: id_(id),
title_(title),
type_(type),
checked_(checked),
contexts_(contexts),
parent_id_(0) {
}
ExtensionMenuItem::~ExtensionMenuItem() {
STLDeleteElements(&children_);
}
ExtensionMenuItem* ExtensionMenuItem::ReleaseChild(const Id& child_id,
bool recursive) {
for (List::iterator i = children_.begin(); i != children_.end(); ++i) {
ExtensionMenuItem* child = NULL;
if ((*i)->id() == child_id) {
child = *i;
children_.erase(i);
return child;
} else if (recursive) {
child = (*i)->ReleaseChild(child_id, recursive);
if (child)
return child;
}
}
return NULL;
}
std::set<ExtensionMenuItem::Id> ExtensionMenuItem::RemoveAllDescendants() {
std::set<Id> result;
for (List::iterator i = children_.begin(); i != children_.end(); ++i) {
ExtensionMenuItem* child = *i;
result.insert(child->id());
std::set<Id> removed = child->RemoveAllDescendants();
result.insert(removed.begin(), removed.end());
}
STLDeleteElements(&children_);
return result;
}
string16 ExtensionMenuItem::TitleWithReplacement(
const string16& selection, size_t max_length) const {
string16 result = UTF8ToUTF16(title_);
// TODO(asargent) - Change this to properly handle %% escaping so you can
// put "%s" in titles that won't get substituted.
ReplaceSubstringsAfterOffset(&result, 0, ASCIIToUTF16("%s"), selection);
if (result.length() > max_length)
result = l10n_util::TruncateString(result, max_length);
return result;
}
bool ExtensionMenuItem::SetChecked(bool checked) {
if (type_ != CHECKBOX && type_ != RADIO)
return false;
checked_ = checked;
return true;
}
void ExtensionMenuItem::AddChild(ExtensionMenuItem* item) {
item->parent_id_.reset(new Id(id_));
children_.push_back(item);
}
const int ExtensionMenuManager::kAllowedSchemes =
URLPattern::SCHEME_HTTP | URLPattern::SCHEME_HTTPS;
ExtensionMenuManager::ExtensionMenuManager() {
registrar_.Add(this, NotificationType::EXTENSION_UNLOADED,
NotificationService::AllSources());
}
ExtensionMenuManager::~ExtensionMenuManager() {
MenuItemMap::iterator i;
for (i = context_items_.begin(); i != context_items_.end(); ++i) {
STLDeleteElements(&(i->second));
}
}
std::set<std::string> ExtensionMenuManager::ExtensionIds() {
std::set<std::string> id_set;
for (MenuItemMap::const_iterator i = context_items_.begin();
i != context_items_.end(); ++i) {
id_set.insert(i->first);
}
return id_set;
}
const ExtensionMenuItem::List* ExtensionMenuManager::MenuItems(
const std::string& extension_id) {
MenuItemMap::iterator i = context_items_.find(extension_id);
if (i != context_items_.end()) {
return &(i->second);
}
return NULL;
}
bool ExtensionMenuManager::AddContextItem(const Extension* extension,
ExtensionMenuItem* item) {
const std::string& extension_id = item->extension_id();
// The item must have a non-empty extension id, and not have already been
// added.
if (extension_id.empty() || ContainsKey(items_by_id_, item->id()))
return false;
DCHECK_EQ(extension->id(), extension_id);
bool first_item = !ContainsKey(context_items_, extension_id);
context_items_[extension_id].push_back(item);
items_by_id_[item->id()] = item;
if (item->type() == ExtensionMenuItem::RADIO && item->checked())
RadioItemSelected(item);
// If this is the first item for this extension, start loading its icon.
if (first_item)
icon_manager_.LoadIcon(extension);
return true;
}
bool ExtensionMenuManager::AddChildItem(const ExtensionMenuItem::Id& parent_id,
ExtensionMenuItem* child) {
ExtensionMenuItem* parent = GetItemById(parent_id);
if (!parent || parent->type() != ExtensionMenuItem::NORMAL ||
parent->extension_id() != child->extension_id() ||
ContainsKey(items_by_id_, child->id()))
return false;
parent->AddChild(child);
items_by_id_[child->id()] = child;
return true;
}
bool ExtensionMenuManager::DescendantOf(
ExtensionMenuItem* item,
const ExtensionMenuItem::Id& ancestor_id) {
// Work our way up the tree until we find the ancestor or NULL.
ExtensionMenuItem::Id* id = item->parent_id();
while (id != NULL) {
DCHECK(*id != item->id()); // Catch circular graphs.
if (*id == ancestor_id)
return true;
ExtensionMenuItem* next = GetItemById(*id);
if (!next) {
NOTREACHED();
return false;
}
id = next->parent_id();
}
return false;
}
bool ExtensionMenuManager::ChangeParent(
const ExtensionMenuItem::Id& child_id,
const ExtensionMenuItem::Id* parent_id) {
ExtensionMenuItem* child = GetItemById(child_id);
ExtensionMenuItem* new_parent = parent_id ? GetItemById(*parent_id) : NULL;
if ((parent_id && (child_id == *parent_id)) || !child ||
(!new_parent && parent_id != NULL) ||
(new_parent && (DescendantOf(new_parent, child_id) ||
child->extension_id() != new_parent->extension_id())))
return false;
ExtensionMenuItem::Id* old_parent_id = child->parent_id();
if (old_parent_id != NULL) {
ExtensionMenuItem* old_parent = GetItemById(*old_parent_id);
if (!old_parent) {
NOTREACHED();
return false;
}
ExtensionMenuItem* taken =
old_parent->ReleaseChild(child_id, false /* non-recursive search*/);
DCHECK(taken == child);
} else {
// This is a top-level item, so we need to pull it out of our list of
// top-level items.
MenuItemMap::iterator i = context_items_.find(child->extension_id());
if (i == context_items_.end()) {
NOTREACHED();
return false;
}
ExtensionMenuItem::List& list = i->second;
ExtensionMenuItem::List::iterator j = std::find(list.begin(), list.end(),
child);
if (j == list.end()) {
NOTREACHED();
return false;
}
list.erase(j);
}
if (new_parent) {
new_parent->AddChild(child);
} else {
context_items_[child->extension_id()].push_back(child);
child->parent_id_.reset(NULL);
}
return true;
}
bool ExtensionMenuManager::RemoveContextMenuItem(
const ExtensionMenuItem::Id& id) {
if (!ContainsKey(items_by_id_, id))
return false;
ExtensionMenuItem* menu_item = GetItemById(id);
DCHECK(menu_item);
std::string extension_id = menu_item->extension_id();
MenuItemMap::iterator i = context_items_.find(extension_id);
if (i == context_items_.end()) {
NOTREACHED();
return false;
}
bool result = false;
std::set<ExtensionMenuItem::Id> items_removed;
ExtensionMenuItem::List& list = i->second;
ExtensionMenuItem::List::iterator j;
for (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);
delete *j;
list.erase(j);
result = true;
break;
} else {
// See if the item to remove was found as a descendant of the current
// top-level item.
ExtensionMenuItem* child = (*j)->ReleaseChild(id, true /* recursive */);
if (child) {
items_removed = child->RemoveAllDescendants();
items_removed.insert(id);
delete child;
result = true;
break;
}
}
}
DCHECK(result); // The check at the very top should have prevented this.
// Clear entries from the items_by_id_ map.
std::set<ExtensionMenuItem::Id>::iterator removed_iter;
for (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_id);
icon_manager_.RemoveIcon(extension_id);
}
return result;
}
void ExtensionMenuManager::RemoveAllContextItems(
const std::string& extension_id) {
ExtensionMenuItem::List::iterator i;
for (i = context_items_[extension_id].begin();
i != context_items_[extension_id].end(); ++i) {
ExtensionMenuItem* item = *i;
items_by_id_.erase(item->id());
// Remove descendants from this item and erase them from the lookup cache.
std::set<ExtensionMenuItem::Id> removed_ids = item->RemoveAllDescendants();
std::set<ExtensionMenuItem::Id>::const_iterator j;
for (j = removed_ids.begin(); j != removed_ids.end(); ++j) {
items_by_id_.erase(*j);
}
}
STLDeleteElements(&context_items_[extension_id]);
context_items_.erase(extension_id);
icon_manager_.RemoveIcon(extension_id);
}
ExtensionMenuItem* ExtensionMenuManager::GetItemById(
const ExtensionMenuItem::Id& id) const {
std::map<ExtensionMenuItem::Id, ExtensionMenuItem*>::const_iterator i =
items_by_id_.find(id);
if (i != items_by_id_.end())
return i->second;
else
return NULL;
}
void ExtensionMenuManager::RadioItemSelected(ExtensionMenuItem* 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 ExtensionMenuItem::List* list = NULL;
if (item->parent_id()) {
ExtensionMenuItem* parent = GetItemById(*item->parent_id());
if (!parent) {
NOTREACHED();
return;
}
list = &(parent->children());
} else {
if (context_items_.find(item->extension_id()) == context_items_.end()) {
NOTREACHED();
return;
}
list = &context_items_[item->extension_id()];
}
// Find where |item| is in the list.
ExtensionMenuItem::List::const_iterator item_location;
for (item_location = list->begin(); item_location != list->end();
++item_location) {
if (*item_location == 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.
ExtensionMenuItem::List::const_iterator i;
if (item_location != list->begin()) {
i = item_location;
do {
--i;
if ((*i)->type() != ExtensionMenuItem::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() != ExtensionMenuItem::RADIO)
break;
(*i)->SetChecked(false);
}
}
static void AddURLProperty(DictionaryValue* dictionary,
const std::string& key, const GURL& url) {
if (!url.is_empty())
dictionary->SetString(key, url.possibly_invalid_spec());
}
void ExtensionMenuManager::ExecuteCommand(
Profile* profile,
TabContents* tab_contents,
const ContextMenuParams& params,
const ExtensionMenuItem::Id& menuItemId) {
ExtensionEventRouter* event_router = profile->GetExtensionEventRouter();
if (!event_router)
return;
ExtensionMenuItem* item = GetItemById(menuItemId);
if (!item)
return;
if (item->type() == ExtensionMenuItem::RADIO)
RadioItemSelected(item);
ListValue args;
DictionaryValue* properties = new DictionaryValue();
properties->SetInteger("menuItemId", item->id().uid);
if (item->parent_id())
properties->SetInteger("parentMenuItemId", item->parent_id()->uid);
switch (params.media_type) {
case WebKit::WebContextMenuData::MediaTypeImage:
properties->SetString("mediaType", "image");
break;
case WebKit::WebContextMenuData::MediaTypeVideo:
properties->SetString("mediaType", "video");
break;
case WebKit::WebContextMenuData::MediaTypeAudio:
properties->SetString("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->SetString("selectionText", params.selection_text);
properties->SetBoolean("editable", params.is_editable);
args.Append(properties);
// Add the tab info to the argument list.
if (tab_contents) {
args.Append(ExtensionTabUtil::CreateTabValue(tab_contents));
} else {
args.Append(new DictionaryValue());
}
if (item->type() == ExtensionMenuItem::CHECKBOX ||
item->type() == ExtensionMenuItem::RADIO) {
bool was_checked = item->checked();
properties->SetBoolean("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() == ExtensionMenuItem::RADIO) ? true : !was_checked;
item->SetChecked(checked);
properties->SetBoolean("checked", item->checked());
}
std::string json_args;
base::JSONWriter::Write(&args, false, &json_args);
std::string event_name = "contextMenus";
event_router->DispatchEventToExtension(
item->extension_id(), event_name, json_args, profile, GURL());
}
void ExtensionMenuManager::Observe(NotificationType type,
const NotificationSource& source,
const NotificationDetails& details) {
// Remove menu items for disabled/uninstalled extensions.
if (type != NotificationType::EXTENSION_UNLOADED) {
NOTREACHED();
return;
}
const Extension* extension =
Details<UnloadedExtensionInfo>(details)->extension;
if (ContainsKey(context_items_, extension->id())) {
RemoveAllContextItems(extension->id());
}
}
const SkBitmap& ExtensionMenuManager::GetIconForExtension(
const std::string& extension_id) {
return icon_manager_.GetIcon(extension_id);
}
// static
bool ExtensionMenuManager::HasAllowedScheme(const GURL& url) {
URLPattern pattern(kAllowedSchemes);
return pattern.SetScheme(url.scheme());
}
ExtensionMenuItem::Id::Id()
: profile(NULL), uid(0) {
}
ExtensionMenuItem::Id::Id(Profile* profile,
const std::string& extension_id,
int uid)
: profile(profile), extension_id(extension_id), uid(uid) {
}
ExtensionMenuItem::Id::~Id() {
}
bool ExtensionMenuItem::Id::operator==(const Id& other) const {
return (profile == other.profile &&
extension_id == other.extension_id &&
uid == other.uid);
}
bool ExtensionMenuItem::Id::operator!=(const Id& other) const {
return !(*this == other);
}
bool ExtensionMenuItem::Id::operator<(const Id& other) const {
if (profile < other.profile)
return true;
if (profile == other.profile) {
if (extension_id < other.extension_id)
return true;
if (extension_id == other.extension_id)
return uid < other.uid;
}
return false;
}