command.cc revision 116680a4aac90f2aa7413d9095a592090648e557
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/common/extensions/command.h"
6
7#include "base/logging.h"
8#include "base/strings/string_number_conversions.h"
9#include "base/strings/string_split.h"
10#include "base/strings/string_util.h"
11#include "base/values.h"
12#include "extensions/common/error_utils.h"
13#include "extensions/common/extension.h"
14#include "extensions/common/manifest_constants.h"
15#include "grit/generated_resources.h"
16#include "ui/base/l10n/l10n_util.h"
17
18namespace extensions {
19
20namespace errors = manifest_errors;
21namespace keys = manifest_keys;
22namespace values = manifest_values;
23
24namespace {
25
26static const char kMissing[] = "Missing";
27
28static const char kCommandKeyNotSupported[] =
29    "Command key is not supported. Note: Ctrl means Command on Mac";
30
31bool IsNamedCommand(const std::string& command_name) {
32  return command_name != values::kPageActionCommandEvent &&
33         command_name != values::kBrowserActionCommandEvent;
34}
35
36bool DoesRequireModifier(const std::string& accelerator) {
37  return accelerator != values::kKeyMediaNextTrack &&
38         accelerator != values::kKeyMediaPlayPause &&
39         accelerator != values::kKeyMediaPrevTrack &&
40         accelerator != values::kKeyMediaStop;
41}
42
43ui::Accelerator ParseImpl(const std::string& accelerator,
44                          const std::string& platform_key,
45                          int index,
46                          bool should_parse_media_keys,
47                          base::string16* error) {
48  error->clear();
49  if (platform_key != values::kKeybindingPlatformWin &&
50      platform_key != values::kKeybindingPlatformMac &&
51      platform_key != values::kKeybindingPlatformChromeOs &&
52      platform_key != values::kKeybindingPlatformLinux &&
53      platform_key != values::kKeybindingPlatformDefault) {
54    *error = ErrorUtils::FormatErrorMessageUTF16(
55        errors::kInvalidKeyBindingUnknownPlatform,
56        base::IntToString(index),
57        platform_key);
58    return ui::Accelerator();
59  }
60
61  std::vector<std::string> tokens;
62  base::SplitString(accelerator, '+', &tokens);
63  if (tokens.size() == 0 ||
64      (tokens.size() == 1 && DoesRequireModifier(accelerator)) ||
65      tokens.size() > 3) {
66    *error = ErrorUtils::FormatErrorMessageUTF16(
67        errors::kInvalidKeyBinding,
68        base::IntToString(index),
69        platform_key,
70        accelerator);
71    return ui::Accelerator();
72  }
73
74  // Now, parse it into an accelerator.
75  int modifiers = ui::EF_NONE;
76  ui::KeyboardCode key = ui::VKEY_UNKNOWN;
77  for (size_t i = 0; i < tokens.size(); i++) {
78    if (tokens[i] == values::kKeyCtrl) {
79      modifiers |= ui::EF_CONTROL_DOWN;
80    } else if (tokens[i] == values::kKeyCommand) {
81      if (platform_key == values::kKeybindingPlatformMac) {
82        // Either the developer specified Command+foo in the manifest for Mac or
83        // they specified Ctrl and it got normalized to Command (to get Ctrl on
84        // Mac the developer has to specify MacCtrl). Therefore we treat this
85        // as Command.
86        modifiers |= ui::EF_COMMAND_DOWN;
87#if defined(OS_MACOSX)
88      } else if (platform_key == values::kKeybindingPlatformDefault) {
89        // If we see "Command+foo" in the Default section it can mean two
90        // things, depending on the platform:
91        // The developer specified "Ctrl+foo" for Default and it got normalized
92        // on Mac to "Command+foo". This is fine. Treat it as Command.
93        modifiers |= ui::EF_COMMAND_DOWN;
94#endif
95      } else {
96        // No other platform supports Command.
97        key = ui::VKEY_UNKNOWN;
98        break;
99      }
100    } else if (tokens[i] == values::kKeyAlt) {
101      modifiers |= ui::EF_ALT_DOWN;
102    } else if (tokens[i] == values::kKeyShift) {
103      modifiers |= ui::EF_SHIFT_DOWN;
104    } else if (tokens[i].size() == 1 ||  // A-Z, 0-9.
105               tokens[i] == values::kKeyComma ||
106               tokens[i] == values::kKeyPeriod ||
107               tokens[i] == values::kKeyUp ||
108               tokens[i] == values::kKeyDown ||
109               tokens[i] == values::kKeyLeft ||
110               tokens[i] == values::kKeyRight ||
111               tokens[i] == values::kKeyIns ||
112               tokens[i] == values::kKeyDel ||
113               tokens[i] == values::kKeyHome ||
114               tokens[i] == values::kKeyEnd ||
115               tokens[i] == values::kKeyPgUp ||
116               tokens[i] == values::kKeyPgDwn ||
117               tokens[i] == values::kKeyTab ||
118               tokens[i] == values::kKeyMediaNextTrack ||
119               tokens[i] == values::kKeyMediaPlayPause ||
120               tokens[i] == values::kKeyMediaPrevTrack ||
121               tokens[i] == values::kKeyMediaStop) {
122      if (key != ui::VKEY_UNKNOWN) {
123        // Multiple key assignments.
124        key = ui::VKEY_UNKNOWN;
125        break;
126      }
127
128      if (tokens[i] == values::kKeyComma) {
129        key = ui::VKEY_OEM_COMMA;
130      } else if (tokens[i] == values::kKeyPeriod) {
131        key = ui::VKEY_OEM_PERIOD;
132      } else if (tokens[i] == values::kKeyUp) {
133        key = ui::VKEY_UP;
134      } else if (tokens[i] == values::kKeyDown) {
135        key = ui::VKEY_DOWN;
136      } else if (tokens[i] == values::kKeyLeft) {
137        key = ui::VKEY_LEFT;
138      } else if (tokens[i] == values::kKeyRight) {
139        key = ui::VKEY_RIGHT;
140      } else if (tokens[i] == values::kKeyIns) {
141        key = ui::VKEY_INSERT;
142      } else if (tokens[i] == values::kKeyDel) {
143        key = ui::VKEY_DELETE;
144      } else if (tokens[i] == values::kKeyHome) {
145        key = ui::VKEY_HOME;
146      } else if (tokens[i] == values::kKeyEnd) {
147        key = ui::VKEY_END;
148      } else if (tokens[i] == values::kKeyPgUp) {
149        key = ui::VKEY_PRIOR;
150      } else if (tokens[i] == values::kKeyPgDwn) {
151        key = ui::VKEY_NEXT;
152      } else if (tokens[i] == values::kKeyTab) {
153        key = ui::VKEY_TAB;
154      } else if (tokens[i] == values::kKeyMediaNextTrack &&
155                 should_parse_media_keys) {
156        key = ui::VKEY_MEDIA_NEXT_TRACK;
157      } else if (tokens[i] == values::kKeyMediaPlayPause &&
158                 should_parse_media_keys) {
159        key = ui::VKEY_MEDIA_PLAY_PAUSE;
160      } else if (tokens[i] == values::kKeyMediaPrevTrack &&
161                 should_parse_media_keys) {
162        key = ui::VKEY_MEDIA_PREV_TRACK;
163      } else if (tokens[i] == values::kKeyMediaStop &&
164                 should_parse_media_keys) {
165        key = ui::VKEY_MEDIA_STOP;
166      } else if (tokens[i].size() == 1 &&
167                 tokens[i][0] >= 'A' && tokens[i][0] <= 'Z') {
168        key = static_cast<ui::KeyboardCode>(ui::VKEY_A + (tokens[i][0] - 'A'));
169      } else if (tokens[i].size() == 1 &&
170                 tokens[i][0] >= '0' && tokens[i][0] <= '9') {
171        key = static_cast<ui::KeyboardCode>(ui::VKEY_0 + (tokens[i][0] - '0'));
172      } else {
173        key = ui::VKEY_UNKNOWN;
174        break;
175      }
176    } else {
177      *error = ErrorUtils::FormatErrorMessageUTF16(
178          errors::kInvalidKeyBinding,
179          base::IntToString(index),
180          platform_key,
181          accelerator);
182      return ui::Accelerator();
183    }
184  }
185
186  bool command = (modifiers & ui::EF_COMMAND_DOWN) != 0;
187  bool ctrl = (modifiers & ui::EF_CONTROL_DOWN) != 0;
188  bool alt = (modifiers & ui::EF_ALT_DOWN) != 0;
189  bool shift = (modifiers & ui::EF_SHIFT_DOWN) != 0;
190
191  // We support Ctrl+foo, Alt+foo, Ctrl+Shift+foo, Alt+Shift+foo, but not
192  // Ctrl+Alt+foo and not Shift+foo either. For a more detailed reason why we
193  // don't support Ctrl+Alt+foo see this article:
194  // http://blogs.msdn.com/b/oldnewthing/archive/2004/03/29/101121.aspx.
195  // On Mac Command can also be used in combination with Shift or on its own,
196  // as a modifier.
197  if (key == ui::VKEY_UNKNOWN || (ctrl && alt) || (command && alt) ||
198      (shift && !ctrl && !alt && !command)) {
199    *error = ErrorUtils::FormatErrorMessageUTF16(
200        errors::kInvalidKeyBinding,
201        base::IntToString(index),
202        platform_key,
203        accelerator);
204    return ui::Accelerator();
205  }
206
207  if ((key == ui::VKEY_MEDIA_NEXT_TRACK ||
208       key == ui::VKEY_MEDIA_PREV_TRACK ||
209       key == ui::VKEY_MEDIA_PLAY_PAUSE ||
210       key == ui::VKEY_MEDIA_STOP) &&
211      (shift || ctrl || alt || command)) {
212    *error = ErrorUtils::FormatErrorMessageUTF16(
213        errors::kInvalidKeyBindingMediaKeyWithModifier,
214        base::IntToString(index),
215        platform_key,
216        accelerator);
217    return ui::Accelerator();
218  }
219
220  return ui::Accelerator(key, modifiers);
221}
222
223// For Mac, we convert "Ctrl" to "Command" and "MacCtrl" to "Ctrl". Other
224// platforms leave the shortcut untouched.
225std::string NormalizeShortcutSuggestion(const std::string& suggestion,
226                                        const std::string& platform) {
227  bool normalize = false;
228  if (platform == values::kKeybindingPlatformMac) {
229    normalize = true;
230  } else if (platform == values::kKeybindingPlatformDefault) {
231#if defined(OS_MACOSX)
232    normalize = true;
233#endif
234  }
235
236  if (!normalize)
237    return suggestion;
238
239  std::vector<std::string> tokens;
240  base::SplitString(suggestion, '+', &tokens);
241  for (size_t i = 0; i < tokens.size(); i++) {
242    if (tokens[i] == values::kKeyCtrl)
243      tokens[i] = values::kKeyCommand;
244    else if (tokens[i] == values::kKeyMacCtrl)
245      tokens[i] = values::kKeyCtrl;
246  }
247  return JoinString(tokens, '+');
248}
249
250}  // namespace
251
252Command::Command() : global_(false) {}
253
254Command::Command(const std::string& command_name,
255                 const base::string16& description,
256                 const std::string& accelerator,
257                 bool global)
258    : command_name_(command_name),
259      description_(description),
260      global_(global) {
261  base::string16 error;
262  accelerator_ = ParseImpl(accelerator, CommandPlatform(), 0,
263                           IsNamedCommand(command_name), &error);
264}
265
266Command::~Command() {}
267
268// static
269std::string Command::CommandPlatform() {
270#if defined(OS_WIN)
271  return values::kKeybindingPlatformWin;
272#elif defined(OS_MACOSX)
273  return values::kKeybindingPlatformMac;
274#elif defined(OS_CHROMEOS)
275  return values::kKeybindingPlatformChromeOs;
276#elif defined(OS_LINUX)
277  return values::kKeybindingPlatformLinux;
278#else
279  return "";
280#endif
281}
282
283// static
284ui::Accelerator Command::StringToAccelerator(const std::string& accelerator,
285                                             const std::string& command_name) {
286  base::string16 error;
287  ui::Accelerator parsed =
288      ParseImpl(accelerator, Command::CommandPlatform(), 0,
289                IsNamedCommand(command_name), &error);
290  return parsed;
291}
292
293// static
294std::string Command::AcceleratorToString(const ui::Accelerator& accelerator) {
295  std::string shortcut;
296
297  // Ctrl and Alt are mutually exclusive.
298  if (accelerator.IsCtrlDown())
299    shortcut += values::kKeyCtrl;
300  else if (accelerator.IsAltDown())
301    shortcut += values::kKeyAlt;
302  if (!shortcut.empty())
303    shortcut += values::kKeySeparator;
304
305  if (accelerator.IsCmdDown()) {
306    shortcut += values::kKeyCommand;
307    shortcut += values::kKeySeparator;
308  }
309
310  if (accelerator.IsShiftDown()) {
311    shortcut += values::kKeyShift;
312    shortcut += values::kKeySeparator;
313  }
314
315  if (accelerator.key_code() >= ui::VKEY_0 &&
316      accelerator.key_code() <= ui::VKEY_9) {
317    shortcut += '0' + (accelerator.key_code() - ui::VKEY_0);
318  } else if (accelerator.key_code() >= ui::VKEY_A &&
319           accelerator.key_code() <= ui::VKEY_Z) {
320    shortcut += 'A' + (accelerator.key_code() - ui::VKEY_A);
321  } else {
322    switch (accelerator.key_code()) {
323      case ui::VKEY_OEM_COMMA:
324        shortcut += values::kKeyComma;
325        break;
326      case ui::VKEY_OEM_PERIOD:
327        shortcut += values::kKeyPeriod;
328        break;
329      case ui::VKEY_UP:
330        shortcut += values::kKeyUp;
331        break;
332      case ui::VKEY_DOWN:
333        shortcut += values::kKeyDown;
334        break;
335      case ui::VKEY_LEFT:
336        shortcut += values::kKeyLeft;
337        break;
338      case ui::VKEY_RIGHT:
339        shortcut += values::kKeyRight;
340        break;
341      case ui::VKEY_INSERT:
342        shortcut += values::kKeyIns;
343        break;
344      case ui::VKEY_DELETE:
345        shortcut += values::kKeyDel;
346        break;
347      case ui::VKEY_HOME:
348        shortcut += values::kKeyHome;
349        break;
350      case ui::VKEY_END:
351        shortcut += values::kKeyEnd;
352        break;
353      case ui::VKEY_PRIOR:
354        shortcut += values::kKeyPgUp;
355        break;
356      case ui::VKEY_NEXT:
357        shortcut += values::kKeyPgDwn;
358        break;
359      case ui::VKEY_TAB:
360        shortcut += values::kKeyTab;
361        break;
362      case ui::VKEY_MEDIA_NEXT_TRACK:
363        shortcut += values::kKeyMediaNextTrack;
364        break;
365      case ui::VKEY_MEDIA_PLAY_PAUSE:
366        shortcut += values::kKeyMediaPlayPause;
367        break;
368      case ui::VKEY_MEDIA_PREV_TRACK:
369        shortcut += values::kKeyMediaPrevTrack;
370        break;
371      case ui::VKEY_MEDIA_STOP:
372        shortcut += values::kKeyMediaStop;
373        break;
374      default:
375        return "";
376    }
377  }
378  return shortcut;
379}
380
381// static
382bool Command::IsMediaKey(const ui::Accelerator& accelerator) {
383  if (accelerator.modifiers() != 0)
384    return false;
385
386  return (accelerator.key_code() == ui::VKEY_MEDIA_NEXT_TRACK ||
387          accelerator.key_code() == ui::VKEY_MEDIA_PREV_TRACK ||
388          accelerator.key_code() == ui::VKEY_MEDIA_PLAY_PAUSE ||
389          accelerator.key_code() == ui::VKEY_MEDIA_STOP);
390}
391
392bool Command::Parse(const base::DictionaryValue* command,
393                    const std::string& command_name,
394                    int index,
395                    base::string16* error) {
396  DCHECK(!command_name.empty());
397
398  base::string16 description;
399  if (IsNamedCommand(command_name)) {
400    if (!command->GetString(keys::kDescription, &description) ||
401        description.empty()) {
402      *error = ErrorUtils::FormatErrorMessageUTF16(
403          errors::kInvalidKeyBindingDescription,
404          base::IntToString(index));
405      return false;
406    }
407  }
408
409  // We'll build up a map of platform-to-shortcut suggestions.
410  typedef std::map<const std::string, std::string> SuggestionMap;
411  SuggestionMap suggestions;
412
413  // First try to parse the |suggested_key| as a dictionary.
414  const base::DictionaryValue* suggested_key_dict;
415  if (command->GetDictionary(keys::kSuggestedKey, &suggested_key_dict)) {
416    for (base::DictionaryValue::Iterator iter(*suggested_key_dict);
417         !iter.IsAtEnd(); iter.Advance()) {
418      // For each item in the dictionary, extract the platforms specified.
419      std::string suggested_key_string;
420      if (iter.value().GetAsString(&suggested_key_string) &&
421          !suggested_key_string.empty()) {
422        // Found a platform, add it to the suggestions list.
423        suggestions[iter.key()] = suggested_key_string;
424      } else {
425        *error = ErrorUtils::FormatErrorMessageUTF16(
426            errors::kInvalidKeyBinding,
427            base::IntToString(index),
428            keys::kSuggestedKey,
429            kMissing);
430        return false;
431      }
432    }
433  } else {
434    // No dictionary was found, fall back to using just a string, so developers
435    // don't have to specify a dictionary if they just want to use one default
436    // for all platforms.
437    std::string suggested_key_string;
438    if (command->GetString(keys::kSuggestedKey, &suggested_key_string) &&
439        !suggested_key_string.empty()) {
440      // If only a single string is provided, it must be default for all.
441      suggestions[values::kKeybindingPlatformDefault] = suggested_key_string;
442    } else {
443      suggestions[values::kKeybindingPlatformDefault] = "";
444    }
445  }
446
447  // Check if this is a global or a regular shortcut.
448  bool global = false;
449  command->GetBoolean(keys::kGlobal, &global);
450
451  // Normalize the suggestions.
452  for (SuggestionMap::iterator iter = suggestions.begin();
453       iter != suggestions.end(); ++iter) {
454    // Before we normalize Ctrl to Command we must detect when the developer
455    // specified Command in the Default section, which will work on Mac after
456    // normalization but only fail on other platforms when they try it out on
457    // other platforms, which is not what we want.
458    if (iter->first == values::kKeybindingPlatformDefault &&
459        iter->second.find("Command+") != std::string::npos) {
460      *error = ErrorUtils::FormatErrorMessageUTF16(
461          errors::kInvalidKeyBinding,
462          base::IntToString(index),
463          keys::kSuggestedKey,
464          kCommandKeyNotSupported);
465      return false;
466    }
467
468    suggestions[iter->first] = NormalizeShortcutSuggestion(iter->second,
469                                                           iter->first);
470  }
471
472  std::string platform = CommandPlatform();
473  std::string key = platform;
474  if (suggestions.find(key) == suggestions.end())
475    key = values::kKeybindingPlatformDefault;
476  if (suggestions.find(key) == suggestions.end()) {
477    *error = ErrorUtils::FormatErrorMessageUTF16(
478        errors::kInvalidKeyBindingMissingPlatform,
479        base::IntToString(index),
480        keys::kSuggestedKey,
481        platform);
482    return false;  // No platform specified and no fallback. Bail.
483  }
484
485  // For developer convenience, we parse all the suggestions (and complain about
486  // errors for platforms other than the current one) but use only what we need.
487  std::map<const std::string, std::string>::const_iterator iter =
488      suggestions.begin();
489  for ( ; iter != suggestions.end(); ++iter) {
490    ui::Accelerator accelerator;
491    if (!iter->second.empty()) {
492      // Note that we pass iter->first to pretend we are on a platform we're not
493      // on.
494      accelerator = ParseImpl(iter->second, iter->first, index,
495                              IsNamedCommand(command_name), error);
496      if (accelerator.key_code() == ui::VKEY_UNKNOWN) {
497        if (error->empty()) {
498          *error = ErrorUtils::FormatErrorMessageUTF16(
499              errors::kInvalidKeyBinding,
500              base::IntToString(index),
501              iter->first,
502              iter->second);
503        }
504        return false;
505      }
506    }
507
508    if (iter->first == key) {
509      // This platform is our platform, so grab this key.
510      accelerator_ = accelerator;
511      command_name_ = command_name;
512      description_ = description;
513      global_ = global;
514    }
515  }
516  return true;
517}
518
519base::DictionaryValue* Command::ToValue(const Extension* extension,
520                                        bool active) const {
521  base::DictionaryValue* extension_data = new base::DictionaryValue();
522
523  base::string16 command_description;
524  bool extension_action = false;
525  if (command_name() == values::kBrowserActionCommandEvent ||
526      command_name() == values::kPageActionCommandEvent) {
527    command_description =
528        l10n_util::GetStringUTF16(IDS_EXTENSION_COMMANDS_GENERIC_ACTIVATE);
529    extension_action = true;
530  } else {
531    command_description = description();
532  }
533  extension_data->SetString("description", command_description);
534  extension_data->SetBoolean("active", active);
535  extension_data->SetString("keybinding", accelerator().GetShortcutText());
536  extension_data->SetString("command_name", command_name());
537  extension_data->SetString("extension_id", extension->id());
538  extension_data->SetBoolean("global", global());
539  extension_data->SetBoolean("extension_action", extension_action);
540  return extension_data;
541}
542
543}  // namespace extensions
544