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