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