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