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