command_service.cc revision a1401311d1ab56c4ed0a474bd38c108f75cb0cd9
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_keybinding_registry.h" 19#include "chrome/browser/profiles/profile.h" 20#include "chrome/browser/ui/accelerator_utils.h" 21#include "chrome/common/extensions/api/commands/commands_handler.h" 22#include "chrome/common/extensions/manifest_handlers/settings_overrides_handler.h" 23#include "chrome/common/extensions/manifest_handlers/ui_overrides_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/browser/extension_function_registry.h" 29#include "extensions/browser/extension_prefs.h" 30#include "extensions/browser/extension_registry.h" 31#include "extensions/browser/extension_system.h" 32#include "extensions/common/feature_switch.h" 33#include "extensions/common/manifest_constants.h" 34#include "extensions/common/permissions/permissions_data.h" 35 36using extensions::Extension; 37using extensions::ExtensionPrefs; 38 39namespace { 40 41const char kExtension[] = "extension"; 42const char kCommandName[] = "command_name"; 43const char kGlobal[] = "global"; 44 45// A preference that indicates that the initial keybindings for the given 46// extension have been set. 47const char kInitialBindingsHaveBeenAssigned[] = "initial_keybindings_set"; 48 49std::string GetPlatformKeybindingKeyForAccelerator( 50 const ui::Accelerator& accelerator, const std::string extension_id) { 51 std::string key = extensions::Command::CommandPlatform() + ":" + 52 extensions::Command::AcceleratorToString(accelerator); 53 54 // Media keys have a 1-to-many relationship with targets, unlike regular 55 // shortcut (1-to-1 relationship). That means two or more extensions can 56 // register for the same media key so the extension ID needs to be added to 57 // the key to make sure the key is unique. 58 if (extensions::Command::IsMediaKey(accelerator)) 59 key += ":" + extension_id; 60 61 return key; 62} 63 64bool IsForCurrentPlatform(const std::string& key) { 65 return StartsWithASCII( 66 key, extensions::Command::CommandPlatform() + ":", true); 67} 68 69void SetInitialBindingsHaveBeenAssigned( 70 ExtensionPrefs* prefs, const std::string& extension_id) { 71 prefs->UpdateExtensionPref(extension_id, kInitialBindingsHaveBeenAssigned, 72 new base::FundamentalValue(true)); 73} 74 75bool InitialBindingsHaveBeenAssigned( 76 const ExtensionPrefs* prefs, const std::string& extension_id) { 77 bool assigned = false; 78 if (!prefs || !prefs->ReadPrefAsBoolean(extension_id, 79 kInitialBindingsHaveBeenAssigned, 80 &assigned)) 81 return false; 82 83 return assigned; 84} 85 86// Checks if |extension| is permitted to automatically assign the |accelerator| 87// key. 88bool CanAutoAssign(const ui::Accelerator& accelerator, 89 const Extension* extension, 90 Profile* profile, 91 bool is_named_command, 92 bool is_global) { 93 // Media Keys are non-exclusive, so allow auto-assigning them. 94 if (extensions::Command::IsMediaKey(accelerator)) 95 return true; 96 97 if (is_global) { 98 if (!is_named_command) 99 return false; // Browser and page actions are not global in nature. 100 101 // Global shortcuts are restricted to (Ctrl|Command)+Shift+[0-9]. 102#if defined OS_MACOSX 103 if (!accelerator.IsCmdDown()) 104 return false; 105#else 106 if (!accelerator.IsCtrlDown()) 107 return false; 108#endif 109 if (!accelerator.IsShiftDown()) 110 return false; 111 return (accelerator.key_code() >= ui::VKEY_0 && 112 accelerator.key_code() <= ui::VKEY_9); 113 } else { 114 // Not a global command, check if Chrome shortcut and whether 115 // we can override it. 116 if (accelerator == 117 chrome::GetPrimaryChromeAcceleratorForCommandId(IDC_BOOKMARK_PAGE) && 118 extensions::CommandService::RemovesBookmarkShortcut(extension)) { 119 // If this check fails it either means we have an API to override a 120 // key that isn't a ChromeAccelerator (and the API can therefore be 121 // deprecated) or the IsChromeAccelerator isn't consistently 122 // returning true for all accelerators. 123 DCHECK(chrome::IsChromeAccelerator(accelerator, profile)); 124 return true; 125 } 126 127 return !chrome::IsChromeAccelerator(accelerator, profile); 128 } 129} 130 131} // namespace 132 133namespace extensions { 134 135// static 136void CommandService::RegisterProfilePrefs( 137 user_prefs::PrefRegistrySyncable* registry) { 138 registry->RegisterDictionaryPref( 139 prefs::kExtensionCommands, 140 user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); 141} 142 143CommandService::CommandService(content::BrowserContext* context) 144 : profile_(Profile::FromBrowserContext(context)) { 145 ExtensionFunctionRegistry::GetInstance()-> 146 RegisterFunction<GetAllCommandsFunction>(); 147 148 registrar_.Add(this, 149 chrome::NOTIFICATION_EXTENSION_INSTALLED, 150 content::Source<Profile>(profile_)); 151 registrar_.Add(this, 152 chrome::NOTIFICATION_EXTENSION_UNINSTALLED, 153 content::Source<Profile>(profile_)); 154} 155 156CommandService::~CommandService() { 157} 158 159static base::LazyInstance<BrowserContextKeyedAPIFactory<CommandService> > 160 g_factory = LAZY_INSTANCE_INITIALIZER; 161 162// static 163BrowserContextKeyedAPIFactory<CommandService>* 164CommandService::GetFactoryInstance() { 165 return g_factory.Pointer(); 166} 167 168// static 169CommandService* CommandService::Get(content::BrowserContext* context) { 170 return BrowserContextKeyedAPIFactory<CommandService>::Get(context); 171} 172 173// static 174bool CommandService::RemovesBookmarkShortcut( 175 const extensions::Extension* extension) { 176 using extensions::UIOverrides; 177 using extensions::SettingsOverrides; 178 const UIOverrides* ui_overrides = UIOverrides::Get(extension); 179 const SettingsOverrides* settings_overrides = 180 SettingsOverrides::Get(extension); 181 182 return ((settings_overrides && 183 SettingsOverrides::RemovesBookmarkShortcut(*settings_overrides)) || 184 (ui_overrides && 185 UIOverrides::RemovesBookmarkShortcut(*ui_overrides))) && 186 (extensions::PermissionsData::HasAPIPermission( 187 extension, 188 extensions::APIPermission::kBookmarkManagerPrivate) || 189 extensions::FeatureSwitch::enable_override_bookmarks_ui()-> 190 IsEnabled()); 191} 192 193bool CommandService::GetBrowserActionCommand(const std::string& extension_id, 194 QueryType type, 195 extensions::Command* command, 196 bool* active) const { 197 return GetExtensionActionCommand( 198 extension_id, type, command, active, BROWSER_ACTION); 199} 200 201bool CommandService::GetPageActionCommand(const std::string& extension_id, 202 QueryType type, 203 extensions::Command* command, 204 bool* active) const { 205 return GetExtensionActionCommand( 206 extension_id, type, command, active, PAGE_ACTION); 207} 208 209bool CommandService::GetNamedCommands( 210 const std::string& extension_id, 211 QueryType type, 212 CommandScope scope, 213 extensions::CommandMap* command_map) const { 214 const ExtensionSet& extensions = 215 ExtensionRegistry::Get(profile_)->enabled_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(!Command::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(const std::string& extension_id, 355 const std::string& command) const { 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 390bool CommandService::GetBoundExtensionCommand( 391 const std::string& extension_id, 392 const ui::Accelerator& accelerator, 393 extensions::Command* command, 394 ExtensionCommandType* command_type) const { 395 const ExtensionSet& extensions = 396 ExtensionRegistry::Get(profile_)->enabled_extensions(); 397 const Extension* extension = extensions.GetByID(extension_id); 398 CHECK(extension); 399 400 extensions::Command prospective_command; 401 extensions::CommandMap command_map; 402 bool active = false; 403 if (GetBrowserActionCommand(extension_id, 404 extensions::CommandService::ACTIVE_ONLY, 405 &prospective_command, 406 &active) && active && 407 accelerator == prospective_command.accelerator()) { 408 if (command) 409 *command = prospective_command; 410 if (command_type) 411 *command_type = BROWSER_ACTION; 412 return true; 413 } else if (GetPageActionCommand(extension_id, 414 extensions::CommandService::ACTIVE_ONLY, 415 &prospective_command, 416 &active) && active && 417 accelerator == prospective_command.accelerator()) { 418 if (command) 419 *command = prospective_command; 420 if (command_type) 421 *command_type = PAGE_ACTION; 422 return true; 423 } else if (GetNamedCommands(extension_id, 424 extensions::CommandService::ACTIVE_ONLY, 425 extensions::CommandService::REGULAR, 426 &command_map)) { 427 for (extensions::CommandMap::const_iterator it = command_map.begin(); 428 it != command_map.end(); ++it) { 429 if (accelerator == it->second.accelerator()) { 430 if (command) 431 *command = it->second; 432 if (command_type) 433 *command_type = NAMED; 434 return true; 435 } 436 } 437 } 438 return false; 439} 440 441bool CommandService::OverridesBookmarkShortcut( 442 const extensions::Extension* extension) const { 443 return RemovesBookmarkShortcut(extension) && 444 GetBoundExtensionCommand( 445 extension->id(), 446 chrome::GetPrimaryChromeAcceleratorForCommandId(IDC_BOOKMARK_PAGE), 447 NULL, 448 NULL); 449} 450 451void CommandService::AssignInitialKeybindings(const Extension* extension) { 452 const extensions::CommandMap* commands = 453 CommandsInfo::GetNamedCommands(extension); 454 if (!commands) 455 return; 456 457 ExtensionPrefs* extension_prefs = ExtensionPrefs::Get(profile_); 458 if (InitialBindingsHaveBeenAssigned(extension_prefs, extension->id())) 459 return; 460 SetInitialBindingsHaveBeenAssigned(extension_prefs, extension->id()); 461 462 extensions::CommandMap::const_iterator iter = commands->begin(); 463 for (; iter != commands->end(); ++iter) { 464 const extensions::Command command = iter->second; 465 if (CanAutoAssign(command.accelerator(), 466 extension, 467 profile_, 468 true, // Is a named command. 469 command.global())) { 470 AddKeybindingPref(command.accelerator(), 471 extension->id(), 472 command.command_name(), 473 false, // Overwriting not allowed. 474 command.global()); 475 } 476 } 477 478 const extensions::Command* browser_action_command = 479 CommandsInfo::GetBrowserActionCommand(extension); 480 if (browser_action_command && 481 CanAutoAssign(browser_action_command->accelerator(), 482 extension, 483 profile_, 484 false, // Not a named command. 485 false)) { // Not global. 486 AddKeybindingPref(browser_action_command->accelerator(), 487 extension->id(), 488 browser_action_command->command_name(), 489 false, // Overwriting not allowed. 490 false); // Not global. 491 } 492 493 const extensions::Command* page_action_command = 494 CommandsInfo::GetPageActionCommand(extension); 495 if (page_action_command && 496 CanAutoAssign(page_action_command->accelerator(), 497 extension, 498 profile_, 499 false, // Not a named command. 500 false)) { // Not global. 501 AddKeybindingPref(page_action_command->accelerator(), 502 extension->id(), 503 page_action_command->command_name(), 504 false, // Overwriting not allowed. 505 false); // Not global. 506 } 507} 508 509void CommandService::RemoveKeybindingPrefs(const std::string& extension_id, 510 const std::string& command_name) { 511 DictionaryPrefUpdate updater(profile_->GetPrefs(), 512 prefs::kExtensionCommands); 513 base::DictionaryValue* bindings = updater.Get(); 514 515 typedef std::vector<std::string> KeysToRemove; 516 KeysToRemove keys_to_remove; 517 for (base::DictionaryValue::Iterator it(*bindings); !it.IsAtEnd(); 518 it.Advance()) { 519 // Removal of keybinding preference should be limited to current platform. 520 if (!IsForCurrentPlatform(it.key())) 521 continue; 522 523 const base::DictionaryValue* item = NULL; 524 it.value().GetAsDictionary(&item); 525 526 std::string extension; 527 item->GetString(kExtension, &extension); 528 529 if (extension == extension_id) { 530 // If |command_name| is specified, delete only that command. Otherwise, 531 // delete all commands. 532 if (!command_name.empty()) { 533 std::string command; 534 item->GetString(kCommandName, &command); 535 if (command_name != command) 536 continue; 537 } 538 539 keys_to_remove.push_back(it.key()); 540 } 541 } 542 543 for (KeysToRemove::const_iterator it = keys_to_remove.begin(); 544 it != keys_to_remove.end(); ++it) { 545 std::string key = *it; 546 bindings->Remove(key, NULL); 547 548 std::pair<const std::string, const std::string> details = 549 std::make_pair(extension_id, command_name); 550 content::NotificationService::current()->Notify( 551 chrome::NOTIFICATION_EXTENSION_COMMAND_REMOVED, 552 content::Source<Profile>(profile_), 553 content::Details< 554 std::pair<const std::string, const std::string> >(&details)); 555 } 556} 557 558bool CommandService::GetExtensionActionCommand( 559 const std::string& extension_id, 560 QueryType query_type, 561 extensions::Command* command, 562 bool* active, 563 ExtensionCommandType action_type) const { 564 const ExtensionSet& extensions = 565 ExtensionRegistry::Get(profile_)->enabled_extensions(); 566 const Extension* extension = extensions.GetByID(extension_id); 567 CHECK(extension); 568 569 if (active) 570 *active = false; 571 572 const extensions::Command* requested_command = NULL; 573 switch (action_type) { 574 case BROWSER_ACTION: 575 requested_command = CommandsInfo::GetBrowserActionCommand(extension); 576 break; 577 case PAGE_ACTION: 578 requested_command = CommandsInfo::GetPageActionCommand(extension); 579 break; 580 case NAMED: 581 NOTREACHED(); 582 return false; 583 } 584 if (!requested_command) 585 return false; 586 587 // Look up to see if the user has overridden how the command should work. 588 extensions::Command saved_command = 589 FindCommandByName(extension_id, requested_command->command_name()); 590 ui::Accelerator shortcut_assigned = saved_command.accelerator(); 591 592 if (active) 593 *active = (shortcut_assigned.key_code() != ui::VKEY_UNKNOWN); 594 595 if (query_type == ACTIVE_ONLY && 596 shortcut_assigned.key_code() == ui::VKEY_UNKNOWN) 597 return false; 598 599 *command = *requested_command; 600 if (shortcut_assigned.key_code() != ui::VKEY_UNKNOWN) 601 command->set_accelerator(shortcut_assigned); 602 603 return true; 604} 605 606template <> 607void 608BrowserContextKeyedAPIFactory<CommandService>::DeclareFactoryDependencies() { 609 DependsOn(ExtensionCommandsGlobalRegistry::GetFactoryInstance()); 610} 611 612} // namespace extensions 613