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.
5 #include "chrome/browser/extensions/api/commands/command_service.h"
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"
36 using extensions::Extension;
37 using extensions::ExtensionPrefs;
38 using extensions::SettingsOverrides;
39 using extensions::UIOverrides;
43 const char kExtension[] = "extension";
44 const char kCommandName[] = "command_name";
45 const char kGlobal[] = "global";
47 // A preference that indicates that the initial keybindings for the given
48 // extension have been set.
49 const char kInitialBindingsHaveBeenAssigned[] = "initial_keybindings_set";
51 std::string GetPlatformKeybindingKeyForAccelerator(
52 const ui::Accelerator& accelerator, const std::string extension_id) {
53 std::string key = extensions::Command::CommandPlatform() + ":" +
54 extensions::Command::AcceleratorToString(accelerator);
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;
66 bool IsForCurrentPlatform(const std::string& key) {
67 return StartsWithASCII(
68 key, extensions::Command::CommandPlatform() + ":", true);
71 void SetInitialBindingsHaveBeenAssigned(
72 ExtensionPrefs* prefs, const std::string& extension_id) {
73 prefs->UpdateExtensionPref(extension_id, kInitialBindingsHaveBeenAssigned,
74 new base::FundamentalValue(true));
77 bool InitialBindingsHaveBeenAssigned(
78 const ExtensionPrefs* prefs, const std::string& extension_id) {
79 bool assigned = false;
80 if (!prefs || !prefs->ReadPrefAsBoolean(extension_id,
81 kInitialBindingsHaveBeenAssigned,
88 // Checks if |extension| is permitted to automatically assign the |accelerator|
90 bool CanAutoAssign(const ui::Accelerator& accelerator,
91 const Extension* extension,
93 bool is_named_command,
95 // Media Keys are non-exclusive, so allow auto-assigning them.
96 if (extensions::Command::IsMediaKey(accelerator))
100 if (!is_named_command)
101 return false; // Browser and page actions are not global in nature.
103 // Global shortcuts are restricted to (Ctrl|Command)+Shift+[0-9].
104 #if defined OS_MACOSX
105 if (!accelerator.IsCmdDown())
108 if (!accelerator.IsCtrlDown())
111 if (!accelerator.IsShiftDown())
113 return (accelerator.key_code() >= ui::VKEY_0 &&
114 accelerator.key_code() <= ui::VKEY_9);
116 // Not a global command, check if Chrome shortcut and whether
117 // we can override it.
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));
129 return !chrome::IsChromeAccelerator(accelerator, profile);
135 namespace extensions {
138 void CommandService::RegisterProfilePrefs(
139 user_prefs::PrefRegistrySyncable* registry) {
140 registry->RegisterDictionaryPref(
141 prefs::kExtensionCommands,
142 user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
145 CommandService::CommandService(content::BrowserContext* context)
146 : profile_(Profile::FromBrowserContext(context)) {
147 ExtensionFunctionRegistry::GetInstance()->
148 RegisterFunction<GetAllCommandsFunction>();
151 chrome::NOTIFICATION_EXTENSION_INSTALLED,
152 content::Source<Profile>(profile_));
154 chrome::NOTIFICATION_EXTENSION_UNINSTALLED,
155 content::Source<Profile>(profile_));
158 CommandService::~CommandService() {
161 static base::LazyInstance<BrowserContextKeyedAPIFactory<CommandService> >
162 g_factory = LAZY_INSTANCE_INITIALIZER;
165 BrowserContextKeyedAPIFactory<CommandService>*
166 CommandService::GetFactoryInstance() {
167 return g_factory.Pointer();
171 CommandService* CommandService::Get(content::BrowserContext* context) {
172 return BrowserContextKeyedAPIFactory<CommandService>::Get(context);
176 bool CommandService::RemovesBookmarkShortcut(
177 const extensions::Extension* extension) {
178 const UIOverrides* ui_overrides = UIOverrides::Get(extension);
179 const SettingsOverrides* settings_overrides =
180 SettingsOverrides::Get(extension);
182 return ((settings_overrides &&
183 SettingsOverrides::RemovesBookmarkShortcut(*settings_overrides)) ||
185 UIOverrides::RemovesBookmarkShortcut(*ui_overrides))) &&
186 (extensions::PermissionsData::HasAPIPermission(
188 extensions::APIPermission::kBookmarkManagerPrivate) ||
189 extensions::FeatureSwitch::enable_override_bookmarks_ui()->
194 bool CommandService::RemovesBookmarkOpenPagesShortcut(
195 const extensions::Extension* extension) {
196 const UIOverrides* ui_overrides = UIOverrides::Get(extension);
197 const SettingsOverrides* settings_overrides =
198 SettingsOverrides::Get(extension);
200 return ((settings_overrides &&
201 SettingsOverrides::RemovesBookmarkOpenPagesShortcut(
202 *settings_overrides)) ||
204 UIOverrides::RemovesBookmarkOpenPagesShortcut(*ui_overrides))) &&
205 (extensions::PermissionsData::HasAPIPermission(
207 extensions::APIPermission::kBookmarkManagerPrivate) ||
208 extensions::FeatureSwitch::enable_override_bookmarks_ui()->
212 bool CommandService::GetBrowserActionCommand(const std::string& extension_id,
214 extensions::Command* command,
215 bool* active) const {
216 return GetExtensionActionCommand(
217 extension_id, type, command, active, BROWSER_ACTION);
220 bool CommandService::GetPageActionCommand(const std::string& extension_id,
222 extensions::Command* command,
223 bool* active) const {
224 return GetExtensionActionCommand(
225 extension_id, type, command, active, PAGE_ACTION);
228 bool CommandService::GetNamedCommands(
229 const std::string& extension_id,
232 extensions::CommandMap* command_map) const {
233 const ExtensionSet& extensions =
234 ExtensionRegistry::Get(profile_)->enabled_extensions();
235 const Extension* extension = extensions.GetByID(extension_id);
238 command_map->clear();
239 const extensions::CommandMap* commands =
240 CommandsInfo::GetNamedCommands(extension);
244 extensions::CommandMap::const_iterator iter = commands->begin();
245 for (; iter != commands->end(); ++iter) {
246 // Look up to see if the user has overridden how the command should work.
247 extensions::Command saved_command =
248 FindCommandByName(extension_id, iter->second.command_name());
249 ui::Accelerator shortcut_assigned = saved_command.accelerator();
251 if (type == ACTIVE_ONLY && shortcut_assigned.key_code() == ui::VKEY_UNKNOWN)
254 extensions::Command command = iter->second;
255 if (scope != ANY_SCOPE && ((scope == GLOBAL) != saved_command.global()))
258 if (shortcut_assigned.key_code() != ui::VKEY_UNKNOWN)
259 command.set_accelerator(shortcut_assigned);
260 command.set_global(saved_command.global());
262 (*command_map)[iter->second.command_name()] = command;
268 bool CommandService::AddKeybindingPref(
269 const ui::Accelerator& accelerator,
270 std::string extension_id,
271 std::string command_name,
272 bool allow_overrides,
274 if (accelerator.key_code() == ui::VKEY_UNKNOWN)
277 // Media Keys are allowed to be used by named command only.
278 DCHECK(!Command::IsMediaKey(accelerator) ||
279 (command_name != manifest_values::kPageActionCommandEvent &&
280 command_name != manifest_values::kBrowserActionCommandEvent));
282 DictionaryPrefUpdate updater(profile_->GetPrefs(),
283 prefs::kExtensionCommands);
284 base::DictionaryValue* bindings = updater.Get();
286 std::string key = GetPlatformKeybindingKeyForAccelerator(accelerator,
289 if (bindings->HasKey(key)) {
290 if (!allow_overrides)
291 return false; // Already taken.
293 // If the shortcut has been assigned to another command, it should be
294 // removed before overriding, so that |ExtensionKeybindingRegistry| can get
295 // a chance to do clean-up.
296 const base::DictionaryValue* item = NULL;
297 bindings->GetDictionary(key, &item);
298 std::string old_extension_id;
299 std::string old_command_name;
300 item->GetString(kExtension, &old_extension_id);
301 item->GetString(kCommandName, &old_command_name);
302 RemoveKeybindingPrefs(old_extension_id, old_command_name);
305 base::DictionaryValue* keybinding = new base::DictionaryValue();
306 keybinding->SetString(kExtension, extension_id);
307 keybinding->SetString(kCommandName, command_name);
308 keybinding->SetBoolean(kGlobal, global);
310 bindings->Set(key, keybinding);
312 std::pair<const std::string, const std::string> details =
313 std::make_pair(extension_id, command_name);
314 content::NotificationService::current()->Notify(
315 chrome::NOTIFICATION_EXTENSION_COMMAND_ADDED,
316 content::Source<Profile>(profile_),
318 std::pair<const std::string, const std::string> >(&details));
323 void CommandService::Observe(
325 const content::NotificationSource& source,
326 const content::NotificationDetails& details) {
328 case chrome::NOTIFICATION_EXTENSION_INSTALLED:
329 AssignInitialKeybindings(
330 content::Details<const InstalledExtensionInfo>(details)->extension);
332 case chrome::NOTIFICATION_EXTENSION_UNINSTALLED:
333 RemoveKeybindingPrefs(
334 content::Details<const Extension>(details)->id(),
343 void CommandService::UpdateKeybindingPrefs(const std::string& extension_id,
344 const std::string& command_name,
345 const std::string& keystroke) {
346 extensions::Command command = FindCommandByName(extension_id, command_name);
348 // The extension command might be assigned another shortcut. Remove that
349 // shortcut before proceeding.
350 RemoveKeybindingPrefs(extension_id, command_name);
352 ui::Accelerator accelerator =
353 Command::StringToAccelerator(keystroke, command_name);
354 AddKeybindingPref(accelerator, extension_id, command_name,
355 true, command.global());
358 bool CommandService::SetScope(const std::string& extension_id,
359 const std::string& command_name,
361 extensions::Command command = FindCommandByName(extension_id, command_name);
362 if (global == command.global())
365 // Pre-existing shortcuts must be removed before proceeding because the
366 // handlers for global and non-global extensions are not one and the same.
367 RemoveKeybindingPrefs(extension_id, command_name);
368 AddKeybindingPref(command.accelerator(), extension_id,
369 command_name, true, global);
373 Command CommandService::FindCommandByName(const std::string& extension_id,
374 const std::string& command) const {
375 const base::DictionaryValue* bindings =
376 profile_->GetPrefs()->GetDictionary(prefs::kExtensionCommands);
377 for (base::DictionaryValue::Iterator it(*bindings); !it.IsAtEnd();
379 const base::DictionaryValue* item = NULL;
380 it.value().GetAsDictionary(&item);
382 std::string extension;
383 item->GetString(kExtension, &extension);
384 if (extension != extension_id)
386 std::string command_name;
387 item->GetString(kCommandName, &command_name);
388 if (command != command_name)
390 // Format stored in Preferences is: "Platform:Shortcut[:ExtensionId]".
391 std::string shortcut = it.key();
392 if (!IsForCurrentPlatform(shortcut))
395 if (FeatureSwitch::global_commands()->IsEnabled())
396 item->GetBoolean(kGlobal, &global);
398 std::vector<std::string> tokens;
399 base::SplitString(shortcut, ':', &tokens);
400 CHECK(tokens.size() >= 2);
401 shortcut = tokens[1];
403 return Command(command_name, base::string16(), shortcut, global);
409 bool CommandService::GetBoundExtensionCommand(
410 const std::string& extension_id,
411 const ui::Accelerator& accelerator,
412 extensions::Command* command,
413 ExtensionCommandType* command_type) const {
414 const ExtensionSet& extensions =
415 ExtensionRegistry::Get(profile_)->enabled_extensions();
416 const Extension* extension = extensions.GetByID(extension_id);
419 extensions::Command prospective_command;
420 extensions::CommandMap command_map;
422 if (GetBrowserActionCommand(extension_id,
423 extensions::CommandService::ACTIVE_ONLY,
424 &prospective_command,
425 &active) && active &&
426 accelerator == prospective_command.accelerator()) {
428 *command = prospective_command;
430 *command_type = BROWSER_ACTION;
432 } else if (GetPageActionCommand(extension_id,
433 extensions::CommandService::ACTIVE_ONLY,
434 &prospective_command,
435 &active) && active &&
436 accelerator == prospective_command.accelerator()) {
438 *command = prospective_command;
440 *command_type = PAGE_ACTION;
442 } else if (GetNamedCommands(extension_id,
443 extensions::CommandService::ACTIVE_ONLY,
444 extensions::CommandService::REGULAR,
446 for (extensions::CommandMap::const_iterator it = command_map.begin();
447 it != command_map.end(); ++it) {
448 if (accelerator == it->second.accelerator()) {
450 *command = it->second;
452 *command_type = NAMED;
460 bool CommandService::OverridesBookmarkShortcut(
461 const extensions::Extension* extension) const {
462 return RemovesBookmarkShortcut(extension) &&
463 GetBoundExtensionCommand(
465 chrome::GetPrimaryChromeAcceleratorForCommandId(IDC_BOOKMARK_PAGE),
470 void CommandService::AssignInitialKeybindings(const Extension* extension) {
471 const extensions::CommandMap* commands =
472 CommandsInfo::GetNamedCommands(extension);
476 ExtensionPrefs* extension_prefs = ExtensionPrefs::Get(profile_);
477 if (InitialBindingsHaveBeenAssigned(extension_prefs, extension->id()))
479 SetInitialBindingsHaveBeenAssigned(extension_prefs, extension->id());
481 extensions::CommandMap::const_iterator iter = commands->begin();
482 for (; iter != commands->end(); ++iter) {
483 const extensions::Command command = iter->second;
484 if (CanAutoAssign(command.accelerator(),
487 true, // Is a named command.
489 AddKeybindingPref(command.accelerator(),
491 command.command_name(),
492 false, // Overwriting not allowed.
497 const extensions::Command* browser_action_command =
498 CommandsInfo::GetBrowserActionCommand(extension);
499 if (browser_action_command &&
500 CanAutoAssign(browser_action_command->accelerator(),
503 false, // Not a named command.
504 false)) { // Not global.
505 AddKeybindingPref(browser_action_command->accelerator(),
507 browser_action_command->command_name(),
508 false, // Overwriting not allowed.
509 false); // Not global.
512 const extensions::Command* page_action_command =
513 CommandsInfo::GetPageActionCommand(extension);
514 if (page_action_command &&
515 CanAutoAssign(page_action_command->accelerator(),
518 false, // Not a named command.
519 false)) { // Not global.
520 AddKeybindingPref(page_action_command->accelerator(),
522 page_action_command->command_name(),
523 false, // Overwriting not allowed.
524 false); // Not global.
528 void CommandService::RemoveKeybindingPrefs(const std::string& extension_id,
529 const std::string& command_name) {
530 DictionaryPrefUpdate updater(profile_->GetPrefs(),
531 prefs::kExtensionCommands);
532 base::DictionaryValue* bindings = updater.Get();
534 typedef std::vector<std::string> KeysToRemove;
535 KeysToRemove keys_to_remove;
536 for (base::DictionaryValue::Iterator it(*bindings); !it.IsAtEnd();
538 // Removal of keybinding preference should be limited to current platform.
539 if (!IsForCurrentPlatform(it.key()))
542 const base::DictionaryValue* item = NULL;
543 it.value().GetAsDictionary(&item);
545 std::string extension;
546 item->GetString(kExtension, &extension);
548 if (extension == extension_id) {
549 // If |command_name| is specified, delete only that command. Otherwise,
550 // delete all commands.
551 if (!command_name.empty()) {
553 item->GetString(kCommandName, &command);
554 if (command_name != command)
558 keys_to_remove.push_back(it.key());
562 for (KeysToRemove::const_iterator it = keys_to_remove.begin();
563 it != keys_to_remove.end(); ++it) {
564 std::string key = *it;
565 bindings->Remove(key, NULL);
567 std::pair<const std::string, const std::string> details =
568 std::make_pair(extension_id, command_name);
569 content::NotificationService::current()->Notify(
570 chrome::NOTIFICATION_EXTENSION_COMMAND_REMOVED,
571 content::Source<Profile>(profile_),
573 std::pair<const std::string, const std::string> >(&details));
577 bool CommandService::GetExtensionActionCommand(
578 const std::string& extension_id,
579 QueryType query_type,
580 extensions::Command* command,
582 ExtensionCommandType action_type) const {
583 const ExtensionSet& extensions =
584 ExtensionRegistry::Get(profile_)->enabled_extensions();
585 const Extension* extension = extensions.GetByID(extension_id);
591 const extensions::Command* requested_command = NULL;
592 switch (action_type) {
594 requested_command = CommandsInfo::GetBrowserActionCommand(extension);
597 requested_command = CommandsInfo::GetPageActionCommand(extension);
603 if (!requested_command)
606 // Look up to see if the user has overridden how the command should work.
607 extensions::Command saved_command =
608 FindCommandByName(extension_id, requested_command->command_name());
609 ui::Accelerator shortcut_assigned = saved_command.accelerator();
612 *active = (shortcut_assigned.key_code() != ui::VKEY_UNKNOWN);
614 if (query_type == ACTIVE_ONLY &&
615 shortcut_assigned.key_code() == ui::VKEY_UNKNOWN)
618 *command = *requested_command;
619 if (shortcut_assigned.key_code() != ui::VKEY_UNKNOWN)
620 command->set_accelerator(shortcut_assigned);
627 BrowserContextKeyedAPIFactory<CommandService>::DeclareFactoryDependencies() {
628 DependsOn(ExtensionCommandsGlobalRegistry::GetFactoryInstance());
631 } // namespace extensions