| // Copyright 2012 The Chromium Authors |
| // 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 <memory> |
| #include <string> |
| |
| #include "base/check.h" |
| #include "base/functional/callback_forward.h" |
| #include "base/strings/string_number_conversions.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/android_buildflags.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/command.h" |
| #include "ui/base/accelerators/command_constants.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"; |
| |
| // For Mac, we convert "Ctrl" to "Command" and "MacCtrl" to "Ctrl". Other |
| // platforms leave the shortcut untouched. |
| std::string NormalizeShortcutSuggestion(std::string_view suggestion, |
| std::string_view platform) { |
| bool normalize = false; |
| if (platform == ui::kKeybindingPlatformMac) { |
| normalize = true; |
| } else if (platform == ui::kKeybindingPlatformDefault) { |
| #if BUILDFLAG(IS_MAC) |
| normalize = true; |
| #endif |
| } |
| |
| if (!normalize) { |
| return std::string{suggestion}; |
| } |
| |
| std::vector<std::string_view> tokens = base::SplitStringPiece( |
| suggestion, "+", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); |
| for (auto& token : tokens) { |
| if (token == ui::kKeyCtrl) { |
| token = ui::kKeyCommand; |
| } else if (token == ui::kKeyMacCtrl) { |
| token = ui::kKeyCtrl; |
| } |
| } |
| return base::JoinString(tokens, "+"); |
| } |
| |
| void SetAcceleratorParseErrorMessage(std::u16string* error, |
| int index, |
| std::string_view platform_key, |
| std::string_view accelerator_string, |
| ui::AcceleratorParseError parse_error) { |
| error->clear(); |
| switch (parse_error) { |
| case ui::AcceleratorParseError::kMalformedInput: |
| *error = ErrorUtils::FormatErrorMessageUTF16( |
| errors::kInvalidKeyBinding, base::NumberToString(index), platform_key, |
| accelerator_string); |
| break; |
| case ui::AcceleratorParseError::kMediaKeyWithModifier: |
| *error = ErrorUtils::FormatErrorMessageUTF16( |
| errors::kInvalidKeyBindingMediaKeyWithModifier, |
| base::NumberToString(index), platform_key, accelerator_string); |
| break; |
| case ui::AcceleratorParseError::kUnsupportedPlatform: |
| *error = ErrorUtils::FormatErrorMessageUTF16( |
| errors::kInvalidKeyBindingUnknownPlatform, |
| base::NumberToString(index), platform_key); |
| break; |
| } |
| } |
| |
| } // namespace |
| |
| Command::Command(std::string_view command_name, |
| std::u16string_view description, |
| std::string_view accelerator, |
| bool global) |
| : ui::Command(command_name, description, global) { |
| if (!accelerator.empty()) { |
| std::u16string error; |
| AcceleratorParseErrorCallback on_parse_error = |
| base::BindOnce(SetAcceleratorParseErrorMessage, &error, 0, |
| CommandPlatform(), accelerator); |
| set_accelerator(ParseImpl(accelerator, CommandPlatform(), |
| !IsActionRelatedCommand(command_name), |
| std::move(on_parse_error))); |
| } |
| } |
| |
| // static |
| std::string Command::CommandPlatform() { |
| #if BUILDFLAG(IS_WIN) |
| return ui::kKeybindingPlatformWin; |
| #elif BUILDFLAG(IS_MAC) |
| return ui::kKeybindingPlatformMac; |
| #elif BUILDFLAG(IS_CHROMEOS) |
| return ui::kKeybindingPlatformChromeOs; |
| #elif BUILDFLAG(IS_LINUX) |
| return ui::kKeybindingPlatformLinux; |
| #elif BUILDFLAG(IS_DESKTOP_ANDROID) |
| // For now, we use linux keybindings on desktop android. |
| // TODO(https://crbug.com/356905053): Should this be ChromeOS keybindings? |
| return ui::kKeybindingPlatformLinux; |
| #else |
| #error Unsupported platform |
| #endif |
| } |
| |
| // static |
| ui::Accelerator Command::StringToAccelerator(std::string_view accelerator, |
| std::string_view command_name) { |
| std::u16string error; |
| AcceleratorParseErrorCallback on_parse_error = |
| base::BindOnce(SetAcceleratorParseErrorMessage, &error, 0, |
| CommandPlatform(), accelerator); |
| ui::Accelerator parsed = ParseImpl(accelerator, CommandPlatform(), |
| !IsActionRelatedCommand(command_name), |
| std::move(on_parse_error)); |
| return parsed; |
| } |
| |
| // static |
| bool Command::IsActionRelatedCommand(std::string_view command_name) { |
| return command_name == values::kActionCommandEvent || |
| command_name == values::kBrowserActionCommandEvent || |
| command_name == values::kPageActionCommandEvent; |
| } |
| |
| bool Command::Parse(const base::Value::Dict& command, |
| std::string_view command_name, |
| int index, |
| std::u16string* error) { |
| DCHECK(!command_name.empty()); |
| |
| std::u16string description; |
| if (!IsActionRelatedCommand(command_name)) { |
| const std::string* description_ptr = command.FindString(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. |
| using SuggestionMap = std::map<const std::string, std::string>; |
| SuggestionMap suggestions; |
| |
| // First try to parse the |suggested_key| as a dictionary. |
| |
| if (const base::Value::Dict* suggested_key_dict = |
| command.FindDict(keys::kSuggestedKey)) { |
| for (const auto item : *suggested_key_dict) { |
| // For each item in the dictionary, extract the platforms specified. |
| const std::string* suggested_key_string = item.second.GetIfString(); |
| if (suggested_key_string && !suggested_key_string->empty()) { |
| // Found a platform, add it to the suggestions list. |
| suggestions[item.first] = *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.FindString(keys::kSuggestedKey); |
| if (suggested_key_string && !suggested_key_string->empty()) { |
| // If only a single string is provided, it must be default for all. |
| suggestions[ui::kKeybindingPlatformDefault] = *suggested_key_string; |
| } else { |
| suggestions[ui::kKeybindingPlatformDefault] = ""; |
| } |
| } |
| |
| // Check if this is a global or a regular shortcut. |
| bool global = command.FindBoolByDottedPath(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 == ui::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 = ui::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. |
| AcceleratorParseErrorCallback on_parse_error = |
| base::BindOnce(SetAcceleratorParseErrorMessage, error, index, |
| iter->first, iter->second); |
| accelerator = ParseImpl(iter->second, iter->first, |
| !IsActionRelatedCommand(command_name), |
| std::move(on_parse_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. |
| set_accelerator(accelerator); |
| set_command_name(command_name); |
| set_description(description); |
| set_global(global); |
| } |
| } |
| return true; |
| } |
| |
| } // namespace extensions |