command_service.cc revision 5d1f7b1de12d16ceb2c938c56701a3e8bfa558f7
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_function_registry.h"
19#include "chrome/browser/extensions/extension_keybinding_registry.h"
20#include "chrome/browser/extensions/extension_service.h"
21#include "chrome/browser/profiles/profile.h"
22#include "chrome/browser/ui/accelerator_utils.h"
23#include "chrome/common/extensions/api/commands/commands_handler.h"
24#include "chrome/common/extensions/manifest_handlers/settings_overrides_handler.h"
25#include "chrome/common/pref_names.h"
26#include "components/user_prefs/pref_registry_syncable.h"
27#include "content/public/browser/notification_details.h"
28#include "content/public/browser/notification_service.h"
29#include "extensions/browser/extension_system.h"
30#include "extensions/common/feature_switch.h"
31#include "extensions/common/manifest_constants.h"
32#include "extensions/common/permissions/permissions_data.h"
33
34using extensions::Extension;
35using extensions::ExtensionPrefs;
36
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 = extensions::Command::CommandPlatform() + ":" +
50                    extensions::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 (extensions::CommandService::IsMediaKey(accelerator))
57    key += ":" + extension_id;
58
59  return key;
60}
61
62bool IsForCurrentPlatform(const std::string& key) {
63  return StartsWithASCII(
64      key, extensions::Command::CommandPlatform() + ":", true);
65}
66
67void SetInitialBindingsHaveBeenAssigned(
68    ExtensionPrefs* prefs, const std::string& extension_id) {
69  prefs->UpdateExtensionPref(extension_id, kInitialBindingsHaveBeenAssigned,
70                             new base::FundamentalValue(true));
71}
72
73bool InitialBindingsHaveBeenAssigned(
74    const ExtensionPrefs* prefs, const std::string& extension_id) {
75  bool assigned = false;
76  if (!prefs || !prefs->ReadPrefAsBoolean(extension_id,
77                                          kInitialBindingsHaveBeenAssigned,
78                                          &assigned))
79    return false;
80
81  return assigned;
82}
83
84// Checks if |extension| is permitted to automatically assign the |accelerator|
85// key.
86bool CanAutoAssign(const ui::Accelerator& accelerator,
87                   const Extension* extension,
88                   Profile* profile,
89                   bool is_named_command,
90                   bool is_global) {
91  // Media Keys are non-exclusive, so allow auto-assigning them.
92  if (extensions::CommandService::IsMediaKey(accelerator))
93    return true;
94
95  if (is_global) {
96    if (!is_named_command)
97      return false;  // Browser and page actions are not global in nature.
98
99    // Global shortcuts are restricted to (Ctrl|Command)+Shift+[0-9].
100#if defined OS_MACOSX
101    if (!accelerator.IsCmdDown())
102      return false;
103#else
104    if (!accelerator.IsCtrlDown())
105      return false;
106#endif
107    if (!accelerator.IsShiftDown())
108      return false;
109    return (accelerator.key_code() >= ui::VKEY_0 &&
110            accelerator.key_code() <= ui::VKEY_9);
111  } else {
112    // Not a global command, check if Chrome shortcut and whether
113    // we can override it.
114    if (accelerator ==
115        chrome::GetPrimaryChromeAcceleratorForCommandId(IDC_BOOKMARK_PAGE)) {
116      using extensions::SettingsOverrides;
117      using extensions::FeatureSwitch;
118      const SettingsOverrides* settings_overrides =
119          SettingsOverrides::Get(extension);
120      if (settings_overrides &&
121          SettingsOverrides::RemovesBookmarkShortcut(*settings_overrides) &&
122          (extensions::PermissionsData::HasAPIPermission(
123              extension,
124              extensions::APIPermission::kBookmarkManagerPrivate) ||
125           FeatureSwitch::enable_override_bookmarks_ui()->IsEnabled())) {
126        // If this check fails it either means we have an API to override a
127        // key that isn't a ChromeAccelerator (and the API can therefore be
128        // deprecated) or the IsChromeAccelerator isn't consistently
129        // returning true for all accelerators.
130        DCHECK(chrome::IsChromeAccelerator(accelerator, profile));
131        return true;
132      }
133    }
134
135    return !chrome::IsChromeAccelerator(accelerator, profile);
136  }
137}
138
139}  // namespace
140
141namespace extensions {
142
143// static
144void CommandService::RegisterProfilePrefs(
145    user_prefs::PrefRegistrySyncable* registry) {
146  registry->RegisterDictionaryPref(
147      prefs::kExtensionCommands,
148      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
149}
150
151CommandService::CommandService(Profile* profile)
152    : profile_(profile) {
153  ExtensionFunctionRegistry::GetInstance()->
154      RegisterFunction<GetAllCommandsFunction>();
155
156  registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_INSTALLED,
157      content::Source<Profile>(profile));
158  registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNINSTALLED,
159      content::Source<Profile>(profile));
160}
161
162CommandService::~CommandService() {
163}
164
165static base::LazyInstance<ProfileKeyedAPIFactory<CommandService> >
166g_factory = LAZY_INSTANCE_INITIALIZER;
167
168// static
169ProfileKeyedAPIFactory<CommandService>* CommandService::GetFactoryInstance() {
170  return g_factory.Pointer();
171}
172
173// static
174CommandService* CommandService::Get(Profile* profile) {
175  return ProfileKeyedAPIFactory<CommandService>::GetForProfile(profile);
176}
177
178// static
179bool CommandService::IsMediaKey(const ui::Accelerator& accelerator) {
180  if (accelerator.modifiers() != 0)
181    return false;
182
183  return (accelerator.key_code() == ui::VKEY_MEDIA_NEXT_TRACK ||
184          accelerator.key_code() == ui::VKEY_MEDIA_PREV_TRACK ||
185          accelerator.key_code() == ui::VKEY_MEDIA_PLAY_PAUSE ||
186          accelerator.key_code() == ui::VKEY_MEDIA_STOP);
187}
188
189bool CommandService::GetBrowserActionCommand(
190    const std::string& extension_id,
191    QueryType type,
192    extensions::Command* command,
193    bool* active) {
194  return GetExtensionActionCommand(
195      extension_id, type, command, active, BROWSER_ACTION);
196}
197
198bool CommandService::GetPageActionCommand(
199    const std::string& extension_id,
200    QueryType type,
201    extensions::Command* command,
202    bool* active) {
203  return GetExtensionActionCommand(
204      extension_id, type, command, active, PAGE_ACTION);
205}
206
207bool CommandService::GetNamedCommands(const std::string& extension_id,
208                                      QueryType type,
209                                      CommandScope scope,
210                                      extensions::CommandMap* command_map) {
211  ExtensionService* extension_service =
212      ExtensionSystem::Get(profile_)->extension_service();
213  if (!extension_service)
214    return false;  // Can occur during testing.
215  const ExtensionSet* extensions = extension_service->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(!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(
355    const std::string& extension_id, const std::string& command) {
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
390void CommandService::AssignInitialKeybindings(const Extension* extension) {
391  const extensions::CommandMap* commands =
392      CommandsInfo::GetNamedCommands(extension);
393  if (!commands)
394    return;
395
396  ExtensionService* extension_service =
397      ExtensionSystem::Get(profile_)->extension_service();
398  ExtensionPrefs* extension_prefs = extension_service->extension_prefs();
399  if (InitialBindingsHaveBeenAssigned(extension_prefs, extension->id()))
400    return;
401  SetInitialBindingsHaveBeenAssigned(extension_prefs, extension->id());
402
403  extensions::CommandMap::const_iterator iter = commands->begin();
404  for (; iter != commands->end(); ++iter) {
405    const extensions::Command command = iter->second;
406    if (CanAutoAssign(command.accelerator(),
407                      extension,
408                      profile_,
409                      true,  // Is a named command.
410                      command.global())) {
411      AddKeybindingPref(command.accelerator(),
412                        extension->id(),
413                        command.command_name(),
414                        false,  // Overwriting not allowed.
415                        command.global());
416    }
417  }
418
419  const extensions::Command* browser_action_command =
420      CommandsInfo::GetBrowserActionCommand(extension);
421  if (browser_action_command &&
422      CanAutoAssign(browser_action_command->accelerator(),
423                    extension,
424                    profile_,
425                    false,     // Not a named command.
426                    false)) {  // Not global.
427    AddKeybindingPref(browser_action_command->accelerator(),
428                      extension->id(),
429                      browser_action_command->command_name(),
430                      false,   // Overwriting not allowed.
431                      false);  // Not global.
432  }
433
434  const extensions::Command* page_action_command =
435      CommandsInfo::GetPageActionCommand(extension);
436  if (page_action_command &&
437      CanAutoAssign(page_action_command->accelerator(),
438                    extension,
439                    profile_,
440                    false,     // Not a named command.
441                    false)) {  // Not global.
442    AddKeybindingPref(page_action_command->accelerator(),
443                      extension->id(),
444                      page_action_command->command_name(),
445                      false,   // Overwriting not allowed.
446                      false);  // Not global.
447  }
448}
449
450void CommandService::RemoveKeybindingPrefs(const std::string& extension_id,
451                                           const std::string& command_name) {
452  DictionaryPrefUpdate updater(profile_->GetPrefs(),
453                               prefs::kExtensionCommands);
454  base::DictionaryValue* bindings = updater.Get();
455
456  typedef std::vector<std::string> KeysToRemove;
457  KeysToRemove keys_to_remove;
458  for (base::DictionaryValue::Iterator it(*bindings); !it.IsAtEnd();
459       it.Advance()) {
460    // Removal of keybinding preference should be limited to current platform.
461    if (!IsForCurrentPlatform(it.key()))
462      continue;
463
464    const base::DictionaryValue* item = NULL;
465    it.value().GetAsDictionary(&item);
466
467    std::string extension;
468    item->GetString(kExtension, &extension);
469
470    if (extension == extension_id) {
471      // If |command_name| is specified, delete only that command. Otherwise,
472      // delete all commands.
473      if (!command_name.empty()) {
474        std::string command;
475        item->GetString(kCommandName, &command);
476        if (command_name != command)
477          continue;
478      }
479
480      keys_to_remove.push_back(it.key());
481    }
482  }
483
484  for (KeysToRemove::const_iterator it = keys_to_remove.begin();
485       it != keys_to_remove.end(); ++it) {
486    std::string key = *it;
487    bindings->Remove(key, NULL);
488
489    std::pair<const std::string, const std::string> details =
490        std::make_pair(extension_id, command_name);
491    content::NotificationService::current()->Notify(
492        chrome::NOTIFICATION_EXTENSION_COMMAND_REMOVED,
493        content::Source<Profile>(profile_),
494        content::Details<
495            std::pair<const std::string, const std::string> >(&details));
496  }
497}
498
499bool CommandService::GetExtensionActionCommand(
500    const std::string& extension_id,
501    QueryType query_type,
502    extensions::Command* command,
503    bool* active,
504    ExtensionActionType action_type) {
505  ExtensionService* service =
506      ExtensionSystem::Get(profile_)->extension_service();
507  if (!service)
508    return false;  // Can happen in tests.
509  const ExtensionSet* extensions = service->extensions();
510  const Extension* extension = extensions->GetByID(extension_id);
511  CHECK(extension);
512
513  if (active)
514    *active = false;
515
516  const extensions::Command* requested_command = NULL;
517  switch (action_type) {
518    case BROWSER_ACTION:
519      requested_command = CommandsInfo::GetBrowserActionCommand(extension);
520      break;
521    case PAGE_ACTION:
522      requested_command = CommandsInfo::GetPageActionCommand(extension);
523      break;
524  }
525  if (!requested_command)
526    return false;
527
528  // Look up to see if the user has overridden how the command should work.
529  extensions::Command saved_command =
530      FindCommandByName(extension_id, requested_command->command_name());
531  ui::Accelerator shortcut_assigned = saved_command.accelerator();
532
533  if (active)
534    *active = (shortcut_assigned.key_code() != ui::VKEY_UNKNOWN);
535
536  if (query_type == ACTIVE_ONLY &&
537      shortcut_assigned.key_code() == ui::VKEY_UNKNOWN)
538    return false;
539
540  *command = *requested_command;
541  if (shortcut_assigned.key_code() != ui::VKEY_UNKNOWN)
542    command->set_accelerator(shortcut_assigned);
543
544  return true;
545}
546
547template <>
548void ProfileKeyedAPIFactory<CommandService>::DeclareFactoryDependencies() {
549  DependsOn(ExtensionCommandsGlobalRegistry::GetFactoryInstance());
550}
551
552}  // namespace extensions
553