• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
19 namespace extensions {
20 
21 namespace errors = manifest_errors;
22 namespace keys = manifest_keys;
23 namespace values = manifest_values;
24 
25 namespace {
26 
27 static const char kMissing[] = "Missing";
28 
29 static const char kCommandKeyNotSupported[] =
30     "Command key is not supported. Note: Ctrl means Command on Mac";
31 
IsNamedCommand(const std::string & command_name)32 bool IsNamedCommand(const std::string& command_name) {
33   return command_name != values::kPageActionCommandEvent &&
34          command_name != values::kBrowserActionCommandEvent;
35 }
36 
DoesRequireModifier(const std::string & accelerator)37 bool DoesRequireModifier(const std::string& accelerator) {
38   return accelerator != values::kKeyMediaNextTrack &&
39          accelerator != values::kKeyMediaPlayPause &&
40          accelerator != values::kKeyMediaPrevTrack &&
41          accelerator != values::kKeyMediaStop;
42 }
43 
ParseImpl(const std::string & accelerator,const std::string & platform_key,int index,bool should_parse_media_keys,base::string16 * error)44 ui::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.
NormalizeShortcutSuggestion(const std::string & suggestion,const std::string & platform)226 std::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 
Command()253 Command::Command() : global_(false) {}
254 
Command(const std::string & command_name,const base::string16 & description,const std::string & accelerator,bool global)255 Command::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 
~Command()267 Command::~Command() {}
268 
269 // static
CommandPlatform()270 std::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
StringToAccelerator(const std::string & accelerator,const std::string & command_name)285 ui::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
AcceleratorToString(const ui::Accelerator & accelerator)295 std::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
IsMediaKey(const ui::Accelerator & accelerator)383 bool 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 
Parse(const base::DictionaryValue * command,const std::string & command_name,int index,base::string16 * error)393 bool 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 
ToValue(const Extension * extension,bool active) const521 base::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