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