| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2012 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.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_names.h" | |
| 16 #include "chrome/browser/extensions/extension_event_router.h" | |
| 17 #include "chrome/browser/extensions/extension_service.h" | |
| 18 #include "chrome/browser/extensions/extension_system.h" | |
| 19 #include "chrome/browser/extensions/extension_tab_helper.h" | |
| 20 #include "chrome/browser/extensions/extension_tab_util.h" | |
| 21 #include "chrome/browser/extensions/state_store.h" | |
| 22 #include "chrome/browser/profiles/profile.h" | |
| 23 #include "chrome/browser/ui/tab_contents/tab_contents.h" | |
| 24 #include "chrome/common/chrome_notification_types.h" | |
| 25 #include "chrome/common/extensions/extension.h" | |
| 26 #include "content/public/browser/notification_details.h" | |
| 27 #include "content/public/browser/notification_source.h" | |
| 28 #include "content/public/browser/web_contents.h" | |
| 29 #include "content/public/common/context_menu_params.h" | |
| 30 #include "ui/base/text/text_elider.h" | |
| 31 #include "ui/gfx/favicon_size.h" | |
| 32 | |
| 33 using content::WebContents; | |
| 34 | |
| 35 namespace { | |
| 36 | |
| 37 // Keys for serialization to and from Value to store in the preferences. | |
| 38 const char kContextMenusKey[] = "context_menus"; | |
| 39 | |
| 40 const char kCheckedKey[] = "checked"; | |
| 41 const char kContextsKey[] = "contexts"; | |
| 42 const char kDocumentURLPatternsKey[] = "document_url_patterns"; | |
| 43 const char kEnabledKey[] = "enabled"; | |
| 44 const char kIncognitoKey[] = "incognito"; | |
| 45 const char kParentUIDKey[] = "parent_uid"; | |
| 46 const char kStringUIDKey[] = "string_uid"; | |
| 47 const char kTargetURLPatternsKey[] = "target_url_patterns"; | |
| 48 const char kTitleKey[] = "title"; | |
| 49 const char kTypeKey[] = "type"; | |
| 50 | |
| 51 void SetIdKeyValue(base::DictionaryValue* properties, | |
| 52 const char* key, | |
| 53 const ExtensionMenuItem::Id& id) { | |
| 54 if (id.uid == 0) | |
| 55 properties->SetString(key, id.string_uid); | |
| 56 else | |
| 57 properties->SetInteger(key, id.uid); | |
| 58 } | |
| 59 | |
| 60 ExtensionMenuItem::List MenuItemsFromValue(const std::string& extension_id, | |
| 61 base::Value* value) { | |
| 62 ExtensionMenuItem::List items; | |
| 63 | |
| 64 base::ListValue* list = NULL; | |
| 65 if (!value || !value->GetAsList(&list)) | |
| 66 return items; | |
| 67 | |
| 68 for (size_t i = 0; i < list->GetSize(); ++i) { | |
| 69 base::DictionaryValue* dict = NULL; | |
| 70 if (!list->GetDictionary(i, &dict)) | |
| 71 continue; | |
| 72 ExtensionMenuItem* item = ExtensionMenuItem::Populate( | |
| 73 extension_id, *dict, NULL); | |
| 74 if (!item) | |
| 75 continue; | |
| 76 items.push_back(item); | |
| 77 } | |
| 78 return items; | |
| 79 } | |
| 80 | |
| 81 scoped_ptr<base::Value> MenuItemsToValue( | |
| 82 const ExtensionMenuItem::List& items) { | |
| 83 scoped_ptr<base::ListValue> list(new ListValue()); | |
| 84 for (size_t i = 0; i < items.size(); ++i) | |
| 85 list->Append(items[i]->ToValue().release()); | |
| 86 return scoped_ptr<Value>(list.release()); | |
| 87 } | |
| 88 | |
| 89 } // namespace | |
| 90 | |
| 91 ExtensionMenuItem::ExtensionMenuItem(const Id& id, | |
| 92 const std::string& title, | |
| 93 bool checked, | |
| 94 bool enabled, | |
| 95 Type type, | |
| 96 const ContextList& contexts) | |
| 97 : id_(id), | |
| 98 title_(title), | |
| 99 type_(type), | |
| 100 checked_(checked), | |
| 101 enabled_(enabled), | |
| 102 contexts_(contexts), | |
| 103 parent_id_(0) { | |
| 104 } | |
| 105 | |
| 106 ExtensionMenuItem::~ExtensionMenuItem() { | |
| 107 STLDeleteElements(&children_); | |
| 108 } | |
| 109 | |
| 110 ExtensionMenuItem* ExtensionMenuItem::ReleaseChild(const Id& child_id, | |
| 111 bool recursive) { | |
| 112 for (List::iterator i = children_.begin(); i != children_.end(); ++i) { | |
| 113 ExtensionMenuItem* child = NULL; | |
| 114 if ((*i)->id() == child_id) { | |
| 115 child = *i; | |
| 116 children_.erase(i); | |
| 117 return child; | |
| 118 } else if (recursive) { | |
| 119 child = (*i)->ReleaseChild(child_id, recursive); | |
| 120 if (child) | |
| 121 return child; | |
| 122 } | |
| 123 } | |
| 124 return NULL; | |
| 125 } | |
| 126 | |
| 127 void ExtensionMenuItem::GetFlattenedSubtree(ExtensionMenuItem::List* list) { | |
| 128 list->push_back(this); | |
| 129 for (List::iterator i = children_.begin(); i != children_.end(); ++i) | |
| 130 (*i)->GetFlattenedSubtree(list); | |
| 131 } | |
| 132 | |
| 133 std::set<ExtensionMenuItem::Id> ExtensionMenuItem::RemoveAllDescendants() { | |
| 134 std::set<Id> result; | |
| 135 for (List::iterator i = children_.begin(); i != children_.end(); ++i) { | |
| 136 ExtensionMenuItem* child = *i; | |
| 137 result.insert(child->id()); | |
| 138 std::set<Id> removed = child->RemoveAllDescendants(); | |
| 139 result.insert(removed.begin(), removed.end()); | |
| 140 } | |
| 141 STLDeleteElements(&children_); | |
| 142 return result; | |
| 143 } | |
| 144 | |
| 145 string16 ExtensionMenuItem::TitleWithReplacement( | |
| 146 const string16& selection, size_t max_length) const { | |
| 147 string16 result = UTF8ToUTF16(title_); | |
| 148 // TODO(asargent) - Change this to properly handle %% escaping so you can | |
| 149 // put "%s" in titles that won't get substituted. | |
| 150 ReplaceSubstringsAfterOffset(&result, 0, ASCIIToUTF16("%s"), selection); | |
| 151 | |
| 152 if (result.length() > max_length) | |
| 153 result = ui::TruncateString(result, max_length); | |
| 154 return result; | |
| 155 } | |
| 156 | |
| 157 bool ExtensionMenuItem::SetChecked(bool checked) { | |
| 158 if (type_ != CHECKBOX && type_ != RADIO) | |
| 159 return false; | |
| 160 checked_ = checked; | |
| 161 return true; | |
| 162 } | |
| 163 | |
| 164 void ExtensionMenuItem::AddChild(ExtensionMenuItem* item) { | |
| 165 item->parent_id_.reset(new Id(id_)); | |
| 166 children_.push_back(item); | |
| 167 } | |
| 168 | |
| 169 scoped_ptr<DictionaryValue> ExtensionMenuItem::ToValue() const { | |
| 170 scoped_ptr<DictionaryValue> value(new DictionaryValue); | |
| 171 // Should only be called for extensions with event pages, which only have | |
| 172 // string IDs for items. | |
| 173 DCHECK_EQ(0, id_.uid); | |
| 174 value->SetString(kStringUIDKey, id_.string_uid); | |
| 175 value->SetBoolean(kIncognitoKey, id_.incognito); | |
| 176 value->SetInteger(kTypeKey, type_); | |
| 177 if (type_ != SEPARATOR) | |
| 178 value->SetString(kTitleKey, title_); | |
| 179 if (type_ == CHECKBOX || type_ == RADIO) | |
| 180 value->SetBoolean(kCheckedKey, checked_); | |
| 181 value->SetBoolean(kEnabledKey, enabled_); | |
| 182 value->Set(kContextsKey, contexts_.ToValue().release()); | |
| 183 if (parent_id_.get()) { | |
| 184 DCHECK_EQ(0, parent_id_->uid); | |
| 185 value->SetString(kParentUIDKey, parent_id_->string_uid); | |
| 186 } | |
| 187 value->Set(kDocumentURLPatternsKey, | |
| 188 document_url_patterns_.ToValue().release()); | |
| 189 value->Set(kTargetURLPatternsKey, target_url_patterns_.ToValue().release()); | |
| 190 return value.Pass(); | |
| 191 } | |
| 192 | |
| 193 // static | |
| 194 ExtensionMenuItem* ExtensionMenuItem::Populate(const std::string& extension_id, | |
| 195 const DictionaryValue& value, | |
| 196 std::string* error) { | |
| 197 bool incognito = false; | |
| 198 if (!value.GetBoolean(kIncognitoKey, &incognito)) | |
| 199 return NULL; | |
| 200 Id id(incognito, extension_id); | |
| 201 if (!value.GetString(kStringUIDKey, &id.string_uid)) | |
| 202 return NULL; | |
| 203 int type_int; | |
| 204 Type type; | |
| 205 if (!value.GetInteger(kTypeKey, &type_int)) | |
| 206 return NULL; | |
| 207 type = static_cast<Type>(type_int); | |
| 208 std::string title; | |
| 209 if (type != SEPARATOR && !value.GetString(kTitleKey, &title)) | |
| 210 return NULL; | |
| 211 bool checked; | |
| 212 if ((type == CHECKBOX || type == RADIO) && | |
| 213 !value.GetBoolean(kCheckedKey, &checked)) { | |
| 214 return NULL; | |
| 215 } | |
| 216 bool enabled = true; | |
| 217 if (!value.GetBoolean(kEnabledKey, &enabled)) | |
| 218 return NULL; | |
| 219 ContextList contexts; | |
| 220 Value* contexts_value = NULL; | |
| 221 if (!value.Get(kContextsKey, &contexts_value)) | |
| 222 return NULL; | |
| 223 if (!contexts.Populate(*contexts_value)) | |
| 224 return NULL; | |
| 225 | |
| 226 scoped_ptr<ExtensionMenuItem> result(new ExtensionMenuItem( | |
| 227 id, title, checked, enabled, type, contexts)); | |
| 228 | |
| 229 if (!result->PopulateURLPatterns( | |
| 230 value, kDocumentURLPatternsKey, kTargetURLPatternsKey, error)) | |
| 231 return NULL; | |
| 232 | |
| 233 // parent_id is filled in from the value, but it might not be valid. It's left | |
| 234 // to be validated upon being added (via AddChildItem) to the menu manager. | |
| 235 scoped_ptr<Id> parent_id(new Id(incognito, extension_id)); | |
| 236 if (value.HasKey(kParentUIDKey)) { | |
| 237 if (!value.GetString(kParentUIDKey, &parent_id->string_uid)) | |
| 238 return NULL; | |
| 239 result->parent_id_.swap(parent_id); | |
| 240 } | |
| 241 return result.release(); | |
| 242 } | |
| 243 | |
| 244 bool ExtensionMenuItem::PopulateURLPatterns( | |
| 245 const DictionaryValue& properties, | |
| 246 const char* document_url_patterns_key, | |
| 247 const char* target_url_patterns_key, | |
| 248 std::string* error) { | |
| 249 if (properties.HasKey(document_url_patterns_key)) { | |
| 250 ListValue* list = NULL; | |
| 251 if (!properties.GetList(document_url_patterns_key, &list)) | |
| 252 return false; | |
| 253 if (!document_url_patterns_.Populate( | |
| 254 *list, URLPattern::SCHEME_ALL, true, error)) { | |
| 255 return false; | |
| 256 } | |
| 257 } | |
| 258 if (properties.HasKey(target_url_patterns_key)) { | |
| 259 ListValue* list = NULL; | |
| 260 if (!properties.GetList(target_url_patterns_key, &list)) | |
| 261 return false; | |
| 262 if (!target_url_patterns_.Populate( | |
| 263 *list, URLPattern::SCHEME_ALL, true, error)) { | |
| 264 return false; | |
| 265 } | |
| 266 } | |
| 267 return true; | |
| 268 } | |
| 269 | |
| 270 ExtensionMenuManager::ExtensionMenuManager(Profile* profile) | |
| 271 : profile_(profile) { | |
| 272 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_LOADED, | |
| 273 content::Source<Profile>(profile)); | |
| 274 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNLOADED, | |
| 275 content::Source<Profile>(profile)); | |
| 276 | |
| 277 extensions::StateStore* store = ExtensionSystem::Get(profile_)->state_store(); | |
| 278 if (store) | |
| 279 store->RegisterKey(kContextMenusKey); | |
| 280 } | |
| 281 | |
| 282 ExtensionMenuManager::~ExtensionMenuManager() { | |
| 283 MenuItemMap::iterator i; | |
| 284 for (i = context_items_.begin(); i != context_items_.end(); ++i) { | |
| 285 STLDeleteElements(&(i->second)); | |
| 286 } | |
| 287 } | |
| 288 | |
| 289 std::set<std::string> ExtensionMenuManager::ExtensionIds() { | |
| 290 std::set<std::string> id_set; | |
| 291 for (MenuItemMap::const_iterator i = context_items_.begin(); | |
| 292 i != context_items_.end(); ++i) { | |
| 293 id_set.insert(i->first); | |
| 294 } | |
| 295 return id_set; | |
| 296 } | |
| 297 | |
| 298 const ExtensionMenuItem::List* ExtensionMenuManager::MenuItems( | |
| 299 const std::string& extension_id) { | |
| 300 MenuItemMap::iterator i = context_items_.find(extension_id); | |
| 301 if (i != context_items_.end()) { | |
| 302 return &(i->second); | |
| 303 } | |
| 304 return NULL; | |
| 305 } | |
| 306 | |
| 307 bool ExtensionMenuManager::AddContextItem( | |
| 308 const extensions::Extension* extension, | |
| 309 ExtensionMenuItem* item) { | |
| 310 const std::string& extension_id = item->extension_id(); | |
| 311 // The item must have a non-empty extension id, and not have already been | |
| 312 // added. | |
| 313 if (extension_id.empty() || ContainsKey(items_by_id_, item->id())) | |
| 314 return false; | |
| 315 | |
| 316 DCHECK_EQ(extension->id(), extension_id); | |
| 317 | |
| 318 bool first_item = !ContainsKey(context_items_, extension_id); | |
| 319 context_items_[extension_id].push_back(item); | |
| 320 items_by_id_[item->id()] = item; | |
| 321 | |
| 322 if (item->type() == ExtensionMenuItem::RADIO) { | |
| 323 if (item->checked()) | |
| 324 RadioItemSelected(item); | |
| 325 else | |
| 326 SanitizeRadioList(context_items_[extension_id]); | |
| 327 } | |
| 328 | |
| 329 // If this is the first item for this extension, start loading its icon. | |
| 330 if (first_item) | |
| 331 icon_manager_.LoadIcon(extension); | |
| 332 | |
| 333 return true; | |
| 334 } | |
| 335 | |
| 336 bool ExtensionMenuManager::AddChildItem(const ExtensionMenuItem::Id& parent_id, | |
| 337 ExtensionMenuItem* child) { | |
| 338 ExtensionMenuItem* parent = GetItemById(parent_id); | |
| 339 if (!parent || parent->type() != ExtensionMenuItem::NORMAL || | |
| 340 parent->incognito() != child->incognito() || | |
| 341 parent->extension_id() != child->extension_id() || | |
| 342 ContainsKey(items_by_id_, child->id())) | |
| 343 return false; | |
| 344 parent->AddChild(child); | |
| 345 items_by_id_[child->id()] = child; | |
| 346 | |
| 347 if (child->type() == ExtensionMenuItem::RADIO) | |
| 348 SanitizeRadioList(parent->children()); | |
| 349 return true; | |
| 350 } | |
| 351 | |
| 352 bool ExtensionMenuManager::DescendantOf( | |
| 353 ExtensionMenuItem* item, | |
| 354 const ExtensionMenuItem::Id& ancestor_id) { | |
| 355 // Work our way up the tree until we find the ancestor or NULL. | |
| 356 ExtensionMenuItem::Id* id = item->parent_id(); | |
| 357 while (id != NULL) { | |
| 358 DCHECK(*id != item->id()); // Catch circular graphs. | |
| 359 if (*id == ancestor_id) | |
| 360 return true; | |
| 361 ExtensionMenuItem* next = GetItemById(*id); | |
| 362 if (!next) { | |
| 363 NOTREACHED(); | |
| 364 return false; | |
| 365 } | |
| 366 id = next->parent_id(); | |
| 367 } | |
| 368 return false; | |
| 369 } | |
| 370 | |
| 371 bool ExtensionMenuManager::ChangeParent( | |
| 372 const ExtensionMenuItem::Id& child_id, | |
| 373 const ExtensionMenuItem::Id* parent_id) { | |
| 374 ExtensionMenuItem* child = GetItemById(child_id); | |
| 375 ExtensionMenuItem* new_parent = parent_id ? GetItemById(*parent_id) : NULL; | |
| 376 if ((parent_id && (child_id == *parent_id)) || !child || | |
| 377 (!new_parent && parent_id != NULL) || | |
| 378 (new_parent && (DescendantOf(new_parent, child_id) || | |
| 379 child->incognito() != new_parent->incognito() || | |
| 380 child->extension_id() != new_parent->extension_id()))) | |
| 381 return false; | |
| 382 | |
| 383 ExtensionMenuItem::Id* old_parent_id = child->parent_id(); | |
| 384 if (old_parent_id != NULL) { | |
| 385 ExtensionMenuItem* old_parent = GetItemById(*old_parent_id); | |
| 386 if (!old_parent) { | |
| 387 NOTREACHED(); | |
| 388 return false; | |
| 389 } | |
| 390 ExtensionMenuItem* taken = | |
| 391 old_parent->ReleaseChild(child_id, false /* non-recursive search*/); | |
| 392 DCHECK(taken == child); | |
| 393 SanitizeRadioList(old_parent->children()); | |
| 394 } else { | |
| 395 // This is a top-level item, so we need to pull it out of our list of | |
| 396 // top-level items. | |
| 397 MenuItemMap::iterator i = context_items_.find(child->extension_id()); | |
| 398 if (i == context_items_.end()) { | |
| 399 NOTREACHED(); | |
| 400 return false; | |
| 401 } | |
| 402 ExtensionMenuItem::List& list = i->second; | |
| 403 ExtensionMenuItem::List::iterator j = std::find(list.begin(), list.end(), | |
| 404 child); | |
| 405 if (j == list.end()) { | |
| 406 NOTREACHED(); | |
| 407 return false; | |
| 408 } | |
| 409 list.erase(j); | |
| 410 SanitizeRadioList(list); | |
| 411 } | |
| 412 | |
| 413 if (new_parent) { | |
| 414 new_parent->AddChild(child); | |
| 415 SanitizeRadioList(new_parent->children()); | |
| 416 } else { | |
| 417 context_items_[child->extension_id()].push_back(child); | |
| 418 child->parent_id_.reset(NULL); | |
| 419 SanitizeRadioList(context_items_[child->extension_id()]); | |
| 420 } | |
| 421 return true; | |
| 422 } | |
| 423 | |
| 424 bool ExtensionMenuManager::RemoveContextMenuItem( | |
| 425 const ExtensionMenuItem::Id& id) { | |
| 426 if (!ContainsKey(items_by_id_, id)) | |
| 427 return false; | |
| 428 | |
| 429 ExtensionMenuItem* menu_item = GetItemById(id); | |
| 430 DCHECK(menu_item); | |
| 431 std::string extension_id = menu_item->extension_id(); | |
| 432 MenuItemMap::iterator i = context_items_.find(extension_id); | |
| 433 if (i == context_items_.end()) { | |
| 434 NOTREACHED(); | |
| 435 return false; | |
| 436 } | |
| 437 | |
| 438 bool result = false; | |
| 439 std::set<ExtensionMenuItem::Id> items_removed; | |
| 440 ExtensionMenuItem::List& list = i->second; | |
| 441 ExtensionMenuItem::List::iterator j; | |
| 442 for (j = list.begin(); j < list.end(); ++j) { | |
| 443 // See if the current top-level item is a match. | |
| 444 if ((*j)->id() == id) { | |
| 445 items_removed = (*j)->RemoveAllDescendants(); | |
| 446 items_removed.insert(id); | |
| 447 delete *j; | |
| 448 list.erase(j); | |
| 449 result = true; | |
| 450 SanitizeRadioList(list); | |
| 451 break; | |
| 452 } else { | |
| 453 // See if the item to remove was found as a descendant of the current | |
| 454 // top-level item. | |
| 455 ExtensionMenuItem* child = (*j)->ReleaseChild(id, true /* recursive */); | |
| 456 if (child) { | |
| 457 items_removed = child->RemoveAllDescendants(); | |
| 458 items_removed.insert(id); | |
| 459 SanitizeRadioList(GetItemById(*child->parent_id())->children()); | |
| 460 delete child; | |
| 461 result = true; | |
| 462 break; | |
| 463 } | |
| 464 } | |
| 465 } | |
| 466 DCHECK(result); // The check at the very top should have prevented this. | |
| 467 | |
| 468 // Clear entries from the items_by_id_ map. | |
| 469 std::set<ExtensionMenuItem::Id>::iterator removed_iter; | |
| 470 for (removed_iter = items_removed.begin(); | |
| 471 removed_iter != items_removed.end(); | |
| 472 ++removed_iter) { | |
| 473 items_by_id_.erase(*removed_iter); | |
| 474 } | |
| 475 | |
| 476 if (list.empty()) { | |
| 477 context_items_.erase(extension_id); | |
| 478 icon_manager_.RemoveIcon(extension_id); | |
| 479 } | |
| 480 return result; | |
| 481 } | |
| 482 | |
| 483 void ExtensionMenuManager::RemoveAllContextItems( | |
| 484 const std::string& extension_id) { | |
| 485 ExtensionMenuItem::List::iterator i; | |
| 486 for (i = context_items_[extension_id].begin(); | |
| 487 i != context_items_[extension_id].end(); ++i) { | |
| 488 ExtensionMenuItem* item = *i; | |
| 489 items_by_id_.erase(item->id()); | |
| 490 | |
| 491 // Remove descendants from this item and erase them from the lookup cache. | |
| 492 std::set<ExtensionMenuItem::Id> removed_ids = item->RemoveAllDescendants(); | |
| 493 std::set<ExtensionMenuItem::Id>::const_iterator j; | |
| 494 for (j = removed_ids.begin(); j != removed_ids.end(); ++j) { | |
| 495 items_by_id_.erase(*j); | |
| 496 } | |
| 497 } | |
| 498 STLDeleteElements(&context_items_[extension_id]); | |
| 499 context_items_.erase(extension_id); | |
| 500 icon_manager_.RemoveIcon(extension_id); | |
| 501 } | |
| 502 | |
| 503 ExtensionMenuItem* ExtensionMenuManager::GetItemById( | |
| 504 const ExtensionMenuItem::Id& id) const { | |
| 505 std::map<ExtensionMenuItem::Id, ExtensionMenuItem*>::const_iterator i = | |
| 506 items_by_id_.find(id); | |
| 507 if (i != items_by_id_.end()) | |
| 508 return i->second; | |
| 509 else | |
| 510 return NULL; | |
| 511 } | |
| 512 | |
| 513 void ExtensionMenuManager::RadioItemSelected(ExtensionMenuItem* item) { | |
| 514 // If this is a child item, we need to get a handle to the list from its | |
| 515 // parent. Otherwise get a handle to the top-level list. | |
| 516 const ExtensionMenuItem::List* list = NULL; | |
| 517 if (item->parent_id()) { | |
| 518 ExtensionMenuItem* parent = GetItemById(*item->parent_id()); | |
| 519 if (!parent) { | |
| 520 NOTREACHED(); | |
| 521 return; | |
| 522 } | |
| 523 list = &(parent->children()); | |
| 524 } else { | |
| 525 if (context_items_.find(item->extension_id()) == context_items_.end()) { | |
| 526 NOTREACHED(); | |
| 527 return; | |
| 528 } | |
| 529 list = &context_items_[item->extension_id()]; | |
| 530 } | |
| 531 | |
| 532 // Find where |item| is in the list. | |
| 533 ExtensionMenuItem::List::const_iterator item_location; | |
| 534 for (item_location = list->begin(); item_location != list->end(); | |
| 535 ++item_location) { | |
| 536 if (*item_location == item) | |
| 537 break; | |
| 538 } | |
| 539 if (item_location == list->end()) { | |
| 540 NOTREACHED(); // We should have found the item. | |
| 541 return; | |
| 542 } | |
| 543 | |
| 544 // Iterate backwards from |item| and uncheck any adjacent radio items. | |
| 545 ExtensionMenuItem::List::const_iterator i; | |
| 546 if (item_location != list->begin()) { | |
| 547 i = item_location; | |
| 548 do { | |
| 549 --i; | |
| 550 if ((*i)->type() != ExtensionMenuItem::RADIO) | |
| 551 break; | |
| 552 (*i)->SetChecked(false); | |
| 553 } while (i != list->begin()); | |
| 554 } | |
| 555 | |
| 556 // Now iterate forwards from |item| and uncheck any adjacent radio items. | |
| 557 for (i = item_location + 1; i != list->end(); ++i) { | |
| 558 if ((*i)->type() != ExtensionMenuItem::RADIO) | |
| 559 break; | |
| 560 (*i)->SetChecked(false); | |
| 561 } | |
| 562 } | |
| 563 | |
| 564 static void AddURLProperty(DictionaryValue* dictionary, | |
| 565 const std::string& key, const GURL& url) { | |
| 566 if (!url.is_empty()) | |
| 567 dictionary->SetString(key, url.possibly_invalid_spec()); | |
| 568 } | |
| 569 | |
| 570 void ExtensionMenuManager::ExecuteCommand( | |
| 571 Profile* profile, | |
| 572 WebContents* web_contents, | |
| 573 const content::ContextMenuParams& params, | |
| 574 const ExtensionMenuItem::Id& menu_item_id) { | |
| 575 ExtensionEventRouter* event_router = profile->GetExtensionEventRouter(); | |
| 576 if (!event_router) | |
| 577 return; | |
| 578 | |
| 579 ExtensionMenuItem* item = GetItemById(menu_item_id); | |
| 580 if (!item) | |
| 581 return; | |
| 582 | |
| 583 // ExtensionService/Extension can be NULL in unit tests :( | |
| 584 ExtensionService* service = | |
| 585 ExtensionSystem::Get(profile_)->extension_service(); | |
| 586 const extensions::Extension* extension = service ? | |
| 587 service->extensions()->GetByID(menu_item_id.extension_id) : NULL; | |
| 588 | |
| 589 if (item->type() == ExtensionMenuItem::RADIO) | |
| 590 RadioItemSelected(item); | |
| 591 | |
| 592 ListValue args; | |
| 593 | |
| 594 DictionaryValue* properties = new DictionaryValue(); | |
| 595 SetIdKeyValue(properties, "menuItemId", item->id()); | |
| 596 if (item->parent_id()) | |
| 597 SetIdKeyValue(properties, "parentMenuItemId", item->id()); | |
| 598 | |
| 599 switch (params.media_type) { | |
| 600 case WebKit::WebContextMenuData::MediaTypeImage: | |
| 601 properties->SetString("mediaType", "image"); | |
| 602 break; | |
| 603 case WebKit::WebContextMenuData::MediaTypeVideo: | |
| 604 properties->SetString("mediaType", "video"); | |
| 605 break; | |
| 606 case WebKit::WebContextMenuData::MediaTypeAudio: | |
| 607 properties->SetString("mediaType", "audio"); | |
| 608 break; | |
| 609 default: {} // Do nothing. | |
| 610 } | |
| 611 | |
| 612 AddURLProperty(properties, "linkUrl", params.unfiltered_link_url); | |
| 613 AddURLProperty(properties, "srcUrl", params.src_url); | |
| 614 AddURLProperty(properties, "pageUrl", params.page_url); | |
| 615 AddURLProperty(properties, "frameUrl", params.frame_url); | |
| 616 | |
| 617 if (params.selection_text.length() > 0) | |
| 618 properties->SetString("selectionText", params.selection_text); | |
| 619 | |
| 620 properties->SetBoolean("editable", params.is_editable); | |
| 621 | |
| 622 args.Append(properties); | |
| 623 | |
| 624 // Add the tab info to the argument list. | |
| 625 // Note: web_contents only NULL in unit tests :( | |
| 626 if (web_contents) | |
| 627 args.Append(ExtensionTabUtil::CreateTabValue(web_contents)); | |
| 628 else | |
| 629 args.Append(new DictionaryValue()); | |
| 630 | |
| 631 if (item->type() == ExtensionMenuItem::CHECKBOX || | |
| 632 item->type() == ExtensionMenuItem::RADIO) { | |
| 633 bool was_checked = item->checked(); | |
| 634 properties->SetBoolean("wasChecked", was_checked); | |
| 635 | |
| 636 // RADIO items always get set to true when you click on them, but CHECKBOX | |
| 637 // items get their state toggled. | |
| 638 bool checked = | |
| 639 (item->type() == ExtensionMenuItem::RADIO) ? true : !was_checked; | |
| 640 | |
| 641 item->SetChecked(checked); | |
| 642 properties->SetBoolean("checked", item->checked()); | |
| 643 | |
| 644 if (extension) | |
| 645 WriteToStorage(extension); | |
| 646 } | |
| 647 | |
| 648 TabContents* tab_contents = web_contents ? | |
| 649 TabContents::FromWebContents(web_contents) : NULL; | |
| 650 if (tab_contents && extension) { | |
| 651 tab_contents->extension_tab_helper()->active_tab_permission_manager()-> | |
| 652 GrantIfRequested(extension); | |
| 653 } | |
| 654 | |
| 655 std::string json_args; | |
| 656 base::JSONWriter::Write(&args, &json_args); | |
| 657 event_router->DispatchEventToExtension( | |
| 658 item->extension_id(), extension_event_names::kOnContextMenus, | |
| 659 json_args, profile, GURL(), | |
| 660 ExtensionEventRouter::USER_GESTURE_ENABLED); | |
| 661 event_router->DispatchEventToExtension( | |
| 662 item->extension_id(), extension_event_names::kOnContextMenuClicked, | |
| 663 json_args, profile, GURL(), | |
| 664 ExtensionEventRouter::USER_GESTURE_ENABLED); | |
| 665 } | |
| 666 | |
| 667 void ExtensionMenuManager::SanitizeRadioList( | |
| 668 const ExtensionMenuItem::List& item_list) { | |
| 669 ExtensionMenuItem::List::const_iterator i = item_list.begin(); | |
| 670 while (i != item_list.end()) { | |
| 671 if ((*i)->type() != ExtensionMenuItem::RADIO) { | |
| 672 ++i; | |
| 673 break; | |
| 674 } | |
| 675 | |
| 676 // Uncheck any checked radio items in the run, and at the end reset | |
| 677 // the appropriate one to checked. If no check radio items were found, | |
| 678 // then check the first radio item in the run. | |
| 679 ExtensionMenuItem::List::const_iterator last_checked = item_list.end(); | |
| 680 ExtensionMenuItem::List::const_iterator radio_run_iter; | |
| 681 for (radio_run_iter = i; radio_run_iter != item_list.end(); | |
| 682 ++radio_run_iter) { | |
| 683 if ((*radio_run_iter)->type() != ExtensionMenuItem::RADIO) { | |
| 684 break; | |
| 685 } | |
| 686 | |
| 687 if ((*radio_run_iter)->checked()) { | |
| 688 last_checked = radio_run_iter; | |
| 689 (*radio_run_iter)->SetChecked(false); | |
| 690 } | |
| 691 } | |
| 692 | |
| 693 if (last_checked != item_list.end()) | |
| 694 (*last_checked)->SetChecked(true); | |
| 695 else | |
| 696 (*i)->SetChecked(true); | |
| 697 | |
| 698 i = radio_run_iter; | |
| 699 } | |
| 700 } | |
| 701 | |
| 702 bool ExtensionMenuManager::ItemUpdated(const ExtensionMenuItem::Id& id) { | |
| 703 if (!ContainsKey(items_by_id_, id)) | |
| 704 return false; | |
| 705 | |
| 706 ExtensionMenuItem* menu_item = GetItemById(id); | |
| 707 DCHECK(menu_item); | |
| 708 | |
| 709 if (menu_item->parent_id()) { | |
| 710 SanitizeRadioList(GetItemById(*menu_item->parent_id())->children()); | |
| 711 } else { | |
| 712 std::string extension_id = menu_item->extension_id(); | |
| 713 MenuItemMap::iterator i = context_items_.find(extension_id); | |
| 714 if (i == context_items_.end()) { | |
| 715 NOTREACHED(); | |
| 716 return false; | |
| 717 } | |
| 718 SanitizeRadioList(i->second); | |
| 719 } | |
| 720 | |
| 721 return true; | |
| 722 } | |
| 723 | |
| 724 void ExtensionMenuManager::WriteToStorage( | |
| 725 const extensions::Extension* extension) { | |
| 726 if (!extension->has_lazy_background_page()) | |
| 727 return; | |
| 728 const ExtensionMenuItem::List* top_items = MenuItems(extension->id()); | |
| 729 ExtensionMenuItem::List all_items; | |
| 730 if (top_items) { | |
| 731 for (ExtensionMenuItem::List::const_iterator i = top_items->begin(); | |
| 732 i != top_items->end(); ++i) { | |
| 733 (*i)->GetFlattenedSubtree(&all_items); | |
| 734 } | |
| 735 } | |
| 736 | |
| 737 extensions::StateStore* store = ExtensionSystem::Get(profile_)->state_store(); | |
| 738 if (store) | |
| 739 store->SetExtensionValue(extension->id(), kContextMenusKey, | |
| 740 MenuItemsToValue(all_items)); | |
| 741 } | |
| 742 | |
| 743 void ExtensionMenuManager::ReadFromStorage(const std::string& extension_id, | |
| 744 scoped_ptr<base::Value> value) { | |
| 745 const extensions::Extension* extension = | |
| 746 ExtensionSystem::Get(profile_)->extension_service()->extensions()-> | |
| 747 GetByID(extension_id); | |
| 748 if (!extension) | |
| 749 return; | |
| 750 | |
| 751 ExtensionMenuItem::List items = MenuItemsFromValue(extension_id, value.get()); | |
| 752 for (size_t i = 0; i < items.size(); ++i) { | |
| 753 if (items[i]->parent_id()) { | |
| 754 // Parent IDs are stored in the parent_id field for convenience, but | |
| 755 // they have not yet been validated. Separate them out here. | |
| 756 // Because of the order in which we store items in the prefs, parents will | |
| 757 // precede children, so we should already know about any parent items. | |
| 758 scoped_ptr<ExtensionMenuItem::Id> parent_id; | |
| 759 parent_id.swap(items[i]->parent_id_); | |
| 760 AddChildItem(*parent_id, items[i]); | |
| 761 } else { | |
| 762 AddContextItem(extension, items[i]); | |
| 763 } | |
| 764 } | |
| 765 } | |
| 766 | |
| 767 void ExtensionMenuManager::Observe( | |
| 768 int type, | |
| 769 const content::NotificationSource& source, | |
| 770 const content::NotificationDetails& details) { | |
| 771 if (type == chrome::NOTIFICATION_EXTENSION_UNLOADED) { | |
| 772 // Remove menu items for disabled/uninstalled extensions. | |
| 773 const extensions::Extension* extension = | |
| 774 content::Details<extensions::UnloadedExtensionInfo>(details)->extension; | |
| 775 if (ContainsKey(context_items_, extension->id())) { | |
| 776 RemoveAllContextItems(extension->id()); | |
| 777 } | |
| 778 } else if (type == chrome::NOTIFICATION_EXTENSION_LOADED) { | |
| 779 const extensions::Extension* extension = | |
| 780 content::Details<const extensions::Extension>(details).ptr(); | |
| 781 extensions::StateStore* store = | |
| 782 ExtensionSystem::Get(profile_)->state_store(); | |
| 783 if (store && extension->has_lazy_background_page()) { | |
| 784 store->GetExtensionValue(extension->id(), kContextMenusKey, | |
| 785 base::Bind(&ExtensionMenuManager::ReadFromStorage, | |
| 786 AsWeakPtr(), extension->id())); | |
| 787 } | |
| 788 } | |
| 789 } | |
| 790 | |
| 791 const SkBitmap& ExtensionMenuManager::GetIconForExtension( | |
| 792 const std::string& extension_id) { | |
| 793 return icon_manager_.GetIcon(extension_id); | |
| 794 } | |
| 795 | |
| 796 ExtensionMenuItem::Id::Id() | |
| 797 : incognito(false), extension_id(""), uid(0), string_uid("") { | |
| 798 } | |
| 799 | |
| 800 ExtensionMenuItem::Id::Id(bool incognito, const std::string& extension_id) | |
| 801 : incognito(incognito), extension_id(extension_id), uid(0), | |
| 802 string_uid("") { | |
| 803 } | |
| 804 | |
| 805 ExtensionMenuItem::Id::~Id() { | |
| 806 } | |
| 807 | |
| 808 bool ExtensionMenuItem::Id::operator==(const Id& other) const { | |
| 809 return (incognito == other.incognito && | |
| 810 extension_id == other.extension_id && | |
| 811 uid == other.uid && | |
| 812 string_uid == other.string_uid); | |
| 813 } | |
| 814 | |
| 815 bool ExtensionMenuItem::Id::operator!=(const Id& other) const { | |
| 816 return !(*this == other); | |
| 817 } | |
| 818 | |
| 819 bool ExtensionMenuItem::Id::operator<(const Id& other) const { | |
| 820 if (incognito < other.incognito) | |
| 821 return true; | |
| 822 if (incognito == other.incognito) { | |
| 823 if (extension_id < other.extension_id) | |
| 824 return true; | |
| 825 if (extension_id == other.extension_id) { | |
| 826 if (uid < other.uid) | |
| 827 return true; | |
| 828 if (uid == other.uid) | |
| 829 return string_uid < other.string_uid; | |
| 830 } | |
| 831 } | |
| 832 return false; | |
| 833 } | |
| OLD | NEW |