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