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