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