• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (c) 2011 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/extension_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-inl.h"
12 #include "base/string_util.h"
13 #include "base/utf_string_conversions.h"
14 #include "base/values.h"
15 #include "chrome/browser/extensions/extension_event_router.h"
16 #include "chrome/browser/extensions/extension_tabs_module.h"
17 #include "chrome/browser/profiles/profile.h"
18 #include "chrome/common/extensions/extension.h"
19 #include "content/common/notification_service.h"
20 #include "ui/base/l10n/l10n_util.h"
21 #include "ui/gfx/favicon_size.h"
22 #include "webkit/glue/context_menu.h"
23 
ExtensionMenuItem(const Id & id,const std::string & title,bool checked,Type type,const ContextList & contexts)24 ExtensionMenuItem::ExtensionMenuItem(const Id& id,
25                                      const std::string& title,
26                                      bool checked,
27                                      Type type,
28                                      const ContextList& contexts)
29     : id_(id),
30       title_(title),
31       type_(type),
32       checked_(checked),
33       contexts_(contexts),
34       parent_id_(0) {
35 }
36 
~ExtensionMenuItem()37 ExtensionMenuItem::~ExtensionMenuItem() {
38   STLDeleteElements(&children_);
39 }
40 
ReleaseChild(const Id & child_id,bool recursive)41 ExtensionMenuItem* ExtensionMenuItem::ReleaseChild(const Id& child_id,
42                                                    bool recursive) {
43   for (List::iterator i = children_.begin(); i != children_.end(); ++i) {
44     ExtensionMenuItem* child = NULL;
45     if ((*i)->id() == child_id) {
46       child = *i;
47       children_.erase(i);
48       return child;
49     } else if (recursive) {
50       child = (*i)->ReleaseChild(child_id, recursive);
51       if (child)
52         return child;
53     }
54   }
55   return NULL;
56 }
57 
RemoveAllDescendants()58 std::set<ExtensionMenuItem::Id> ExtensionMenuItem::RemoveAllDescendants() {
59   std::set<Id> result;
60   for (List::iterator i = children_.begin(); i != children_.end(); ++i) {
61     ExtensionMenuItem* child = *i;
62     result.insert(child->id());
63     std::set<Id> removed = child->RemoveAllDescendants();
64     result.insert(removed.begin(), removed.end());
65   }
66   STLDeleteElements(&children_);
67   return result;
68 }
69 
TitleWithReplacement(const string16 & selection,size_t max_length) const70 string16 ExtensionMenuItem::TitleWithReplacement(
71     const string16& selection, size_t max_length) const {
72   string16 result = UTF8ToUTF16(title_);
73   // TODO(asargent) - Change this to properly handle %% escaping so you can
74   // put "%s" in titles that won't get substituted.
75   ReplaceSubstringsAfterOffset(&result, 0, ASCIIToUTF16("%s"), selection);
76 
77   if (result.length() > max_length)
78     result = l10n_util::TruncateString(result, max_length);
79   return result;
80 }
81 
SetChecked(bool checked)82 bool ExtensionMenuItem::SetChecked(bool checked) {
83   if (type_ != CHECKBOX && type_ != RADIO)
84     return false;
85   checked_ = checked;
86   return true;
87 }
88 
AddChild(ExtensionMenuItem * item)89 void ExtensionMenuItem::AddChild(ExtensionMenuItem* item) {
90   item->parent_id_.reset(new Id(id_));
91   children_.push_back(item);
92 }
93 
94 const int ExtensionMenuManager::kAllowedSchemes =
95     URLPattern::SCHEME_HTTP | URLPattern::SCHEME_HTTPS;
96 
ExtensionMenuManager()97 ExtensionMenuManager::ExtensionMenuManager() {
98   registrar_.Add(this, NotificationType::EXTENSION_UNLOADED,
99                  NotificationService::AllSources());
100 }
101 
~ExtensionMenuManager()102 ExtensionMenuManager::~ExtensionMenuManager() {
103   MenuItemMap::iterator i;
104   for (i = context_items_.begin(); i != context_items_.end(); ++i) {
105     STLDeleteElements(&(i->second));
106   }
107 }
108 
ExtensionIds()109 std::set<std::string> ExtensionMenuManager::ExtensionIds() {
110   std::set<std::string> id_set;
111   for (MenuItemMap::const_iterator i = context_items_.begin();
112        i != context_items_.end(); ++i) {
113     id_set.insert(i->first);
114   }
115   return id_set;
116 }
117 
MenuItems(const std::string & extension_id)118 const ExtensionMenuItem::List* ExtensionMenuManager::MenuItems(
119     const std::string& extension_id) {
120   MenuItemMap::iterator i = context_items_.find(extension_id);
121   if (i != context_items_.end()) {
122     return &(i->second);
123   }
124   return NULL;
125 }
126 
AddContextItem(const Extension * extension,ExtensionMenuItem * item)127 bool ExtensionMenuManager::AddContextItem(const Extension* extension,
128                                           ExtensionMenuItem* item) {
129   const std::string& extension_id = item->extension_id();
130   // The item must have a non-empty extension id, and not have already been
131   // added.
132   if (extension_id.empty() || ContainsKey(items_by_id_, item->id()))
133     return false;
134 
135   DCHECK_EQ(extension->id(), extension_id);
136 
137   bool first_item = !ContainsKey(context_items_, extension_id);
138   context_items_[extension_id].push_back(item);
139   items_by_id_[item->id()] = item;
140 
141   if (item->type() == ExtensionMenuItem::RADIO && item->checked())
142     RadioItemSelected(item);
143 
144   // If this is the first item for this extension, start loading its icon.
145   if (first_item)
146     icon_manager_.LoadIcon(extension);
147 
148   return true;
149 }
150 
AddChildItem(const ExtensionMenuItem::Id & parent_id,ExtensionMenuItem * child)151 bool ExtensionMenuManager::AddChildItem(const ExtensionMenuItem::Id& parent_id,
152                                         ExtensionMenuItem* child) {
153   ExtensionMenuItem* parent = GetItemById(parent_id);
154   if (!parent || parent->type() != ExtensionMenuItem::NORMAL ||
155       parent->extension_id() != child->extension_id() ||
156       ContainsKey(items_by_id_, child->id()))
157     return false;
158   parent->AddChild(child);
159   items_by_id_[child->id()] = child;
160   return true;
161 }
162 
DescendantOf(ExtensionMenuItem * item,const ExtensionMenuItem::Id & ancestor_id)163 bool ExtensionMenuManager::DescendantOf(
164     ExtensionMenuItem* item,
165     const ExtensionMenuItem::Id& ancestor_id) {
166   // Work our way up the tree until we find the ancestor or NULL.
167   ExtensionMenuItem::Id* id = item->parent_id();
168   while (id != NULL) {
169     DCHECK(*id != item->id());  // Catch circular graphs.
170     if (*id == ancestor_id)
171       return true;
172     ExtensionMenuItem* next = GetItemById(*id);
173     if (!next) {
174       NOTREACHED();
175       return false;
176     }
177     id = next->parent_id();
178   }
179   return false;
180 }
181 
ChangeParent(const ExtensionMenuItem::Id & child_id,const ExtensionMenuItem::Id * parent_id)182 bool ExtensionMenuManager::ChangeParent(
183     const ExtensionMenuItem::Id& child_id,
184     const ExtensionMenuItem::Id* parent_id) {
185   ExtensionMenuItem* child = GetItemById(child_id);
186   ExtensionMenuItem* new_parent = parent_id ? GetItemById(*parent_id) : NULL;
187   if ((parent_id && (child_id == *parent_id)) || !child ||
188       (!new_parent && parent_id != NULL) ||
189       (new_parent && (DescendantOf(new_parent, child_id) ||
190                       child->extension_id() != new_parent->extension_id())))
191     return false;
192 
193   ExtensionMenuItem::Id* old_parent_id = child->parent_id();
194   if (old_parent_id != NULL) {
195     ExtensionMenuItem* old_parent = GetItemById(*old_parent_id);
196     if (!old_parent) {
197       NOTREACHED();
198       return false;
199     }
200     ExtensionMenuItem* taken =
201       old_parent->ReleaseChild(child_id, false /* non-recursive search*/);
202     DCHECK(taken == child);
203   } else {
204     // This is a top-level item, so we need to pull it out of our list of
205     // top-level items.
206     MenuItemMap::iterator i = context_items_.find(child->extension_id());
207     if (i == context_items_.end()) {
208       NOTREACHED();
209       return false;
210     }
211     ExtensionMenuItem::List& list = i->second;
212     ExtensionMenuItem::List::iterator j = std::find(list.begin(), list.end(),
213                                                     child);
214     if (j == list.end()) {
215       NOTREACHED();
216       return false;
217     }
218     list.erase(j);
219   }
220 
221   if (new_parent) {
222     new_parent->AddChild(child);
223   } else {
224     context_items_[child->extension_id()].push_back(child);
225     child->parent_id_.reset(NULL);
226   }
227   return true;
228 }
229 
RemoveContextMenuItem(const ExtensionMenuItem::Id & id)230 bool ExtensionMenuManager::RemoveContextMenuItem(
231     const ExtensionMenuItem::Id& id) {
232   if (!ContainsKey(items_by_id_, id))
233     return false;
234 
235   ExtensionMenuItem* menu_item = GetItemById(id);
236   DCHECK(menu_item);
237   std::string extension_id = menu_item->extension_id();
238   MenuItemMap::iterator i = context_items_.find(extension_id);
239   if (i == context_items_.end()) {
240     NOTREACHED();
241     return false;
242   }
243 
244   bool result = false;
245   std::set<ExtensionMenuItem::Id> items_removed;
246   ExtensionMenuItem::List& list = i->second;
247   ExtensionMenuItem::List::iterator j;
248   for (j = list.begin(); j < list.end(); ++j) {
249     // See if the current top-level item is a match.
250     if ((*j)->id() == id) {
251       items_removed = (*j)->RemoveAllDescendants();
252       items_removed.insert(id);
253       delete *j;
254       list.erase(j);
255       result = true;
256       break;
257     } else {
258       // See if the item to remove was found as a descendant of the current
259       // top-level item.
260       ExtensionMenuItem* child = (*j)->ReleaseChild(id, true /* recursive */);
261       if (child) {
262         items_removed = child->RemoveAllDescendants();
263         items_removed.insert(id);
264         delete child;
265         result = true;
266         break;
267       }
268     }
269   }
270   DCHECK(result);  // The check at the very top should have prevented this.
271 
272   // Clear entries from the items_by_id_ map.
273   std::set<ExtensionMenuItem::Id>::iterator removed_iter;
274   for (removed_iter = items_removed.begin();
275        removed_iter != items_removed.end();
276        ++removed_iter) {
277     items_by_id_.erase(*removed_iter);
278   }
279 
280   if (list.empty()) {
281     context_items_.erase(extension_id);
282     icon_manager_.RemoveIcon(extension_id);
283   }
284 
285   return result;
286 }
287 
RemoveAllContextItems(const std::string & extension_id)288 void ExtensionMenuManager::RemoveAllContextItems(
289     const std::string& extension_id) {
290   ExtensionMenuItem::List::iterator i;
291   for (i = context_items_[extension_id].begin();
292        i != context_items_[extension_id].end(); ++i) {
293     ExtensionMenuItem* item = *i;
294     items_by_id_.erase(item->id());
295 
296     // Remove descendants from this item and erase them from the lookup cache.
297     std::set<ExtensionMenuItem::Id> removed_ids = item->RemoveAllDescendants();
298     std::set<ExtensionMenuItem::Id>::const_iterator j;
299     for (j = removed_ids.begin(); j != removed_ids.end(); ++j) {
300       items_by_id_.erase(*j);
301     }
302   }
303   STLDeleteElements(&context_items_[extension_id]);
304   context_items_.erase(extension_id);
305   icon_manager_.RemoveIcon(extension_id);
306 }
307 
GetItemById(const ExtensionMenuItem::Id & id) const308 ExtensionMenuItem* ExtensionMenuManager::GetItemById(
309     const ExtensionMenuItem::Id& id) const {
310   std::map<ExtensionMenuItem::Id, ExtensionMenuItem*>::const_iterator i =
311       items_by_id_.find(id);
312   if (i != items_by_id_.end())
313     return i->second;
314   else
315     return NULL;
316 }
317 
RadioItemSelected(ExtensionMenuItem * item)318 void ExtensionMenuManager::RadioItemSelected(ExtensionMenuItem* item) {
319   // If this is a child item, we need to get a handle to the list from its
320   // parent. Otherwise get a handle to the top-level list.
321   const ExtensionMenuItem::List* list = NULL;
322   if (item->parent_id()) {
323     ExtensionMenuItem* parent = GetItemById(*item->parent_id());
324     if (!parent) {
325       NOTREACHED();
326       return;
327     }
328     list = &(parent->children());
329   } else {
330     if (context_items_.find(item->extension_id()) == context_items_.end()) {
331       NOTREACHED();
332       return;
333     }
334     list = &context_items_[item->extension_id()];
335   }
336 
337   // Find where |item| is in the list.
338   ExtensionMenuItem::List::const_iterator item_location;
339   for (item_location = list->begin(); item_location != list->end();
340        ++item_location) {
341     if (*item_location == item)
342       break;
343   }
344   if (item_location == list->end()) {
345     NOTREACHED();  // We should have found the item.
346     return;
347   }
348 
349   // Iterate backwards from |item| and uncheck any adjacent radio items.
350   ExtensionMenuItem::List::const_iterator i;
351   if (item_location != list->begin()) {
352     i = item_location;
353     do {
354       --i;
355       if ((*i)->type() != ExtensionMenuItem::RADIO)
356         break;
357       (*i)->SetChecked(false);
358     } while (i != list->begin());
359   }
360 
361   // Now iterate forwards from |item| and uncheck any adjacent radio items.
362   for (i = item_location + 1; i != list->end(); ++i) {
363     if ((*i)->type() != ExtensionMenuItem::RADIO)
364       break;
365     (*i)->SetChecked(false);
366   }
367 }
368 
AddURLProperty(DictionaryValue * dictionary,const std::string & key,const GURL & url)369 static void AddURLProperty(DictionaryValue* dictionary,
370                            const std::string& key, const GURL& url) {
371   if (!url.is_empty())
372     dictionary->SetString(key, url.possibly_invalid_spec());
373 }
374 
ExecuteCommand(Profile * profile,TabContents * tab_contents,const ContextMenuParams & params,const ExtensionMenuItem::Id & menuItemId)375 void ExtensionMenuManager::ExecuteCommand(
376     Profile* profile,
377     TabContents* tab_contents,
378     const ContextMenuParams& params,
379     const ExtensionMenuItem::Id& menuItemId) {
380   ExtensionEventRouter* event_router = profile->GetExtensionEventRouter();
381   if (!event_router)
382     return;
383 
384   ExtensionMenuItem* item = GetItemById(menuItemId);
385   if (!item)
386     return;
387 
388   if (item->type() == ExtensionMenuItem::RADIO)
389     RadioItemSelected(item);
390 
391   ListValue args;
392 
393   DictionaryValue* properties = new DictionaryValue();
394   properties->SetInteger("menuItemId", item->id().uid);
395   if (item->parent_id())
396     properties->SetInteger("parentMenuItemId", item->parent_id()->uid);
397 
398   switch (params.media_type) {
399     case WebKit::WebContextMenuData::MediaTypeImage:
400       properties->SetString("mediaType", "image");
401       break;
402     case WebKit::WebContextMenuData::MediaTypeVideo:
403       properties->SetString("mediaType", "video");
404       break;
405     case WebKit::WebContextMenuData::MediaTypeAudio:
406       properties->SetString("mediaType", "audio");
407       break;
408     default:  {}  // Do nothing.
409   }
410 
411   AddURLProperty(properties, "linkUrl", params.unfiltered_link_url);
412   AddURLProperty(properties, "srcUrl", params.src_url);
413   AddURLProperty(properties, "pageUrl", params.page_url);
414   AddURLProperty(properties, "frameUrl", params.frame_url);
415 
416   if (params.selection_text.length() > 0)
417     properties->SetString("selectionText", params.selection_text);
418 
419   properties->SetBoolean("editable", params.is_editable);
420 
421   args.Append(properties);
422 
423   // Add the tab info to the argument list.
424   if (tab_contents) {
425     args.Append(ExtensionTabUtil::CreateTabValue(tab_contents));
426   } else {
427     args.Append(new DictionaryValue());
428   }
429 
430   if (item->type() == ExtensionMenuItem::CHECKBOX ||
431       item->type() == ExtensionMenuItem::RADIO) {
432     bool was_checked = item->checked();
433     properties->SetBoolean("wasChecked", was_checked);
434 
435     // RADIO items always get set to true when you click on them, but CHECKBOX
436     // items get their state toggled.
437     bool checked =
438         (item->type() == ExtensionMenuItem::RADIO) ? true : !was_checked;
439 
440     item->SetChecked(checked);
441     properties->SetBoolean("checked", item->checked());
442   }
443 
444   std::string json_args;
445   base::JSONWriter::Write(&args, false, &json_args);
446   std::string event_name = "contextMenus";
447   event_router->DispatchEventToExtension(
448       item->extension_id(), event_name, json_args, profile, GURL());
449 }
450 
Observe(NotificationType type,const NotificationSource & source,const NotificationDetails & details)451 void ExtensionMenuManager::Observe(NotificationType type,
452                                    const NotificationSource& source,
453                                    const NotificationDetails& details) {
454   // Remove menu items for disabled/uninstalled extensions.
455   if (type != NotificationType::EXTENSION_UNLOADED) {
456     NOTREACHED();
457     return;
458   }
459   const Extension* extension =
460       Details<UnloadedExtensionInfo>(details)->extension;
461   if (ContainsKey(context_items_, extension->id())) {
462     RemoveAllContextItems(extension->id());
463   }
464 }
465 
GetIconForExtension(const std::string & extension_id)466 const SkBitmap& ExtensionMenuManager::GetIconForExtension(
467     const std::string& extension_id) {
468   return icon_manager_.GetIcon(extension_id);
469 }
470 
471 // static
HasAllowedScheme(const GURL & url)472 bool ExtensionMenuManager::HasAllowedScheme(const GURL& url) {
473   URLPattern pattern(kAllowedSchemes);
474   return pattern.SetScheme(url.scheme());
475 }
476 
Id()477 ExtensionMenuItem::Id::Id()
478     : profile(NULL), uid(0) {
479 }
480 
Id(Profile * profile,const std::string & extension_id,int uid)481 ExtensionMenuItem::Id::Id(Profile* profile,
482                           const std::string& extension_id,
483                           int uid)
484     : profile(profile), extension_id(extension_id), uid(uid) {
485 }
486 
~Id()487 ExtensionMenuItem::Id::~Id() {
488 }
489 
operator ==(const Id & other) const490 bool ExtensionMenuItem::Id::operator==(const Id& other) const {
491   return (profile == other.profile &&
492           extension_id == other.extension_id &&
493           uid == other.uid);
494 }
495 
operator !=(const Id & other) const496 bool ExtensionMenuItem::Id::operator!=(const Id& other) const {
497   return !(*this == other);
498 }
499 
operator <(const Id & other) const500 bool ExtensionMenuItem::Id::operator<(const Id& other) const {
501   if (profile < other.profile)
502     return true;
503   if (profile == other.profile) {
504     if (extension_id < other.extension_id)
505       return true;
506     if (extension_id == other.extension_id)
507       return uid < other.uid;
508   }
509   return false;
510 }
511