command.cc 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  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. #include "extensions/common/command.h"
  5. #include <stddef.h>
  6. #include "base/check.h"
  7. #include "base/strings/string_number_conversions.h"
  8. #include "base/strings/string_piece.h"
  9. #include "base/strings/string_split.h"
  10. #include "base/strings/string_util.h"
  11. #include "base/strings/utf_string_conversions.h"
  12. #include "base/values.h"
  13. #include "build/build_config.h"
  14. #include "extensions/common/error_utils.h"
  15. #include "extensions/common/extension.h"
  16. #include "extensions/common/manifest_constants.h"
  17. #include "ui/base/accelerators/media_keys_listener.h"
  18. namespace extensions {
  19. namespace errors = manifest_errors;
  20. namespace keys = manifest_keys;
  21. namespace values = manifest_values;
  22. namespace {
  23. static const char kMissing[] = "Missing";
  24. static const char kCommandKeyNotSupported[] =
  25. "Command key is not supported. Note: Ctrl means Command on Mac";
  26. #if BUILDFLAG(IS_CHROMEOS)
  27. // ChromeOS supports an additional modifier 'Search', which can result in longer
  28. // sequences.
  29. static const int kMaxTokenSize = 4;
  30. #else
  31. static const int kMaxTokenSize = 3;
  32. #endif // BUILDFLAG(IS_CHROMEOS)
  33. // TODO(devlin): Expose this on Command, since many places implicitly check
  34. // this.
  35. bool IsNamedCommand(const std::string& command_name) {
  36. return command_name != values::kPageActionCommandEvent &&
  37. command_name != values::kBrowserActionCommandEvent &&
  38. command_name != values::kActionCommandEvent;
  39. }
  40. bool DoesRequireModifier(const std::string& accelerator) {
  41. return accelerator != values::kKeyMediaNextTrack &&
  42. accelerator != values::kKeyMediaPlayPause &&
  43. accelerator != values::kKeyMediaPrevTrack &&
  44. accelerator != values::kKeyMediaStop;
  45. }
  46. // Parse an |accelerator| for a given platform (specified by |platform_key|) and
  47. // return the result as a ui::Accelerator if successful, or VKEY_UNKNOWN if not.
  48. // |index| is used when constructing an |error| messages to show which command
  49. // in the manifest is failing and |should_parse_media_keys| specifies whether
  50. // media keys are to be considered for parsing.
  51. // Note: If the parsing rules here are changed, make sure to update the
  52. // corresponding extension_command_list.js validation, which validates the user
  53. // input for chrome://extensions/configureCommands.
  54. ui::Accelerator ParseImpl(const std::string& accelerator,
  55. const std::string& platform_key,
  56. int index,
  57. bool should_parse_media_keys,
  58. std::u16string* error) {
  59. error->clear();
  60. if (platform_key != values::kKeybindingPlatformWin &&
  61. platform_key != values::kKeybindingPlatformMac &&
  62. platform_key != values::kKeybindingPlatformChromeOs &&
  63. platform_key != values::kKeybindingPlatformLinux &&
  64. platform_key != values::kKeybindingPlatformDefault) {
  65. *error = ErrorUtils::FormatErrorMessageUTF16(
  66. errors::kInvalidKeyBindingUnknownPlatform, base::NumberToString(index),
  67. platform_key);
  68. return ui::Accelerator();
  69. }
  70. std::vector<std::string> tokens = base::SplitString(
  71. accelerator, "+", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
  72. if (tokens.size() == 0 ||
  73. (tokens.size() == 1 && DoesRequireModifier(accelerator)) ||
  74. tokens.size() > kMaxTokenSize) {
  75. *error = ErrorUtils::FormatErrorMessageUTF16(errors::kInvalidKeyBinding,
  76. base::NumberToString(index),
  77. platform_key, accelerator);
  78. return ui::Accelerator();
  79. }
  80. // Now, parse it into an accelerator.
  81. int modifiers = ui::EF_NONE;
  82. ui::KeyboardCode key = ui::VKEY_UNKNOWN;
  83. for (size_t i = 0; i < tokens.size(); i++) {
  84. if (tokens[i] == values::kKeyCtrl) {
  85. modifiers |= ui::EF_CONTROL_DOWN;
  86. } else if (tokens[i] == values::kKeyCommand) {
  87. if (platform_key == values::kKeybindingPlatformMac) {
  88. // Either the developer specified Command+foo in the manifest for Mac or
  89. // they specified Ctrl and it got normalized to Command (to get Ctrl on
  90. // Mac the developer has to specify MacCtrl). Therefore we treat this
  91. // as Command.
  92. modifiers |= ui::EF_COMMAND_DOWN;
  93. #if BUILDFLAG(IS_MAC)
  94. } else if (platform_key == values::kKeybindingPlatformDefault) {
  95. // If we see "Command+foo" in the Default section it can mean two
  96. // things, depending on the platform:
  97. // The developer specified "Ctrl+foo" for Default and it got normalized
  98. // on Mac to "Command+foo". This is fine. Treat it as Command.
  99. modifiers |= ui::EF_COMMAND_DOWN;
  100. #endif
  101. } else {
  102. // No other platform supports Command.
  103. key = ui::VKEY_UNKNOWN;
  104. break;
  105. }
  106. } else if (tokens[i] == values::kKeySearch) {
  107. // Search is a special modifier only on ChromeOS and maps to 'Command'.
  108. if (platform_key == values::kKeybindingPlatformChromeOs) {
  109. modifiers |= ui::EF_COMMAND_DOWN;
  110. } else {
  111. // No other platform supports Search.
  112. key = ui::VKEY_UNKNOWN;
  113. break;
  114. }
  115. } else if (tokens[i] == values::kKeyAlt) {
  116. modifiers |= ui::EF_ALT_DOWN;
  117. } else if (tokens[i] == values::kKeyShift) {
  118. modifiers |= ui::EF_SHIFT_DOWN;
  119. } else if (tokens[i].size() == 1 || // A-Z, 0-9.
  120. tokens[i] == values::kKeyComma ||
  121. tokens[i] == values::kKeyPeriod || tokens[i] == values::kKeyUp ||
  122. tokens[i] == values::kKeyDown || tokens[i] == values::kKeyLeft ||
  123. tokens[i] == values::kKeyRight || tokens[i] == values::kKeyIns ||
  124. tokens[i] == values::kKeyDel || tokens[i] == values::kKeyHome ||
  125. tokens[i] == values::kKeyEnd || tokens[i] == values::kKeyPgUp ||
  126. tokens[i] == values::kKeyPgDwn ||
  127. tokens[i] == values::kKeySpace || tokens[i] == values::kKeyTab ||
  128. tokens[i] == values::kKeyMediaNextTrack ||
  129. tokens[i] == values::kKeyMediaPlayPause ||
  130. tokens[i] == values::kKeyMediaPrevTrack ||
  131. tokens[i] == values::kKeyMediaStop) {
  132. if (key != ui::VKEY_UNKNOWN) {
  133. // Multiple key assignments.
  134. key = ui::VKEY_UNKNOWN;
  135. break;
  136. }
  137. if (tokens[i] == values::kKeyComma) {
  138. key = ui::VKEY_OEM_COMMA;
  139. } else if (tokens[i] == values::kKeyPeriod) {
  140. key = ui::VKEY_OEM_PERIOD;
  141. } else if (tokens[i] == values::kKeyUp) {
  142. key = ui::VKEY_UP;
  143. } else if (tokens[i] == values::kKeyDown) {
  144. key = ui::VKEY_DOWN;
  145. } else if (tokens[i] == values::kKeyLeft) {
  146. key = ui::VKEY_LEFT;
  147. } else if (tokens[i] == values::kKeyRight) {
  148. key = ui::VKEY_RIGHT;
  149. } else if (tokens[i] == values::kKeyIns) {
  150. key = ui::VKEY_INSERT;
  151. } else if (tokens[i] == values::kKeyDel) {
  152. key = ui::VKEY_DELETE;
  153. } else if (tokens[i] == values::kKeyHome) {
  154. key = ui::VKEY_HOME;
  155. } else if (tokens[i] == values::kKeyEnd) {
  156. key = ui::VKEY_END;
  157. } else if (tokens[i] == values::kKeyPgUp) {
  158. key = ui::VKEY_PRIOR;
  159. } else if (tokens[i] == values::kKeyPgDwn) {
  160. key = ui::VKEY_NEXT;
  161. } else if (tokens[i] == values::kKeySpace) {
  162. key = ui::VKEY_SPACE;
  163. } else if (tokens[i] == values::kKeyTab) {
  164. key = ui::VKEY_TAB;
  165. } else if (tokens[i] == values::kKeyMediaNextTrack &&
  166. should_parse_media_keys) {
  167. key = ui::VKEY_MEDIA_NEXT_TRACK;
  168. } else if (tokens[i] == values::kKeyMediaPlayPause &&
  169. should_parse_media_keys) {
  170. key = ui::VKEY_MEDIA_PLAY_PAUSE;
  171. } else if (tokens[i] == values::kKeyMediaPrevTrack &&
  172. should_parse_media_keys) {
  173. key = ui::VKEY_MEDIA_PREV_TRACK;
  174. } else if (tokens[i] == values::kKeyMediaStop &&
  175. should_parse_media_keys) {
  176. key = ui::VKEY_MEDIA_STOP;
  177. } else if (tokens[i].size() == 1 && base::IsAsciiUpper(tokens[i][0])) {
  178. key = static_cast<ui::KeyboardCode>(ui::VKEY_A + (tokens[i][0] - 'A'));
  179. } else if (tokens[i].size() == 1 && base::IsAsciiDigit(tokens[i][0])) {
  180. key = static_cast<ui::KeyboardCode>(ui::VKEY_0 + (tokens[i][0] - '0'));
  181. } else {
  182. key = ui::VKEY_UNKNOWN;
  183. break;
  184. }
  185. } else {
  186. *error = ErrorUtils::FormatErrorMessageUTF16(errors::kInvalidKeyBinding,
  187. base::NumberToString(index),
  188. platform_key, accelerator);
  189. return ui::Accelerator();
  190. }
  191. }
  192. bool command = (modifiers & ui::EF_COMMAND_DOWN) != 0;
  193. bool ctrl = (modifiers & ui::EF_CONTROL_DOWN) != 0;
  194. bool alt = (modifiers & ui::EF_ALT_DOWN) != 0;
  195. bool shift = (modifiers & ui::EF_SHIFT_DOWN) != 0;
  196. // We support Ctrl+foo, Alt+foo, Ctrl+Shift+foo, Alt+Shift+foo, but not
  197. // Ctrl+Alt+foo and not Shift+foo either. For a more detailed reason why we
  198. // don't support Ctrl+Alt+foo see this article:
  199. // http://blogs.msdn.com/b/oldnewthing/archive/2004/03/29/101121.aspx.
  200. // On Mac Command can also be used in combination with Shift or on its own,
  201. // as a modifier.
  202. if (key == ui::VKEY_UNKNOWN || (ctrl && alt) || (command && alt) ||
  203. (shift && !ctrl && !alt && !command)) {
  204. *error = ErrorUtils::FormatErrorMessageUTF16(errors::kInvalidKeyBinding,
  205. base::NumberToString(index),
  206. platform_key, accelerator);
  207. return ui::Accelerator();
  208. }
  209. if (ui::MediaKeysListener::IsMediaKeycode(key) &&
  210. (shift || ctrl || alt || command)) {
  211. *error = ErrorUtils::FormatErrorMessageUTF16(
  212. errors::kInvalidKeyBindingMediaKeyWithModifier,
  213. base::NumberToString(index), platform_key, accelerator);
  214. return ui::Accelerator();
  215. }
  216. return ui::Accelerator(key, modifiers);
  217. }
  218. // For Mac, we convert "Ctrl" to "Command" and "MacCtrl" to "Ctrl". Other
  219. // platforms leave the shortcut untouched.
  220. std::string NormalizeShortcutSuggestion(const std::string& suggestion,
  221. const std::string& platform) {
  222. bool normalize = false;
  223. if (platform == values::kKeybindingPlatformMac) {
  224. normalize = true;
  225. } else if (platform == values::kKeybindingPlatformDefault) {
  226. #if BUILDFLAG(IS_MAC)
  227. normalize = true;
  228. #endif
  229. }
  230. if (!normalize)
  231. return suggestion;
  232. std::vector<base::StringPiece> tokens = base::SplitStringPiece(
  233. suggestion, "+", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
  234. for (size_t i = 0; i < tokens.size(); i++) {
  235. if (tokens[i] == values::kKeyCtrl)
  236. tokens[i] = values::kKeyCommand;
  237. else if (tokens[i] == values::kKeyMacCtrl)
  238. tokens[i] = values::kKeyCtrl;
  239. }
  240. return base::JoinString(tokens, "+");
  241. }
  242. } // namespace
  243. Command::Command() : global_(false) {}
  244. Command::Command(const std::string& command_name,
  245. const std::u16string& description,
  246. const std::string& accelerator,
  247. bool global)
  248. : command_name_(command_name), description_(description), global_(global) {
  249. std::u16string error;
  250. accelerator_ = ParseImpl(accelerator, CommandPlatform(), 0,
  251. IsNamedCommand(command_name), &error);
  252. }
  253. Command::Command(const Command& other) = default;
  254. Command::~Command() {}
  255. // static
  256. std::string Command::CommandPlatform() {
  257. #if BUILDFLAG(IS_WIN)
  258. return values::kKeybindingPlatformWin;
  259. #elif BUILDFLAG(IS_MAC)
  260. return values::kKeybindingPlatformMac;
  261. #elif BUILDFLAG(IS_CHROMEOS)
  262. return values::kKeybindingPlatformChromeOs;
  263. #elif BUILDFLAG(IS_LINUX)
  264. return values::kKeybindingPlatformLinux;
  265. #elif BUILDFLAG(IS_FUCHSIA)
  266. // TODO(crbug.com/1312215): Change this once we decide what string should be
  267. // used for Fuchsia.
  268. return values::kKeybindingPlatformLinux;
  269. #else
  270. #error Unsupported platform
  271. #endif
  272. }
  273. // static
  274. ui::Accelerator Command::StringToAccelerator(const std::string& accelerator,
  275. const std::string& command_name) {
  276. std::u16string error;
  277. ui::Accelerator parsed = ParseImpl(accelerator, Command::CommandPlatform(), 0,
  278. IsNamedCommand(command_name), &error);
  279. return parsed;
  280. }
  281. // static
  282. std::string Command::AcceleratorToString(const ui::Accelerator& accelerator) {
  283. std::string shortcut;
  284. // Ctrl and Alt are mutually exclusive.
  285. if (accelerator.IsCtrlDown())
  286. shortcut += values::kKeyCtrl;
  287. else if (accelerator.IsAltDown())
  288. shortcut += values::kKeyAlt;
  289. if (!shortcut.empty())
  290. shortcut += values::kKeySeparator;
  291. if (accelerator.IsCmdDown()) {
  292. #if BUILDFLAG(IS_CHROMEOS)
  293. // Chrome OS treats the Search key like the Command key.
  294. shortcut += values::kKeySearch;
  295. #else
  296. shortcut += values::kKeyCommand;
  297. #endif
  298. shortcut += values::kKeySeparator;
  299. }
  300. if (accelerator.IsShiftDown()) {
  301. shortcut += values::kKeyShift;
  302. shortcut += values::kKeySeparator;
  303. }
  304. if (accelerator.key_code() >= ui::VKEY_0 &&
  305. accelerator.key_code() <= ui::VKEY_9) {
  306. shortcut += '0' + (accelerator.key_code() - ui::VKEY_0);
  307. } else if (accelerator.key_code() >= ui::VKEY_A &&
  308. accelerator.key_code() <= ui::VKEY_Z) {
  309. shortcut += 'A' + (accelerator.key_code() - ui::VKEY_A);
  310. } else {
  311. switch (accelerator.key_code()) {
  312. case ui::VKEY_OEM_COMMA:
  313. shortcut += values::kKeyComma;
  314. break;
  315. case ui::VKEY_OEM_PERIOD:
  316. shortcut += values::kKeyPeriod;
  317. break;
  318. case ui::VKEY_UP:
  319. shortcut += values::kKeyUp;
  320. break;
  321. case ui::VKEY_DOWN:
  322. shortcut += values::kKeyDown;
  323. break;
  324. case ui::VKEY_LEFT:
  325. shortcut += values::kKeyLeft;
  326. break;
  327. case ui::VKEY_RIGHT:
  328. shortcut += values::kKeyRight;
  329. break;
  330. case ui::VKEY_INSERT:
  331. shortcut += values::kKeyIns;
  332. break;
  333. case ui::VKEY_DELETE:
  334. shortcut += values::kKeyDel;
  335. break;
  336. case ui::VKEY_HOME:
  337. shortcut += values::kKeyHome;
  338. break;
  339. case ui::VKEY_END:
  340. shortcut += values::kKeyEnd;
  341. break;
  342. case ui::VKEY_PRIOR:
  343. shortcut += values::kKeyPgUp;
  344. break;
  345. case ui::VKEY_NEXT:
  346. shortcut += values::kKeyPgDwn;
  347. break;
  348. case ui::VKEY_SPACE:
  349. shortcut += values::kKeySpace;
  350. break;
  351. case ui::VKEY_TAB:
  352. shortcut += values::kKeyTab;
  353. break;
  354. case ui::VKEY_MEDIA_NEXT_TRACK:
  355. shortcut += values::kKeyMediaNextTrack;
  356. break;
  357. case ui::VKEY_MEDIA_PLAY_PAUSE:
  358. shortcut += values::kKeyMediaPlayPause;
  359. break;
  360. case ui::VKEY_MEDIA_PREV_TRACK:
  361. shortcut += values::kKeyMediaPrevTrack;
  362. break;
  363. case ui::VKEY_MEDIA_STOP:
  364. shortcut += values::kKeyMediaStop;
  365. break;
  366. default:
  367. return "";
  368. }
  369. }
  370. return shortcut;
  371. }
  372. // static
  373. bool Command::IsMediaKey(const ui::Accelerator& accelerator) {
  374. if (accelerator.modifiers() != 0)
  375. return false;
  376. return ui::MediaKeysListener::IsMediaKeycode(accelerator.key_code());
  377. }
  378. bool Command::Parse(const base::DictionaryValue* command,
  379. const std::string& command_name,
  380. int index,
  381. std::u16string* error) {
  382. DCHECK(!command_name.empty());
  383. std::u16string description;
  384. if (IsNamedCommand(command_name)) {
  385. const std::string* description_ptr =
  386. command->FindStringKey(keys::kDescription);
  387. if (!description_ptr || description_ptr->empty()) {
  388. *error = ErrorUtils::FormatErrorMessageUTF16(
  389. errors::kInvalidKeyBindingDescription, base::NumberToString(index));
  390. return false;
  391. }
  392. description = base::UTF8ToUTF16(*description_ptr);
  393. }
  394. // We'll build up a map of platform-to-shortcut suggestions.
  395. typedef std::map<const std::string, std::string> SuggestionMap;
  396. SuggestionMap suggestions;
  397. // First try to parse the |suggested_key| as a dictionary.
  398. const base::DictionaryValue* suggested_key_dict;
  399. if (command->GetDictionary(keys::kSuggestedKey, &suggested_key_dict)) {
  400. for (base::DictionaryValue::Iterator iter(*suggested_key_dict);
  401. !iter.IsAtEnd(); iter.Advance()) {
  402. // For each item in the dictionary, extract the platforms specified.
  403. const std::string* suggested_key_string = iter.value().GetIfString();
  404. if (suggested_key_string && !suggested_key_string->empty()) {
  405. // Found a platform, add it to the suggestions list.
  406. suggestions[iter.key()] = *suggested_key_string;
  407. } else {
  408. *error = ErrorUtils::FormatErrorMessageUTF16(
  409. errors::kInvalidKeyBinding, base::NumberToString(index),
  410. keys::kSuggestedKey, kMissing);
  411. return false;
  412. }
  413. }
  414. } else {
  415. // No dictionary was found, fall back to using just a string, so developers
  416. // don't have to specify a dictionary if they just want to use one default
  417. // for all platforms.
  418. const std::string* suggested_key_string =
  419. command->FindStringKey(keys::kSuggestedKey);
  420. if (suggested_key_string && !suggested_key_string->empty()) {
  421. // If only a single string is provided, it must be default for all.
  422. suggestions[values::kKeybindingPlatformDefault] = *suggested_key_string;
  423. } else {
  424. suggestions[values::kKeybindingPlatformDefault] = "";
  425. }
  426. }
  427. // Check if this is a global or a regular shortcut.
  428. bool global = command->FindBoolPath(keys::kGlobal).value_or(false);
  429. // Normalize the suggestions.
  430. for (auto iter = suggestions.begin(); iter != suggestions.end(); ++iter) {
  431. // Before we normalize Ctrl to Command we must detect when the developer
  432. // specified Command in the Default section, which will work on Mac after
  433. // normalization but only fail on other platforms when they try it out on
  434. // other platforms, which is not what we want.
  435. if (iter->first == values::kKeybindingPlatformDefault &&
  436. iter->second.find("Command+") != std::string::npos) {
  437. *error = ErrorUtils::FormatErrorMessageUTF16(
  438. errors::kInvalidKeyBinding, base::NumberToString(index),
  439. keys::kSuggestedKey, kCommandKeyNotSupported);
  440. return false;
  441. }
  442. suggestions[iter->first] =
  443. NormalizeShortcutSuggestion(iter->second, iter->first);
  444. }
  445. std::string platform = CommandPlatform();
  446. std::string key = platform;
  447. if (suggestions.find(key) == suggestions.end())
  448. key = values::kKeybindingPlatformDefault;
  449. if (suggestions.find(key) == suggestions.end()) {
  450. *error = ErrorUtils::FormatErrorMessageUTF16(
  451. errors::kInvalidKeyBindingMissingPlatform, base::NumberToString(index),
  452. keys::kSuggestedKey, platform);
  453. return false; // No platform specified and no fallback. Bail.
  454. }
  455. // For developer convenience, we parse all the suggestions (and complain about
  456. // errors for platforms other than the current one) but use only what we need.
  457. std::map<const std::string, std::string>::const_iterator iter =
  458. suggestions.begin();
  459. for (; iter != suggestions.end(); ++iter) {
  460. ui::Accelerator accelerator;
  461. if (!iter->second.empty()) {
  462. // Note that we pass iter->first to pretend we are on a platform we're not
  463. // on.
  464. accelerator = ParseImpl(iter->second, iter->first, index,
  465. IsNamedCommand(command_name), error);
  466. if (accelerator.key_code() == ui::VKEY_UNKNOWN) {
  467. if (error->empty()) {
  468. *error = ErrorUtils::FormatErrorMessageUTF16(
  469. errors::kInvalidKeyBinding, base::NumberToString(index),
  470. iter->first, iter->second);
  471. }
  472. return false;
  473. }
  474. }
  475. if (iter->first == key) {
  476. // This platform is our platform, so grab this key.
  477. accelerator_ = accelerator;
  478. command_name_ = command_name;
  479. description_ = description;
  480. global_ = global;
  481. }
  482. }
  483. return true;
  484. }
  485. } // namespace extensions