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
GetPlatformKeybindingKeyForAccelerator(const ui::Accelerator & accelerator,const std::string extension_id)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
IsForCurrentPlatform(const std::string & key)59 bool IsForCurrentPlatform(const std::string& key) {
60 return StartsWithASCII(
61 key, extensions::Command::CommandPlatform() + ":", true);
62 }
63
SetInitialBindingsHaveBeenAssigned(ExtensionPrefs * prefs,const std::string & extension_id)64 void SetInitialBindingsHaveBeenAssigned(
65 ExtensionPrefs* prefs, const std::string& extension_id) {
66 prefs->UpdateExtensionPref(extension_id, kInitialBindingsHaveBeenAssigned,
67 new base::FundamentalValue(true));
68 }
69
InitialBindingsHaveBeenAssigned(const ExtensionPrefs * prefs,const std::string & extension_id)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
IsWhitelistedGlobalShortcut(const extensions::Command & command)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
RegisterProfilePrefs(user_prefs::PrefRegistrySyncable * registry)97 void CommandService::RegisterProfilePrefs(
98 user_prefs::PrefRegistrySyncable* registry) {
99 registry->RegisterDictionaryPref(
100 prefs::kExtensionCommands,
101 user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
102 }
103
CommandService(Profile * profile)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
~CommandService()115 CommandService::~CommandService() {
116 }
117
118 static base::LazyInstance<ProfileKeyedAPIFactory<CommandService> >
119 g_factory = LAZY_INSTANCE_INITIALIZER;
120
121 // static
GetFactoryInstance()122 ProfileKeyedAPIFactory<CommandService>* CommandService::GetFactoryInstance() {
123 return &g_factory.Get();
124 }
125
126 // static
Get(Profile * profile)127 CommandService* CommandService::Get(Profile* profile) {
128 return ProfileKeyedAPIFactory<CommandService>::GetForProfile(profile);
129 }
130
131 // static
IsMediaKey(const ui::Accelerator & accelerator)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
GetBrowserActionCommand(const std::string & extension_id,QueryType type,extensions::Command * command,bool * active)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
GetPageActionCommand(const std::string & extension_id,QueryType type,extensions::Command * command,bool * active)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
GetScriptBadgeCommand(const std::string & extension_id,QueryType type,extensions::Command * command,bool * active)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
GetNamedCommands(const std::string & extension_id,QueryType type,CommandScope scope,extensions::CommandMap * command_map)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
AddKeybindingPref(const ui::Accelerator & accelerator,std::string extension_id,std::string command_name,bool allow_overrides,bool global)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
Observe(int type,const content::NotificationSource & source,const content::NotificationDetails & details)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
UpdateKeybindingPrefs(const std::string & extension_id,const std::string & command_name,const std::string & keystroke)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
SetScope(const std::string & extension_id,const std::string & command_name,bool global)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
FindCommandByName(const std::string & extension_id,const std::string & command)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
AssignInitialKeybindings(const Extension * extension)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
RemoveKeybindingPrefs(const std::string & extension_id,const std::string & command_name)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
GetExtensionActionCommand(const std::string & extension_id,QueryType query_type,extensions::Command * command,bool * active,ExtensionActionType action_type)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 <>
DeclareFactoryDependencies()522 void ProfileKeyedAPIFactory<CommandService>::DeclareFactoryDependencies() {
523 DependsOn(ExtensionCommandsGlobalRegistry::GetFactoryInstance());
524 }
525
526 } // namespace extensions
527