• 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 "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