extension_menu_manager.cc revision 21d179b334e59e9a3bfcaed4c4430bef1bc5759d
1// Copyright (c) 2010 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 "app/l10n_util.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 "base/json/json_writer.h"
16#include "chrome/browser/extensions/extension_event_router.h"
17#include "chrome/browser/extensions/extension_tabs_module.h"
18#include "chrome/browser/profiles/profile.h"
19#include "chrome/common/extensions/extension.h"
20#include "chrome/common/notification_service.h"
21#include "gfx/favicon_size.h"
22#include "webkit/glue/context_menu.h"
23
24ExtensionMenuItem::ExtensionMenuItem(const Id& id,
25                                     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  std::string extension_id = GetItemById(id)->extension_id();
236  MenuItemMap::iterator i = context_items_.find(extension_id);
237  if (i == context_items_.end()) {
238    NOTREACHED();
239    return false;
240  }
241
242  bool result = false;
243  std::set<ExtensionMenuItem::Id> items_removed;
244  ExtensionMenuItem::List& list = i->second;
245  ExtensionMenuItem::List::iterator j;
246  for (j = list.begin(); j < list.end(); ++j) {
247    // See if the current top-level item is a match.
248    if ((*j)->id() == id) {
249      items_removed = (*j)->RemoveAllDescendants();
250      items_removed.insert(id);
251      delete *j;
252      list.erase(j);
253      result = true;
254      break;
255    } else {
256      // See if the item to remove was found as a descendant of the current
257      // top-level item.
258      ExtensionMenuItem* child = (*j)->ReleaseChild(id, true /* recursive */);
259      if (child) {
260        items_removed = child->RemoveAllDescendants();
261        items_removed.insert(id);
262        delete child;
263        result = true;
264        break;
265      }
266    }
267  }
268  DCHECK(result);  // The check at the very top should have prevented this.
269
270  // Clear entries from the items_by_id_ map.
271  std::set<ExtensionMenuItem::Id>::iterator removed_iter;
272  for (removed_iter = items_removed.begin();
273       removed_iter != items_removed.end();
274       ++removed_iter) {
275    items_by_id_.erase(*removed_iter);
276  }
277
278  if (list.empty()) {
279    context_items_.erase(extension_id);
280    icon_manager_.RemoveIcon(extension_id);
281  }
282
283  return result;
284}
285
286void ExtensionMenuManager::RemoveAllContextItems(std::string extension_id) {
287  ExtensionMenuItem::List::iterator i;
288  for (i = context_items_[extension_id].begin();
289       i != context_items_[extension_id].end(); ++i) {
290    ExtensionMenuItem* item = *i;
291    items_by_id_.erase(item->id());
292
293    // Remove descendants from this item and erase them from the lookup cache.
294    std::set<ExtensionMenuItem::Id> removed_ids = item->RemoveAllDescendants();
295    std::set<ExtensionMenuItem::Id>::const_iterator j;
296    for (j = removed_ids.begin(); j != removed_ids.end(); ++j) {
297      items_by_id_.erase(*j);
298    }
299  }
300  STLDeleteElements(&context_items_[extension_id]);
301  context_items_.erase(extension_id);
302  icon_manager_.RemoveIcon(extension_id);
303}
304
305ExtensionMenuItem* ExtensionMenuManager::GetItemById(
306    const ExtensionMenuItem::Id& id) const {
307  std::map<ExtensionMenuItem::Id, ExtensionMenuItem*>::const_iterator i =
308      items_by_id_.find(id);
309  if (i != items_by_id_.end())
310    return i->second;
311  else
312    return NULL;
313}
314
315void ExtensionMenuManager::RadioItemSelected(ExtensionMenuItem* item) {
316  // If this is a child item, we need to get a handle to the list from its
317  // parent. Otherwise get a handle to the top-level list.
318  const ExtensionMenuItem::List* list = NULL;
319  if (item->parent_id()) {
320    ExtensionMenuItem* parent = GetItemById(*item->parent_id());
321    if (!parent) {
322      NOTREACHED();
323      return;
324    }
325    list = &(parent->children());
326  } else {
327    if (context_items_.find(item->extension_id()) == context_items_.end()) {
328      NOTREACHED();
329      return;
330    }
331    list = &context_items_[item->extension_id()];
332  }
333
334  // Find where |item| is in the list.
335  ExtensionMenuItem::List::const_iterator item_location;
336  for (item_location = list->begin(); item_location != list->end();
337       ++item_location) {
338    if (*item_location == item)
339      break;
340  }
341  if (item_location == list->end()) {
342    NOTREACHED();  // We should have found the item.
343    return;
344  }
345
346  // Iterate backwards from |item| and uncheck any adjacent radio items.
347  ExtensionMenuItem::List::const_iterator i;
348  if (item_location != list->begin()) {
349    i = item_location;
350    do {
351      --i;
352      if ((*i)->type() != ExtensionMenuItem::RADIO)
353        break;
354      (*i)->SetChecked(false);
355    } while (i != list->begin());
356  }
357
358  // Now iterate forwards from |item| and uncheck any adjacent radio items.
359  for (i = item_location + 1; i != list->end(); ++i) {
360    if ((*i)->type() != ExtensionMenuItem::RADIO)
361      break;
362    (*i)->SetChecked(false);
363  }
364}
365
366static void AddURLProperty(DictionaryValue* dictionary,
367                           const std::string& key, const GURL& url) {
368  if (!url.is_empty())
369    dictionary->SetString(key, url.possibly_invalid_spec());
370}
371
372void ExtensionMenuManager::ExecuteCommand(
373    Profile* profile,
374    TabContents* tab_contents,
375    const ContextMenuParams& params,
376    const ExtensionMenuItem::Id& menuItemId) {
377  ExtensionEventRouter* event_router = profile->GetExtensionEventRouter();
378  if (!event_router)
379    return;
380
381  ExtensionMenuItem* item = GetItemById(menuItemId);
382  if (!item)
383    return;
384
385  if (item->type() == ExtensionMenuItem::RADIO)
386    RadioItemSelected(item);
387
388  ListValue args;
389
390  DictionaryValue* properties = new DictionaryValue();
391  properties->SetInteger("menuItemId", item->id().uid);
392  if (item->parent_id())
393    properties->SetInteger("parentMenuItemId", item->parent_id()->uid);
394
395  switch (params.media_type) {
396    case WebKit::WebContextMenuData::MediaTypeImage:
397      properties->SetString("mediaType", "image");
398      break;
399    case WebKit::WebContextMenuData::MediaTypeVideo:
400      properties->SetString("mediaType", "video");
401      break;
402    case WebKit::WebContextMenuData::MediaTypeAudio:
403      properties->SetString("mediaType", "audio");
404      break;
405    default:  {}  // Do nothing.
406  }
407
408  AddURLProperty(properties, "linkUrl", params.unfiltered_link_url);
409  AddURLProperty(properties, "srcUrl", params.src_url);
410  AddURLProperty(properties, "pageUrl", params.page_url);
411  AddURLProperty(properties, "frameUrl", params.frame_url);
412
413  if (params.selection_text.length() > 0)
414    properties->SetString("selectionText", params.selection_text);
415
416  properties->SetBoolean("editable", params.is_editable);
417
418  args.Append(properties);
419
420  // Add the tab info to the argument list.
421  if (tab_contents) {
422    args.Append(ExtensionTabUtil::CreateTabValue(tab_contents));
423  } else {
424    args.Append(new DictionaryValue());
425  }
426
427  if (item->type() == ExtensionMenuItem::CHECKBOX ||
428      item->type() == ExtensionMenuItem::RADIO) {
429    bool was_checked = item->checked();
430    properties->SetBoolean("wasChecked", was_checked);
431
432    // RADIO items always get set to true when you click on them, but CHECKBOX
433    // items get their state toggled.
434    bool checked =
435        (item->type() == ExtensionMenuItem::RADIO) ? true : !was_checked;
436
437    item->SetChecked(checked);
438    properties->SetBoolean("checked", item->checked());
439  }
440
441  std::string json_args;
442  base::JSONWriter::Write(&args, false, &json_args);
443  std::string event_name = "contextMenus";
444  event_router->DispatchEventToExtension(
445      item->extension_id(), event_name, json_args, profile, GURL());
446}
447
448void ExtensionMenuManager::Observe(NotificationType type,
449                                   const NotificationSource& source,
450                                   const NotificationDetails& details) {
451  // Remove menu items for disabled/uninstalled extensions.
452  if (type != NotificationType::EXTENSION_UNLOADED) {
453    NOTREACHED();
454    return;
455  }
456  const Extension* extension =
457      Details<UnloadedExtensionInfo>(details)->extension;
458  if (ContainsKey(context_items_, extension->id())) {
459    RemoveAllContextItems(extension->id());
460  }
461}
462
463const SkBitmap& ExtensionMenuManager::GetIconForExtension(
464    const std::string& extension_id) {
465  return icon_manager_.GetIcon(extension_id);
466}
467
468// static
469bool ExtensionMenuManager::HasAllowedScheme(const GURL& url) {
470  URLPattern pattern(kAllowedSchemes);
471  return pattern.SetScheme(url.scheme());
472}
473
474ExtensionMenuItem::Id::Id()
475    : profile(NULL), uid(0) {
476}
477
478ExtensionMenuItem::Id::Id(Profile* profile, std::string extension_id, int uid)
479    : profile(profile), extension_id(extension_id), uid(uid) {
480}
481
482ExtensionMenuItem::Id::~Id() {
483}
484
485bool ExtensionMenuItem::Id::operator==(const Id& other) const {
486  return (profile == other.profile &&
487          extension_id == other.extension_id &&
488          uid == other.uid);
489}
490
491bool ExtensionMenuItem::Id::operator!=(const Id& other) const {
492  return !(*this == other);
493}
494
495bool ExtensionMenuItem::Id::operator<(const Id& other) const {
496  if (profile < other.profile)
497    return true;
498  if (profile == other.profile) {
499    if (extension_id < other.extension_id)
500      return true;
501    if (extension_id == other.extension_id)
502      return uid < other.uid;
503  }
504  return false;
505}
506