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