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