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