command_service.cc revision a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7
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/browser/chrome_notification_types.h"
15#include "chrome/browser/extensions/api/commands/commands.h"
16#include "chrome/browser/extensions/extension_commands_global_registry.h"
17#include "chrome/browser/extensions/extension_function_registry.h"
18#include "chrome/browser/extensions/extension_keybinding_registry.h"
19#include "chrome/browser/extensions/extension_service.h"
20#include "chrome/browser/extensions/extension_system.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/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/common/feature_switch.h"
29#include "extensions/common/manifest_constants.h"
30
31using extensions::Extension;
32using extensions::ExtensionPrefs;
33
34namespace {
35
36const char kExtension[] = "extension";
37const char kCommandName[] = "command_name";
38const char kGlobal[] = "global";
39
40// A preference that indicates that the initial keybindings for the given
41// extension have been set.
42const char kInitialBindingsHaveBeenAssigned[] = "initial_keybindings_set";
43
44std::string GetPlatformKeybindingKeyForAccelerator(
45    const ui::Accelerator& accelerator, const std::string extension_id) {
46  std::string key = extensions::Command::CommandPlatform() + ":" +
47                    extensions::Command::AcceleratorToString(accelerator);
48
49  // Media keys have a 1-to-many relationship with targets, unlike regular
50  // shortcut (1-to-1 relationship). That means two or more extensions can
51  // register for the same media key so the extension ID needs to be added to
52  // the key to make sure the key is unique.
53  if (extensions::CommandService::IsMediaKey(accelerator))
54    key += ":" + extension_id;
55
56  return key;
57}
58
59bool IsForCurrentPlatform(const std::string& key) {
60  return StartsWithASCII(
61      key, extensions::Command::CommandPlatform() + ":", true);
62}
63
64void SetInitialBindingsHaveBeenAssigned(
65    ExtensionPrefs* prefs, const std::string& extension_id) {
66  prefs->UpdateExtensionPref(extension_id, kInitialBindingsHaveBeenAssigned,
67                             new base::FundamentalValue(true));
68}
69
70bool InitialBindingsHaveBeenAssigned(
71    const ExtensionPrefs* prefs, const std::string& extension_id) {
72  bool assigned = false;
73  if (!prefs || !prefs->ReadPrefAsBoolean(extension_id,
74                                          kInitialBindingsHaveBeenAssigned,
75                                          &assigned))
76    return false;
77
78  return assigned;
79}
80
81bool IsWhitelistedGlobalShortcut(const extensions::Command& command) {
82  if (!command.global())
83    return true;
84  if (!command.accelerator().IsCtrlDown())
85    return false;
86  if (!command.accelerator().IsShiftDown())
87    return false;
88  return (command.accelerator().key_code() >= ui::VKEY_0 &&
89          command.accelerator().key_code() <= ui::VKEY_9);
90}
91
92}  // namespace
93
94namespace extensions {
95
96// static
97void CommandService::RegisterProfilePrefs(
98    user_prefs::PrefRegistrySyncable* registry) {
99  registry->RegisterDictionaryPref(
100      prefs::kExtensionCommands,
101      user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
102}
103
104CommandService::CommandService(Profile* profile)
105    : profile_(profile) {
106  ExtensionFunctionRegistry::GetInstance()->
107      RegisterFunction<GetAllCommandsFunction>();
108
109  registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_INSTALLED,
110      content::Source<Profile>(profile));
111  registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNINSTALLED,
112      content::Source<Profile>(profile));
113}
114
115CommandService::~CommandService() {
116}
117
118static base::LazyInstance<ProfileKeyedAPIFactory<CommandService> >
119g_factory = LAZY_INSTANCE_INITIALIZER;
120
121// static
122ProfileKeyedAPIFactory<CommandService>* CommandService::GetFactoryInstance() {
123  return &g_factory.Get();
124}
125
126// static
127CommandService* CommandService::Get(Profile* profile) {
128  return ProfileKeyedAPIFactory<CommandService>::GetForProfile(profile);
129}
130
131// static
132bool CommandService::IsMediaKey(const ui::Accelerator& accelerator) {
133  if (accelerator.modifiers() != 0)
134    return false;
135
136  return (accelerator.key_code() == ui::VKEY_MEDIA_NEXT_TRACK ||
137          accelerator.key_code() == ui::VKEY_MEDIA_PREV_TRACK ||
138          accelerator.key_code() == ui::VKEY_MEDIA_PLAY_PAUSE ||
139          accelerator.key_code() == ui::VKEY_MEDIA_STOP);
140}
141
142bool CommandService::GetBrowserActionCommand(
143    const std::string& extension_id,
144    QueryType type,
145    extensions::Command* command,
146    bool* active) {
147  return GetExtensionActionCommand(
148      extension_id, type, command, active, BROWSER_ACTION);
149}
150
151bool CommandService::GetPageActionCommand(
152    const std::string& extension_id,
153    QueryType type,
154    extensions::Command* command,
155    bool* active) {
156  return GetExtensionActionCommand(
157      extension_id, type, command, active, PAGE_ACTION);
158}
159
160bool CommandService::GetScriptBadgeCommand(
161    const std::string& extension_id,
162    QueryType type,
163    extensions::Command* command,
164    bool* active) {
165  return GetExtensionActionCommand(
166      extension_id, type, command, active, SCRIPT_BADGE);
167}
168
169bool CommandService::GetNamedCommands(const std::string& extension_id,
170                                      QueryType type,
171                                      CommandScope scope,
172                                      extensions::CommandMap* command_map) {
173  ExtensionService* extension_service =
174      ExtensionSystem::Get(profile_)->extension_service();
175  if (!extension_service)
176    return false;  // Can occur during testing.
177  const ExtensionSet* extensions = extension_service->extensions();
178  const Extension* extension = extensions->GetByID(extension_id);
179  CHECK(extension);
180
181  command_map->clear();
182  const extensions::CommandMap* commands =
183      CommandsInfo::GetNamedCommands(extension);
184  if (!commands)
185    return false;
186
187  extensions::CommandMap::const_iterator iter = commands->begin();
188  for (; iter != commands->end(); ++iter) {
189    // Look up to see if the user has overridden how the command should work.
190    extensions::Command saved_command =
191        FindCommandByName(extension_id, iter->second.command_name());
192    ui::Accelerator shortcut_assigned = saved_command.accelerator();
193
194    if (type == ACTIVE_ONLY && shortcut_assigned.key_code() == ui::VKEY_UNKNOWN)
195      continue;
196
197    extensions::Command command = iter->second;
198    if (scope != ANY_SCOPE && ((scope == GLOBAL) != saved_command.global()))
199      continue;
200
201    if (shortcut_assigned.key_code() != ui::VKEY_UNKNOWN)
202      command.set_accelerator(shortcut_assigned);
203    command.set_global(saved_command.global());
204
205    (*command_map)[iter->second.command_name()] = command;
206  }
207
208  return true;
209}
210
211bool CommandService::AddKeybindingPref(
212    const ui::Accelerator& accelerator,
213    std::string extension_id,
214    std::string command_name,
215    bool allow_overrides,
216    bool global) {
217  if (accelerator.key_code() == ui::VKEY_UNKNOWN)
218    return false;
219
220  // Media Keys are allowed to be used by named command only.
221  DCHECK(!IsMediaKey(accelerator) ||
222         (command_name != manifest_values::kPageActionCommandEvent &&
223          command_name != manifest_values::kBrowserActionCommandEvent &&
224          command_name != manifest_values::kScriptBadgeCommandEvent));
225
226  DictionaryPrefUpdate updater(profile_->GetPrefs(),
227                               prefs::kExtensionCommands);
228  base::DictionaryValue* bindings = updater.Get();
229
230  std::string key = GetPlatformKeybindingKeyForAccelerator(accelerator,
231                                                           extension_id);
232
233  if (bindings->HasKey(key)) {
234    if (!allow_overrides)
235      return false;  // Already taken.
236
237    // If the shortcut has been assigned to another command, it should be
238    // removed before overriding, so that |ExtensionKeybindingRegistry| can get
239    // a chance to do clean-up.
240    const base::DictionaryValue* item = NULL;
241    bindings->GetDictionary(key, &item);
242    std::string old_extension_id;
243    std::string old_command_name;
244    item->GetString(kExtension, &old_extension_id);
245    item->GetString(kCommandName, &old_command_name);
246    RemoveKeybindingPrefs(old_extension_id, old_command_name);
247  }
248
249  base::DictionaryValue* keybinding = new base::DictionaryValue();
250  keybinding->SetString(kExtension, extension_id);
251  keybinding->SetString(kCommandName, command_name);
252  keybinding->SetBoolean(kGlobal, global);
253
254  bindings->Set(key, keybinding);
255
256  std::pair<const std::string, const std::string> details =
257      std::make_pair(extension_id, command_name);
258  content::NotificationService::current()->Notify(
259      chrome::NOTIFICATION_EXTENSION_COMMAND_ADDED,
260      content::Source<Profile>(profile_),
261      content::Details<
262          std::pair<const std::string, const std::string> >(&details));
263
264  return true;
265}
266
267void CommandService::Observe(
268    int type,
269    const content::NotificationSource& source,
270    const content::NotificationDetails& details) {
271  switch (type) {
272    case chrome::NOTIFICATION_EXTENSION_INSTALLED:
273      AssignInitialKeybindings(
274          content::Details<const InstalledExtensionInfo>(details)->extension);
275      break;
276    case chrome::NOTIFICATION_EXTENSION_UNINSTALLED:
277      RemoveKeybindingPrefs(
278          content::Details<const Extension>(details)->id(),
279          std::string());
280      break;
281    default:
282      NOTREACHED();
283      break;
284  }
285}
286
287void CommandService::UpdateKeybindingPrefs(const std::string& extension_id,
288                                           const std::string& command_name,
289                                           const std::string& keystroke) {
290  extensions::Command command = FindCommandByName(extension_id, command_name);
291
292  // The extension command might be assigned another shortcut. Remove that
293  // shortcut before proceeding.
294  RemoveKeybindingPrefs(extension_id, command_name);
295
296  ui::Accelerator accelerator =
297      Command::StringToAccelerator(keystroke, command_name);
298  AddKeybindingPref(accelerator, extension_id, command_name,
299                    true, command.global());
300}
301
302bool CommandService::SetScope(const std::string& extension_id,
303                              const std::string& command_name,
304                              bool global) {
305  extensions::Command command = FindCommandByName(extension_id, command_name);
306  if (global == command.global())
307    return false;
308
309  // Pre-existing shortcuts must be removed before proceeding because the
310  // handlers for global and non-global extensions are not one and the same.
311  RemoveKeybindingPrefs(extension_id, command_name);
312  AddKeybindingPref(command.accelerator(), extension_id,
313                    command_name, true, global);
314  return true;
315}
316
317Command CommandService::FindCommandByName(
318    const std::string& extension_id, const std::string& command) {
319  const base::DictionaryValue* bindings =
320      profile_->GetPrefs()->GetDictionary(prefs::kExtensionCommands);
321  for (base::DictionaryValue::Iterator it(*bindings); !it.IsAtEnd();
322       it.Advance()) {
323    const base::DictionaryValue* item = NULL;
324    it.value().GetAsDictionary(&item);
325
326    std::string extension;
327    item->GetString(kExtension, &extension);
328    if (extension != extension_id)
329      continue;
330    std::string command_name;
331    item->GetString(kCommandName, &command_name);
332    if (command != command_name)
333      continue;
334    // Format stored in Preferences is: "Platform:Shortcut[:ExtensionId]".
335    std::string shortcut = it.key();
336    if (!IsForCurrentPlatform(shortcut))
337      continue;
338    bool global = false;
339    if (FeatureSwitch::global_commands()->IsEnabled())
340      item->GetBoolean(kGlobal, &global);
341
342    std::vector<std::string> tokens;
343    base::SplitString(shortcut, ':', &tokens);
344    CHECK(tokens.size() >= 2);
345    shortcut = tokens[1];
346
347    return Command(command_name, base::string16(), shortcut, global);
348  }
349
350  return Command();
351}
352
353void CommandService::AssignInitialKeybindings(const Extension* extension) {
354  const extensions::CommandMap* commands =
355      CommandsInfo::GetNamedCommands(extension);
356  if (!commands)
357    return;
358
359  ExtensionService* extension_service =
360      ExtensionSystem::Get(profile_)->extension_service();
361  ExtensionPrefs* extension_prefs = extension_service->extension_prefs();
362  if (InitialBindingsHaveBeenAssigned(extension_prefs, extension->id()))
363    return;
364  SetInitialBindingsHaveBeenAssigned(extension_prefs, extension->id());
365
366  extensions::CommandMap::const_iterator iter = commands->begin();
367  for (; iter != commands->end(); ++iter) {
368    // Make sure registered Chrome shortcuts cannot be automatically assigned
369    // (overwritten) by extension developers. Media keys are an exception here.
370    if ((!chrome::IsChromeAccelerator(iter->second.accelerator(), profile_) &&
371        IsWhitelistedGlobalShortcut(iter->second)) ||
372        extensions::CommandService::IsMediaKey(iter->second.accelerator())) {
373      AddKeybindingPref(iter->second.accelerator(),
374                        extension->id(),
375                        iter->second.command_name(),
376                        false,  // Overwriting not allowed.
377                        iter->second.global());
378    }
379  }
380
381  const extensions::Command* browser_action_command =
382      CommandsInfo::GetBrowserActionCommand(extension);
383  if (browser_action_command) {
384    if (!chrome::IsChromeAccelerator(
385        browser_action_command->accelerator(), profile_)) {
386      AddKeybindingPref(browser_action_command->accelerator(),
387                        extension->id(),
388                        browser_action_command->command_name(),
389                        false,   // Overwriting not allowed.
390                        false);  // Browser actions can't be global.
391    }
392  }
393
394  const extensions::Command* page_action_command =
395      CommandsInfo::GetPageActionCommand(extension);
396  if (page_action_command) {
397    if (!chrome::IsChromeAccelerator(
398        page_action_command->accelerator(), profile_)) {
399      AddKeybindingPref(page_action_command->accelerator(),
400                        extension->id(),
401                        page_action_command->command_name(),
402                        false,   // Overwriting not allowed.
403                        false);  // Page actions can't be global.
404    }
405  }
406
407  const extensions::Command* script_badge_command =
408      CommandsInfo::GetScriptBadgeCommand(extension);
409  if (script_badge_command) {
410    if (!chrome::IsChromeAccelerator(
411        script_badge_command->accelerator(), profile_)) {
412      AddKeybindingPref(script_badge_command->accelerator(),
413                        extension->id(),
414                        script_badge_command->command_name(),
415                        false,   // Overwriting not allowed.
416                        false);  // Script badges can't be global.
417    }
418  }
419}
420
421void CommandService::RemoveKeybindingPrefs(const std::string& extension_id,
422                                           const std::string& command_name) {
423  DictionaryPrefUpdate updater(profile_->GetPrefs(),
424                               prefs::kExtensionCommands);
425  base::DictionaryValue* bindings = updater.Get();
426
427  typedef std::vector<std::string> KeysToRemove;
428  KeysToRemove keys_to_remove;
429  for (base::DictionaryValue::Iterator it(*bindings); !it.IsAtEnd();
430       it.Advance()) {
431    // Removal of keybinding preference should be limited to current platform.
432    if (!IsForCurrentPlatform(it.key()))
433      continue;
434
435    const base::DictionaryValue* item = NULL;
436    it.value().GetAsDictionary(&item);
437
438    std::string extension;
439    item->GetString(kExtension, &extension);
440
441    if (extension == extension_id) {
442      // If |command_name| is specified, delete only that command. Otherwise,
443      // delete all commands.
444      if (!command_name.empty()) {
445        std::string command;
446        item->GetString(kCommandName, &command);
447        if (command_name != command)
448          continue;
449      }
450
451      keys_to_remove.push_back(it.key());
452    }
453  }
454
455  for (KeysToRemove::const_iterator it = keys_to_remove.begin();
456       it != keys_to_remove.end(); ++it) {
457    std::string key = *it;
458    bindings->Remove(key, NULL);
459
460    std::pair<const std::string, const std::string> details =
461        std::make_pair(extension_id, command_name);
462    content::NotificationService::current()->Notify(
463        chrome::NOTIFICATION_EXTENSION_COMMAND_REMOVED,
464        content::Source<Profile>(profile_),
465        content::Details<
466            std::pair<const std::string, const std::string> >(&details));
467  }
468}
469
470bool CommandService::GetExtensionActionCommand(
471    const std::string& extension_id,
472    QueryType query_type,
473    extensions::Command* command,
474    bool* active,
475    ExtensionActionType action_type) {
476  ExtensionService* service =
477      ExtensionSystem::Get(profile_)->extension_service();
478  if (!service)
479    return false;  // Can happen in tests.
480  const ExtensionSet* extensions = service->extensions();
481  const Extension* extension = extensions->GetByID(extension_id);
482  CHECK(extension);
483
484  if (active)
485    *active = false;
486
487  const extensions::Command* requested_command = NULL;
488  switch (action_type) {
489    case BROWSER_ACTION:
490      requested_command = CommandsInfo::GetBrowserActionCommand(extension);
491      break;
492    case PAGE_ACTION:
493      requested_command = CommandsInfo::GetPageActionCommand(extension);
494      break;
495    case SCRIPT_BADGE:
496      requested_command = CommandsInfo::GetScriptBadgeCommand(extension);
497      break;
498  }
499  if (!requested_command)
500    return false;
501
502  // Look up to see if the user has overridden how the command should work.
503  extensions::Command saved_command =
504      FindCommandByName(extension_id, requested_command->command_name());
505  ui::Accelerator shortcut_assigned = saved_command.accelerator();
506
507  if (active)
508    *active = (shortcut_assigned.key_code() != ui::VKEY_UNKNOWN);
509
510  if (query_type == ACTIVE_ONLY &&
511      shortcut_assigned.key_code() == ui::VKEY_UNKNOWN)
512    return false;
513
514  *command = *requested_command;
515  if (shortcut_assigned.key_code() != ui::VKEY_UNKNOWN)
516    command->set_accelerator(shortcut_assigned);
517
518  return true;
519}
520
521template <>
522void ProfileKeyedAPIFactory<CommandService>::DeclareFactoryDependencies() {
523  DependsOn(ExtensionCommandsGlobalRegistry::GetFactoryInstance());
524}
525
526}  // namespace extensions
527