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 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 37 ExtensionMenuItem::~ExtensionMenuItem() { 38 STLDeleteElements(&children_); 39 } 40 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 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 70 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 82 bool ExtensionMenuItem::SetChecked(bool checked) { 83 if (type_ != CHECKBOX && type_ != RADIO) 84 return false; 85 checked_ = checked; 86 return true; 87 } 88 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 97 ExtensionMenuManager::ExtensionMenuManager() { 98 registrar_.Add(this, NotificationType::EXTENSION_UNLOADED, 99 NotificationService::AllSources()); 100 } 101 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 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 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 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 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 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 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 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 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 308 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 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 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 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 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 466 const SkBitmap& ExtensionMenuManager::GetIconForExtension( 467 const std::string& extension_id) { 468 return icon_manager_.GetIcon(extension_id); 469 } 470 471 // static 472 bool ExtensionMenuManager::HasAllowedScheme(const GURL& url) { 473 URLPattern pattern(kAllowedSchemes); 474 return pattern.SetScheme(url.scheme()); 475 } 476 477 ExtensionMenuItem::Id::Id() 478 : profile(NULL), uid(0) { 479 } 480 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 487 ExtensionMenuItem::Id::~Id() { 488 } 489 490 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 496 bool ExtensionMenuItem::Id::operator!=(const Id& other) const { 497 return !(*this == other); 498 } 499 500 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