command.cc revision 5821806d5e7f356e8fa4b058a389a808ea183019
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/string_number_conversions.h"
9#include "base/string_split.h"
10#include "base/string_util.h"
11#include "base/values.h"
12#include "chrome/common/extensions/extension.h"
13#include "chrome/common/extensions/extension_error_utils.h"
14#include "chrome/common/extensions/extension_manifest_constants.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
22namespace {
23
24static const char kMissing[] = "Missing";
25
26static const char kCommandKeyNotSupported[] =
27    "Command key is not supported. Note: Ctrl means Command on Mac";
28
29
30ui::Accelerator ParseImpl(const std::string& accelerator,
31                          const std::string& platform_key,
32                          int index,
33                          string16* error) {
34  if (platform_key != values::kKeybindingPlatformWin &&
35      platform_key != values::kKeybindingPlatformMac &&
36      platform_key != values::kKeybindingPlatformChromeOs &&
37      platform_key != values::kKeybindingPlatformLinux &&
38      platform_key != values::kKeybindingPlatformDefault) {
39    *error = ExtensionErrorUtils::FormatErrorMessageUTF16(
40        errors::kInvalidKeyBindingUnknownPlatform,
41        base::IntToString(index),
42        platform_key);
43    return ui::Accelerator();
44  }
45
46  std::vector<std::string> tokens;
47  base::SplitString(accelerator, '+', &tokens);
48  if (tokens.size() < 2 || tokens.size() > 3) {
49    *error = ExtensionErrorUtils::FormatErrorMessageUTF16(
50        errors::kInvalidKeyBinding,
51        base::IntToString(index),
52        platform_key,
53        accelerator);
54    return ui::Accelerator();
55  }
56
57  // Now, parse it into an accelerator.
58  int modifiers = ui::EF_NONE;
59  ui::KeyboardCode key = ui::VKEY_UNKNOWN;
60  for (size_t i = 0; i < tokens.size(); i++) {
61    if (tokens[i] == "Ctrl") {
62      modifiers |= ui::EF_CONTROL_DOWN;
63    } else if (tokens[i] == "Command") {
64      if (platform_key == "mac") {
65        // Either the developer specified Command+foo in the manifest for Mac or
66        // they specified Ctrl and it got normalized to Command (to get Ctrl on
67        // Mac the developer has to specify MacCtrl). Therefore we treat this
68        // as Command.
69        modifiers |= ui::EF_COMMAND_DOWN;
70#if defined(OS_MACOSX)
71      } else if (platform_key == "default") {
72        // If we see "Command+foo" in the Default section it can mean two
73        // things, depending on the platform:
74        // The developer specified "Ctrl+foo" for Default and it got normalized
75        // on Mac to "Command+foo". This is fine. Treat it as Command.
76        modifiers |= ui::EF_COMMAND_DOWN;
77#endif
78      } else {
79        // No other platform supports Command.
80        key = ui::VKEY_UNKNOWN;
81        break;
82      }
83    } else if (tokens[i] == "Alt") {
84      modifiers |= ui::EF_ALT_DOWN;
85    } else if (tokens[i] == "Shift") {
86      modifiers |= ui::EF_SHIFT_DOWN;
87    } else if (tokens[i].size() == 1) {
88      if (key != ui::VKEY_UNKNOWN) {
89        // Multiple key assignments.
90        key = ui::VKEY_UNKNOWN;
91        break;
92      }
93      if (tokens[i][0] >= 'A' && tokens[i][0] <= 'Z') {
94        key = static_cast<ui::KeyboardCode>(ui::VKEY_A + (tokens[i][0] - 'A'));
95      } else if (tokens[i][0] >= '0' && tokens[i][0] <= '9') {
96        key = static_cast<ui::KeyboardCode>(ui::VKEY_0 + (tokens[i][0] - '0'));
97      } else {
98        key = ui::VKEY_UNKNOWN;
99        break;
100      }
101    } else {
102      *error = ExtensionErrorUtils::FormatErrorMessageUTF16(
103          errors::kInvalidKeyBinding,
104          base::IntToString(index),
105          platform_key,
106          accelerator);
107      return ui::Accelerator();
108    }
109  }
110  bool command = (modifiers & ui::EF_COMMAND_DOWN) != 0;
111  bool ctrl = (modifiers & ui::EF_CONTROL_DOWN) != 0;
112  bool alt = (modifiers & ui::EF_ALT_DOWN) != 0;
113  bool shift = (modifiers & ui::EF_SHIFT_DOWN) != 0;
114
115  // We support Ctrl+foo, Alt+foo, Ctrl+Shift+foo, Alt+Shift+foo, but not
116  // Ctrl+Alt+foo and not Shift+foo either. For a more detailed reason why we
117  // don't support Ctrl+Alt+foo see this article:
118  // http://blogs.msdn.com/b/oldnewthing/archive/2004/03/29/101121.aspx.
119  // On Mac Command can also be used in combination with Shift or on its own,
120  // as a modifier.
121  if (key == ui::VKEY_UNKNOWN || (ctrl && alt) || (command && alt) ||
122      (shift && !ctrl && !alt && !command)) {
123    *error = ExtensionErrorUtils::FormatErrorMessageUTF16(
124        errors::kInvalidKeyBinding,
125        base::IntToString(index),
126        platform_key,
127        accelerator);
128    return ui::Accelerator();
129  }
130
131  return ui::Accelerator(key, modifiers);
132}
133
134// For Mac, we convert "Ctrl" to "Command" and "MacCtrl" to "Ctrl". Other
135// platforms leave the shortcut untouched.
136std::string NormalizeShortcutSuggestion(const std::string& suggestion,
137                                        const std::string& platform) {
138  bool normalize = false;
139  if (platform == "mac") {
140    normalize = true;
141  } else if (platform == "default") {
142#if defined(OS_MACOSX)
143    normalize = true;
144#endif
145  }
146
147  if (!normalize)
148    return suggestion;
149
150  std::string key;
151  std::vector<std::string> tokens;
152  base::SplitString(suggestion, '+', &tokens);
153  for (size_t i = 0; i < tokens.size(); i++) {
154    if (tokens[i] == "Ctrl")
155      tokens[i] = "Command";
156    else if (tokens[i] == "MacCtrl")
157      tokens[i] = "Ctrl";
158  }
159  return JoinString(tokens, '+');
160}
161
162}  // namespace
163
164namespace extensions {
165
166Command::Command() {}
167
168Command::Command(const std::string& command_name,
169                 const string16& description,
170                 const std::string& accelerator)
171    : command_name_(command_name),
172      description_(description) {
173  string16 error;
174  accelerator_ = ParseImpl(accelerator, CommandPlatform(), 0, &error);
175}
176
177Command::~Command() {}
178
179// static
180std::string Command::CommandPlatform() {
181#if defined(OS_WIN)
182  return values::kKeybindingPlatformWin;
183#elif defined(OS_MACOSX)
184  return values::kKeybindingPlatformMac;
185#elif defined(OS_CHROMEOS)
186  return values::kKeybindingPlatformChromeOs;
187#elif defined(OS_LINUX)
188  return values::kKeybindingPlatformLinux;
189#else
190  return "";
191#endif
192}
193
194// static
195ui::Accelerator Command::StringToAccelerator(const std::string& accelerator) {
196  string16 error;
197  Command command;
198  ui::Accelerator parsed =
199      ParseImpl(accelerator, Command::CommandPlatform(), 0, &error);
200  return parsed;
201}
202
203bool Command::Parse(DictionaryValue* command,
204                    const std::string& command_name,
205                    int index,
206                    string16* error) {
207  DCHECK(!command_name.empty());
208
209  // We'll build up a map of platform-to-shortcut suggestions.
210  typedef std::map<const std::string, std::string> SuggestionMap;
211  SuggestionMap suggestions;
212
213  // First try to parse the |suggested_key| as a dictionary.
214  DictionaryValue* suggested_key_dict;
215  if (command->GetDictionary(keys::kSuggestedKey, &suggested_key_dict)) {
216    DictionaryValue::key_iterator iter = suggested_key_dict->begin_keys();
217    for ( ; iter != suggested_key_dict->end_keys(); ++iter) {
218      // For each item in the dictionary, extract the platforms specified.
219      std::string suggested_key_string;
220      if (suggested_key_dict->GetString(*iter, &suggested_key_string) &&
221          !suggested_key_string.empty()) {
222        // Found a platform, add it to the suggestions list.
223        suggestions[*iter] = suggested_key_string;
224      } else {
225        *error = ExtensionErrorUtils::FormatErrorMessageUTF16(
226            errors::kInvalidKeyBinding,
227            base::IntToString(index),
228            keys::kSuggestedKey,
229            kMissing);
230        return false;
231      }
232    }
233  } else {
234    // No dictionary was found, fall back to using just a string, so developers
235    // don't have to specify a dictionary if they just want to use one default
236    // for all platforms.
237    std::string suggested_key_string;
238    if (command->GetString(keys::kSuggestedKey, &suggested_key_string) &&
239        !suggested_key_string.empty()) {
240      // If only a single string is provided, it must be default for all.
241      suggestions["default"] = suggested_key_string;
242    } else {
243      *error = ExtensionErrorUtils::FormatErrorMessageUTF16(
244          errors::kInvalidKeyBinding,
245          base::IntToString(index),
246          keys::kSuggestedKey,
247          kMissing);
248       return false;
249    }
250  }
251
252  // Normalize the suggestions.
253  for (SuggestionMap::iterator iter = suggestions.begin();
254       iter != suggestions.end(); ++iter) {
255    // Before we normalize Ctrl to Command we must detect when the developer
256    // specified Command in the Default section, which will work on Mac after
257    // normalization but only fail on other platforms when they try it out on
258    // other platforms, which is not what we want.
259    if (iter->first == "default" &&
260        iter->second.find("Command+") != std::string::npos) {
261      *error = ExtensionErrorUtils::FormatErrorMessageUTF16(
262          errors::kInvalidKeyBinding,
263          base::IntToString(index),
264          keys::kSuggestedKey,
265          kCommandKeyNotSupported);
266      return false;
267    }
268
269    suggestions[iter->first] = NormalizeShortcutSuggestion(iter->second,
270                                                           iter->first);
271  }
272
273  std::string platform = CommandPlatform();
274  std::string key = platform;
275  if (suggestions.find(key) == suggestions.end())
276    key = values::kKeybindingPlatformDefault;
277  if (suggestions.find(key) == suggestions.end()) {
278    *error = ExtensionErrorUtils::FormatErrorMessageUTF16(
279        errors::kInvalidKeyBindingMissingPlatform,
280        base::IntToString(index),
281        keys::kSuggestedKey,
282        platform);
283    return false;  // No platform specified and no fallback. Bail.
284  }
285
286  // For developer convenience, we parse all the suggestions (and complain about
287  // errors for platforms other than the current one) but use only what we need.
288  std::map<const std::string, std::string>::const_iterator iter =
289      suggestions.begin();
290  for ( ; iter != suggestions.end(); ++iter) {
291    // Note that we pass iter->first to pretend we are on a platform we're not
292    // on.
293    ui::Accelerator accelerator =
294        ParseImpl(iter->second, iter->first, index, error);
295    if (accelerator.key_code() == ui::VKEY_UNKNOWN) {
296      *error = ExtensionErrorUtils::FormatErrorMessageUTF16(
297          errors::kInvalidKeyBinding,
298          base::IntToString(index),
299          iter->first,
300          iter->second);
301      return false;
302    }
303
304    if (iter->first == key) {
305      // This platform is our platform, so grab this key.
306      accelerator_ = accelerator;
307      command_name_ = command_name;
308
309      if (command_name !=
310              extension_manifest_values::kPageActionCommandEvent &&
311          command_name !=
312              extension_manifest_values::kBrowserActionCommandEvent &&
313          command_name !=
314              extension_manifest_values::kScriptBadgeCommandEvent) {
315        if (!command->GetString(keys::kDescription, &description_) ||
316            description_.empty()) {
317          *error = ExtensionErrorUtils::FormatErrorMessageUTF16(
318              errors::kInvalidKeyBindingDescription,
319              base::IntToString(index));
320          return false;
321        }
322      }
323    }
324  }
325  return true;
326}
327
328DictionaryValue* Command::ToValue(const Extension* extension,
329                                  bool active) const {
330  DictionaryValue* extension_data = new DictionaryValue();
331
332  string16 command_description;
333  if (command_name() == values::kBrowserActionCommandEvent ||
334      command_name() == values::kPageActionCommandEvent ||
335      command_name() == values::kScriptBadgeCommandEvent) {
336    command_description =
337        l10n_util::GetStringUTF16(IDS_EXTENSION_COMMANDS_GENERIC_ACTIVATE);
338  } else {
339    command_description = description();
340  }
341  extension_data->SetString("description", command_description);
342  extension_data->SetBoolean("active", active);
343  extension_data->SetString("keybinding", accelerator().GetShortcutText());
344  extension_data->SetString("command_name", command_name());
345  extension_data->SetString("extension_id", extension->id());
346
347  return extension_data;
348}
349
350}  // namespace extensions
351