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