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