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