| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/common/extensions/command.h" |
| |
| #include "base/logging.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/values.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "extensions/common/error_utils.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/manifest_constants.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| namespace extensions { |
| |
| namespace errors = manifest_errors; |
| namespace keys = manifest_keys; |
| namespace values = manifest_values; |
| |
| namespace { |
| |
| static const char kMissing[] = "Missing"; |
| |
| static const char kCommandKeyNotSupported[] = |
| "Command key is not supported. Note: Ctrl means Command on Mac"; |
| |
| #if defined(OS_CHROMEOS) |
| // ChromeOS supports an additional modifier 'Search', which can result in longer |
| // sequences. |
| static const int kMaxTokenSize = 4; |
| #else |
| static const int kMaxTokenSize = 3; |
| #endif // OS_CHROMEOS |
| |
| bool IsNamedCommand(const std::string& command_name) { |
| return command_name != values::kPageActionCommandEvent && |
| command_name != values::kBrowserActionCommandEvent; |
| } |
| |
| bool DoesRequireModifier(const std::string& accelerator) { |
| return accelerator != values::kKeyMediaNextTrack && |
| accelerator != values::kKeyMediaPlayPause && |
| accelerator != values::kKeyMediaPrevTrack && |
| accelerator != values::kKeyMediaStop; |
| } |
| |
| // Parse an |accelerator| for a given platform (specified by |platform_key|) and |
| // return the result as a ui::Accelerator if successful, or VKEY_UNKNOWN if not. |
| // |index| is used when constructing an |error| messages to show which command |
| // in the manifest is failing and |should_parse_media_keys| specifies whether |
| // media keys are to be considered for parsing. |
| // Note: If the parsing rules here are changed, make sure to update the |
| // corresponding extension_command_list.js validation, which validates the user |
| // input for chrome://extensions/configureCommands. |
| ui::Accelerator ParseImpl(const std::string& accelerator, |
| const std::string& platform_key, |
| int index, |
| bool should_parse_media_keys, |
| base::string16* error) { |
| error->clear(); |
| if (platform_key != values::kKeybindingPlatformWin && |
| platform_key != values::kKeybindingPlatformMac && |
| platform_key != values::kKeybindingPlatformChromeOs && |
| platform_key != values::kKeybindingPlatformLinux && |
| platform_key != values::kKeybindingPlatformDefault) { |
| *error = ErrorUtils::FormatErrorMessageUTF16( |
| errors::kInvalidKeyBindingUnknownPlatform, |
| base::IntToString(index), |
| platform_key); |
| return ui::Accelerator(); |
| } |
| |
| std::vector<std::string> tokens; |
| base::SplitString(accelerator, '+', &tokens); |
| if (tokens.size() == 0 || |
| (tokens.size() == 1 && DoesRequireModifier(accelerator)) || |
| tokens.size() > kMaxTokenSize) { |
| *error = ErrorUtils::FormatErrorMessageUTF16( |
| errors::kInvalidKeyBinding, |
| base::IntToString(index), |
| platform_key, |
| accelerator); |
| return ui::Accelerator(); |
| } |
| |
| // Now, parse it into an accelerator. |
| int modifiers = ui::EF_NONE; |
| ui::KeyboardCode key = ui::VKEY_UNKNOWN; |
| for (size_t i = 0; i < tokens.size(); i++) { |
| if (tokens[i] == values::kKeyCtrl) { |
| modifiers |= ui::EF_CONTROL_DOWN; |
| } else if (tokens[i] == values::kKeyCommand) { |
| if (platform_key == values::kKeybindingPlatformMac) { |
| // Either the developer specified Command+foo in the manifest for Mac or |
| // they specified Ctrl and it got normalized to Command (to get Ctrl on |
| // Mac the developer has to specify MacCtrl). Therefore we treat this |
| // as Command. |
| modifiers |= ui::EF_COMMAND_DOWN; |
| #if defined(OS_MACOSX) |
| } else if (platform_key == values::kKeybindingPlatformDefault) { |
| // If we see "Command+foo" in the Default section it can mean two |
| // things, depending on the platform: |
| // The developer specified "Ctrl+foo" for Default and it got normalized |
| // on Mac to "Command+foo". This is fine. Treat it as Command. |
| modifiers |= ui::EF_COMMAND_DOWN; |
| #endif |
| } else { |
| // No other platform supports Command. |
| key = ui::VKEY_UNKNOWN; |
| break; |
| } |
| } else if (tokens[i] == values::kKeySearch) { |
| // Search is a special modifier only on ChromeOS and maps to 'Command'. |
| if (platform_key == values::kKeybindingPlatformChromeOs) { |
| modifiers |= ui::EF_COMMAND_DOWN; |
| } else { |
| // No other platform supports Search. |
| key = ui::VKEY_UNKNOWN; |
| break; |
| } |
| } else if (tokens[i] == values::kKeyAlt) { |
| modifiers |= ui::EF_ALT_DOWN; |
| } else if (tokens[i] == values::kKeyShift) { |
| modifiers |= ui::EF_SHIFT_DOWN; |
| } else if (tokens[i].size() == 1 || // A-Z, 0-9. |
| tokens[i] == values::kKeyComma || |
| tokens[i] == values::kKeyPeriod || |
| tokens[i] == values::kKeyUp || |
| tokens[i] == values::kKeyDown || |
| tokens[i] == values::kKeyLeft || |
| tokens[i] == values::kKeyRight || |
| tokens[i] == values::kKeyIns || |
| tokens[i] == values::kKeyDel || |
| tokens[i] == values::kKeyHome || |
| tokens[i] == values::kKeyEnd || |
| tokens[i] == values::kKeyPgUp || |
| tokens[i] == values::kKeyPgDwn || |
| tokens[i] == values::kKeyTab || |
| tokens[i] == values::kKeyMediaNextTrack || |
| tokens[i] == values::kKeyMediaPlayPause || |
| tokens[i] == values::kKeyMediaPrevTrack || |
| tokens[i] == values::kKeyMediaStop) { |
| if (key != ui::VKEY_UNKNOWN) { |
| // Multiple key assignments. |
| key = ui::VKEY_UNKNOWN; |
| break; |
| } |
| |
| if (tokens[i] == values::kKeyComma) { |
| key = ui::VKEY_OEM_COMMA; |
| } else if (tokens[i] == values::kKeyPeriod) { |
| key = ui::VKEY_OEM_PERIOD; |
| } else if (tokens[i] == values::kKeyUp) { |
| key = ui::VKEY_UP; |
| } else if (tokens[i] == values::kKeyDown) { |
| key = ui::VKEY_DOWN; |
| } else if (tokens[i] == values::kKeyLeft) { |
| key = ui::VKEY_LEFT; |
| } else if (tokens[i] == values::kKeyRight) { |
| key = ui::VKEY_RIGHT; |
| } else if (tokens[i] == values::kKeyIns) { |
| key = ui::VKEY_INSERT; |
| } else if (tokens[i] == values::kKeyDel) { |
| key = ui::VKEY_DELETE; |
| } else if (tokens[i] == values::kKeyHome) { |
| key = ui::VKEY_HOME; |
| } else if (tokens[i] == values::kKeyEnd) { |
| key = ui::VKEY_END; |
| } else if (tokens[i] == values::kKeyPgUp) { |
| key = ui::VKEY_PRIOR; |
| } else if (tokens[i] == values::kKeyPgDwn) { |
| key = ui::VKEY_NEXT; |
| } else if (tokens[i] == values::kKeyTab) { |
| key = ui::VKEY_TAB; |
| } else if (tokens[i] == values::kKeyMediaNextTrack && |
| should_parse_media_keys) { |
| key = ui::VKEY_MEDIA_NEXT_TRACK; |
| } else if (tokens[i] == values::kKeyMediaPlayPause && |
| should_parse_media_keys) { |
| key = ui::VKEY_MEDIA_PLAY_PAUSE; |
| } else if (tokens[i] == values::kKeyMediaPrevTrack && |
| should_parse_media_keys) { |
| key = ui::VKEY_MEDIA_PREV_TRACK; |
| } else if (tokens[i] == values::kKeyMediaStop && |
| should_parse_media_keys) { |
| key = ui::VKEY_MEDIA_STOP; |
| } else if (tokens[i].size() == 1 && |
| tokens[i][0] >= 'A' && tokens[i][0] <= 'Z') { |
| key = static_cast<ui::KeyboardCode>(ui::VKEY_A + (tokens[i][0] - 'A')); |
| } else if (tokens[i].size() == 1 && |
| tokens[i][0] >= '0' && tokens[i][0] <= '9') { |
| key = static_cast<ui::KeyboardCode>(ui::VKEY_0 + (tokens[i][0] - '0')); |
| } else { |
| key = ui::VKEY_UNKNOWN; |
| break; |
| } |
| } else { |
| *error = ErrorUtils::FormatErrorMessageUTF16( |
| errors::kInvalidKeyBinding, |
| base::IntToString(index), |
| platform_key, |
| accelerator); |
| return ui::Accelerator(); |
| } |
| } |
| |
| bool command = (modifiers & ui::EF_COMMAND_DOWN) != 0; |
| bool ctrl = (modifiers & ui::EF_CONTROL_DOWN) != 0; |
| bool alt = (modifiers & ui::EF_ALT_DOWN) != 0; |
| bool shift = (modifiers & ui::EF_SHIFT_DOWN) != 0; |
| |
| // We support Ctrl+foo, Alt+foo, Ctrl+Shift+foo, Alt+Shift+foo, but not |
| // Ctrl+Alt+foo and not Shift+foo either. For a more detailed reason why we |
| // don't support Ctrl+Alt+foo see this article: |
| // http://blogs.msdn.com/b/oldnewthing/archive/2004/03/29/101121.aspx. |
| // On Mac Command can also be used in combination with Shift or on its own, |
| // as a modifier. |
| if (key == ui::VKEY_UNKNOWN || (ctrl && alt) || (command && alt) || |
| (shift && !ctrl && !alt && !command)) { |
| *error = ErrorUtils::FormatErrorMessageUTF16( |
| errors::kInvalidKeyBinding, |
| base::IntToString(index), |
| platform_key, |
| accelerator); |
| return ui::Accelerator(); |
| } |
| |
| if ((key == ui::VKEY_MEDIA_NEXT_TRACK || |
| key == ui::VKEY_MEDIA_PREV_TRACK || |
| key == ui::VKEY_MEDIA_PLAY_PAUSE || |
| key == ui::VKEY_MEDIA_STOP) && |
| (shift || ctrl || alt || command)) { |
| *error = ErrorUtils::FormatErrorMessageUTF16( |
| errors::kInvalidKeyBindingMediaKeyWithModifier, |
| base::IntToString(index), |
| platform_key, |
| accelerator); |
| return ui::Accelerator(); |
| } |
| |
| return ui::Accelerator(key, modifiers); |
| } |
| |
| // For Mac, we convert "Ctrl" to "Command" and "MacCtrl" to "Ctrl". Other |
| // platforms leave the shortcut untouched. |
| std::string NormalizeShortcutSuggestion(const std::string& suggestion, |
| const std::string& platform) { |
| bool normalize = false; |
| if (platform == values::kKeybindingPlatformMac) { |
| normalize = true; |
| } else if (platform == values::kKeybindingPlatformDefault) { |
| #if defined(OS_MACOSX) |
| normalize = true; |
| #endif |
| } |
| |
| if (!normalize) |
| return suggestion; |
| |
| std::vector<std::string> tokens; |
| base::SplitString(suggestion, '+', &tokens); |
| for (size_t i = 0; i < tokens.size(); i++) { |
| if (tokens[i] == values::kKeyCtrl) |
| tokens[i] = values::kKeyCommand; |
| else if (tokens[i] == values::kKeyMacCtrl) |
| tokens[i] = values::kKeyCtrl; |
| } |
| return JoinString(tokens, '+'); |
| } |
| |
| } // namespace |
| |
| Command::Command() : global_(false) {} |
| |
| Command::Command(const std::string& command_name, |
| const base::string16& description, |
| const std::string& accelerator, |
| bool global) |
| : command_name_(command_name), |
| description_(description), |
| global_(global) { |
| base::string16 error; |
| accelerator_ = ParseImpl(accelerator, CommandPlatform(), 0, |
| IsNamedCommand(command_name), &error); |
| } |
| |
| Command::~Command() {} |
| |
| // static |
| std::string Command::CommandPlatform() { |
| #if defined(OS_WIN) |
| return values::kKeybindingPlatformWin; |
| #elif defined(OS_MACOSX) |
| return values::kKeybindingPlatformMac; |
| #elif defined(OS_CHROMEOS) |
| return values::kKeybindingPlatformChromeOs; |
| #elif defined(OS_LINUX) |
| return values::kKeybindingPlatformLinux; |
| #else |
| return ""; |
| #endif |
| } |
| |
| // static |
| ui::Accelerator Command::StringToAccelerator(const std::string& accelerator, |
| const std::string& command_name) { |
| base::string16 error; |
| ui::Accelerator parsed = |
| ParseImpl(accelerator, Command::CommandPlatform(), 0, |
| IsNamedCommand(command_name), &error); |
| return parsed; |
| } |
| |
| // static |
| std::string Command::AcceleratorToString(const ui::Accelerator& accelerator) { |
| std::string shortcut; |
| |
| // Ctrl and Alt are mutually exclusive. |
| if (accelerator.IsCtrlDown()) |
| shortcut += values::kKeyCtrl; |
| else if (accelerator.IsAltDown()) |
| shortcut += values::kKeyAlt; |
| if (!shortcut.empty()) |
| shortcut += values::kKeySeparator; |
| |
| if (accelerator.IsCmdDown()) { |
| #if defined(OS_CHROMEOS) |
| // Chrome OS treats the Search key like the Command key. |
| shortcut += values::kKeySearch; |
| #else |
| shortcut += values::kKeyCommand; |
| #endif |
| shortcut += values::kKeySeparator; |
| } |
| |
| if (accelerator.IsShiftDown()) { |
| shortcut += values::kKeyShift; |
| shortcut += values::kKeySeparator; |
| } |
| |
| if (accelerator.key_code() >= ui::VKEY_0 && |
| accelerator.key_code() <= ui::VKEY_9) { |
| shortcut += '0' + (accelerator.key_code() - ui::VKEY_0); |
| } else if (accelerator.key_code() >= ui::VKEY_A && |
| accelerator.key_code() <= ui::VKEY_Z) { |
| shortcut += 'A' + (accelerator.key_code() - ui::VKEY_A); |
| } else { |
| switch (accelerator.key_code()) { |
| case ui::VKEY_OEM_COMMA: |
| shortcut += values::kKeyComma; |
| break; |
| case ui::VKEY_OEM_PERIOD: |
| shortcut += values::kKeyPeriod; |
| break; |
| case ui::VKEY_UP: |
| shortcut += values::kKeyUp; |
| break; |
| case ui::VKEY_DOWN: |
| shortcut += values::kKeyDown; |
| break; |
| case ui::VKEY_LEFT: |
| shortcut += values::kKeyLeft; |
| break; |
| case ui::VKEY_RIGHT: |
| shortcut += values::kKeyRight; |
| break; |
| case ui::VKEY_INSERT: |
| shortcut += values::kKeyIns; |
| break; |
| case ui::VKEY_DELETE: |
| shortcut += values::kKeyDel; |
| break; |
| case ui::VKEY_HOME: |
| shortcut += values::kKeyHome; |
| break; |
| case ui::VKEY_END: |
| shortcut += values::kKeyEnd; |
| break; |
| case ui::VKEY_PRIOR: |
| shortcut += values::kKeyPgUp; |
| break; |
| case ui::VKEY_NEXT: |
| shortcut += values::kKeyPgDwn; |
| break; |
| case ui::VKEY_TAB: |
| shortcut += values::kKeyTab; |
| break; |
| case ui::VKEY_MEDIA_NEXT_TRACK: |
| shortcut += values::kKeyMediaNextTrack; |
| break; |
| case ui::VKEY_MEDIA_PLAY_PAUSE: |
| shortcut += values::kKeyMediaPlayPause; |
| break; |
| case ui::VKEY_MEDIA_PREV_TRACK: |
| shortcut += values::kKeyMediaPrevTrack; |
| break; |
| case ui::VKEY_MEDIA_STOP: |
| shortcut += values::kKeyMediaStop; |
| break; |
| default: |
| return ""; |
| } |
| } |
| return shortcut; |
| } |
| |
| // static |
| bool Command::IsMediaKey(const ui::Accelerator& accelerator) { |
| if (accelerator.modifiers() != 0) |
| return false; |
| |
| return (accelerator.key_code() == ui::VKEY_MEDIA_NEXT_TRACK || |
| accelerator.key_code() == ui::VKEY_MEDIA_PREV_TRACK || |
| accelerator.key_code() == ui::VKEY_MEDIA_PLAY_PAUSE || |
| accelerator.key_code() == ui::VKEY_MEDIA_STOP); |
| } |
| |
| bool Command::Parse(const base::DictionaryValue* command, |
| const std::string& command_name, |
| int index, |
| base::string16* error) { |
| DCHECK(!command_name.empty()); |
| |
| base::string16 description; |
| if (IsNamedCommand(command_name)) { |
| if (!command->GetString(keys::kDescription, &description) || |
| description.empty()) { |
| *error = ErrorUtils::FormatErrorMessageUTF16( |
| errors::kInvalidKeyBindingDescription, |
| base::IntToString(index)); |
| return false; |
| } |
| } |
| |
| // We'll build up a map of platform-to-shortcut suggestions. |
| typedef std::map<const std::string, std::string> SuggestionMap; |
| SuggestionMap suggestions; |
| |
| // First try to parse the |suggested_key| as a dictionary. |
| const base::DictionaryValue* suggested_key_dict; |
| if (command->GetDictionary(keys::kSuggestedKey, &suggested_key_dict)) { |
| for (base::DictionaryValue::Iterator iter(*suggested_key_dict); |
| !iter.IsAtEnd(); iter.Advance()) { |
| // For each item in the dictionary, extract the platforms specified. |
| std::string suggested_key_string; |
| if (iter.value().GetAsString(&suggested_key_string) && |
| !suggested_key_string.empty()) { |
| // Found a platform, add it to the suggestions list. |
| suggestions[iter.key()] = suggested_key_string; |
| } else { |
| *error = ErrorUtils::FormatErrorMessageUTF16( |
| errors::kInvalidKeyBinding, |
| base::IntToString(index), |
| keys::kSuggestedKey, |
| kMissing); |
| return false; |
| } |
| } |
| } else { |
| // No dictionary was found, fall back to using just a string, so developers |
| // don't have to specify a dictionary if they just want to use one default |
| // for all platforms. |
| std::string suggested_key_string; |
| if (command->GetString(keys::kSuggestedKey, &suggested_key_string) && |
| !suggested_key_string.empty()) { |
| // If only a single string is provided, it must be default for all. |
| suggestions[values::kKeybindingPlatformDefault] = suggested_key_string; |
| } else { |
| suggestions[values::kKeybindingPlatformDefault] = ""; |
| } |
| } |
| |
| // Check if this is a global or a regular shortcut. |
| bool global = false; |
| command->GetBoolean(keys::kGlobal, &global); |
| |
| // Normalize the suggestions. |
| for (SuggestionMap::iterator iter = suggestions.begin(); |
| iter != suggestions.end(); ++iter) { |
| // Before we normalize Ctrl to Command we must detect when the developer |
| // specified Command in the Default section, which will work on Mac after |
| // normalization but only fail on other platforms when they try it out on |
| // other platforms, which is not what we want. |
| if (iter->first == values::kKeybindingPlatformDefault && |
| iter->second.find("Command+") != std::string::npos) { |
| *error = ErrorUtils::FormatErrorMessageUTF16( |
| errors::kInvalidKeyBinding, |
| base::IntToString(index), |
| keys::kSuggestedKey, |
| kCommandKeyNotSupported); |
| return false; |
| } |
| |
| suggestions[iter->first] = NormalizeShortcutSuggestion(iter->second, |
| iter->first); |
| } |
| |
| std::string platform = CommandPlatform(); |
| std::string key = platform; |
| if (suggestions.find(key) == suggestions.end()) |
| key = values::kKeybindingPlatformDefault; |
| if (suggestions.find(key) == suggestions.end()) { |
| *error = ErrorUtils::FormatErrorMessageUTF16( |
| errors::kInvalidKeyBindingMissingPlatform, |
| base::IntToString(index), |
| keys::kSuggestedKey, |
| platform); |
| return false; // No platform specified and no fallback. Bail. |
| } |
| |
| // For developer convenience, we parse all the suggestions (and complain about |
| // errors for platforms other than the current one) but use only what we need. |
| std::map<const std::string, std::string>::const_iterator iter = |
| suggestions.begin(); |
| for ( ; iter != suggestions.end(); ++iter) { |
| ui::Accelerator accelerator; |
| if (!iter->second.empty()) { |
| // Note that we pass iter->first to pretend we are on a platform we're not |
| // on. |
| accelerator = ParseImpl(iter->second, iter->first, index, |
| IsNamedCommand(command_name), error); |
| if (accelerator.key_code() == ui::VKEY_UNKNOWN) { |
| if (error->empty()) { |
| *error = ErrorUtils::FormatErrorMessageUTF16( |
| errors::kInvalidKeyBinding, |
| base::IntToString(index), |
| iter->first, |
| iter->second); |
| } |
| return false; |
| } |
| } |
| |
| if (iter->first == key) { |
| // This platform is our platform, so grab this key. |
| accelerator_ = accelerator; |
| command_name_ = command_name; |
| description_ = description; |
| global_ = global; |
| } |
| } |
| return true; |
| } |
| |
| base::DictionaryValue* Command::ToValue(const Extension* extension, |
| bool active) const { |
| base::DictionaryValue* extension_data = new base::DictionaryValue(); |
| |
| base::string16 command_description; |
| bool extension_action = false; |
| if (command_name() == values::kBrowserActionCommandEvent || |
| command_name() == values::kPageActionCommandEvent) { |
| command_description = |
| l10n_util::GetStringUTF16(IDS_EXTENSION_COMMANDS_GENERIC_ACTIVATE); |
| extension_action = true; |
| } else { |
| command_description = description(); |
| } |
| extension_data->SetString("description", command_description); |
| extension_data->SetBoolean("active", active); |
| extension_data->SetString("keybinding", accelerator().GetShortcutText()); |
| extension_data->SetString("command_name", command_name()); |
| extension_data->SetString("extension_id", extension->id()); |
| extension_data->SetBoolean("global", global()); |
| extension_data->SetBoolean("extension_action", extension_action); |
| return extension_data; |
| } |
| |
| } // namespace extensions |