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