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/commands/command_service.h" 6 7 #include <vector> 8 9 #include "base/lazy_instance.h" 10 #include "base/prefs/scoped_user_pref_update.h" 11 #include "base/strings/string_split.h" 12 #include "base/strings/string_util.h" 13 #include "base/strings/utf_string_conversions.h" 14 #include "chrome/browser/chrome_notification_types.h" 15 #include "chrome/browser/extensions/api/commands/commands.h" 16 #include "chrome/browser/extensions/extension_commands_global_registry.h" 17 #include "chrome/browser/extensions/extension_function_registry.h" 18 #include "chrome/browser/extensions/extension_keybinding_registry.h" 19 #include "chrome/browser/extensions/extension_service.h" 20 #include "chrome/browser/extensions/extension_system.h" 21 #include "chrome/browser/profiles/profile.h" 22 #include "chrome/browser/ui/accelerator_utils.h" 23 #include "chrome/common/extensions/api/commands/commands_handler.h" 24 #include "chrome/common/pref_names.h" 25 #include "components/user_prefs/pref_registry_syncable.h" 26 #include "content/public/browser/notification_details.h" 27 #include "content/public/browser/notification_service.h" 28 #include "extensions/common/feature_switch.h" 29 #include "extensions/common/manifest_constants.h" 30 31 using extensions::Extension; 32 using extensions::ExtensionPrefs; 33 34 namespace { 35 36 const char kExtension[] = "extension"; 37 const char kCommandName[] = "command_name"; 38 const char kGlobal[] = "global"; 39 40 // A preference that indicates that the initial keybindings for the given 41 // extension have been set. 42 const char kInitialBindingsHaveBeenAssigned[] = "initial_keybindings_set"; 43 44 std::string GetPlatformKeybindingKeyForAccelerator( 45 const ui::Accelerator& accelerator, const std::string extension_id) { 46 std::string key = extensions::Command::CommandPlatform() + ":" + 47 extensions::Command::AcceleratorToString(accelerator); 48 49 // Media keys have a 1-to-many relationship with targets, unlike regular 50 // shortcut (1-to-1 relationship). That means two or more extensions can 51 // register for the same media key so the extension ID needs to be added to 52 // the key to make sure the key is unique. 53 if (extensions::CommandService::IsMediaKey(accelerator)) 54 key += ":" + extension_id; 55 56 return key; 57 } 58 59 bool IsForCurrentPlatform(const std::string& key) { 60 return StartsWithASCII( 61 key, extensions::Command::CommandPlatform() + ":", true); 62 } 63 64 void SetInitialBindingsHaveBeenAssigned( 65 ExtensionPrefs* prefs, const std::string& extension_id) { 66 prefs->UpdateExtensionPref(extension_id, kInitialBindingsHaveBeenAssigned, 67 new base::FundamentalValue(true)); 68 } 69 70 bool InitialBindingsHaveBeenAssigned( 71 const ExtensionPrefs* prefs, const std::string& extension_id) { 72 bool assigned = false; 73 if (!prefs || !prefs->ReadPrefAsBoolean(extension_id, 74 kInitialBindingsHaveBeenAssigned, 75 &assigned)) 76 return false; 77 78 return assigned; 79 } 80 81 bool IsWhitelistedGlobalShortcut(const extensions::Command& command) { 82 if (!command.global()) 83 return true; 84 if (!command.accelerator().IsCtrlDown()) 85 return false; 86 if (!command.accelerator().IsShiftDown()) 87 return false; 88 return (command.accelerator().key_code() >= ui::VKEY_0 && 89 command.accelerator().key_code() <= ui::VKEY_9); 90 } 91 92 } // namespace 93 94 namespace extensions { 95 96 // static 97 void CommandService::RegisterProfilePrefs( 98 user_prefs::PrefRegistrySyncable* registry) { 99 registry->RegisterDictionaryPref( 100 prefs::kExtensionCommands, 101 user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); 102 } 103 104 CommandService::CommandService(Profile* profile) 105 : profile_(profile) { 106 ExtensionFunctionRegistry::GetInstance()-> 107 RegisterFunction<GetAllCommandsFunction>(); 108 109 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_INSTALLED, 110 content::Source<Profile>(profile)); 111 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNINSTALLED, 112 content::Source<Profile>(profile)); 113 } 114 115 CommandService::~CommandService() { 116 } 117 118 static base::LazyInstance<ProfileKeyedAPIFactory<CommandService> > 119 g_factory = LAZY_INSTANCE_INITIALIZER; 120 121 // static 122 ProfileKeyedAPIFactory<CommandService>* CommandService::GetFactoryInstance() { 123 return &g_factory.Get(); 124 } 125 126 // static 127 CommandService* CommandService::Get(Profile* profile) { 128 return ProfileKeyedAPIFactory<CommandService>::GetForProfile(profile); 129 } 130 131 // static 132 bool CommandService::IsMediaKey(const ui::Accelerator& accelerator) { 133 if (accelerator.modifiers() != 0) 134 return false; 135 136 return (accelerator.key_code() == ui::VKEY_MEDIA_NEXT_TRACK || 137 accelerator.key_code() == ui::VKEY_MEDIA_PREV_TRACK || 138 accelerator.key_code() == ui::VKEY_MEDIA_PLAY_PAUSE || 139 accelerator.key_code() == ui::VKEY_MEDIA_STOP); 140 } 141 142 bool CommandService::GetBrowserActionCommand( 143 const std::string& extension_id, 144 QueryType type, 145 extensions::Command* command, 146 bool* active) { 147 return GetExtensionActionCommand( 148 extension_id, type, command, active, BROWSER_ACTION); 149 } 150 151 bool CommandService::GetPageActionCommand( 152 const std::string& extension_id, 153 QueryType type, 154 extensions::Command* command, 155 bool* active) { 156 return GetExtensionActionCommand( 157 extension_id, type, command, active, PAGE_ACTION); 158 } 159 160 bool CommandService::GetScriptBadgeCommand( 161 const std::string& extension_id, 162 QueryType type, 163 extensions::Command* command, 164 bool* active) { 165 return GetExtensionActionCommand( 166 extension_id, type, command, active, SCRIPT_BADGE); 167 } 168 169 bool CommandService::GetNamedCommands(const std::string& extension_id, 170 QueryType type, 171 CommandScope scope, 172 extensions::CommandMap* command_map) { 173 ExtensionService* extension_service = 174 ExtensionSystem::Get(profile_)->extension_service(); 175 if (!extension_service) 176 return false; // Can occur during testing. 177 const ExtensionSet* extensions = extension_service->extensions(); 178 const Extension* extension = extensions->GetByID(extension_id); 179 CHECK(extension); 180 181 command_map->clear(); 182 const extensions::CommandMap* commands = 183 CommandsInfo::GetNamedCommands(extension); 184 if (!commands) 185 return false; 186 187 extensions::CommandMap::const_iterator iter = commands->begin(); 188 for (; iter != commands->end(); ++iter) { 189 // Look up to see if the user has overridden how the command should work. 190 extensions::Command saved_command = 191 FindCommandByName(extension_id, iter->second.command_name()); 192 ui::Accelerator shortcut_assigned = saved_command.accelerator(); 193 194 if (type == ACTIVE_ONLY && shortcut_assigned.key_code() == ui::VKEY_UNKNOWN) 195 continue; 196 197 extensions::Command command = iter->second; 198 if (scope != ANY_SCOPE && ((scope == GLOBAL) != saved_command.global())) 199 continue; 200 201 if (shortcut_assigned.key_code() != ui::VKEY_UNKNOWN) 202 command.set_accelerator(shortcut_assigned); 203 command.set_global(saved_command.global()); 204 205 (*command_map)[iter->second.command_name()] = command; 206 } 207 208 return true; 209 } 210 211 bool CommandService::AddKeybindingPref( 212 const ui::Accelerator& accelerator, 213 std::string extension_id, 214 std::string command_name, 215 bool allow_overrides, 216 bool global) { 217 if (accelerator.key_code() == ui::VKEY_UNKNOWN) 218 return false; 219 220 // Media Keys are allowed to be used by named command only. 221 DCHECK(!IsMediaKey(accelerator) || 222 (command_name != manifest_values::kPageActionCommandEvent && 223 command_name != manifest_values::kBrowserActionCommandEvent && 224 command_name != manifest_values::kScriptBadgeCommandEvent)); 225 226 DictionaryPrefUpdate updater(profile_->GetPrefs(), 227 prefs::kExtensionCommands); 228 base::DictionaryValue* bindings = updater.Get(); 229 230 std::string key = GetPlatformKeybindingKeyForAccelerator(accelerator, 231 extension_id); 232 233 if (bindings->HasKey(key)) { 234 if (!allow_overrides) 235 return false; // Already taken. 236 237 // If the shortcut has been assigned to another command, it should be 238 // removed before overriding, so that |ExtensionKeybindingRegistry| can get 239 // a chance to do clean-up. 240 const base::DictionaryValue* item = NULL; 241 bindings->GetDictionary(key, &item); 242 std::string old_extension_id; 243 std::string old_command_name; 244 item->GetString(kExtension, &old_extension_id); 245 item->GetString(kCommandName, &old_command_name); 246 RemoveKeybindingPrefs(old_extension_id, old_command_name); 247 } 248 249 base::DictionaryValue* keybinding = new base::DictionaryValue(); 250 keybinding->SetString(kExtension, extension_id); 251 keybinding->SetString(kCommandName, command_name); 252 keybinding->SetBoolean(kGlobal, global); 253 254 bindings->Set(key, keybinding); 255 256 std::pair<const std::string, const std::string> details = 257 std::make_pair(extension_id, command_name); 258 content::NotificationService::current()->Notify( 259 chrome::NOTIFICATION_EXTENSION_COMMAND_ADDED, 260 content::Source<Profile>(profile_), 261 content::Details< 262 std::pair<const std::string, const std::string> >(&details)); 263 264 return true; 265 } 266 267 void CommandService::Observe( 268 int type, 269 const content::NotificationSource& source, 270 const content::NotificationDetails& details) { 271 switch (type) { 272 case chrome::NOTIFICATION_EXTENSION_INSTALLED: 273 AssignInitialKeybindings( 274 content::Details<const InstalledExtensionInfo>(details)->extension); 275 break; 276 case chrome::NOTIFICATION_EXTENSION_UNINSTALLED: 277 RemoveKeybindingPrefs( 278 content::Details<const Extension>(details)->id(), 279 std::string()); 280 break; 281 default: 282 NOTREACHED(); 283 break; 284 } 285 } 286 287 void CommandService::UpdateKeybindingPrefs(const std::string& extension_id, 288 const std::string& command_name, 289 const std::string& keystroke) { 290 extensions::Command command = FindCommandByName(extension_id, command_name); 291 292 // The extension command might be assigned another shortcut. Remove that 293 // shortcut before proceeding. 294 RemoveKeybindingPrefs(extension_id, command_name); 295 296 ui::Accelerator accelerator = 297 Command::StringToAccelerator(keystroke, command_name); 298 AddKeybindingPref(accelerator, extension_id, command_name, 299 true, command.global()); 300 } 301 302 bool CommandService::SetScope(const std::string& extension_id, 303 const std::string& command_name, 304 bool global) { 305 extensions::Command command = FindCommandByName(extension_id, command_name); 306 if (global == command.global()) 307 return false; 308 309 // Pre-existing shortcuts must be removed before proceeding because the 310 // handlers for global and non-global extensions are not one and the same. 311 RemoveKeybindingPrefs(extension_id, command_name); 312 AddKeybindingPref(command.accelerator(), extension_id, 313 command_name, true, global); 314 return true; 315 } 316 317 Command CommandService::FindCommandByName( 318 const std::string& extension_id, const std::string& command) { 319 const base::DictionaryValue* bindings = 320 profile_->GetPrefs()->GetDictionary(prefs::kExtensionCommands); 321 for (base::DictionaryValue::Iterator it(*bindings); !it.IsAtEnd(); 322 it.Advance()) { 323 const base::DictionaryValue* item = NULL; 324 it.value().GetAsDictionary(&item); 325 326 std::string extension; 327 item->GetString(kExtension, &extension); 328 if (extension != extension_id) 329 continue; 330 std::string command_name; 331 item->GetString(kCommandName, &command_name); 332 if (command != command_name) 333 continue; 334 // Format stored in Preferences is: "Platform:Shortcut[:ExtensionId]". 335 std::string shortcut = it.key(); 336 if (!IsForCurrentPlatform(shortcut)) 337 continue; 338 bool global = false; 339 if (FeatureSwitch::global_commands()->IsEnabled()) 340 item->GetBoolean(kGlobal, &global); 341 342 std::vector<std::string> tokens; 343 base::SplitString(shortcut, ':', &tokens); 344 CHECK(tokens.size() >= 2); 345 shortcut = tokens[1]; 346 347 return Command(command_name, base::string16(), shortcut, global); 348 } 349 350 return Command(); 351 } 352 353 void CommandService::AssignInitialKeybindings(const Extension* extension) { 354 const extensions::CommandMap* commands = 355 CommandsInfo::GetNamedCommands(extension); 356 if (!commands) 357 return; 358 359 ExtensionService* extension_service = 360 ExtensionSystem::Get(profile_)->extension_service(); 361 ExtensionPrefs* extension_prefs = extension_service->extension_prefs(); 362 if (InitialBindingsHaveBeenAssigned(extension_prefs, extension->id())) 363 return; 364 SetInitialBindingsHaveBeenAssigned(extension_prefs, extension->id()); 365 366 extensions::CommandMap::const_iterator iter = commands->begin(); 367 for (; iter != commands->end(); ++iter) { 368 // Make sure registered Chrome shortcuts cannot be automatically assigned 369 // (overwritten) by extension developers. Media keys are an exception here. 370 if ((!chrome::IsChromeAccelerator(iter->second.accelerator(), profile_) && 371 IsWhitelistedGlobalShortcut(iter->second)) || 372 extensions::CommandService::IsMediaKey(iter->second.accelerator())) { 373 AddKeybindingPref(iter->second.accelerator(), 374 extension->id(), 375 iter->second.command_name(), 376 false, // Overwriting not allowed. 377 iter->second.global()); 378 } 379 } 380 381 const extensions::Command* browser_action_command = 382 CommandsInfo::GetBrowserActionCommand(extension); 383 if (browser_action_command) { 384 if (!chrome::IsChromeAccelerator( 385 browser_action_command->accelerator(), profile_)) { 386 AddKeybindingPref(browser_action_command->accelerator(), 387 extension->id(), 388 browser_action_command->command_name(), 389 false, // Overwriting not allowed. 390 false); // Browser actions can't be global. 391 } 392 } 393 394 const extensions::Command* page_action_command = 395 CommandsInfo::GetPageActionCommand(extension); 396 if (page_action_command) { 397 if (!chrome::IsChromeAccelerator( 398 page_action_command->accelerator(), profile_)) { 399 AddKeybindingPref(page_action_command->accelerator(), 400 extension->id(), 401 page_action_command->command_name(), 402 false, // Overwriting not allowed. 403 false); // Page actions can't be global. 404 } 405 } 406 407 const extensions::Command* script_badge_command = 408 CommandsInfo::GetScriptBadgeCommand(extension); 409 if (script_badge_command) { 410 if (!chrome::IsChromeAccelerator( 411 script_badge_command->accelerator(), profile_)) { 412 AddKeybindingPref(script_badge_command->accelerator(), 413 extension->id(), 414 script_badge_command->command_name(), 415 false, // Overwriting not allowed. 416 false); // Script badges can't be global. 417 } 418 } 419 } 420 421 void CommandService::RemoveKeybindingPrefs(const std::string& extension_id, 422 const std::string& command_name) { 423 DictionaryPrefUpdate updater(profile_->GetPrefs(), 424 prefs::kExtensionCommands); 425 base::DictionaryValue* bindings = updater.Get(); 426 427 typedef std::vector<std::string> KeysToRemove; 428 KeysToRemove keys_to_remove; 429 for (base::DictionaryValue::Iterator it(*bindings); !it.IsAtEnd(); 430 it.Advance()) { 431 // Removal of keybinding preference should be limited to current platform. 432 if (!IsForCurrentPlatform(it.key())) 433 continue; 434 435 const base::DictionaryValue* item = NULL; 436 it.value().GetAsDictionary(&item); 437 438 std::string extension; 439 item->GetString(kExtension, &extension); 440 441 if (extension == extension_id) { 442 // If |command_name| is specified, delete only that command. Otherwise, 443 // delete all commands. 444 if (!command_name.empty()) { 445 std::string command; 446 item->GetString(kCommandName, &command); 447 if (command_name != command) 448 continue; 449 } 450 451 keys_to_remove.push_back(it.key()); 452 } 453 } 454 455 for (KeysToRemove::const_iterator it = keys_to_remove.begin(); 456 it != keys_to_remove.end(); ++it) { 457 std::string key = *it; 458 bindings->Remove(key, NULL); 459 460 std::pair<const std::string, const std::string> details = 461 std::make_pair(extension_id, command_name); 462 content::NotificationService::current()->Notify( 463 chrome::NOTIFICATION_EXTENSION_COMMAND_REMOVED, 464 content::Source<Profile>(profile_), 465 content::Details< 466 std::pair<const std::string, const std::string> >(&details)); 467 } 468 } 469 470 bool CommandService::GetExtensionActionCommand( 471 const std::string& extension_id, 472 QueryType query_type, 473 extensions::Command* command, 474 bool* active, 475 ExtensionActionType action_type) { 476 ExtensionService* service = 477 ExtensionSystem::Get(profile_)->extension_service(); 478 if (!service) 479 return false; // Can happen in tests. 480 const ExtensionSet* extensions = service->extensions(); 481 const Extension* extension = extensions->GetByID(extension_id); 482 CHECK(extension); 483 484 if (active) 485 *active = false; 486 487 const extensions::Command* requested_command = NULL; 488 switch (action_type) { 489 case BROWSER_ACTION: 490 requested_command = CommandsInfo::GetBrowserActionCommand(extension); 491 break; 492 case PAGE_ACTION: 493 requested_command = CommandsInfo::GetPageActionCommand(extension); 494 break; 495 case SCRIPT_BADGE: 496 requested_command = CommandsInfo::GetScriptBadgeCommand(extension); 497 break; 498 } 499 if (!requested_command) 500 return false; 501 502 // Look up to see if the user has overridden how the command should work. 503 extensions::Command saved_command = 504 FindCommandByName(extension_id, requested_command->command_name()); 505 ui::Accelerator shortcut_assigned = saved_command.accelerator(); 506 507 if (active) 508 *active = (shortcut_assigned.key_code() != ui::VKEY_UNKNOWN); 509 510 if (query_type == ACTIVE_ONLY && 511 shortcut_assigned.key_code() == ui::VKEY_UNKNOWN) 512 return false; 513 514 *command = *requested_command; 515 if (shortcut_assigned.key_code() != ui::VKEY_UNKNOWN) 516 command->set_accelerator(shortcut_assigned); 517 518 return true; 519 } 520 521 template <> 522 void ProfileKeyedAPIFactory<CommandService>::DeclareFactoryDependencies() { 523 DependsOn(ExtensionCommandsGlobalRegistry::GetFactoryInstance()); 524 } 525 526 } // namespace extensions 527