Home | History | Annotate | Download | only in commands
      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/browser/extensions/api/commands/command_service.h"
      6 
      7 #include <vector>
      8 
      9 #include "base/lazy_instance.h"
     10 #include "base/prefs/scoped_user_pref_update.h"
     11 #include "base/strings/string_split.h"
     12 #include "base/strings/string_util.h"
     13 #include "base/strings/utf_string_conversions.h"
     14 #include "chrome/browser/chrome_notification_types.h"
     15 #include "chrome/browser/extensions/api/commands/commands.h"
     16 #include "chrome/browser/extensions/extension_commands_global_registry.h"
     17 #include "chrome/browser/extensions/extension_function_registry.h"
     18 #include "chrome/browser/extensions/extension_keybinding_registry.h"
     19 #include "chrome/browser/extensions/extension_service.h"
     20 #include "chrome/browser/extensions/extension_system.h"
     21 #include "chrome/browser/profiles/profile.h"
     22 #include "chrome/browser/ui/accelerator_utils.h"
     23 #include "chrome/common/extensions/api/commands/commands_handler.h"
     24 #include "chrome/common/pref_names.h"
     25 #include "components/user_prefs/pref_registry_syncable.h"
     26 #include "content/public/browser/notification_details.h"
     27 #include "content/public/browser/notification_service.h"
     28 #include "extensions/common/feature_switch.h"
     29 #include "extensions/common/manifest_constants.h"
     30 
     31 using extensions::Extension;
     32 using extensions::ExtensionPrefs;
     33 
     34 namespace {
     35 
     36 const char kExtension[] = "extension";
     37 const char kCommandName[] = "command_name";
     38 const char kGlobal[] = "global";
     39 
     40 // A preference that indicates that the initial keybindings for the given
     41 // extension have been set.
     42 const char kInitialBindingsHaveBeenAssigned[] = "initial_keybindings_set";
     43 
     44 std::string GetPlatformKeybindingKeyForAccelerator(
     45     const ui::Accelerator& accelerator, const std::string extension_id) {
     46   std::string key = extensions::Command::CommandPlatform() + ":" +
     47                     extensions::Command::AcceleratorToString(accelerator);
     48 
     49   // Media keys have a 1-to-many relationship with targets, unlike regular
     50   // shortcut (1-to-1 relationship). That means two or more extensions can
     51   // register for the same media key so the extension ID needs to be added to
     52   // the key to make sure the key is unique.
     53   if (extensions::CommandService::IsMediaKey(accelerator))
     54     key += ":" + extension_id;
     55 
     56   return key;
     57 }
     58 
     59 bool IsForCurrentPlatform(const std::string& key) {
     60   return StartsWithASCII(
     61       key, extensions::Command::CommandPlatform() + ":", true);
     62 }
     63 
     64 void SetInitialBindingsHaveBeenAssigned(
     65     ExtensionPrefs* prefs, const std::string& extension_id) {
     66   prefs->UpdateExtensionPref(extension_id, kInitialBindingsHaveBeenAssigned,
     67                              new base::FundamentalValue(true));
     68 }
     69 
     70 bool InitialBindingsHaveBeenAssigned(
     71     const ExtensionPrefs* prefs, const std::string& extension_id) {
     72   bool assigned = false;
     73   if (!prefs || !prefs->ReadPrefAsBoolean(extension_id,
     74                                           kInitialBindingsHaveBeenAssigned,
     75                                           &assigned))
     76     return false;
     77 
     78   return assigned;
     79 }
     80 
     81 bool IsWhitelistedGlobalShortcut(const extensions::Command& command) {
     82   if (!command.global())
     83     return true;
     84   if (!command.accelerator().IsCtrlDown())
     85     return false;
     86   if (!command.accelerator().IsShiftDown())
     87     return false;
     88   return (command.accelerator().key_code() >= ui::VKEY_0 &&
     89           command.accelerator().key_code() <= ui::VKEY_9);
     90 }
     91 
     92 }  // namespace
     93 
     94 namespace extensions {
     95 
     96 // static
     97 void CommandService::RegisterProfilePrefs(
     98     user_prefs::PrefRegistrySyncable* registry) {
     99   registry->RegisterDictionaryPref(
    100       prefs::kExtensionCommands,
    101       user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
    102 }
    103 
    104 CommandService::CommandService(Profile* profile)
    105     : profile_(profile) {
    106   ExtensionFunctionRegistry::GetInstance()->
    107       RegisterFunction<GetAllCommandsFunction>();
    108 
    109   registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_INSTALLED,
    110       content::Source<Profile>(profile));
    111   registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNINSTALLED,
    112       content::Source<Profile>(profile));
    113 }
    114 
    115 CommandService::~CommandService() {
    116 }
    117 
    118 static base::LazyInstance<ProfileKeyedAPIFactory<CommandService> >
    119 g_factory = LAZY_INSTANCE_INITIALIZER;
    120 
    121 // static
    122 ProfileKeyedAPIFactory<CommandService>* CommandService::GetFactoryInstance() {
    123   return &g_factory.Get();
    124 }
    125 
    126 // static
    127 CommandService* CommandService::Get(Profile* profile) {
    128   return ProfileKeyedAPIFactory<CommandService>::GetForProfile(profile);
    129 }
    130 
    131 // static
    132 bool CommandService::IsMediaKey(const ui::Accelerator& accelerator) {
    133   if (accelerator.modifiers() != 0)
    134     return false;
    135 
    136   return (accelerator.key_code() == ui::VKEY_MEDIA_NEXT_TRACK ||
    137           accelerator.key_code() == ui::VKEY_MEDIA_PREV_TRACK ||
    138           accelerator.key_code() == ui::VKEY_MEDIA_PLAY_PAUSE ||
    139           accelerator.key_code() == ui::VKEY_MEDIA_STOP);
    140 }
    141 
    142 bool CommandService::GetBrowserActionCommand(
    143     const std::string& extension_id,
    144     QueryType type,
    145     extensions::Command* command,
    146     bool* active) {
    147   return GetExtensionActionCommand(
    148       extension_id, type, command, active, BROWSER_ACTION);
    149 }
    150 
    151 bool CommandService::GetPageActionCommand(
    152     const std::string& extension_id,
    153     QueryType type,
    154     extensions::Command* command,
    155     bool* active) {
    156   return GetExtensionActionCommand(
    157       extension_id, type, command, active, PAGE_ACTION);
    158 }
    159 
    160 bool CommandService::GetScriptBadgeCommand(
    161     const std::string& extension_id,
    162     QueryType type,
    163     extensions::Command* command,
    164     bool* active) {
    165   return GetExtensionActionCommand(
    166       extension_id, type, command, active, SCRIPT_BADGE);
    167 }
    168 
    169 bool CommandService::GetNamedCommands(const std::string& extension_id,
    170                                       QueryType type,
    171                                       CommandScope scope,
    172                                       extensions::CommandMap* command_map) {
    173   ExtensionService* extension_service =
    174       ExtensionSystem::Get(profile_)->extension_service();
    175   if (!extension_service)
    176     return false;  // Can occur during testing.
    177   const ExtensionSet* extensions = extension_service->extensions();
    178   const Extension* extension = extensions->GetByID(extension_id);
    179   CHECK(extension);
    180 
    181   command_map->clear();
    182   const extensions::CommandMap* commands =
    183       CommandsInfo::GetNamedCommands(extension);
    184   if (!commands)
    185     return false;
    186 
    187   extensions::CommandMap::const_iterator iter = commands->begin();
    188   for (; iter != commands->end(); ++iter) {
    189     // Look up to see if the user has overridden how the command should work.
    190     extensions::Command saved_command =
    191         FindCommandByName(extension_id, iter->second.command_name());
    192     ui::Accelerator shortcut_assigned = saved_command.accelerator();
    193 
    194     if (type == ACTIVE_ONLY && shortcut_assigned.key_code() == ui::VKEY_UNKNOWN)
    195       continue;
    196 
    197     extensions::Command command = iter->second;
    198     if (scope != ANY_SCOPE && ((scope == GLOBAL) != saved_command.global()))
    199       continue;
    200 
    201     if (shortcut_assigned.key_code() != ui::VKEY_UNKNOWN)
    202       command.set_accelerator(shortcut_assigned);
    203     command.set_global(saved_command.global());
    204 
    205     (*command_map)[iter->second.command_name()] = command;
    206   }
    207 
    208   return true;
    209 }
    210 
    211 bool CommandService::AddKeybindingPref(
    212     const ui::Accelerator& accelerator,
    213     std::string extension_id,
    214     std::string command_name,
    215     bool allow_overrides,
    216     bool global) {
    217   if (accelerator.key_code() == ui::VKEY_UNKNOWN)
    218     return false;
    219 
    220   // Media Keys are allowed to be used by named command only.
    221   DCHECK(!IsMediaKey(accelerator) ||
    222          (command_name != manifest_values::kPageActionCommandEvent &&
    223           command_name != manifest_values::kBrowserActionCommandEvent &&
    224           command_name != manifest_values::kScriptBadgeCommandEvent));
    225 
    226   DictionaryPrefUpdate updater(profile_->GetPrefs(),
    227                                prefs::kExtensionCommands);
    228   base::DictionaryValue* bindings = updater.Get();
    229 
    230   std::string key = GetPlatformKeybindingKeyForAccelerator(accelerator,
    231                                                            extension_id);
    232 
    233   if (bindings->HasKey(key)) {
    234     if (!allow_overrides)
    235       return false;  // Already taken.
    236 
    237     // If the shortcut has been assigned to another command, it should be
    238     // removed before overriding, so that |ExtensionKeybindingRegistry| can get
    239     // a chance to do clean-up.
    240     const base::DictionaryValue* item = NULL;
    241     bindings->GetDictionary(key, &item);
    242     std::string old_extension_id;
    243     std::string old_command_name;
    244     item->GetString(kExtension, &old_extension_id);
    245     item->GetString(kCommandName, &old_command_name);
    246     RemoveKeybindingPrefs(old_extension_id, old_command_name);
    247   }
    248 
    249   base::DictionaryValue* keybinding = new base::DictionaryValue();
    250   keybinding->SetString(kExtension, extension_id);
    251   keybinding->SetString(kCommandName, command_name);
    252   keybinding->SetBoolean(kGlobal, global);
    253 
    254   bindings->Set(key, keybinding);
    255 
    256   std::pair<const std::string, const std::string> details =
    257       std::make_pair(extension_id, command_name);
    258   content::NotificationService::current()->Notify(
    259       chrome::NOTIFICATION_EXTENSION_COMMAND_ADDED,
    260       content::Source<Profile>(profile_),
    261       content::Details<
    262           std::pair<const std::string, const std::string> >(&details));
    263 
    264   return true;
    265 }
    266 
    267 void CommandService::Observe(
    268     int type,
    269     const content::NotificationSource& source,
    270     const content::NotificationDetails& details) {
    271   switch (type) {
    272     case chrome::NOTIFICATION_EXTENSION_INSTALLED:
    273       AssignInitialKeybindings(
    274           content::Details<const InstalledExtensionInfo>(details)->extension);
    275       break;
    276     case chrome::NOTIFICATION_EXTENSION_UNINSTALLED:
    277       RemoveKeybindingPrefs(
    278           content::Details<const Extension>(details)->id(),
    279           std::string());
    280       break;
    281     default:
    282       NOTREACHED();
    283       break;
    284   }
    285 }
    286 
    287 void CommandService::UpdateKeybindingPrefs(const std::string& extension_id,
    288                                            const std::string& command_name,
    289                                            const std::string& keystroke) {
    290   extensions::Command command = FindCommandByName(extension_id, command_name);
    291 
    292   // The extension command might be assigned another shortcut. Remove that
    293   // shortcut before proceeding.
    294   RemoveKeybindingPrefs(extension_id, command_name);
    295 
    296   ui::Accelerator accelerator =
    297       Command::StringToAccelerator(keystroke, command_name);
    298   AddKeybindingPref(accelerator, extension_id, command_name,
    299                     true, command.global());
    300 }
    301 
    302 bool CommandService::SetScope(const std::string& extension_id,
    303                               const std::string& command_name,
    304                               bool global) {
    305   extensions::Command command = FindCommandByName(extension_id, command_name);
    306   if (global == command.global())
    307     return false;
    308 
    309   // Pre-existing shortcuts must be removed before proceeding because the
    310   // handlers for global and non-global extensions are not one and the same.
    311   RemoveKeybindingPrefs(extension_id, command_name);
    312   AddKeybindingPref(command.accelerator(), extension_id,
    313                     command_name, true, global);
    314   return true;
    315 }
    316 
    317 Command CommandService::FindCommandByName(
    318     const std::string& extension_id, const std::string& command) {
    319   const base::DictionaryValue* bindings =
    320       profile_->GetPrefs()->GetDictionary(prefs::kExtensionCommands);
    321   for (base::DictionaryValue::Iterator it(*bindings); !it.IsAtEnd();
    322        it.Advance()) {
    323     const base::DictionaryValue* item = NULL;
    324     it.value().GetAsDictionary(&item);
    325 
    326     std::string extension;
    327     item->GetString(kExtension, &extension);
    328     if (extension != extension_id)
    329       continue;
    330     std::string command_name;
    331     item->GetString(kCommandName, &command_name);
    332     if (command != command_name)
    333       continue;
    334     // Format stored in Preferences is: "Platform:Shortcut[:ExtensionId]".
    335     std::string shortcut = it.key();
    336     if (!IsForCurrentPlatform(shortcut))
    337       continue;
    338     bool global = false;
    339     if (FeatureSwitch::global_commands()->IsEnabled())
    340       item->GetBoolean(kGlobal, &global);
    341 
    342     std::vector<std::string> tokens;
    343     base::SplitString(shortcut, ':', &tokens);
    344     CHECK(tokens.size() >= 2);
    345     shortcut = tokens[1];
    346 
    347     return Command(command_name, base::string16(), shortcut, global);
    348   }
    349 
    350   return Command();
    351 }
    352 
    353 void CommandService::AssignInitialKeybindings(const Extension* extension) {
    354   const extensions::CommandMap* commands =
    355       CommandsInfo::GetNamedCommands(extension);
    356   if (!commands)
    357     return;
    358 
    359   ExtensionService* extension_service =
    360       ExtensionSystem::Get(profile_)->extension_service();
    361   ExtensionPrefs* extension_prefs = extension_service->extension_prefs();
    362   if (InitialBindingsHaveBeenAssigned(extension_prefs, extension->id()))
    363     return;
    364   SetInitialBindingsHaveBeenAssigned(extension_prefs, extension->id());
    365 
    366   extensions::CommandMap::const_iterator iter = commands->begin();
    367   for (; iter != commands->end(); ++iter) {
    368     // Make sure registered Chrome shortcuts cannot be automatically assigned
    369     // (overwritten) by extension developers. Media keys are an exception here.
    370     if ((!chrome::IsChromeAccelerator(iter->second.accelerator(), profile_) &&
    371         IsWhitelistedGlobalShortcut(iter->second)) ||
    372         extensions::CommandService::IsMediaKey(iter->second.accelerator())) {
    373       AddKeybindingPref(iter->second.accelerator(),
    374                         extension->id(),
    375                         iter->second.command_name(),
    376                         false,  // Overwriting not allowed.
    377                         iter->second.global());
    378     }
    379   }
    380 
    381   const extensions::Command* browser_action_command =
    382       CommandsInfo::GetBrowserActionCommand(extension);
    383   if (browser_action_command) {
    384     if (!chrome::IsChromeAccelerator(
    385         browser_action_command->accelerator(), profile_)) {
    386       AddKeybindingPref(browser_action_command->accelerator(),
    387                         extension->id(),
    388                         browser_action_command->command_name(),
    389                         false,   // Overwriting not allowed.
    390                         false);  // Browser actions can't be global.
    391     }
    392   }
    393 
    394   const extensions::Command* page_action_command =
    395       CommandsInfo::GetPageActionCommand(extension);
    396   if (page_action_command) {
    397     if (!chrome::IsChromeAccelerator(
    398         page_action_command->accelerator(), profile_)) {
    399       AddKeybindingPref(page_action_command->accelerator(),
    400                         extension->id(),
    401                         page_action_command->command_name(),
    402                         false,   // Overwriting not allowed.
    403                         false);  // Page actions can't be global.
    404     }
    405   }
    406 
    407   const extensions::Command* script_badge_command =
    408       CommandsInfo::GetScriptBadgeCommand(extension);
    409   if (script_badge_command) {
    410     if (!chrome::IsChromeAccelerator(
    411         script_badge_command->accelerator(), profile_)) {
    412       AddKeybindingPref(script_badge_command->accelerator(),
    413                         extension->id(),
    414                         script_badge_command->command_name(),
    415                         false,   // Overwriting not allowed.
    416                         false);  // Script badges can't be global.
    417     }
    418   }
    419 }
    420 
    421 void CommandService::RemoveKeybindingPrefs(const std::string& extension_id,
    422                                            const std::string& command_name) {
    423   DictionaryPrefUpdate updater(profile_->GetPrefs(),
    424                                prefs::kExtensionCommands);
    425   base::DictionaryValue* bindings = updater.Get();
    426 
    427   typedef std::vector<std::string> KeysToRemove;
    428   KeysToRemove keys_to_remove;
    429   for (base::DictionaryValue::Iterator it(*bindings); !it.IsAtEnd();
    430        it.Advance()) {
    431     // Removal of keybinding preference should be limited to current platform.
    432     if (!IsForCurrentPlatform(it.key()))
    433       continue;
    434 
    435     const base::DictionaryValue* item = NULL;
    436     it.value().GetAsDictionary(&item);
    437 
    438     std::string extension;
    439     item->GetString(kExtension, &extension);
    440 
    441     if (extension == extension_id) {
    442       // If |command_name| is specified, delete only that command. Otherwise,
    443       // delete all commands.
    444       if (!command_name.empty()) {
    445         std::string command;
    446         item->GetString(kCommandName, &command);
    447         if (command_name != command)
    448           continue;
    449       }
    450 
    451       keys_to_remove.push_back(it.key());
    452     }
    453   }
    454 
    455   for (KeysToRemove::const_iterator it = keys_to_remove.begin();
    456        it != keys_to_remove.end(); ++it) {
    457     std::string key = *it;
    458     bindings->Remove(key, NULL);
    459 
    460     std::pair<const std::string, const std::string> details =
    461         std::make_pair(extension_id, command_name);
    462     content::NotificationService::current()->Notify(
    463         chrome::NOTIFICATION_EXTENSION_COMMAND_REMOVED,
    464         content::Source<Profile>(profile_),
    465         content::Details<
    466             std::pair<const std::string, const std::string> >(&details));
    467   }
    468 }
    469 
    470 bool CommandService::GetExtensionActionCommand(
    471     const std::string& extension_id,
    472     QueryType query_type,
    473     extensions::Command* command,
    474     bool* active,
    475     ExtensionActionType action_type) {
    476   ExtensionService* service =
    477       ExtensionSystem::Get(profile_)->extension_service();
    478   if (!service)
    479     return false;  // Can happen in tests.
    480   const ExtensionSet* extensions = service->extensions();
    481   const Extension* extension = extensions->GetByID(extension_id);
    482   CHECK(extension);
    483 
    484   if (active)
    485     *active = false;
    486 
    487   const extensions::Command* requested_command = NULL;
    488   switch (action_type) {
    489     case BROWSER_ACTION:
    490       requested_command = CommandsInfo::GetBrowserActionCommand(extension);
    491       break;
    492     case PAGE_ACTION:
    493       requested_command = CommandsInfo::GetPageActionCommand(extension);
    494       break;
    495     case SCRIPT_BADGE:
    496       requested_command = CommandsInfo::GetScriptBadgeCommand(extension);
    497       break;
    498   }
    499   if (!requested_command)
    500     return false;
    501 
    502   // Look up to see if the user has overridden how the command should work.
    503   extensions::Command saved_command =
    504       FindCommandByName(extension_id, requested_command->command_name());
    505   ui::Accelerator shortcut_assigned = saved_command.accelerator();
    506 
    507   if (active)
    508     *active = (shortcut_assigned.key_code() != ui::VKEY_UNKNOWN);
    509 
    510   if (query_type == ACTIVE_ONLY &&
    511       shortcut_assigned.key_code() == ui::VKEY_UNKNOWN)
    512     return false;
    513 
    514   *command = *requested_command;
    515   if (shortcut_assigned.key_code() != ui::VKEY_UNKNOWN)
    516     command->set_accelerator(shortcut_assigned);
    517 
    518   return true;
    519 }
    520 
    521 template <>
    522 void ProfileKeyedAPIFactory<CommandService>::DeclareFactoryDependencies() {
    523   DependsOn(ExtensionCommandsGlobalRegistry::GetFactoryInstance());
    524 }
    525 
    526 }  // namespace extensions
    527