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 "extensions/common/error_utils.h" 13 #include "extensions/common/extension.h" 14 #include "extensions/common/feature_switch.h" 15 #include "extensions/common/manifest_constants.h" 16 #include "grit/generated_resources.h" 17 #include "ui/base/l10n/l10n_util.h" 18 19 namespace extensions { 20 21 namespace errors = manifest_errors; 22 namespace keys = manifest_keys; 23 namespace values = manifest_values; 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 bool IsNamedCommand(const std::string& command_name) { 33 return command_name != values::kPageActionCommandEvent && 34 command_name != values::kBrowserActionCommandEvent; 35 } 36 37 bool DoesRequireModifier(const std::string& accelerator) { 38 return accelerator != values::kKeyMediaNextTrack && 39 accelerator != values::kKeyMediaPlayPause && 40 accelerator != values::kKeyMediaPrevTrack && 41 accelerator != values::kKeyMediaStop; 42 } 43 44 ui::Accelerator ParseImpl(const std::string& accelerator, 45 const std::string& platform_key, 46 int index, 47 bool should_parse_media_keys, 48 base::string16* error) { 49 error->clear(); 50 if (platform_key != values::kKeybindingPlatformWin && 51 platform_key != values::kKeybindingPlatformMac && 52 platform_key != values::kKeybindingPlatformChromeOs && 53 platform_key != values::kKeybindingPlatformLinux && 54 platform_key != values::kKeybindingPlatformDefault) { 55 *error = ErrorUtils::FormatErrorMessageUTF16( 56 errors::kInvalidKeyBindingUnknownPlatform, 57 base::IntToString(index), 58 platform_key); 59 return ui::Accelerator(); 60 } 61 62 std::vector<std::string> tokens; 63 base::SplitString(accelerator, '+', &tokens); 64 if (tokens.size() == 0 || 65 (tokens.size() == 1 && DoesRequireModifier(accelerator)) || 66 tokens.size() > 3) { 67 *error = ErrorUtils::FormatErrorMessageUTF16( 68 errors::kInvalidKeyBinding, 69 base::IntToString(index), 70 platform_key, 71 accelerator); 72 return ui::Accelerator(); 73 } 74 75 // Now, parse it into an accelerator. 76 int modifiers = ui::EF_NONE; 77 ui::KeyboardCode key = ui::VKEY_UNKNOWN; 78 for (size_t i = 0; i < tokens.size(); i++) { 79 if (tokens[i] == values::kKeyCtrl) { 80 modifiers |= ui::EF_CONTROL_DOWN; 81 } else if (tokens[i] == values::kKeyCommand) { 82 if (platform_key == values::kKeybindingPlatformMac) { 83 // Either the developer specified Command+foo in the manifest for Mac or 84 // they specified Ctrl and it got normalized to Command (to get Ctrl on 85 // Mac the developer has to specify MacCtrl). Therefore we treat this 86 // as Command. 87 modifiers |= ui::EF_COMMAND_DOWN; 88 #if defined(OS_MACOSX) 89 } else if (platform_key == values::kKeybindingPlatformDefault) { 90 // If we see "Command+foo" in the Default section it can mean two 91 // things, depending on the platform: 92 // The developer specified "Ctrl+foo" for Default and it got normalized 93 // on Mac to "Command+foo". This is fine. Treat it as Command. 94 modifiers |= ui::EF_COMMAND_DOWN; 95 #endif 96 } else { 97 // No other platform supports Command. 98 key = ui::VKEY_UNKNOWN; 99 break; 100 } 101 } else if (tokens[i] == values::kKeyAlt) { 102 modifiers |= ui::EF_ALT_DOWN; 103 } else if (tokens[i] == values::kKeyShift) { 104 modifiers |= ui::EF_SHIFT_DOWN; 105 } else if (tokens[i].size() == 1 || // A-Z, 0-9. 106 tokens[i] == values::kKeyComma || 107 tokens[i] == values::kKeyPeriod || 108 tokens[i] == values::kKeyUp || 109 tokens[i] == values::kKeyDown || 110 tokens[i] == values::kKeyLeft || 111 tokens[i] == values::kKeyRight || 112 tokens[i] == values::kKeyIns || 113 tokens[i] == values::kKeyDel || 114 tokens[i] == values::kKeyHome || 115 tokens[i] == values::kKeyEnd || 116 tokens[i] == values::kKeyPgUp || 117 tokens[i] == values::kKeyPgDwn || 118 tokens[i] == values::kKeyTab || 119 tokens[i] == values::kKeyMediaNextTrack || 120 tokens[i] == values::kKeyMediaPlayPause || 121 tokens[i] == values::kKeyMediaPrevTrack || 122 tokens[i] == values::kKeyMediaStop) { 123 if (key != ui::VKEY_UNKNOWN) { 124 // Multiple key assignments. 125 key = ui::VKEY_UNKNOWN; 126 break; 127 } 128 129 if (tokens[i] == values::kKeyComma) { 130 key = ui::VKEY_OEM_COMMA; 131 } else if (tokens[i] == values::kKeyPeriod) { 132 key = ui::VKEY_OEM_PERIOD; 133 } else if (tokens[i] == values::kKeyUp) { 134 key = ui::VKEY_UP; 135 } else if (tokens[i] == values::kKeyDown) { 136 key = ui::VKEY_DOWN; 137 } else if (tokens[i] == values::kKeyLeft) { 138 key = ui::VKEY_LEFT; 139 } else if (tokens[i] == values::kKeyRight) { 140 key = ui::VKEY_RIGHT; 141 } else if (tokens[i] == values::kKeyIns) { 142 key = ui::VKEY_INSERT; 143 } else if (tokens[i] == values::kKeyDel) { 144 key = ui::VKEY_DELETE; 145 } else if (tokens[i] == values::kKeyHome) { 146 key = ui::VKEY_HOME; 147 } else if (tokens[i] == values::kKeyEnd) { 148 key = ui::VKEY_END; 149 } else if (tokens[i] == values::kKeyPgUp) { 150 key = ui::VKEY_PRIOR; 151 } else if (tokens[i] == values::kKeyPgDwn) { 152 key = ui::VKEY_NEXT; 153 } else if (tokens[i] == values::kKeyTab) { 154 key = ui::VKEY_TAB; 155 } else if (tokens[i] == values::kKeyMediaNextTrack && 156 should_parse_media_keys) { 157 key = ui::VKEY_MEDIA_NEXT_TRACK; 158 } else if (tokens[i] == values::kKeyMediaPlayPause && 159 should_parse_media_keys) { 160 key = ui::VKEY_MEDIA_PLAY_PAUSE; 161 } else if (tokens[i] == values::kKeyMediaPrevTrack && 162 should_parse_media_keys) { 163 key = ui::VKEY_MEDIA_PREV_TRACK; 164 } else if (tokens[i] == values::kKeyMediaStop && 165 should_parse_media_keys) { 166 key = ui::VKEY_MEDIA_STOP; 167 } else if (tokens[i].size() == 1 && 168 tokens[i][0] >= 'A' && tokens[i][0] <= 'Z') { 169 key = static_cast<ui::KeyboardCode>(ui::VKEY_A + (tokens[i][0] - 'A')); 170 } else if (tokens[i].size() == 1 && 171 tokens[i][0] >= '0' && tokens[i][0] <= '9') { 172 key = static_cast<ui::KeyboardCode>(ui::VKEY_0 + (tokens[i][0] - '0')); 173 } else { 174 key = ui::VKEY_UNKNOWN; 175 break; 176 } 177 } else { 178 *error = ErrorUtils::FormatErrorMessageUTF16( 179 errors::kInvalidKeyBinding, 180 base::IntToString(index), 181 platform_key, 182 accelerator); 183 return ui::Accelerator(); 184 } 185 } 186 187 bool command = (modifiers & ui::EF_COMMAND_DOWN) != 0; 188 bool ctrl = (modifiers & ui::EF_CONTROL_DOWN) != 0; 189 bool alt = (modifiers & ui::EF_ALT_DOWN) != 0; 190 bool shift = (modifiers & ui::EF_SHIFT_DOWN) != 0; 191 192 // We support Ctrl+foo, Alt+foo, Ctrl+Shift+foo, Alt+Shift+foo, but not 193 // Ctrl+Alt+foo and not Shift+foo either. For a more detailed reason why we 194 // don't support Ctrl+Alt+foo see this article: 195 // http://blogs.msdn.com/b/oldnewthing/archive/2004/03/29/101121.aspx. 196 // On Mac Command can also be used in combination with Shift or on its own, 197 // as a modifier. 198 if (key == ui::VKEY_UNKNOWN || (ctrl && alt) || (command && alt) || 199 (shift && !ctrl && !alt && !command)) { 200 *error = ErrorUtils::FormatErrorMessageUTF16( 201 errors::kInvalidKeyBinding, 202 base::IntToString(index), 203 platform_key, 204 accelerator); 205 return ui::Accelerator(); 206 } 207 208 if ((key == ui::VKEY_MEDIA_NEXT_TRACK || 209 key == ui::VKEY_MEDIA_PREV_TRACK || 210 key == ui::VKEY_MEDIA_PLAY_PAUSE || 211 key == ui::VKEY_MEDIA_STOP) && 212 (shift || ctrl || alt || command)) { 213 *error = ErrorUtils::FormatErrorMessageUTF16( 214 errors::kInvalidKeyBindingMediaKeyWithModifier, 215 base::IntToString(index), 216 platform_key, 217 accelerator); 218 return ui::Accelerator(); 219 } 220 221 return ui::Accelerator(key, modifiers); 222 } 223 224 // For Mac, we convert "Ctrl" to "Command" and "MacCtrl" to "Ctrl". Other 225 // platforms leave the shortcut untouched. 226 std::string NormalizeShortcutSuggestion(const std::string& suggestion, 227 const std::string& platform) { 228 bool normalize = false; 229 if (platform == values::kKeybindingPlatformMac) { 230 normalize = true; 231 } else if (platform == values::kKeybindingPlatformDefault) { 232 #if defined(OS_MACOSX) 233 normalize = true; 234 #endif 235 } 236 237 if (!normalize) 238 return suggestion; 239 240 std::vector<std::string> tokens; 241 base::SplitString(suggestion, '+', &tokens); 242 for (size_t i = 0; i < tokens.size(); i++) { 243 if (tokens[i] == values::kKeyCtrl) 244 tokens[i] = values::kKeyCommand; 245 else if (tokens[i] == values::kKeyMacCtrl) 246 tokens[i] = values::kKeyCtrl; 247 } 248 return JoinString(tokens, '+'); 249 } 250 251 } // namespace 252 253 Command::Command() : global_(false) {} 254 255 Command::Command(const std::string& command_name, 256 const base::string16& description, 257 const std::string& accelerator, 258 bool global) 259 : command_name_(command_name), 260 description_(description), 261 global_(global) { 262 base::string16 error; 263 accelerator_ = ParseImpl(accelerator, CommandPlatform(), 0, 264 IsNamedCommand(command_name), &error); 265 } 266 267 Command::~Command() {} 268 269 // static 270 std::string Command::CommandPlatform() { 271 #if defined(OS_WIN) 272 return values::kKeybindingPlatformWin; 273 #elif defined(OS_MACOSX) 274 return values::kKeybindingPlatformMac; 275 #elif defined(OS_CHROMEOS) 276 return values::kKeybindingPlatformChromeOs; 277 #elif defined(OS_LINUX) 278 return values::kKeybindingPlatformLinux; 279 #else 280 return ""; 281 #endif 282 } 283 284 // static 285 ui::Accelerator Command::StringToAccelerator(const std::string& accelerator, 286 const std::string& command_name) { 287 base::string16 error; 288 ui::Accelerator parsed = 289 ParseImpl(accelerator, Command::CommandPlatform(), 0, 290 IsNamedCommand(command_name), &error); 291 return parsed; 292 } 293 294 // static 295 std::string Command::AcceleratorToString(const ui::Accelerator& accelerator) { 296 std::string shortcut; 297 298 // Ctrl and Alt are mutually exclusive. 299 if (accelerator.IsCtrlDown()) 300 shortcut += values::kKeyCtrl; 301 else if (accelerator.IsAltDown()) 302 shortcut += values::kKeyAlt; 303 if (!shortcut.empty()) 304 shortcut += values::kKeySeparator; 305 306 if (accelerator.IsCmdDown()) { 307 shortcut += values::kKeyCommand; 308 shortcut += values::kKeySeparator; 309 } 310 311 if (accelerator.IsShiftDown()) { 312 shortcut += values::kKeyShift; 313 shortcut += values::kKeySeparator; 314 } 315 316 if (accelerator.key_code() >= ui::VKEY_0 && 317 accelerator.key_code() <= ui::VKEY_9) { 318 shortcut += '0' + (accelerator.key_code() - ui::VKEY_0); 319 } else if (accelerator.key_code() >= ui::VKEY_A && 320 accelerator.key_code() <= ui::VKEY_Z) { 321 shortcut += 'A' + (accelerator.key_code() - ui::VKEY_A); 322 } else { 323 switch (accelerator.key_code()) { 324 case ui::VKEY_OEM_COMMA: 325 shortcut += values::kKeyComma; 326 break; 327 case ui::VKEY_OEM_PERIOD: 328 shortcut += values::kKeyPeriod; 329 break; 330 case ui::VKEY_UP: 331 shortcut += values::kKeyUp; 332 break; 333 case ui::VKEY_DOWN: 334 shortcut += values::kKeyDown; 335 break; 336 case ui::VKEY_LEFT: 337 shortcut += values::kKeyLeft; 338 break; 339 case ui::VKEY_RIGHT: 340 shortcut += values::kKeyRight; 341 break; 342 case ui::VKEY_INSERT: 343 shortcut += values::kKeyIns; 344 break; 345 case ui::VKEY_DELETE: 346 shortcut += values::kKeyDel; 347 break; 348 case ui::VKEY_HOME: 349 shortcut += values::kKeyHome; 350 break; 351 case ui::VKEY_END: 352 shortcut += values::kKeyEnd; 353 break; 354 case ui::VKEY_PRIOR: 355 shortcut += values::kKeyPgUp; 356 break; 357 case ui::VKEY_NEXT: 358 shortcut += values::kKeyPgDwn; 359 break; 360 case ui::VKEY_TAB: 361 shortcut += values::kKeyTab; 362 break; 363 case ui::VKEY_MEDIA_NEXT_TRACK: 364 shortcut += values::kKeyMediaNextTrack; 365 break; 366 case ui::VKEY_MEDIA_PLAY_PAUSE: 367 shortcut += values::kKeyMediaPlayPause; 368 break; 369 case ui::VKEY_MEDIA_PREV_TRACK: 370 shortcut += values::kKeyMediaPrevTrack; 371 break; 372 case ui::VKEY_MEDIA_STOP: 373 shortcut += values::kKeyMediaStop; 374 break; 375 default: 376 return ""; 377 } 378 } 379 return shortcut; 380 } 381 382 // static 383 bool Command::IsMediaKey(const ui::Accelerator& accelerator) { 384 if (accelerator.modifiers() != 0) 385 return false; 386 387 return (accelerator.key_code() == ui::VKEY_MEDIA_NEXT_TRACK || 388 accelerator.key_code() == ui::VKEY_MEDIA_PREV_TRACK || 389 accelerator.key_code() == ui::VKEY_MEDIA_PLAY_PAUSE || 390 accelerator.key_code() == ui::VKEY_MEDIA_STOP); 391 } 392 393 bool Command::Parse(const base::DictionaryValue* command, 394 const std::string& command_name, 395 int index, 396 base::string16* error) { 397 DCHECK(!command_name.empty()); 398 399 base::string16 description; 400 if (IsNamedCommand(command_name)) { 401 if (!command->GetString(keys::kDescription, &description) || 402 description.empty()) { 403 *error = ErrorUtils::FormatErrorMessageUTF16( 404 errors::kInvalidKeyBindingDescription, 405 base::IntToString(index)); 406 return false; 407 } 408 } 409 410 // We'll build up a map of platform-to-shortcut suggestions. 411 typedef std::map<const std::string, std::string> SuggestionMap; 412 SuggestionMap suggestions; 413 414 // First try to parse the |suggested_key| as a dictionary. 415 const base::DictionaryValue* suggested_key_dict; 416 if (command->GetDictionary(keys::kSuggestedKey, &suggested_key_dict)) { 417 for (base::DictionaryValue::Iterator iter(*suggested_key_dict); 418 !iter.IsAtEnd(); iter.Advance()) { 419 // For each item in the dictionary, extract the platforms specified. 420 std::string suggested_key_string; 421 if (iter.value().GetAsString(&suggested_key_string) && 422 !suggested_key_string.empty()) { 423 // Found a platform, add it to the suggestions list. 424 suggestions[iter.key()] = suggested_key_string; 425 } else { 426 *error = ErrorUtils::FormatErrorMessageUTF16( 427 errors::kInvalidKeyBinding, 428 base::IntToString(index), 429 keys::kSuggestedKey, 430 kMissing); 431 return false; 432 } 433 } 434 } else { 435 // No dictionary was found, fall back to using just a string, so developers 436 // don't have to specify a dictionary if they just want to use one default 437 // for all platforms. 438 std::string suggested_key_string; 439 if (command->GetString(keys::kSuggestedKey, &suggested_key_string) && 440 !suggested_key_string.empty()) { 441 // If only a single string is provided, it must be default for all. 442 suggestions[values::kKeybindingPlatformDefault] = suggested_key_string; 443 } else { 444 suggestions[values::kKeybindingPlatformDefault] = ""; 445 } 446 } 447 448 // Check if this is a global or a regular shortcut. 449 bool global = false; 450 if (FeatureSwitch::global_commands()->IsEnabled()) 451 command->GetBoolean(keys::kGlobal, &global); 452 453 // Normalize the suggestions. 454 for (SuggestionMap::iterator iter = suggestions.begin(); 455 iter != suggestions.end(); ++iter) { 456 // Before we normalize Ctrl to Command we must detect when the developer 457 // specified Command in the Default section, which will work on Mac after 458 // normalization but only fail on other platforms when they try it out on 459 // other platforms, which is not what we want. 460 if (iter->first == values::kKeybindingPlatformDefault && 461 iter->second.find("Command+") != std::string::npos) { 462 *error = ErrorUtils::FormatErrorMessageUTF16( 463 errors::kInvalidKeyBinding, 464 base::IntToString(index), 465 keys::kSuggestedKey, 466 kCommandKeyNotSupported); 467 return false; 468 } 469 470 suggestions[iter->first] = NormalizeShortcutSuggestion(iter->second, 471 iter->first); 472 } 473 474 std::string platform = CommandPlatform(); 475 std::string key = platform; 476 if (suggestions.find(key) == suggestions.end()) 477 key = values::kKeybindingPlatformDefault; 478 if (suggestions.find(key) == suggestions.end()) { 479 *error = ErrorUtils::FormatErrorMessageUTF16( 480 errors::kInvalidKeyBindingMissingPlatform, 481 base::IntToString(index), 482 keys::kSuggestedKey, 483 platform); 484 return false; // No platform specified and no fallback. Bail. 485 } 486 487 // For developer convenience, we parse all the suggestions (and complain about 488 // errors for platforms other than the current one) but use only what we need. 489 std::map<const std::string, std::string>::const_iterator iter = 490 suggestions.begin(); 491 for ( ; iter != suggestions.end(); ++iter) { 492 ui::Accelerator accelerator; 493 if (!iter->second.empty()) { 494 // Note that we pass iter->first to pretend we are on a platform we're not 495 // on. 496 accelerator = ParseImpl(iter->second, iter->first, index, 497 IsNamedCommand(command_name), error); 498 if (accelerator.key_code() == ui::VKEY_UNKNOWN) { 499 if (error->empty()) { 500 *error = ErrorUtils::FormatErrorMessageUTF16( 501 errors::kInvalidKeyBinding, 502 base::IntToString(index), 503 iter->first, 504 iter->second); 505 } 506 return false; 507 } 508 } 509 510 if (iter->first == key) { 511 // This platform is our platform, so grab this key. 512 accelerator_ = accelerator; 513 command_name_ = command_name; 514 description_ = description; 515 global_ = global; 516 } 517 } 518 return true; 519 } 520 521 base::DictionaryValue* Command::ToValue(const Extension* extension, 522 bool active) const { 523 base::DictionaryValue* extension_data = new base::DictionaryValue(); 524 525 base::string16 command_description; 526 bool extension_action = false; 527 if (command_name() == values::kBrowserActionCommandEvent || 528 command_name() == values::kPageActionCommandEvent) { 529 command_description = 530 l10n_util::GetStringUTF16(IDS_EXTENSION_COMMANDS_GENERIC_ACTIVATE); 531 extension_action = true; 532 } else { 533 command_description = description(); 534 } 535 extension_data->SetString("description", command_description); 536 extension_data->SetBoolean("active", active); 537 extension_data->SetString("keybinding", accelerator().GetShortcutText()); 538 extension_data->SetString("command_name", command_name()); 539 extension_data->SetString("extension_id", extension->id()); 540 extension_data->SetBoolean("global", global()); 541 extension_data->SetBoolean("extension_action", extension_action); 542 543 if (FeatureSwitch::global_commands()->IsEnabled()) 544 extension_data->SetBoolean("scope_ui_visible", true); 545 546 return extension_data; 547 } 548 549 } // namespace extensions 550