Home | History | Annotate | Download | only in extensions
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "chrome/browser/extensions/menu_manager.h"
      6 
      7 #include <algorithm>
      8 
      9 #include "base/json/json_writer.h"
     10 #include "base/logging.h"
     11 #include "base/stl_util.h"
     12 #include "base/strings/string_util.h"
     13 #include "base/strings/utf_string_conversions.h"
     14 #include "base/values.h"
     15 #include "chrome/browser/chrome_notification_types.h"
     16 #include "chrome/browser/extensions/event_names.h"
     17 #include "chrome/browser/extensions/event_router.h"
     18 #include "chrome/browser/extensions/extension_service.h"
     19 #include "chrome/browser/extensions/extension_system.h"
     20 #include "chrome/browser/extensions/extension_tab_util.h"
     21 #include "chrome/browser/extensions/state_store.h"
     22 #include "chrome/browser/extensions/tab_helper.h"
     23 #include "chrome/browser/profiles/profile.h"
     24 #include "chrome/common/extensions/background_info.h"
     25 #include "chrome/common/extensions/extension.h"
     26 #include "content/public/browser/notification_details.h"
     27 #include "content/public/browser/notification_source.h"
     28 #include "content/public/browser/web_contents.h"
     29 #include "content/public/common/context_menu_params.h"
     30 #include "ui/base/text/text_elider.h"
     31 #include "ui/gfx/favicon_size.h"
     32 
     33 using content::WebContents;
     34 using extensions::ExtensionSystem;
     35 
     36 namespace extensions {
     37 
     38 namespace {
     39 
     40 // Keys for serialization to and from Value to store in the preferences.
     41 const char kContextMenusKey[] = "context_menus";
     42 
     43 const char kCheckedKey[] = "checked";
     44 const char kContextsKey[] = "contexts";
     45 const char kDocumentURLPatternsKey[] = "document_url_patterns";
     46 const char kEnabledKey[] = "enabled";
     47 const char kIncognitoKey[] = "incognito";
     48 const char kParentUIDKey[] = "parent_uid";
     49 const char kStringUIDKey[] = "string_uid";
     50 const char kTargetURLPatternsKey[] = "target_url_patterns";
     51 const char kTitleKey[] = "title";
     52 const char kTypeKey[] = "type";
     53 
     54 void SetIdKeyValue(base::DictionaryValue* properties,
     55                    const char* key,
     56                    const MenuItem::Id& id) {
     57   if (id.uid == 0)
     58     properties->SetString(key, id.string_uid);
     59   else
     60     properties->SetInteger(key, id.uid);
     61 }
     62 
     63 MenuItem::List MenuItemsFromValue(const std::string& extension_id,
     64                                   base::Value* value) {
     65   MenuItem::List items;
     66 
     67   base::ListValue* list = NULL;
     68   if (!value || !value->GetAsList(&list))
     69     return items;
     70 
     71   for (size_t i = 0; i < list->GetSize(); ++i) {
     72     base::DictionaryValue* dict = NULL;
     73     if (!list->GetDictionary(i, &dict))
     74       continue;
     75     MenuItem* item = MenuItem::Populate(
     76         extension_id, *dict, NULL);
     77     if (!item)
     78       continue;
     79     items.push_back(item);
     80   }
     81   return items;
     82 }
     83 
     84 scoped_ptr<base::Value> MenuItemsToValue(const MenuItem::List& items) {
     85   scoped_ptr<base::ListValue> list(new base::ListValue());
     86   for (size_t i = 0; i < items.size(); ++i)
     87     list->Append(items[i]->ToValue().release());
     88   return scoped_ptr<Value>(list.release());
     89 }
     90 
     91 bool GetStringList(const DictionaryValue& dict,
     92                    const std::string& key,
     93                    std::vector<std::string>* out) {
     94   if (!dict.HasKey(key))
     95     return true;
     96 
     97   const base::ListValue* list = NULL;
     98   if (!dict.GetListWithoutPathExpansion(key, &list))
     99     return false;
    100 
    101   for (size_t i = 0; i < list->GetSize(); ++i) {
    102     std::string pattern;
    103     if (!list->GetString(i, &pattern))
    104       return false;
    105     out->push_back(pattern);
    106   }
    107 
    108   return true;
    109 }
    110 
    111 }  // namespace
    112 
    113 MenuItem::MenuItem(const Id& id,
    114                    const std::string& title,
    115                    bool checked,
    116                    bool enabled,
    117                    Type type,
    118                    const ContextList& contexts)
    119     : id_(id),
    120       title_(title),
    121       type_(type),
    122       checked_(checked),
    123       enabled_(enabled),
    124       contexts_(contexts) {}
    125 
    126 MenuItem::~MenuItem() {
    127   STLDeleteElements(&children_);
    128 }
    129 
    130 MenuItem* MenuItem::ReleaseChild(const Id& child_id,
    131                                  bool recursive) {
    132   for (List::iterator i = children_.begin(); i != children_.end(); ++i) {
    133     MenuItem* child = NULL;
    134     if ((*i)->id() == child_id) {
    135       child = *i;
    136       children_.erase(i);
    137       return child;
    138     } else if (recursive) {
    139       child = (*i)->ReleaseChild(child_id, recursive);
    140       if (child)
    141         return child;
    142     }
    143   }
    144   return NULL;
    145 }
    146 
    147 void MenuItem::GetFlattenedSubtree(MenuItem::List* list) {
    148   list->push_back(this);
    149   for (List::iterator i = children_.begin(); i != children_.end(); ++i)
    150     (*i)->GetFlattenedSubtree(list);
    151 }
    152 
    153 std::set<MenuItem::Id> MenuItem::RemoveAllDescendants() {
    154   std::set<Id> result;
    155   for (List::iterator i = children_.begin(); i != children_.end(); ++i) {
    156     MenuItem* child = *i;
    157     result.insert(child->id());
    158     std::set<Id> removed = child->RemoveAllDescendants();
    159     result.insert(removed.begin(), removed.end());
    160   }
    161   STLDeleteElements(&children_);
    162   return result;
    163 }
    164 
    165 string16 MenuItem::TitleWithReplacement(
    166     const string16& selection, size_t max_length) const {
    167   string16 result = UTF8ToUTF16(title_);
    168   // TODO(asargent) - Change this to properly handle %% escaping so you can
    169   // put "%s" in titles that won't get substituted.
    170   ReplaceSubstringsAfterOffset(&result, 0, ASCIIToUTF16("%s"), selection);
    171 
    172   if (result.length() > max_length)
    173     result = ui::TruncateString(result, max_length);
    174   return result;
    175 }
    176 
    177 bool MenuItem::SetChecked(bool checked) {
    178   if (type_ != CHECKBOX && type_ != RADIO)
    179     return false;
    180   checked_ = checked;
    181   return true;
    182 }
    183 
    184 void MenuItem::AddChild(MenuItem* item) {
    185   item->parent_id_.reset(new Id(id_));
    186   children_.push_back(item);
    187 }
    188 
    189 scoped_ptr<DictionaryValue> MenuItem::ToValue() const {
    190   scoped_ptr<DictionaryValue> value(new DictionaryValue);
    191   // Should only be called for extensions with event pages, which only have
    192   // string IDs for items.
    193   DCHECK_EQ(0, id_.uid);
    194   value->SetString(kStringUIDKey, id_.string_uid);
    195   value->SetBoolean(kIncognitoKey, id_.incognito);
    196   value->SetInteger(kTypeKey, type_);
    197   if (type_ != SEPARATOR)
    198     value->SetString(kTitleKey, title_);
    199   if (type_ == CHECKBOX || type_ == RADIO)
    200     value->SetBoolean(kCheckedKey, checked_);
    201   value->SetBoolean(kEnabledKey, enabled_);
    202   value->Set(kContextsKey, contexts_.ToValue().release());
    203   if (parent_id_) {
    204     DCHECK_EQ(0, parent_id_->uid);
    205     value->SetString(kParentUIDKey, parent_id_->string_uid);
    206   }
    207   value->Set(kDocumentURLPatternsKey,
    208              document_url_patterns_.ToValue().release());
    209   value->Set(kTargetURLPatternsKey, target_url_patterns_.ToValue().release());
    210   return value.Pass();
    211 }
    212 
    213 // static
    214 MenuItem* MenuItem::Populate(const std::string& extension_id,
    215                              const DictionaryValue& value,
    216                              std::string* error) {
    217   bool incognito = false;
    218   if (!value.GetBoolean(kIncognitoKey, &incognito))
    219     return NULL;
    220   Id id(incognito, extension_id);
    221   if (!value.GetString(kStringUIDKey, &id.string_uid))
    222     return NULL;
    223   int type_int;
    224   Type type = NORMAL;
    225   if (!value.GetInteger(kTypeKey, &type_int))
    226     return NULL;
    227   type = static_cast<Type>(type_int);
    228   std::string title;
    229   if (type != SEPARATOR && !value.GetString(kTitleKey, &title))
    230     return NULL;
    231   bool checked = false;
    232   if ((type == CHECKBOX || type == RADIO) &&
    233       !value.GetBoolean(kCheckedKey, &checked)) {
    234     return NULL;
    235   }
    236   bool enabled = true;
    237   if (!value.GetBoolean(kEnabledKey, &enabled))
    238     return NULL;
    239   ContextList contexts;
    240   const Value* contexts_value = NULL;
    241   if (!value.Get(kContextsKey, &contexts_value))
    242     return NULL;
    243   if (!contexts.Populate(*contexts_value))
    244     return NULL;
    245 
    246   scoped_ptr<MenuItem> result(new MenuItem(
    247       id, title, checked, enabled, type, contexts));
    248 
    249   std::vector<std::string> document_url_patterns;
    250   if (!GetStringList(value, kDocumentURLPatternsKey, &document_url_patterns))
    251     return NULL;
    252   std::vector<std::string> target_url_patterns;
    253   if (!GetStringList(value, kTargetURLPatternsKey, &target_url_patterns))
    254     return NULL;
    255 
    256   if (!result->PopulateURLPatterns(&document_url_patterns,
    257                                    &target_url_patterns,
    258                                    error)) {
    259     return NULL;
    260   }
    261 
    262   // parent_id is filled in from the value, but it might not be valid. It's left
    263   // to be validated upon being added (via AddChildItem) to the menu manager.
    264   scoped_ptr<Id> parent_id(new Id(incognito, extension_id));
    265   if (value.HasKey(kParentUIDKey)) {
    266     if (!value.GetString(kParentUIDKey, &parent_id->string_uid))
    267       return NULL;
    268     result->parent_id_.swap(parent_id);
    269   }
    270   return result.release();
    271 }
    272 
    273 bool MenuItem::PopulateURLPatterns(
    274     std::vector<std::string>* document_url_patterns,
    275     std::vector<std::string>* target_url_patterns,
    276     std::string* error) {
    277   if (document_url_patterns) {
    278     if (!document_url_patterns_.Populate(
    279             *document_url_patterns, URLPattern::SCHEME_ALL, true, error)) {
    280       return false;
    281     }
    282   }
    283   if (target_url_patterns) {
    284     if (!target_url_patterns_.Populate(
    285             *target_url_patterns, URLPattern::SCHEME_ALL, true, error)) {
    286       return false;
    287     }
    288   }
    289   return true;
    290 }
    291 
    292 MenuManager::MenuManager(Profile* profile)
    293     : profile_(profile) {
    294   registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_LOADED,
    295                  content::Source<Profile>(profile));
    296   registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNLOADED,
    297                  content::Source<Profile>(profile));
    298 
    299   StateStore* store = ExtensionSystem::Get(profile_)->state_store();
    300   if (store)
    301     store->RegisterKey(kContextMenusKey);
    302 }
    303 
    304 MenuManager::~MenuManager() {
    305   MenuItemMap::iterator i;
    306   for (i = context_items_.begin(); i != context_items_.end(); ++i) {
    307     STLDeleteElements(&(i->second));
    308   }
    309 }
    310 
    311 std::set<std::string> MenuManager::ExtensionIds() {
    312   std::set<std::string> id_set;
    313   for (MenuItemMap::const_iterator i = context_items_.begin();
    314        i != context_items_.end(); ++i) {
    315     id_set.insert(i->first);
    316   }
    317   return id_set;
    318 }
    319 
    320 const MenuItem::List* MenuManager::MenuItems(
    321     const std::string& extension_id) {
    322   MenuItemMap::iterator i = context_items_.find(extension_id);
    323   if (i != context_items_.end()) {
    324     return &(i->second);
    325   }
    326   return NULL;
    327 }
    328 
    329 bool MenuManager::AddContextItem(
    330     const Extension* extension,
    331     MenuItem* item) {
    332   const std::string& extension_id = item->extension_id();
    333   // The item must have a non-empty extension id, and not have already been
    334   // added.
    335   if (extension_id.empty() || ContainsKey(items_by_id_, item->id()))
    336     return false;
    337 
    338   DCHECK_EQ(extension->id(), extension_id);
    339 
    340   bool first_item = !ContainsKey(context_items_, extension_id);
    341   context_items_[extension_id].push_back(item);
    342   items_by_id_[item->id()] = item;
    343 
    344   if (item->type() == MenuItem::RADIO) {
    345     if (item->checked())
    346       RadioItemSelected(item);
    347     else
    348       SanitizeRadioList(context_items_[extension_id]);
    349   }
    350 
    351   // If this is the first item for this extension, start loading its icon.
    352   if (first_item)
    353     icon_manager_.LoadIcon(profile_, extension);
    354 
    355   return true;
    356 }
    357 
    358 bool MenuManager::AddChildItem(const MenuItem::Id& parent_id,
    359                                MenuItem* child) {
    360   MenuItem* parent = GetItemById(parent_id);
    361   if (!parent || parent->type() != MenuItem::NORMAL ||
    362       parent->incognito() != child->incognito() ||
    363       parent->extension_id() != child->extension_id() ||
    364       ContainsKey(items_by_id_, child->id()))
    365     return false;
    366   parent->AddChild(child);
    367   items_by_id_[child->id()] = child;
    368 
    369   if (child->type() == MenuItem::RADIO)
    370     SanitizeRadioList(parent->children());
    371   return true;
    372 }
    373 
    374 bool MenuManager::DescendantOf(MenuItem* item,
    375                                const MenuItem::Id& ancestor_id) {
    376   // Work our way up the tree until we find the ancestor or NULL.
    377   MenuItem::Id* id = item->parent_id();
    378   while (id != NULL) {
    379     DCHECK(*id != item->id());  // Catch circular graphs.
    380     if (*id == ancestor_id)
    381       return true;
    382     MenuItem* next = GetItemById(*id);
    383     if (!next) {
    384       NOTREACHED();
    385       return false;
    386     }
    387     id = next->parent_id();
    388   }
    389   return false;
    390 }
    391 
    392 bool MenuManager::ChangeParent(const MenuItem::Id& child_id,
    393                                const MenuItem::Id* parent_id) {
    394   MenuItem* child = GetItemById(child_id);
    395   MenuItem* new_parent = parent_id ? GetItemById(*parent_id) : NULL;
    396   if ((parent_id && (child_id == *parent_id)) || !child ||
    397       (!new_parent && parent_id != NULL) ||
    398       (new_parent && (DescendantOf(new_parent, child_id) ||
    399                       child->incognito() != new_parent->incognito() ||
    400                       child->extension_id() != new_parent->extension_id())))
    401     return false;
    402 
    403   MenuItem::Id* old_parent_id = child->parent_id();
    404   if (old_parent_id != NULL) {
    405     MenuItem* old_parent = GetItemById(*old_parent_id);
    406     if (!old_parent) {
    407       NOTREACHED();
    408       return false;
    409     }
    410     MenuItem* taken =
    411       old_parent->ReleaseChild(child_id, false /* non-recursive search*/);
    412     DCHECK(taken == child);
    413     SanitizeRadioList(old_parent->children());
    414   } else {
    415     // This is a top-level item, so we need to pull it out of our list of
    416     // top-level items.
    417     MenuItemMap::iterator i = context_items_.find(child->extension_id());
    418     if (i == context_items_.end()) {
    419       NOTREACHED();
    420       return false;
    421     }
    422     MenuItem::List& list = i->second;
    423     MenuItem::List::iterator j = std::find(list.begin(), list.end(),
    424                                                     child);
    425     if (j == list.end()) {
    426       NOTREACHED();
    427       return false;
    428     }
    429     list.erase(j);
    430     SanitizeRadioList(list);
    431   }
    432 
    433   if (new_parent) {
    434     new_parent->AddChild(child);
    435     SanitizeRadioList(new_parent->children());
    436   } else {
    437     context_items_[child->extension_id()].push_back(child);
    438     child->parent_id_.reset(NULL);
    439     SanitizeRadioList(context_items_[child->extension_id()]);
    440   }
    441   return true;
    442 }
    443 
    444 bool MenuManager::RemoveContextMenuItem(const MenuItem::Id& id) {
    445   if (!ContainsKey(items_by_id_, id))
    446     return false;
    447 
    448   MenuItem* menu_item = GetItemById(id);
    449   DCHECK(menu_item);
    450   std::string extension_id = menu_item->extension_id();
    451   MenuItemMap::iterator i = context_items_.find(extension_id);
    452   if (i == context_items_.end()) {
    453     NOTREACHED();
    454     return false;
    455   }
    456 
    457   bool result = false;
    458   std::set<MenuItem::Id> items_removed;
    459   MenuItem::List& list = i->second;
    460   MenuItem::List::iterator j;
    461   for (j = list.begin(); j < list.end(); ++j) {
    462     // See if the current top-level item is a match.
    463     if ((*j)->id() == id) {
    464       items_removed = (*j)->RemoveAllDescendants();
    465       items_removed.insert(id);
    466       delete *j;
    467       list.erase(j);
    468       result = true;
    469       SanitizeRadioList(list);
    470       break;
    471     } else {
    472       // See if the item to remove was found as a descendant of the current
    473       // top-level item.
    474       MenuItem* child = (*j)->ReleaseChild(id, true /* recursive */);
    475       if (child) {
    476         items_removed = child->RemoveAllDescendants();
    477         items_removed.insert(id);
    478         SanitizeRadioList(GetItemById(*child->parent_id())->children());
    479         delete child;
    480         result = true;
    481         break;
    482       }
    483     }
    484   }
    485   DCHECK(result);  // The check at the very top should have prevented this.
    486 
    487   // Clear entries from the items_by_id_ map.
    488   std::set<MenuItem::Id>::iterator removed_iter;
    489   for (removed_iter = items_removed.begin();
    490        removed_iter != items_removed.end();
    491        ++removed_iter) {
    492     items_by_id_.erase(*removed_iter);
    493   }
    494 
    495   if (list.empty()) {
    496     context_items_.erase(extension_id);
    497     icon_manager_.RemoveIcon(extension_id);
    498   }
    499   return result;
    500 }
    501 
    502 void MenuManager::RemoveAllContextItems(const std::string& extension_id) {
    503   MenuItem::List::iterator i;
    504   for (i = context_items_[extension_id].begin();
    505        i != context_items_[extension_id].end(); ++i) {
    506     MenuItem* item = *i;
    507     items_by_id_.erase(item->id());
    508 
    509     // Remove descendants from this item and erase them from the lookup cache.
    510     std::set<MenuItem::Id> removed_ids = item->RemoveAllDescendants();
    511     std::set<MenuItem::Id>::const_iterator j;
    512     for (j = removed_ids.begin(); j != removed_ids.end(); ++j) {
    513       items_by_id_.erase(*j);
    514     }
    515   }
    516   STLDeleteElements(&context_items_[extension_id]);
    517   context_items_.erase(extension_id);
    518   icon_manager_.RemoveIcon(extension_id);
    519 }
    520 
    521 MenuItem* MenuManager::GetItemById(const MenuItem::Id& id) const {
    522   std::map<MenuItem::Id, MenuItem*>::const_iterator i =
    523       items_by_id_.find(id);
    524   if (i != items_by_id_.end())
    525     return i->second;
    526   else
    527     return NULL;
    528 }
    529 
    530 void MenuManager::RadioItemSelected(MenuItem* item) {
    531   // If this is a child item, we need to get a handle to the list from its
    532   // parent. Otherwise get a handle to the top-level list.
    533   const MenuItem::List* list = NULL;
    534   if (item->parent_id()) {
    535     MenuItem* parent = GetItemById(*item->parent_id());
    536     if (!parent) {
    537       NOTREACHED();
    538       return;
    539     }
    540     list = &(parent->children());
    541   } else {
    542     if (context_items_.find(item->extension_id()) == context_items_.end()) {
    543       NOTREACHED();
    544       return;
    545     }
    546     list = &context_items_[item->extension_id()];
    547   }
    548 
    549   // Find where |item| is in the list.
    550   MenuItem::List::const_iterator item_location;
    551   for (item_location = list->begin(); item_location != list->end();
    552        ++item_location) {
    553     if (*item_location == item)
    554       break;
    555   }
    556   if (item_location == list->end()) {
    557     NOTREACHED();  // We should have found the item.
    558     return;
    559   }
    560 
    561   // Iterate backwards from |item| and uncheck any adjacent radio items.
    562   MenuItem::List::const_iterator i;
    563   if (item_location != list->begin()) {
    564     i = item_location;
    565     do {
    566       --i;
    567       if ((*i)->type() != MenuItem::RADIO)
    568         break;
    569       (*i)->SetChecked(false);
    570     } while (i != list->begin());
    571   }
    572 
    573   // Now iterate forwards from |item| and uncheck any adjacent radio items.
    574   for (i = item_location + 1; i != list->end(); ++i) {
    575     if ((*i)->type() != MenuItem::RADIO)
    576       break;
    577     (*i)->SetChecked(false);
    578   }
    579 }
    580 
    581 static void AddURLProperty(DictionaryValue* dictionary,
    582                            const std::string& key, const GURL& url) {
    583   if (!url.is_empty())
    584     dictionary->SetString(key, url.possibly_invalid_spec());
    585 }
    586 
    587 void MenuManager::ExecuteCommand(Profile* profile,
    588                                  WebContents* web_contents,
    589                                  const content::ContextMenuParams& params,
    590                                  const MenuItem::Id& menu_item_id) {
    591   EventRouter* event_router = extensions::ExtensionSystem::Get(profile)->
    592       event_router();
    593   if (!event_router)
    594     return;
    595 
    596   MenuItem* item = GetItemById(menu_item_id);
    597   if (!item)
    598     return;
    599 
    600   // ExtensionService/Extension can be NULL in unit tests :(
    601   ExtensionService* service =
    602       ExtensionSystem::Get(profile_)->extension_service();
    603   const Extension* extension = service ?
    604       service->extensions()->GetByID(menu_item_id.extension_id) : NULL;
    605 
    606   if (item->type() == MenuItem::RADIO)
    607     RadioItemSelected(item);
    608 
    609   scoped_ptr<base::ListValue> args(new base::ListValue());
    610 
    611   DictionaryValue* properties = new DictionaryValue();
    612   SetIdKeyValue(properties, "menuItemId", item->id());
    613   if (item->parent_id())
    614     SetIdKeyValue(properties, "parentMenuItemId", *item->parent_id());
    615 
    616   switch (params.media_type) {
    617     case WebKit::WebContextMenuData::MediaTypeImage:
    618       properties->SetString("mediaType", "image");
    619       break;
    620     case WebKit::WebContextMenuData::MediaTypeVideo:
    621       properties->SetString("mediaType", "video");
    622       break;
    623     case WebKit::WebContextMenuData::MediaTypeAudio:
    624       properties->SetString("mediaType", "audio");
    625       break;
    626     default:  {}  // Do nothing.
    627   }
    628 
    629   AddURLProperty(properties, "linkUrl", params.unfiltered_link_url);
    630   AddURLProperty(properties, "srcUrl", params.src_url);
    631   AddURLProperty(properties, "pageUrl", params.page_url);
    632   AddURLProperty(properties, "frameUrl", params.frame_url);
    633 
    634   if (params.selection_text.length() > 0)
    635     properties->SetString("selectionText", params.selection_text);
    636 
    637   properties->SetBoolean("editable", params.is_editable);
    638 
    639   args->Append(properties);
    640 
    641   // Add the tab info to the argument list.
    642   // No tab info in a platform app.
    643   if (!extension || !extension->is_platform_app()) {
    644     // Note: web_contents are NULL in unit tests :(
    645     if (web_contents) {
    646       args->Append(ExtensionTabUtil::CreateTabValue(web_contents));
    647     } else {
    648       args->Append(new DictionaryValue());
    649     }
    650   }
    651 
    652   if (item->type() == MenuItem::CHECKBOX ||
    653       item->type() == MenuItem::RADIO) {
    654     bool was_checked = item->checked();
    655     properties->SetBoolean("wasChecked", was_checked);
    656 
    657     // RADIO items always get set to true when you click on them, but CHECKBOX
    658     // items get their state toggled.
    659     bool checked =
    660         (item->type() == MenuItem::RADIO) ? true : !was_checked;
    661 
    662     item->SetChecked(checked);
    663     properties->SetBoolean("checked", item->checked());
    664 
    665     if (extension)
    666       WriteToStorage(extension);
    667   }
    668 
    669   // Note: web_contents are NULL in unit tests :(
    670   if (web_contents && extensions::TabHelper::FromWebContents(web_contents)) {
    671     extensions::TabHelper::FromWebContents(web_contents)->
    672         active_tab_permission_granter()->GrantIfRequested(extension);
    673   }
    674 
    675   {
    676     scoped_ptr<Event> event(new Event(
    677         event_names::kOnContextMenus,
    678         scoped_ptr<base::ListValue>(args->DeepCopy())));
    679     event->restrict_to_profile = profile;
    680     event->user_gesture = EventRouter::USER_GESTURE_ENABLED;
    681     event_router->DispatchEventToExtension(item->extension_id(), event.Pass());
    682   }
    683   {
    684     scoped_ptr<Event> event(new Event(event_names::kOnContextMenuClicked,
    685                                       args.Pass()));
    686     event->restrict_to_profile = profile;
    687     event->user_gesture = EventRouter::USER_GESTURE_ENABLED;
    688     event_router->DispatchEventToExtension(item->extension_id(), event.Pass());
    689   }
    690 }
    691 
    692 void MenuManager::SanitizeRadioList(const MenuItem::List& item_list) {
    693   MenuItem::List::const_iterator i = item_list.begin();
    694   while (i != item_list.end()) {
    695     if ((*i)->type() != MenuItem::RADIO) {
    696       ++i;
    697       break;
    698     }
    699 
    700     // Uncheck any checked radio items in the run, and at the end reset
    701     // the appropriate one to checked. If no check radio items were found,
    702     // then check the first radio item in the run.
    703     MenuItem::List::const_iterator last_checked = item_list.end();
    704     MenuItem::List::const_iterator radio_run_iter;
    705     for (radio_run_iter = i; radio_run_iter != item_list.end();
    706         ++radio_run_iter) {
    707       if ((*radio_run_iter)->type() != MenuItem::RADIO) {
    708         break;
    709       }
    710 
    711       if ((*radio_run_iter)->checked()) {
    712         last_checked = radio_run_iter;
    713         (*radio_run_iter)->SetChecked(false);
    714       }
    715     }
    716 
    717     if (last_checked != item_list.end())
    718       (*last_checked)->SetChecked(true);
    719     else
    720       (*i)->SetChecked(true);
    721 
    722     i = radio_run_iter;
    723   }
    724 }
    725 
    726 bool MenuManager::ItemUpdated(const MenuItem::Id& id) {
    727   if (!ContainsKey(items_by_id_, id))
    728     return false;
    729 
    730   MenuItem* menu_item = GetItemById(id);
    731   DCHECK(menu_item);
    732 
    733   if (menu_item->parent_id()) {
    734     SanitizeRadioList(GetItemById(*menu_item->parent_id())->children());
    735   } else {
    736     std::string extension_id = menu_item->extension_id();
    737     MenuItemMap::iterator i = context_items_.find(extension_id);
    738     if (i == context_items_.end()) {
    739       NOTREACHED();
    740       return false;
    741     }
    742     SanitizeRadioList(i->second);
    743   }
    744 
    745   return true;
    746 }
    747 
    748 void MenuManager::WriteToStorage(const Extension* extension) {
    749   if (!BackgroundInfo::HasLazyBackgroundPage(extension))
    750     return;
    751   const MenuItem::List* top_items = MenuItems(extension->id());
    752   MenuItem::List all_items;
    753   if (top_items) {
    754     for (MenuItem::List::const_iterator i = top_items->begin();
    755          i != top_items->end(); ++i) {
    756       (*i)->GetFlattenedSubtree(&all_items);
    757     }
    758   }
    759 
    760   StateStore* store = ExtensionSystem::Get(profile_)->state_store();
    761   if (store)
    762     store->SetExtensionValue(extension->id(), kContextMenusKey,
    763                              MenuItemsToValue(all_items));
    764 }
    765 
    766 void MenuManager::ReadFromStorage(const std::string& extension_id,
    767                                   scoped_ptr<base::Value> value) {
    768   const Extension* extension =
    769       ExtensionSystem::Get(profile_)->extension_service()->extensions()->
    770           GetByID(extension_id);
    771   if (!extension)
    772     return;
    773 
    774   MenuItem::List items = MenuItemsFromValue(extension_id, value.get());
    775   for (size_t i = 0; i < items.size(); ++i) {
    776     if (items[i]->parent_id()) {
    777       // Parent IDs are stored in the parent_id field for convenience, but
    778       // they have not yet been validated. Separate them out here.
    779       // Because of the order in which we store items in the prefs, parents will
    780       // precede children, so we should already know about any parent items.
    781       scoped_ptr<MenuItem::Id> parent_id;
    782       parent_id.swap(items[i]->parent_id_);
    783       AddChildItem(*parent_id, items[i]);
    784     } else {
    785       AddContextItem(extension, items[i]);
    786     }
    787   }
    788 }
    789 
    790 void MenuManager::Observe(int type,
    791                           const content::NotificationSource& source,
    792                           const content::NotificationDetails& details) {
    793   if (type == chrome::NOTIFICATION_EXTENSION_UNLOADED) {
    794     // Remove menu items for disabled/uninstalled extensions.
    795     const Extension* extension =
    796         content::Details<UnloadedExtensionInfo>(details)->extension;
    797     if (ContainsKey(context_items_, extension->id())) {
    798       RemoveAllContextItems(extension->id());
    799     }
    800   } else if (type == chrome::NOTIFICATION_EXTENSION_LOADED) {
    801     const Extension* extension =
    802         content::Details<const Extension>(details).ptr();
    803     StateStore* store = ExtensionSystem::Get(profile_)->state_store();
    804     if (store && BackgroundInfo::HasLazyBackgroundPage(extension)) {
    805       store->GetExtensionValue(extension->id(), kContextMenusKey,
    806           base::Bind(&MenuManager::ReadFromStorage,
    807                      AsWeakPtr(), extension->id()));
    808     }
    809   }
    810 }
    811 
    812 const SkBitmap& MenuManager::GetIconForExtension(
    813     const std::string& extension_id) {
    814   return icon_manager_.GetIcon(extension_id);
    815 }
    816 
    817 void MenuManager::RemoveAllIncognitoContextItems() {
    818   // Get all context menu items with "incognito" set to "split".
    819   std::set<MenuItem::Id> items_to_remove;
    820   std::map<MenuItem::Id, MenuItem*>::const_iterator iter;
    821   for (iter = items_by_id_.begin();
    822        iter != items_by_id_.end();
    823        ++iter) {
    824     if (iter->first.incognito)
    825       items_to_remove.insert(iter->first);
    826   }
    827 
    828   std::set<MenuItem::Id>::iterator remove_iter;
    829   for (remove_iter = items_to_remove.begin();
    830        remove_iter != items_to_remove.end();
    831        ++remove_iter)
    832     RemoveContextMenuItem(*remove_iter);
    833 }
    834 
    835 MenuItem::Id::Id() : incognito(false), uid(0) {}
    836 
    837 MenuItem::Id::Id(bool incognito, const std::string& extension_id)
    838     : incognito(incognito), extension_id(extension_id), uid(0) {}
    839 
    840 MenuItem::Id::~Id() {
    841 }
    842 
    843 bool MenuItem::Id::operator==(const Id& other) const {
    844   return (incognito == other.incognito &&
    845           extension_id == other.extension_id &&
    846           uid == other.uid &&
    847           string_uid == other.string_uid);
    848 }
    849 
    850 bool MenuItem::Id::operator!=(const Id& other) const {
    851   return !(*this == other);
    852 }
    853 
    854 bool MenuItem::Id::operator<(const Id& other) const {
    855   if (incognito < other.incognito)
    856     return true;
    857   if (incognito == other.incognito) {
    858     if (extension_id < other.extension_id)
    859       return true;
    860     if (extension_id == other.extension_id) {
    861       if (uid < other.uid)
    862         return true;
    863       if (uid == other.uid)
    864         return string_uid < other.string_uid;
    865     }
    866   }
    867   return false;
    868 }
    869 
    870 }  // namespace extensions
    871