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 31 bool IsNamedCommand(const std::string& command_name) { 32 return command_name != values::kPageActionCommandEvent && 33 command_name != values::kBrowserActionCommandEvent; 34 } 35 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 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. 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 261 Command::Command() : global_(false) {} 262 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 275 Command::~Command() {} 276 277 // static 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 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 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 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 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 533 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