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