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/api/context_menus/context_menus_api.h" 6 7#include <string> 8 9#include "base/strings/string_number_conversions.h" 10#include "base/strings/string_util.h" 11#include "base/values.h" 12#include "chrome/browser/extensions/menu_manager.h" 13#include "chrome/browser/profiles/profile.h" 14#include "chrome/common/extensions/api/context_menus.h" 15#include "extensions/common/error_utils.h" 16#include "extensions/common/manifest_handlers/background_info.h" 17#include "extensions/common/url_pattern_set.h" 18 19using extensions::ErrorUtils; 20 21namespace { 22 23const char kGeneratedIdKey[] = "generatedId"; 24 25const char kCannotFindItemError[] = "Cannot find menu item with id *"; 26const char kOnclickDisallowedError[] = "Extensions using event pages cannot " 27 "pass an onclick parameter to chrome.contextMenus.create. Instead, use " 28 "the chrome.contextMenus.onClicked event."; 29const char kCheckedError[] = 30 "Only items with type \"radio\" or \"checkbox\" can be checked"; 31const char kDuplicateIDError[] = 32 "Cannot create item with duplicate id *"; 33const char kIdRequiredError[] = "Extensions using event pages must pass an " 34 "id parameter to chrome.contextMenus.create"; 35const char kParentsMustBeNormalError[] = 36 "Parent items must have type \"normal\""; 37const char kTitleNeededError[] = 38 "All menu items except for separators must have a title"; 39const char kLauncherNotAllowedError[] = 40 "Only packaged apps are allowed to use 'launcher' context"; 41 42std::string GetIDString(const extensions::MenuItem::Id& id) { 43 if (id.uid == 0) 44 return id.string_uid; 45 else 46 return base::IntToString(id.uid); 47} 48 49template<typename PropertyWithEnumT> 50extensions::MenuItem::ContextList GetContexts( 51 const PropertyWithEnumT& property) { 52 extensions::MenuItem::ContextList contexts; 53 for (size_t i = 0; i < property.contexts->size(); ++i) { 54 switch (property.contexts->at(i)) { 55 case PropertyWithEnumT::CONTEXTS_TYPE_ALL: 56 contexts.Add(extensions::MenuItem::ALL); 57 break; 58 case PropertyWithEnumT::CONTEXTS_TYPE_PAGE: 59 contexts.Add(extensions::MenuItem::PAGE); 60 break; 61 case PropertyWithEnumT::CONTEXTS_TYPE_SELECTION: 62 contexts.Add(extensions::MenuItem::SELECTION); 63 break; 64 case PropertyWithEnumT::CONTEXTS_TYPE_LINK: 65 contexts.Add(extensions::MenuItem::LINK); 66 break; 67 case PropertyWithEnumT::CONTEXTS_TYPE_EDITABLE: 68 contexts.Add(extensions::MenuItem::EDITABLE); 69 break; 70 case PropertyWithEnumT::CONTEXTS_TYPE_IMAGE: 71 contexts.Add(extensions::MenuItem::IMAGE); 72 break; 73 case PropertyWithEnumT::CONTEXTS_TYPE_VIDEO: 74 contexts.Add(extensions::MenuItem::VIDEO); 75 break; 76 case PropertyWithEnumT::CONTEXTS_TYPE_AUDIO: 77 contexts.Add(extensions::MenuItem::AUDIO); 78 break; 79 case PropertyWithEnumT::CONTEXTS_TYPE_FRAME: 80 contexts.Add(extensions::MenuItem::FRAME); 81 break; 82 case PropertyWithEnumT::CONTEXTS_TYPE_LAUNCHER: 83 contexts.Add(extensions::MenuItem::LAUNCHER); 84 break; 85 case PropertyWithEnumT::CONTEXTS_TYPE_NONE: 86 NOTREACHED(); 87 } 88 } 89 return contexts; 90} 91 92template<typename PropertyWithEnumT> 93extensions::MenuItem::Type GetType(const PropertyWithEnumT& property, 94 extensions::MenuItem::Type default_type) { 95 switch (property.type) { 96 case PropertyWithEnumT::TYPE_NONE: 97 return default_type; 98 case PropertyWithEnumT::TYPE_NORMAL: 99 return extensions::MenuItem::NORMAL; 100 case PropertyWithEnumT::TYPE_CHECKBOX: 101 return extensions::MenuItem::CHECKBOX; 102 case PropertyWithEnumT::TYPE_RADIO: 103 return extensions::MenuItem::RADIO; 104 case PropertyWithEnumT::TYPE_SEPARATOR: 105 return extensions::MenuItem::SEPARATOR; 106 } 107 return extensions::MenuItem::NORMAL; 108} 109 110template<typename PropertyWithEnumT> 111scoped_ptr<extensions::MenuItem::Id> GetParentId( 112 const PropertyWithEnumT& property, 113 bool is_off_the_record, 114 std::string extension_id) { 115 if (!property.parent_id) 116 return scoped_ptr<extensions::MenuItem::Id>(); 117 118 scoped_ptr<extensions::MenuItem::Id> parent_id( 119 new extensions::MenuItem::Id(is_off_the_record, extension_id)); 120 if (property.parent_id->as_integer) 121 parent_id->uid = *property.parent_id->as_integer; 122 else if (property.parent_id->as_string) 123 parent_id->string_uid = *property.parent_id->as_string; 124 else 125 NOTREACHED(); 126 return parent_id.Pass(); 127} 128 129extensions::MenuItem* GetParent(extensions::MenuItem::Id parent_id, 130 const extensions::MenuManager* menu_manager, 131 std::string* error) { 132 extensions::MenuItem* parent = menu_manager->GetItemById(parent_id); 133 if (!parent) { 134 *error = ErrorUtils::FormatErrorMessage( 135 kCannotFindItemError, GetIDString(parent_id)); 136 return NULL; 137 } 138 if (parent->type() != extensions::MenuItem::NORMAL) { 139 *error = kParentsMustBeNormalError; 140 return NULL; 141 } 142 143 return parent; 144} 145 146} // namespace 147 148namespace extensions { 149 150namespace Create = api::context_menus::Create; 151namespace Remove = api::context_menus::Remove; 152namespace Update = api::context_menus::Update; 153 154bool ContextMenusCreateFunction::RunImpl() { 155 MenuItem::Id id(GetProfile()->IsOffTheRecord(), extension_id()); 156 scoped_ptr<Create::Params> params(Create::Params::Create(*args_)); 157 EXTENSION_FUNCTION_VALIDATE(params.get()); 158 159 if (params->create_properties.id.get()) { 160 id.string_uid = *params->create_properties.id; 161 } else { 162 if (BackgroundInfo::HasLazyBackgroundPage(GetExtension())) { 163 error_ = kIdRequiredError; 164 return false; 165 } 166 167 // The Generated Id is added by context_menus_custom_bindings.js. 168 base::DictionaryValue* properties = NULL; 169 EXTENSION_FUNCTION_VALIDATE(args_->GetDictionary(0, &properties)); 170 EXTENSION_FUNCTION_VALIDATE(properties->GetInteger(kGeneratedIdKey, 171 &id.uid)); 172 } 173 174 std::string title; 175 if (params->create_properties.title.get()) 176 title = *params->create_properties.title; 177 178 MenuManager* menu_manager = MenuManager::Get(GetProfile()); 179 180 if (menu_manager->GetItemById(id)) { 181 error_ = ErrorUtils::FormatErrorMessage(kDuplicateIDError, 182 GetIDString(id)); 183 return false; 184 } 185 186 if (BackgroundInfo::HasLazyBackgroundPage(GetExtension()) && 187 params->create_properties.onclick.get()) { 188 error_ = kOnclickDisallowedError; 189 return false; 190 } 191 192 MenuItem::ContextList contexts; 193 if (params->create_properties.contexts.get()) 194 contexts = GetContexts(params->create_properties); 195 else 196 contexts.Add(MenuItem::PAGE); 197 198 if (contexts.Contains(MenuItem::LAUNCHER) && 199 !GetExtension()->is_platform_app()) { 200 error_ = kLauncherNotAllowedError; 201 return false; 202 } 203 204 MenuItem::Type type = GetType(params->create_properties, MenuItem::NORMAL); 205 206 if (title.empty() && type != MenuItem::SEPARATOR) { 207 error_ = kTitleNeededError; 208 return false; 209 } 210 211 bool checked = false; 212 if (params->create_properties.checked.get()) 213 checked = *params->create_properties.checked; 214 215 bool enabled = true; 216 if (params->create_properties.enabled.get()) 217 enabled = *params->create_properties.enabled; 218 219 scoped_ptr<MenuItem> item( 220 new MenuItem(id, title, checked, enabled, type, contexts)); 221 222 if (!item->PopulateURLPatterns( 223 params->create_properties.document_url_patterns.get(), 224 params->create_properties.target_url_patterns.get(), 225 &error_)) { 226 return false; 227 } 228 229 bool success = true; 230 scoped_ptr<MenuItem::Id> parent_id(GetParentId(params->create_properties, 231 GetProfile()->IsOffTheRecord(), 232 extension_id())); 233 if (parent_id.get()) { 234 MenuItem* parent = GetParent(*parent_id, menu_manager, &error_); 235 if (!parent) 236 return false; 237 success = menu_manager->AddChildItem(parent->id(), item.release()); 238 } else { 239 success = menu_manager->AddContextItem(GetExtension(), item.release()); 240 } 241 242 if (!success) 243 return false; 244 245 menu_manager->WriteToStorage(GetExtension()); 246 return true; 247} 248 249bool ContextMenusUpdateFunction::RunImpl() { 250 bool radio_item_updated = false; 251 MenuItem::Id item_id(GetProfile()->IsOffTheRecord(), extension_id()); 252 scoped_ptr<Update::Params> params(Update::Params::Create(*args_)); 253 254 EXTENSION_FUNCTION_VALIDATE(params.get()); 255 if (params->id.as_string) 256 item_id.string_uid = *params->id.as_string; 257 else if (params->id.as_integer) 258 item_id.uid = *params->id.as_integer; 259 else 260 NOTREACHED(); 261 262 MenuManager* manager = MenuManager::Get(GetProfile()); 263 MenuItem* item = manager->GetItemById(item_id); 264 if (!item || item->extension_id() != extension_id()) { 265 error_ = ErrorUtils::FormatErrorMessage( 266 kCannotFindItemError, GetIDString(item_id)); 267 return false; 268 } 269 270 // Type. 271 MenuItem::Type type = GetType(params->update_properties, item->type()); 272 273 if (type != item->type()) { 274 if (type == MenuItem::RADIO || item->type() == MenuItem::RADIO) 275 radio_item_updated = true; 276 item->set_type(type); 277 } 278 279 // Title. 280 if (params->update_properties.title.get()) { 281 std::string title(*params->update_properties.title); 282 if (title.empty() && item->type() != MenuItem::SEPARATOR) { 283 error_ = kTitleNeededError; 284 return false; 285 } 286 item->set_title(title); 287 } 288 289 // Checked state. 290 if (params->update_properties.checked.get()) { 291 bool checked = *params->update_properties.checked; 292 if (checked && 293 item->type() != MenuItem::CHECKBOX && 294 item->type() != MenuItem::RADIO) { 295 error_ = kCheckedError; 296 return false; 297 } 298 if (checked != item->checked()) { 299 if (!item->SetChecked(checked)) { 300 error_ = kCheckedError; 301 return false; 302 } 303 radio_item_updated = true; 304 } 305 } 306 307 // Enabled. 308 if (params->update_properties.enabled.get()) 309 item->set_enabled(*params->update_properties.enabled); 310 311 // Contexts. 312 MenuItem::ContextList contexts; 313 if (params->update_properties.contexts.get()) { 314 contexts = GetContexts(params->update_properties); 315 316 if (contexts.Contains(MenuItem::LAUNCHER) && 317 !GetExtension()->is_platform_app()) { 318 error_ = kLauncherNotAllowedError; 319 return false; 320 } 321 322 if (contexts != item->contexts()) 323 item->set_contexts(contexts); 324 } 325 326 // Parent id. 327 MenuItem* parent = NULL; 328 scoped_ptr<MenuItem::Id> parent_id(GetParentId(params->update_properties, 329 GetProfile()->IsOffTheRecord(), 330 extension_id())); 331 if (parent_id.get()) { 332 MenuItem* parent = GetParent(*parent_id, manager, &error_); 333 if (!parent || !manager->ChangeParent(item->id(), &parent->id())) 334 return false; 335 } 336 337 // URL Patterns. 338 if (!item->PopulateURLPatterns( 339 params->update_properties.document_url_patterns.get(), 340 params->update_properties.target_url_patterns.get(), &error_)) { 341 return false; 342 } 343 344 // There is no need to call ItemUpdated if ChangeParent is called because 345 // all sanitation is taken care of in ChangeParent. 346 if (!parent && radio_item_updated && !manager->ItemUpdated(item->id())) 347 return false; 348 349 manager->WriteToStorage(GetExtension()); 350 return true; 351} 352 353bool ContextMenusRemoveFunction::RunImpl() { 354 scoped_ptr<Remove::Params> params(Remove::Params::Create(*args_)); 355 EXTENSION_FUNCTION_VALIDATE(params.get()); 356 357 MenuManager* manager = MenuManager::Get(GetProfile()); 358 359 MenuItem::Id id(GetProfile()->IsOffTheRecord(), extension_id()); 360 if (params->menu_item_id.as_string) 361 id.string_uid = *params->menu_item_id.as_string; 362 else if (params->menu_item_id.as_integer) 363 id.uid = *params->menu_item_id.as_integer; 364 else 365 NOTREACHED(); 366 367 MenuItem* item = manager->GetItemById(id); 368 // Ensure one extension can't remove another's menu items. 369 if (!item || item->extension_id() != extension_id()) { 370 error_ = ErrorUtils::FormatErrorMessage( 371 kCannotFindItemError, GetIDString(id)); 372 return false; 373 } 374 375 if (!manager->RemoveContextMenuItem(id)) 376 return false; 377 manager->WriteToStorage(GetExtension()); 378 return true; 379} 380 381bool ContextMenusRemoveAllFunction::RunImpl() { 382 MenuManager* manager = MenuManager::Get(GetProfile()); 383 manager->RemoveAllContextItems(GetExtension()->id()); 384 manager->WriteToStorage(GetExtension()); 385 return true; 386} 387 388} // namespace extensions 389