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