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