123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532 |
- // 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 "extensions/common/command.h"
- #include <stddef.h>
- #include "base/check.h"
- #include "base/strings/string_number_conversions.h"
- #include "base/strings/string_piece.h"
- #include "base/strings/string_split.h"
- #include "base/strings/string_util.h"
- #include "base/strings/utf_string_conversions.h"
- #include "base/values.h"
- #include "build/build_config.h"
- #include "extensions/common/error_utils.h"
- #include "extensions/common/extension.h"
- #include "extensions/common/manifest_constants.h"
- #include "ui/base/accelerators/media_keys_listener.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 BUILDFLAG(IS_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 // BUILDFLAG(IS_CHROMEOS)
- // TODO(devlin): Expose this on Command, since many places implicitly check
- // this.
- bool IsNamedCommand(const std::string& command_name) {
- return command_name != values::kPageActionCommandEvent &&
- command_name != values::kBrowserActionCommandEvent &&
- command_name != values::kActionCommandEvent;
- }
- 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,
- std::u16string* 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::NumberToString(index),
- platform_key);
- return ui::Accelerator();
- }
- std::vector<std::string> tokens = base::SplitString(
- accelerator, "+", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
- if (tokens.size() == 0 ||
- (tokens.size() == 1 && DoesRequireModifier(accelerator)) ||
- tokens.size() > kMaxTokenSize) {
- *error = ErrorUtils::FormatErrorMessageUTF16(errors::kInvalidKeyBinding,
- base::NumberToString(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 BUILDFLAG(IS_MAC)
- } 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::kKeySpace || 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::kKeySpace) {
- key = ui::VKEY_SPACE;
- } 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 && base::IsAsciiUpper(tokens[i][0])) {
- key = static_cast<ui::KeyboardCode>(ui::VKEY_A + (tokens[i][0] - 'A'));
- } else if (tokens[i].size() == 1 && base::IsAsciiDigit(tokens[i][0])) {
- 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::NumberToString(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::NumberToString(index),
- platform_key, accelerator);
- return ui::Accelerator();
- }
- if (ui::MediaKeysListener::IsMediaKeycode(key) &&
- (shift || ctrl || alt || command)) {
- *error = ErrorUtils::FormatErrorMessageUTF16(
- errors::kInvalidKeyBindingMediaKeyWithModifier,
- base::NumberToString(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 BUILDFLAG(IS_MAC)
- normalize = true;
- #endif
- }
- if (!normalize)
- return suggestion;
- std::vector<base::StringPiece> tokens = base::SplitStringPiece(
- suggestion, "+", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
- 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 base::JoinString(tokens, "+");
- }
- } // namespace
- Command::Command() : global_(false) {}
- Command::Command(const std::string& command_name,
- const std::u16string& description,
- const std::string& accelerator,
- bool global)
- : command_name_(command_name), description_(description), global_(global) {
- std::u16string error;
- accelerator_ = ParseImpl(accelerator, CommandPlatform(), 0,
- IsNamedCommand(command_name), &error);
- }
- Command::Command(const Command& other) = default;
- Command::~Command() {}
- // static
- std::string Command::CommandPlatform() {
- #if BUILDFLAG(IS_WIN)
- return values::kKeybindingPlatformWin;
- #elif BUILDFLAG(IS_MAC)
- return values::kKeybindingPlatformMac;
- #elif BUILDFLAG(IS_CHROMEOS)
- return values::kKeybindingPlatformChromeOs;
- #elif BUILDFLAG(IS_LINUX)
- return values::kKeybindingPlatformLinux;
- #elif BUILDFLAG(IS_FUCHSIA)
- // TODO(crbug.com/1312215): Change this once we decide what string should be
- // used for Fuchsia.
- return values::kKeybindingPlatformLinux;
- #else
- #error Unsupported platform
- #endif
- }
- // static
- ui::Accelerator Command::StringToAccelerator(const std::string& accelerator,
- const std::string& command_name) {
- std::u16string 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 BUILDFLAG(IS_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_SPACE:
- shortcut += values::kKeySpace;
- 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 ui::MediaKeysListener::IsMediaKeycode(accelerator.key_code());
- }
- bool Command::Parse(const base::DictionaryValue* command,
- const std::string& command_name,
- int index,
- std::u16string* error) {
- DCHECK(!command_name.empty());
- std::u16string description;
- if (IsNamedCommand(command_name)) {
- const std::string* description_ptr =
- command->FindStringKey(keys::kDescription);
- if (!description_ptr || description_ptr->empty()) {
- *error = ErrorUtils::FormatErrorMessageUTF16(
- errors::kInvalidKeyBindingDescription, base::NumberToString(index));
- return false;
- }
- description = base::UTF8ToUTF16(*description_ptr);
- }
- // 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.
- const std::string* suggested_key_string = iter.value().GetIfString();
- if (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::NumberToString(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.
- const std::string* suggested_key_string =
- command->FindStringKey(keys::kSuggestedKey);
- if (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 = command->FindBoolPath(keys::kGlobal).value_or(false);
- // Normalize the suggestions.
- for (auto 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::NumberToString(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::NumberToString(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::NumberToString(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;
- }
- } // namespace extensions
|