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