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