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