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.
5 #include "chrome/common/extensions/command.h"
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"
18 namespace extensions {
20 namespace errors = manifest_errors;
21 namespace keys = manifest_keys;
22 namespace values = manifest_values;
26 static const char kMissing[] = "Missing";
28 static const char kCommandKeyNotSupported[] =
29 "Command key is not supported. Note: Ctrl means Command on Mac";
31 #if defined(OS_CHROMEOS)
32 // ChromeOS supports an additional modifier 'Search', which can result in longer
34 static const int kMaxTokenSize = 4;
36 static const int kMaxTokenSize = 3;
39 bool IsNamedCommand(const std::string& command_name) {
40 return command_name != values::kPageActionCommandEvent &&
41 command_name != values::kBrowserActionCommandEvent;
44 bool DoesRequireModifier(const std::string& accelerator) {
45 return accelerator != values::kKeyMediaNextTrack &&
46 accelerator != values::kKeyMediaPlayPause &&
47 accelerator != values::kKeyMediaPrevTrack &&
48 accelerator != values::kKeyMediaStop;
51 // Parse an |accelerator| for a given platform (specified by |platform_key|) and
52 // return the result as a ui::Accelerator if successful, or VKEY_UNKNOWN if not.
53 // |index| is used when constructing an |error| messages to show which command
54 // in the manifest is failing and |should_parse_media_keys| specifies whether
55 // media keys are to be considered for parsing.
56 // Note: If the parsing rules here are changed, make sure to update the
57 // corresponding extension_command_list.js validation, which validates the user
58 // input for chrome://extensions/configureCommands.
59 ui::Accelerator ParseImpl(const std::string& accelerator,
60 const std::string& platform_key,
62 bool should_parse_media_keys,
63 base::string16* error) {
65 if (platform_key != values::kKeybindingPlatformWin &&
66 platform_key != values::kKeybindingPlatformMac &&
67 platform_key != values::kKeybindingPlatformChromeOs &&
68 platform_key != values::kKeybindingPlatformLinux &&
69 platform_key != values::kKeybindingPlatformDefault) {
70 *error = ErrorUtils::FormatErrorMessageUTF16(
71 errors::kInvalidKeyBindingUnknownPlatform,
72 base::IntToString(index),
74 return ui::Accelerator();
77 std::vector<std::string> tokens;
78 base::SplitString(accelerator, '+', &tokens);
79 if (tokens.size() == 0 ||
80 (tokens.size() == 1 && DoesRequireModifier(accelerator)) ||
81 tokens.size() > kMaxTokenSize) {
82 *error = ErrorUtils::FormatErrorMessageUTF16(
83 errors::kInvalidKeyBinding,
84 base::IntToString(index),
87 return ui::Accelerator();
90 // Now, parse it into an accelerator.
91 int modifiers = ui::EF_NONE;
92 ui::KeyboardCode key = ui::VKEY_UNKNOWN;
93 for (size_t i = 0; i < tokens.size(); i++) {
94 if (tokens[i] == values::kKeyCtrl) {
95 modifiers |= ui::EF_CONTROL_DOWN;
96 } else if (tokens[i] == values::kKeyCommand) {
97 if (platform_key == values::kKeybindingPlatformMac) {
98 // Either the developer specified Command+foo in the manifest for Mac or
99 // they specified Ctrl and it got normalized to Command (to get Ctrl on
100 // Mac the developer has to specify MacCtrl). Therefore we treat this
102 modifiers |= ui::EF_COMMAND_DOWN;
103 #if defined(OS_MACOSX)
104 } else if (platform_key == values::kKeybindingPlatformDefault) {
105 // If we see "Command+foo" in the Default section it can mean two
106 // things, depending on the platform:
107 // The developer specified "Ctrl+foo" for Default and it got normalized
108 // on Mac to "Command+foo". This is fine. Treat it as Command.
109 modifiers |= ui::EF_COMMAND_DOWN;
112 // No other platform supports Command.
113 key = ui::VKEY_UNKNOWN;
116 } else if (tokens[i] == values::kKeySearch) {
117 // Search is a special modifier only on ChromeOS and maps to 'Command'.
118 if (platform_key == values::kKeybindingPlatformChromeOs) {
119 modifiers |= ui::EF_COMMAND_DOWN;
121 // No other platform supports Search.
122 key = ui::VKEY_UNKNOWN;
125 } else if (tokens[i] == values::kKeyAlt) {
126 modifiers |= ui::EF_ALT_DOWN;
127 } else if (tokens[i] == values::kKeyShift) {
128 modifiers |= ui::EF_SHIFT_DOWN;
129 } else if (tokens[i].size() == 1 || // A-Z, 0-9.
130 tokens[i] == values::kKeyComma ||
131 tokens[i] == values::kKeyPeriod ||
132 tokens[i] == values::kKeyUp ||
133 tokens[i] == values::kKeyDown ||
134 tokens[i] == values::kKeyLeft ||
135 tokens[i] == values::kKeyRight ||
136 tokens[i] == values::kKeyIns ||
137 tokens[i] == values::kKeyDel ||
138 tokens[i] == values::kKeyHome ||
139 tokens[i] == values::kKeyEnd ||
140 tokens[i] == values::kKeyPgUp ||
141 tokens[i] == values::kKeyPgDwn ||
142 tokens[i] == values::kKeyTab ||
143 tokens[i] == values::kKeyMediaNextTrack ||
144 tokens[i] == values::kKeyMediaPlayPause ||
145 tokens[i] == values::kKeyMediaPrevTrack ||
146 tokens[i] == values::kKeyMediaStop) {
147 if (key != ui::VKEY_UNKNOWN) {
148 // Multiple key assignments.
149 key = ui::VKEY_UNKNOWN;
153 if (tokens[i] == values::kKeyComma) {
154 key = ui::VKEY_OEM_COMMA;
155 } else if (tokens[i] == values::kKeyPeriod) {
156 key = ui::VKEY_OEM_PERIOD;
157 } else if (tokens[i] == values::kKeyUp) {
159 } else if (tokens[i] == values::kKeyDown) {
161 } else if (tokens[i] == values::kKeyLeft) {
163 } else if (tokens[i] == values::kKeyRight) {
164 key = ui::VKEY_RIGHT;
165 } else if (tokens[i] == values::kKeyIns) {
166 key = ui::VKEY_INSERT;
167 } else if (tokens[i] == values::kKeyDel) {
168 key = ui::VKEY_DELETE;
169 } else if (tokens[i] == values::kKeyHome) {
171 } else if (tokens[i] == values::kKeyEnd) {
173 } else if (tokens[i] == values::kKeyPgUp) {
174 key = ui::VKEY_PRIOR;
175 } else if (tokens[i] == values::kKeyPgDwn) {
177 } else if (tokens[i] == values::kKeyTab) {
179 } else if (tokens[i] == values::kKeyMediaNextTrack &&
180 should_parse_media_keys) {
181 key = ui::VKEY_MEDIA_NEXT_TRACK;
182 } else if (tokens[i] == values::kKeyMediaPlayPause &&
183 should_parse_media_keys) {
184 key = ui::VKEY_MEDIA_PLAY_PAUSE;
185 } else if (tokens[i] == values::kKeyMediaPrevTrack &&
186 should_parse_media_keys) {
187 key = ui::VKEY_MEDIA_PREV_TRACK;
188 } else if (tokens[i] == values::kKeyMediaStop &&
189 should_parse_media_keys) {
190 key = ui::VKEY_MEDIA_STOP;
191 } else if (tokens[i].size() == 1 &&
192 tokens[i][0] >= 'A' && tokens[i][0] <= 'Z') {
193 key = static_cast<ui::KeyboardCode>(ui::VKEY_A + (tokens[i][0] - 'A'));
194 } else if (tokens[i].size() == 1 &&
195 tokens[i][0] >= '0' && tokens[i][0] <= '9') {
196 key = static_cast<ui::KeyboardCode>(ui::VKEY_0 + (tokens[i][0] - '0'));
198 key = ui::VKEY_UNKNOWN;
202 *error = ErrorUtils::FormatErrorMessageUTF16(
203 errors::kInvalidKeyBinding,
204 base::IntToString(index),
207 return ui::Accelerator();
211 bool command = (modifiers & ui::EF_COMMAND_DOWN) != 0;
212 bool ctrl = (modifiers & ui::EF_CONTROL_DOWN) != 0;
213 bool alt = (modifiers & ui::EF_ALT_DOWN) != 0;
214 bool shift = (modifiers & ui::EF_SHIFT_DOWN) != 0;
216 // We support Ctrl+foo, Alt+foo, Ctrl+Shift+foo, Alt+Shift+foo, but not
217 // Ctrl+Alt+foo and not Shift+foo either. For a more detailed reason why we
218 // don't support Ctrl+Alt+foo see this article:
219 // http://blogs.msdn.com/b/oldnewthing/archive/2004/03/29/101121.aspx.
220 // On Mac Command can also be used in combination with Shift or on its own,
222 if (key == ui::VKEY_UNKNOWN || (ctrl && alt) || (command && alt) ||
223 (shift && !ctrl && !alt && !command)) {
224 *error = ErrorUtils::FormatErrorMessageUTF16(
225 errors::kInvalidKeyBinding,
226 base::IntToString(index),
229 return ui::Accelerator();
232 if ((key == ui::VKEY_MEDIA_NEXT_TRACK ||
233 key == ui::VKEY_MEDIA_PREV_TRACK ||
234 key == ui::VKEY_MEDIA_PLAY_PAUSE ||
235 key == ui::VKEY_MEDIA_STOP) &&
236 (shift || ctrl || alt || command)) {
237 *error = ErrorUtils::FormatErrorMessageUTF16(
238 errors::kInvalidKeyBindingMediaKeyWithModifier,
239 base::IntToString(index),
242 return ui::Accelerator();
245 return ui::Accelerator(key, modifiers);
248 // For Mac, we convert "Ctrl" to "Command" and "MacCtrl" to "Ctrl". Other
249 // platforms leave the shortcut untouched.
250 std::string NormalizeShortcutSuggestion(const std::string& suggestion,
251 const std::string& platform) {
252 bool normalize = false;
253 if (platform == values::kKeybindingPlatformMac) {
255 } else if (platform == values::kKeybindingPlatformDefault) {
256 #if defined(OS_MACOSX)
264 std::vector<std::string> tokens;
265 base::SplitString(suggestion, '+', &tokens);
266 for (size_t i = 0; i < tokens.size(); i++) {
267 if (tokens[i] == values::kKeyCtrl)
268 tokens[i] = values::kKeyCommand;
269 else if (tokens[i] == values::kKeyMacCtrl)
270 tokens[i] = values::kKeyCtrl;
272 return JoinString(tokens, '+');
277 Command::Command() : global_(false) {}
279 Command::Command(const std::string& command_name,
280 const base::string16& description,
281 const std::string& accelerator,
283 : command_name_(command_name),
284 description_(description),
286 base::string16 error;
287 accelerator_ = ParseImpl(accelerator, CommandPlatform(), 0,
288 IsNamedCommand(command_name), &error);
291 Command::~Command() {}
294 std::string Command::CommandPlatform() {
296 return values::kKeybindingPlatformWin;
297 #elif defined(OS_MACOSX)
298 return values::kKeybindingPlatformMac;
299 #elif defined(OS_CHROMEOS)
300 return values::kKeybindingPlatformChromeOs;
301 #elif defined(OS_LINUX)
302 return values::kKeybindingPlatformLinux;
309 ui::Accelerator Command::StringToAccelerator(const std::string& accelerator,
310 const std::string& command_name) {
311 base::string16 error;
312 ui::Accelerator parsed =
313 ParseImpl(accelerator, Command::CommandPlatform(), 0,
314 IsNamedCommand(command_name), &error);
319 std::string Command::AcceleratorToString(const ui::Accelerator& accelerator) {
320 std::string shortcut;
322 // Ctrl and Alt are mutually exclusive.
323 if (accelerator.IsCtrlDown())
324 shortcut += values::kKeyCtrl;
325 else if (accelerator.IsAltDown())
326 shortcut += values::kKeyAlt;
327 if (!shortcut.empty())
328 shortcut += values::kKeySeparator;
330 if (accelerator.IsCmdDown()) {
331 #if defined(OS_CHROMEOS)
332 // Chrome OS treats the Search key like the Command key.
333 shortcut += values::kKeySearch;
335 shortcut += values::kKeyCommand;
337 shortcut += values::kKeySeparator;
340 if (accelerator.IsShiftDown()) {
341 shortcut += values::kKeyShift;
342 shortcut += values::kKeySeparator;
345 if (accelerator.key_code() >= ui::VKEY_0 &&
346 accelerator.key_code() <= ui::VKEY_9) {
347 shortcut += '0' + (accelerator.key_code() - ui::VKEY_0);
348 } else if (accelerator.key_code() >= ui::VKEY_A &&
349 accelerator.key_code() <= ui::VKEY_Z) {
350 shortcut += 'A' + (accelerator.key_code() - ui::VKEY_A);
352 switch (accelerator.key_code()) {
353 case ui::VKEY_OEM_COMMA:
354 shortcut += values::kKeyComma;
356 case ui::VKEY_OEM_PERIOD:
357 shortcut += values::kKeyPeriod;
360 shortcut += values::kKeyUp;
363 shortcut += values::kKeyDown;
366 shortcut += values::kKeyLeft;
369 shortcut += values::kKeyRight;
371 case ui::VKEY_INSERT:
372 shortcut += values::kKeyIns;
374 case ui::VKEY_DELETE:
375 shortcut += values::kKeyDel;
378 shortcut += values::kKeyHome;
381 shortcut += values::kKeyEnd;
384 shortcut += values::kKeyPgUp;
387 shortcut += values::kKeyPgDwn;
390 shortcut += values::kKeyTab;
392 case ui::VKEY_MEDIA_NEXT_TRACK:
393 shortcut += values::kKeyMediaNextTrack;
395 case ui::VKEY_MEDIA_PLAY_PAUSE:
396 shortcut += values::kKeyMediaPlayPause;
398 case ui::VKEY_MEDIA_PREV_TRACK:
399 shortcut += values::kKeyMediaPrevTrack;
401 case ui::VKEY_MEDIA_STOP:
402 shortcut += values::kKeyMediaStop;
412 bool Command::IsMediaKey(const ui::Accelerator& accelerator) {
413 if (accelerator.modifiers() != 0)
416 return (accelerator.key_code() == ui::VKEY_MEDIA_NEXT_TRACK ||
417 accelerator.key_code() == ui::VKEY_MEDIA_PREV_TRACK ||
418 accelerator.key_code() == ui::VKEY_MEDIA_PLAY_PAUSE ||
419 accelerator.key_code() == ui::VKEY_MEDIA_STOP);
422 bool Command::Parse(const base::DictionaryValue* command,
423 const std::string& command_name,
425 base::string16* error) {
426 DCHECK(!command_name.empty());
428 base::string16 description;
429 if (IsNamedCommand(command_name)) {
430 if (!command->GetString(keys::kDescription, &description) ||
431 description.empty()) {
432 *error = ErrorUtils::FormatErrorMessageUTF16(
433 errors::kInvalidKeyBindingDescription,
434 base::IntToString(index));
439 // We'll build up a map of platform-to-shortcut suggestions.
440 typedef std::map<const std::string, std::string> SuggestionMap;
441 SuggestionMap suggestions;
443 // First try to parse the |suggested_key| as a dictionary.
444 const base::DictionaryValue* suggested_key_dict;
445 if (command->GetDictionary(keys::kSuggestedKey, &suggested_key_dict)) {
446 for (base::DictionaryValue::Iterator iter(*suggested_key_dict);
447 !iter.IsAtEnd(); iter.Advance()) {
448 // For each item in the dictionary, extract the platforms specified.
449 std::string suggested_key_string;
450 if (iter.value().GetAsString(&suggested_key_string) &&
451 !suggested_key_string.empty()) {
452 // Found a platform, add it to the suggestions list.
453 suggestions[iter.key()] = suggested_key_string;
455 *error = ErrorUtils::FormatErrorMessageUTF16(
456 errors::kInvalidKeyBinding,
457 base::IntToString(index),
464 // No dictionary was found, fall back to using just a string, so developers
465 // don't have to specify a dictionary if they just want to use one default
466 // for all platforms.
467 std::string suggested_key_string;
468 if (command->GetString(keys::kSuggestedKey, &suggested_key_string) &&
469 !suggested_key_string.empty()) {
470 // If only a single string is provided, it must be default for all.
471 suggestions[values::kKeybindingPlatformDefault] = suggested_key_string;
473 suggestions[values::kKeybindingPlatformDefault] = "";
477 // Check if this is a global or a regular shortcut.
479 command->GetBoolean(keys::kGlobal, &global);
481 // Normalize the suggestions.
482 for (SuggestionMap::iterator iter = suggestions.begin();
483 iter != suggestions.end(); ++iter) {
484 // Before we normalize Ctrl to Command we must detect when the developer
485 // specified Command in the Default section, which will work on Mac after
486 // normalization but only fail on other platforms when they try it out on
487 // other platforms, which is not what we want.
488 if (iter->first == values::kKeybindingPlatformDefault &&
489 iter->second.find("Command+") != std::string::npos) {
490 *error = ErrorUtils::FormatErrorMessageUTF16(
491 errors::kInvalidKeyBinding,
492 base::IntToString(index),
494 kCommandKeyNotSupported);
498 suggestions[iter->first] = NormalizeShortcutSuggestion(iter->second,
502 std::string platform = CommandPlatform();
503 std::string key = platform;
504 if (suggestions.find(key) == suggestions.end())
505 key = values::kKeybindingPlatformDefault;
506 if (suggestions.find(key) == suggestions.end()) {
507 *error = ErrorUtils::FormatErrorMessageUTF16(
508 errors::kInvalidKeyBindingMissingPlatform,
509 base::IntToString(index),
512 return false; // No platform specified and no fallback. Bail.
515 // For developer convenience, we parse all the suggestions (and complain about
516 // errors for platforms other than the current one) but use only what we need.
517 std::map<const std::string, std::string>::const_iterator iter =
519 for ( ; iter != suggestions.end(); ++iter) {
520 ui::Accelerator accelerator;
521 if (!iter->second.empty()) {
522 // Note that we pass iter->first to pretend we are on a platform we're not
524 accelerator = ParseImpl(iter->second, iter->first, index,
525 IsNamedCommand(command_name), error);
526 if (accelerator.key_code() == ui::VKEY_UNKNOWN) {
527 if (error->empty()) {
528 *error = ErrorUtils::FormatErrorMessageUTF16(
529 errors::kInvalidKeyBinding,
530 base::IntToString(index),
538 if (iter->first == key) {
539 // This platform is our platform, so grab this key.
540 accelerator_ = accelerator;
541 command_name_ = command_name;
542 description_ = description;
549 base::DictionaryValue* Command::ToValue(const Extension* extension,
551 base::DictionaryValue* extension_data = new base::DictionaryValue();
553 base::string16 command_description;
554 bool extension_action = false;
555 if (command_name() == values::kBrowserActionCommandEvent ||
556 command_name() == values::kPageActionCommandEvent) {
557 command_description =
558 l10n_util::GetStringUTF16(IDS_EXTENSION_COMMANDS_GENERIC_ACTIVATE);
559 extension_action = true;
561 command_description = description();
563 extension_data->SetString("description", command_description);
564 extension_data->SetBoolean("active", active);
565 extension_data->SetString("keybinding", accelerator().GetShortcutText());
566 extension_data->SetString("command_name", command_name());
567 extension_data->SetString("extension_id", extension->id());
568 extension_data->SetBoolean("global", global());
569 extension_data->SetBoolean("extension_action", extension_action);
570 return extension_data;
573 } // namespace extensions