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