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/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 18 namespace errors = extension_manifest_errors; 19 namespace keys = extension_manifest_keys; 20 namespace values = extension_manifest_values; 21 22 using extensions::ErrorUtils; 23 using extensions::Command; 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 32 ui::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] == values::kKeyCtrl) { 64 modifiers |= ui::EF_CONTROL_DOWN; 65 } else if (tokens[i] == values::kKeyCommand) { 66 if (platform_key == values::kKeybindingPlatformMac) { 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 == values::kKeybindingPlatformDefault) { 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] == values::kKeyAlt) { 86 modifiers |= ui::EF_ALT_DOWN; 87 } else if (tokens[i] == values::kKeyShift) { 88 modifiers |= ui::EF_SHIFT_DOWN; 89 } else if (tokens[i].size() == 1 || // A-Z, 0-9. 90 tokens[i] == values::kKeyComma || 91 tokens[i] == values::kKeyPeriod || 92 tokens[i] == values::kKeyUp || 93 tokens[i] == values::kKeyDown || 94 tokens[i] == values::kKeyLeft || 95 tokens[i] == values::kKeyRight || 96 tokens[i] == values::kKeyIns || 97 tokens[i] == values::kKeyDel || 98 tokens[i] == values::kKeyHome || 99 tokens[i] == values::kKeyEnd || 100 tokens[i] == values::kKeyPgUp || 101 tokens[i] == values::kKeyPgDwn || 102 tokens[i] == values::kKeyTab) { 103 if (key != ui::VKEY_UNKNOWN) { 104 // Multiple key assignments. 105 key = ui::VKEY_UNKNOWN; 106 break; 107 } 108 109 if (tokens[i] == values::kKeyComma) { 110 key = ui::VKEY_OEM_COMMA; 111 } else if (tokens[i] == values::kKeyPeriod) { 112 key = ui::VKEY_OEM_PERIOD; 113 } else if (tokens[i] == values::kKeyUp) { 114 key = ui::VKEY_UP; 115 } else if (tokens[i] == values::kKeyDown) { 116 key = ui::VKEY_DOWN; 117 } else if (tokens[i] == values::kKeyLeft) { 118 key = ui::VKEY_LEFT; 119 } else if (tokens[i] == values::kKeyRight) { 120 key = ui::VKEY_RIGHT; 121 } else if (tokens[i] == values::kKeyIns) { 122 key = ui::VKEY_INSERT; 123 } else if (tokens[i] == values::kKeyDel) { 124 key = ui::VKEY_DELETE; 125 } else if (tokens[i] == values::kKeyHome) { 126 key = ui::VKEY_HOME; 127 } else if (tokens[i] == values::kKeyEnd) { 128 key = ui::VKEY_END; 129 } else if (tokens[i] == values::kKeyPgUp) { 130 key = ui::VKEY_PRIOR; 131 } else if (tokens[i] == values::kKeyPgDwn) { 132 key = ui::VKEY_NEXT; 133 } else if (tokens[i] == values::kKeyTab) { 134 key = ui::VKEY_TAB; 135 } else if (tokens[i].size() == 1 && 136 tokens[i][0] >= 'A' && tokens[i][0] <= 'Z') { 137 key = static_cast<ui::KeyboardCode>(ui::VKEY_A + (tokens[i][0] - 'A')); 138 } else if (tokens[i].size() == 1 && 139 tokens[i][0] >= '0' && tokens[i][0] <= '9') { 140 key = static_cast<ui::KeyboardCode>(ui::VKEY_0 + (tokens[i][0] - '0')); 141 } else { 142 key = ui::VKEY_UNKNOWN; 143 break; 144 } 145 } else { 146 *error = ErrorUtils::FormatErrorMessageUTF16( 147 errors::kInvalidKeyBinding, 148 base::IntToString(index), 149 platform_key, 150 accelerator); 151 return ui::Accelerator(); 152 } 153 } 154 bool command = (modifiers & ui::EF_COMMAND_DOWN) != 0; 155 bool ctrl = (modifiers & ui::EF_CONTROL_DOWN) != 0; 156 bool alt = (modifiers & ui::EF_ALT_DOWN) != 0; 157 bool shift = (modifiers & ui::EF_SHIFT_DOWN) != 0; 158 159 // We support Ctrl+foo, Alt+foo, Ctrl+Shift+foo, Alt+Shift+foo, but not 160 // Ctrl+Alt+foo and not Shift+foo either. For a more detailed reason why we 161 // don't support Ctrl+Alt+foo see this article: 162 // http://blogs.msdn.com/b/oldnewthing/archive/2004/03/29/101121.aspx. 163 // On Mac Command can also be used in combination with Shift or on its own, 164 // as a modifier. 165 if (key == ui::VKEY_UNKNOWN || (ctrl && alt) || (command && alt) || 166 (shift && !ctrl && !alt && !command)) { 167 *error = ErrorUtils::FormatErrorMessageUTF16( 168 errors::kInvalidKeyBinding, 169 base::IntToString(index), 170 platform_key, 171 accelerator); 172 return ui::Accelerator(); 173 } 174 175 return ui::Accelerator(key, modifiers); 176 } 177 178 // For Mac, we convert "Ctrl" to "Command" and "MacCtrl" to "Ctrl". Other 179 // platforms leave the shortcut untouched. 180 std::string NormalizeShortcutSuggestion(const std::string& suggestion, 181 const std::string& platform) { 182 bool normalize = false; 183 if (platform == values::kKeybindingPlatformMac) { 184 normalize = true; 185 } else if (platform == values::kKeybindingPlatformDefault) { 186 #if defined(OS_MACOSX) 187 normalize = true; 188 #endif 189 } 190 191 if (!normalize) 192 return suggestion; 193 194 std::vector<std::string> tokens; 195 base::SplitString(suggestion, '+', &tokens); 196 for (size_t i = 0; i < tokens.size(); i++) { 197 if (tokens[i] == values::kKeyCtrl) 198 tokens[i] = values::kKeyCommand; 199 else if (tokens[i] == values::kKeyMacCtrl) 200 tokens[i] = values::kKeyCtrl; 201 } 202 return JoinString(tokens, '+'); 203 } 204 205 } // namespace 206 207 namespace extensions { 208 209 Command::Command() {} 210 211 Command::Command(const std::string& command_name, 212 const string16& description, 213 const std::string& accelerator) 214 : command_name_(command_name), 215 description_(description) { 216 string16 error; 217 accelerator_ = ParseImpl(accelerator, CommandPlatform(), 0, &error); 218 } 219 220 Command::~Command() {} 221 222 // static 223 std::string Command::CommandPlatform() { 224 #if defined(OS_WIN) 225 return values::kKeybindingPlatformWin; 226 #elif defined(OS_MACOSX) 227 return values::kKeybindingPlatformMac; 228 #elif defined(OS_CHROMEOS) 229 return values::kKeybindingPlatformChromeOs; 230 #elif defined(OS_LINUX) 231 return values::kKeybindingPlatformLinux; 232 #else 233 return ""; 234 #endif 235 } 236 237 // static 238 ui::Accelerator Command::StringToAccelerator(const std::string& accelerator) { 239 string16 error; 240 Command command; 241 ui::Accelerator parsed = 242 ParseImpl(accelerator, Command::CommandPlatform(), 0, &error); 243 return parsed; 244 } 245 246 // static 247 std::string Command::AcceleratorToString(const ui::Accelerator& accelerator) { 248 std::string shortcut; 249 250 // Ctrl and Alt are mutually exclusive. 251 if (accelerator.IsCtrlDown()) 252 shortcut += values::kKeyCtrl; 253 else if (accelerator.IsAltDown()) 254 shortcut += values::kKeyAlt; 255 if (!shortcut.empty()) 256 shortcut += values::kKeySeparator; 257 258 if (accelerator.IsCmdDown()) { 259 shortcut += values::kKeyCommand; 260 shortcut += values::kKeySeparator; 261 } 262 263 if (accelerator.IsShiftDown()) { 264 shortcut += values::kKeyShift; 265 shortcut += values::kKeySeparator; 266 } 267 268 if (accelerator.key_code() >= ui::VKEY_0 && 269 accelerator.key_code() <= ui::VKEY_9) { 270 shortcut += '0' + (accelerator.key_code() - ui::VKEY_0); 271 } else if (accelerator.key_code() >= ui::VKEY_A && 272 accelerator.key_code() <= ui::VKEY_Z) { 273 shortcut += 'A' + (accelerator.key_code() - ui::VKEY_A); 274 } else { 275 switch (accelerator.key_code()) { 276 case ui::VKEY_OEM_COMMA: 277 shortcut += values::kKeyComma; 278 break; 279 case ui::VKEY_OEM_PERIOD: 280 shortcut += values::kKeyPeriod; 281 break; 282 case ui::VKEY_UP: 283 shortcut += values::kKeyUp; 284 break; 285 case ui::VKEY_DOWN: 286 shortcut += values::kKeyDown; 287 break; 288 case ui::VKEY_LEFT: 289 shortcut += values::kKeyLeft; 290 break; 291 case ui::VKEY_RIGHT: 292 shortcut += values::kKeyRight; 293 break; 294 case ui::VKEY_INSERT: 295 shortcut += values::kKeyIns; 296 break; 297 case ui::VKEY_DELETE: 298 shortcut += values::kKeyDel; 299 break; 300 case ui::VKEY_HOME: 301 shortcut += values::kKeyHome; 302 break; 303 case ui::VKEY_END: 304 shortcut += values::kKeyEnd; 305 break; 306 case ui::VKEY_PRIOR: 307 shortcut += values::kKeyPgUp; 308 break; 309 case ui::VKEY_NEXT: 310 shortcut += values::kKeyPgDwn; 311 break; 312 case ui::VKEY_TAB: 313 shortcut += values::kKeyTab; 314 break; 315 default: 316 return ""; 317 } 318 } 319 return shortcut; 320 } 321 322 bool Command::Parse(const base::DictionaryValue* command, 323 const std::string& command_name, 324 int index, 325 string16* error) { 326 DCHECK(!command_name.empty()); 327 328 string16 description; 329 if (command_name != values::kPageActionCommandEvent && 330 command_name != values::kBrowserActionCommandEvent && 331 command_name != values::kScriptBadgeCommandEvent) { 332 if (!command->GetString(keys::kDescription, &description) || 333 description.empty()) { 334 *error = ErrorUtils::FormatErrorMessageUTF16( 335 errors::kInvalidKeyBindingDescription, 336 base::IntToString(index)); 337 return false; 338 } 339 } 340 341 // We'll build up a map of platform-to-shortcut suggestions. 342 typedef std::map<const std::string, std::string> SuggestionMap; 343 SuggestionMap suggestions; 344 345 // First try to parse the |suggested_key| as a dictionary. 346 const base::DictionaryValue* suggested_key_dict; 347 if (command->GetDictionary(keys::kSuggestedKey, &suggested_key_dict)) { 348 for (base::DictionaryValue::Iterator iter(*suggested_key_dict); 349 !iter.IsAtEnd(); iter.Advance()) { 350 // For each item in the dictionary, extract the platforms specified. 351 std::string suggested_key_string; 352 if (iter.value().GetAsString(&suggested_key_string) && 353 !suggested_key_string.empty()) { 354 // Found a platform, add it to the suggestions list. 355 suggestions[iter.key()] = suggested_key_string; 356 } else { 357 *error = ErrorUtils::FormatErrorMessageUTF16( 358 errors::kInvalidKeyBinding, 359 base::IntToString(index), 360 keys::kSuggestedKey, 361 kMissing); 362 return false; 363 } 364 } 365 } else { 366 // No dictionary was found, fall back to using just a string, so developers 367 // don't have to specify a dictionary if they just want to use one default 368 // for all platforms. 369 std::string suggested_key_string; 370 if (command->GetString(keys::kSuggestedKey, &suggested_key_string) && 371 !suggested_key_string.empty()) { 372 // If only a single string is provided, it must be default for all. 373 suggestions[values::kKeybindingPlatformDefault] = suggested_key_string; 374 } else { 375 suggestions[values::kKeybindingPlatformDefault] = ""; 376 } 377 } 378 379 // Normalize the suggestions. 380 for (SuggestionMap::iterator iter = suggestions.begin(); 381 iter != suggestions.end(); ++iter) { 382 // Before we normalize Ctrl to Command we must detect when the developer 383 // specified Command in the Default section, which will work on Mac after 384 // normalization but only fail on other platforms when they try it out on 385 // other platforms, which is not what we want. 386 if (iter->first == values::kKeybindingPlatformDefault && 387 iter->second.find("Command+") != std::string::npos) { 388 *error = ErrorUtils::FormatErrorMessageUTF16( 389 errors::kInvalidKeyBinding, 390 base::IntToString(index), 391 keys::kSuggestedKey, 392 kCommandKeyNotSupported); 393 return false; 394 } 395 396 suggestions[iter->first] = NormalizeShortcutSuggestion(iter->second, 397 iter->first); 398 } 399 400 std::string platform = CommandPlatform(); 401 std::string key = platform; 402 if (suggestions.find(key) == suggestions.end()) 403 key = values::kKeybindingPlatformDefault; 404 if (suggestions.find(key) == suggestions.end()) { 405 *error = ErrorUtils::FormatErrorMessageUTF16( 406 errors::kInvalidKeyBindingMissingPlatform, 407 base::IntToString(index), 408 keys::kSuggestedKey, 409 platform); 410 return false; // No platform specified and no fallback. Bail. 411 } 412 413 // For developer convenience, we parse all the suggestions (and complain about 414 // errors for platforms other than the current one) but use only what we need. 415 std::map<const std::string, std::string>::const_iterator iter = 416 suggestions.begin(); 417 for ( ; iter != suggestions.end(); ++iter) { 418 ui::Accelerator accelerator; 419 if (!iter->second.empty()) { 420 // Note that we pass iter->first to pretend we are on a platform we're not 421 // on. 422 accelerator = ParseImpl(iter->second, iter->first, index, error); 423 if (accelerator.key_code() == ui::VKEY_UNKNOWN) { 424 *error = ErrorUtils::FormatErrorMessageUTF16( 425 errors::kInvalidKeyBinding, 426 base::IntToString(index), 427 iter->first, 428 iter->second); 429 return false; 430 } 431 } 432 433 if (iter->first == key) { 434 // This platform is our platform, so grab this key. 435 accelerator_ = accelerator; 436 command_name_ = command_name; 437 description_ = description; 438 } 439 } 440 return true; 441 } 442 443 base::DictionaryValue* Command::ToValue(const Extension* extension, 444 bool active) const { 445 base::DictionaryValue* extension_data = new base::DictionaryValue(); 446 447 string16 command_description; 448 if (command_name() == values::kBrowserActionCommandEvent || 449 command_name() == values::kPageActionCommandEvent || 450 command_name() == values::kScriptBadgeCommandEvent) { 451 command_description = 452 l10n_util::GetStringUTF16(IDS_EXTENSION_COMMANDS_GENERIC_ACTIVATE); 453 } else { 454 command_description = description(); 455 } 456 extension_data->SetString("description", command_description); 457 extension_data->SetBoolean("active", active); 458 extension_data->SetString("keybinding", accelerator().GetShortcutText()); 459 extension_data->SetString("command_name", command_name()); 460 extension_data->SetString("extension_id", extension->id()); 461 462 return extension_data; 463 } 464 465 } // namespace extensions 466