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
18 namespace extensions {
19
20 namespace errors = manifest_errors;
21 namespace keys = manifest_keys;
22 namespace values = manifest_values;
23
24 namespace {
25
26 static const char kMissing[] = "Missing";
27
28 static const char kCommandKeyNotSupported[] =
29 "Command key is not supported. Note: Ctrl means Command on Mac";
30
IsNamedCommand(const std::string & command_name)31 bool IsNamedCommand(const std::string& command_name) {
32 return command_name != values::kPageActionCommandEvent &&
33 command_name != values::kBrowserActionCommandEvent;
34 }
35
DoesRequireModifier(const std::string & accelerator)36 bool DoesRequireModifier(const std::string& accelerator) {
37 return accelerator != values::kKeyMediaNextTrack &&
38 accelerator != values::kKeyMediaPlayPause &&
39 accelerator != values::kKeyMediaPrevTrack &&
40 accelerator != values::kKeyMediaStop;
41 }
42
ParseImpl(const std::string & accelerator,const std::string & platform_key,int index,bool should_parse_media_keys,base::string16 * error)43 ui::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.
NormalizeShortcutSuggestion(const std::string & suggestion,const std::string & platform)234 std::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
Command()261 Command::Command() : global_(false) {}
262
Command(const std::string & command_name,const base::string16 & description,const std::string & accelerator,bool global)263 Command::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
~Command()275 Command::~Command() {}
276
277 // static
CommandPlatform()278 std::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
StringToAccelerator(const std::string & accelerator,const std::string & command_name)293 ui::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
AcceleratorToString(const ui::Accelerator & accelerator)303 std::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
IsMediaKey(const ui::Accelerator & accelerator)396 bool 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
Parse(const base::DictionaryValue * command,const std::string & command_name,int index,base::string16 * error)406 bool 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
ToValue(const Extension * extension,bool active) const533 base::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