extension_menu_manager.cc revision ddb351dbec246cf1fab5ec20d2d5520909041de1
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
24ExtensionMenuItem::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
37ExtensionMenuItem::~ExtensionMenuItem() {
38  STLDeleteElements(&children_);
39}
40
41ExtensionMenuItem* 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
58std::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
70string16 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
82bool ExtensionMenuItem::SetChecked(bool checked) {
83  if (type_ != CHECKBOX && type_ != RADIO)
84    return false;
85  checked_ = checked;
86  return true;
87}
88
89void ExtensionMenuItem::AddChild(ExtensionMenuItem* item) {
90  item->parent_id_.reset(new Id(id_));
91  children_.push_back(item);
92}
93
94const int ExtensionMenuManager::kAllowedSchemes =
95    URLPattern::SCHEME_HTTP | URLPattern::SCHEME_HTTPS;
96
97ExtensionMenuManager::ExtensionMenuManager() {
98  registrar_.Add(this, NotificationType::EXTENSION_UNLOADED,
99                 NotificationService::AllSources());
100}
101
102ExtensionMenuManager::~ExtensionMenuManager() {
103  MenuItemMap::iterator i;
104  for (i = context_items_.begin(); i != context_items_.end(); ++i) {
105    STLDeleteElements(&(i->second));
106  }
107}
108
109std::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
118const 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
127bool 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
151bool 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
163bool 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
182bool 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
230bool 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
288void 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
308ExtensionMenuItem* 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
318void 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
369static 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
375void 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
451void 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
466const SkBitmap& ExtensionMenuManager::GetIconForExtension(
467    const std::string& extension_id) {
468  return icon_manager_.GetIcon(extension_id);
469}
470
471// static
472bool ExtensionMenuManager::HasAllowedScheme(const GURL& url) {
473  URLPattern pattern(kAllowedSchemes);
474  return pattern.SetScheme(url.scheme());
475}
476
477ExtensionMenuItem::Id::Id()
478    : profile(NULL), uid(0) {
479}
480
481ExtensionMenuItem::Id::Id(Profile* profile,
482                          const std::string& extension_id,
483                          int uid)
484    : profile(profile), extension_id(extension_id), uid(uid) {
485}
486
487ExtensionMenuItem::Id::~Id() {
488}
489
490bool ExtensionMenuItem::Id::operator==(const Id& other) const {
491  return (profile == other.profile &&
492          extension_id == other.extension_id &&
493          uid == other.uid);
494}
495
496bool ExtensionMenuItem::Id::operator!=(const Id& other) const {
497  return !(*this == other);
498}
499
500bool 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