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