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/extension_action/extension_action_api.h"
6
7#include "base/lazy_instance.h"
8#include "base/strings/string_number_conversions.h"
9#include "base/values.h"
10#include "chrome/browser/extensions/active_script_controller.h"
11#include "chrome/browser/extensions/extension_action_manager.h"
12#include "chrome/browser/extensions/extension_tab_util.h"
13#include "chrome/browser/extensions/extension_toolbar_model.h"
14#include "chrome/browser/extensions/tab_helper.h"
15#include "chrome/browser/profiles/profile.h"
16#include "chrome/browser/sessions/session_tab_helper.h"
17#include "chrome/browser/ui/browser.h"
18#include "chrome/browser/ui/browser_finder.h"
19#include "chrome/browser/ui/browser_window.h"
20#include "chrome/browser/ui/location_bar/location_bar.h"
21#include "chrome/browser/ui/tabs/tab_strip_model.h"
22#include "chrome/common/extensions/api/extension_action/action_info.h"
23#include "chrome/common/render_messages.h"
24#include "content/public/browser/notification_service.h"
25#include "extensions/browser/event_router.h"
26#include "extensions/browser/extension_function_registry.h"
27#include "extensions/browser/extension_host.h"
28#include "extensions/browser/extension_registry.h"
29#include "extensions/browser/image_util.h"
30#include "extensions/browser/notification_types.h"
31#include "extensions/common/error_utils.h"
32#include "extensions/common/feature_switch.h"
33#include "ui/gfx/image/image.h"
34#include "ui/gfx/image/image_skia.h"
35
36using content::WebContents;
37
38namespace extensions {
39
40namespace {
41
42// Whether the browser action is visible in the toolbar.
43const char kBrowserActionVisible[] = "browser_action_visible";
44
45// Errors.
46const char kNoExtensionActionError[] =
47    "This extension has no action specified.";
48const char kNoTabError[] = "No tab with id: *.";
49const char kOpenPopupError[] =
50    "Failed to show popup either because there is an existing popup or another "
51    "error occurred.";
52
53}  // namespace
54
55//
56// ExtensionActionAPI::Observer
57//
58
59void ExtensionActionAPI::Observer::OnExtensionActionUpdated(
60    ExtensionAction* extension_action,
61    content::WebContents* web_contents,
62    content::BrowserContext* browser_context) {
63}
64
65void ExtensionActionAPI::Observer::OnPageActionsUpdated(
66    content::WebContents* web_contents) {
67}
68
69void ExtensionActionAPI::Observer::OnExtensionActionAPIShuttingDown() {
70}
71
72ExtensionActionAPI::Observer::~Observer() {
73}
74
75//
76// ExtensionActionAPI
77//
78
79static base::LazyInstance<BrowserContextKeyedAPIFactory<ExtensionActionAPI> >
80    g_factory = LAZY_INSTANCE_INITIALIZER;
81
82ExtensionActionAPI::ExtensionActionAPI(content::BrowserContext* context)
83    : browser_context_(context) {
84  ExtensionFunctionRegistry* registry =
85      ExtensionFunctionRegistry::GetInstance();
86
87  // Browser Actions
88  registry->RegisterFunction<BrowserActionSetIconFunction>();
89  registry->RegisterFunction<BrowserActionSetTitleFunction>();
90  registry->RegisterFunction<BrowserActionSetBadgeTextFunction>();
91  registry->RegisterFunction<BrowserActionSetBadgeBackgroundColorFunction>();
92  registry->RegisterFunction<BrowserActionSetPopupFunction>();
93  registry->RegisterFunction<BrowserActionGetTitleFunction>();
94  registry->RegisterFunction<BrowserActionGetBadgeTextFunction>();
95  registry->RegisterFunction<BrowserActionGetBadgeBackgroundColorFunction>();
96  registry->RegisterFunction<BrowserActionGetPopupFunction>();
97  registry->RegisterFunction<BrowserActionEnableFunction>();
98  registry->RegisterFunction<BrowserActionDisableFunction>();
99  registry->RegisterFunction<BrowserActionOpenPopupFunction>();
100
101  // Page Actions
102  registry->RegisterFunction<PageActionShowFunction>();
103  registry->RegisterFunction<PageActionHideFunction>();
104  registry->RegisterFunction<PageActionSetIconFunction>();
105  registry->RegisterFunction<PageActionSetTitleFunction>();
106  registry->RegisterFunction<PageActionSetPopupFunction>();
107  registry->RegisterFunction<PageActionGetTitleFunction>();
108  registry->RegisterFunction<PageActionGetPopupFunction>();
109}
110
111ExtensionActionAPI::~ExtensionActionAPI() {
112}
113
114// static
115BrowserContextKeyedAPIFactory<ExtensionActionAPI>*
116ExtensionActionAPI::GetFactoryInstance() {
117  return g_factory.Pointer();
118}
119
120// static
121ExtensionActionAPI* ExtensionActionAPI::Get(content::BrowserContext* context) {
122  return BrowserContextKeyedAPIFactory<ExtensionActionAPI>::Get(context);
123}
124
125// static
126bool ExtensionActionAPI::GetBrowserActionVisibility(
127    const ExtensionPrefs* prefs,
128    const std::string& extension_id) {
129  bool visible = false;
130  if (!prefs || !prefs->ReadPrefAsBoolean(extension_id,
131                                          kBrowserActionVisible,
132                                          &visible)) {
133    return true;
134  }
135  return visible;
136}
137
138// static
139void ExtensionActionAPI::SetBrowserActionVisibility(
140    ExtensionPrefs* prefs,
141    const std::string& extension_id,
142    bool visible) {
143  if (GetBrowserActionVisibility(prefs, extension_id) == visible)
144    return;
145
146  prefs->UpdateExtensionPref(extension_id,
147                             kBrowserActionVisible,
148                             new base::FundamentalValue(visible));
149  content::NotificationService::current()->Notify(
150      NOTIFICATION_EXTENSION_BROWSER_ACTION_VISIBILITY_CHANGED,
151      content::Source<ExtensionPrefs>(prefs),
152      content::Details<const std::string>(&extension_id));
153}
154
155void ExtensionActionAPI::AddObserver(Observer* observer) {
156  observers_.AddObserver(observer);
157}
158
159void ExtensionActionAPI::RemoveObserver(Observer* observer) {
160  observers_.RemoveObserver(observer);
161}
162
163ExtensionAction::ShowAction ExtensionActionAPI::ExecuteExtensionAction(
164    const Extension* extension,
165    Browser* browser,
166    bool grant_active_tab_permissions) {
167  content::WebContents* web_contents =
168      browser->tab_strip_model()->GetActiveWebContents();
169  if (!web_contents)
170    return ExtensionAction::ACTION_NONE;
171
172  int tab_id = SessionTabHelper::IdForTab(web_contents);
173
174  ActiveScriptController* active_script_controller =
175      ActiveScriptController::GetForWebContents(web_contents);
176  bool has_pending_scripts = false;
177  if (active_script_controller &&
178      active_script_controller->WantsToRun(extension)) {
179    has_pending_scripts = true;
180  }
181
182  // Grant active tab if appropriate.
183  if (grant_active_tab_permissions) {
184    TabHelper::FromWebContents(web_contents)->active_tab_permission_granter()->
185        GrantIfRequested(extension);
186  }
187
188  // If this was a request to run a script, it will have been run once active
189  // tab was granted. Return without executing the action, since we should only
190  // run pending scripts OR the extension action, not both.
191  if (has_pending_scripts)
192    return ExtensionAction::ACTION_NONE;
193
194  ExtensionAction* extension_action =
195      ExtensionActionManager::Get(browser_context_)->GetExtensionAction(
196          *extension);
197
198  // Anything that gets here should have a page or browser action.
199  DCHECK(extension_action);
200  if (!extension_action->GetIsVisible(tab_id))
201    return ExtensionAction::ACTION_NONE;
202
203  if (extension_action->HasPopup(tab_id))
204    return ExtensionAction::ACTION_SHOW_POPUP;
205
206  ExtensionActionExecuted(*extension_action, web_contents);
207  return ExtensionAction::ACTION_NONE;
208}
209
210bool ExtensionActionAPI::ShowExtensionActionPopup(
211    const Extension* extension,
212    Browser* browser,
213    bool grant_active_tab_permissions) {
214  ExtensionAction* extension_action =
215      ExtensionActionManager::Get(browser_context_)->GetExtensionAction(
216          *extension);
217  if (!extension_action)
218    return false;
219
220  if (extension_action->action_type() == ActionInfo::TYPE_PAGE &&
221      !FeatureSwitch::extension_action_redesign()->IsEnabled()) {
222    // We show page actions in the location bar unless the new toolbar is
223    // enabled.
224    return browser->window()->GetLocationBar()->ShowPageActionPopup(
225        extension, grant_active_tab_permissions);
226  } else {
227    return ExtensionToolbarModel::Get(browser->profile())->
228        ShowExtensionActionPopup(
229            extension, browser, grant_active_tab_permissions);
230  }
231}
232
233void ExtensionActionAPI::NotifyChange(ExtensionAction* extension_action,
234                                      content::WebContents* web_contents,
235                                      content::BrowserContext* context) {
236  FOR_EACH_OBSERVER(
237      Observer,
238      observers_,
239      OnExtensionActionUpdated(extension_action, web_contents, context));
240
241  if (extension_action->action_type() == ActionInfo::TYPE_PAGE)
242    NotifyPageActionsChanged(web_contents);
243}
244
245void ExtensionActionAPI::ClearAllValuesForTab(
246    content::WebContents* web_contents) {
247  DCHECK(web_contents);
248  int tab_id = SessionTabHelper::IdForTab(web_contents);
249  content::BrowserContext* browser_context = web_contents->GetBrowserContext();
250  const ExtensionSet& enabled_extensions =
251      ExtensionRegistry::Get(browser_context_)->enabled_extensions();
252  ExtensionActionManager* action_manager =
253      ExtensionActionManager::Get(browser_context_);
254
255  for (ExtensionSet::const_iterator iter = enabled_extensions.begin();
256       iter != enabled_extensions.end(); ++iter) {
257    ExtensionAction* extension_action =
258        action_manager->GetBrowserAction(*iter->get());
259    if (!extension_action)
260      extension_action = action_manager->GetPageAction(*iter->get());
261    if (extension_action) {
262      extension_action->ClearAllValuesForTab(tab_id);
263      NotifyChange(extension_action, web_contents, browser_context);
264    }
265  }
266}
267
268void ExtensionActionAPI::DispatchEventToExtension(
269    content::BrowserContext* context,
270    const std::string& extension_id,
271    const std::string& event_name,
272    scoped_ptr<base::ListValue> event_args) {
273  if (!EventRouter::Get(context))
274    return;
275
276  scoped_ptr<Event> event(new Event(event_name, event_args.Pass()));
277  event->restrict_to_browser_context = context;
278  event->user_gesture = EventRouter::USER_GESTURE_ENABLED;
279  EventRouter::Get(context)
280      ->DispatchEventToExtension(extension_id, event.Pass());
281}
282
283void ExtensionActionAPI::ExtensionActionExecuted(
284    const ExtensionAction& extension_action,
285    WebContents* web_contents) {
286  const char* event_name = NULL;
287  switch (extension_action.action_type()) {
288    case ActionInfo::TYPE_BROWSER:
289      event_name = "browserAction.onClicked";
290      break;
291    case ActionInfo::TYPE_PAGE:
292      event_name = "pageAction.onClicked";
293      break;
294    case ActionInfo::TYPE_SYSTEM_INDICATOR:
295      // The System Indicator handles its own clicks.
296      break;
297  }
298
299  if (event_name) {
300    scoped_ptr<base::ListValue> args(new base::ListValue());
301    base::DictionaryValue* tab_value =
302        ExtensionTabUtil::CreateTabValue(web_contents);
303    args->Append(tab_value);
304
305    DispatchEventToExtension(
306        web_contents->GetBrowserContext(),
307        extension_action.extension_id(),
308        event_name,
309        args.Pass());
310  }
311}
312
313void ExtensionActionAPI::NotifyPageActionsChanged(
314    content::WebContents* web_contents) {
315  Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
316  if (!browser)
317    return;
318  LocationBar* location_bar =
319      browser->window() ? browser->window()->GetLocationBar() : NULL;
320  if (!location_bar)
321    return;
322  location_bar->UpdatePageActions();
323
324  FOR_EACH_OBSERVER(Observer, observers_, OnPageActionsUpdated(web_contents));
325}
326
327void ExtensionActionAPI::Shutdown() {
328  FOR_EACH_OBSERVER(Observer, observers_, OnExtensionActionAPIShuttingDown());
329}
330
331//
332// ExtensionActionFunction
333//
334
335ExtensionActionFunction::ExtensionActionFunction()
336    : details_(NULL),
337      tab_id_(ExtensionAction::kDefaultTabId),
338      contents_(NULL),
339      extension_action_(NULL) {
340}
341
342ExtensionActionFunction::~ExtensionActionFunction() {
343}
344
345bool ExtensionActionFunction::RunSync() {
346  ExtensionActionManager* manager = ExtensionActionManager::Get(GetProfile());
347  if (StartsWithASCII(name(), "systemIndicator.", false)) {
348    extension_action_ = manager->GetSystemIndicator(*extension());
349  } else {
350    extension_action_ = manager->GetBrowserAction(*extension());
351    if (!extension_action_) {
352      extension_action_ = manager->GetPageAction(*extension());
353    }
354  }
355  if (!extension_action_) {
356    // TODO(kalman): ideally the browserAction/pageAction APIs wouldn't event
357    // exist for extensions that don't have one declared. This should come as
358    // part of the Feature system.
359    error_ = kNoExtensionActionError;
360    return false;
361  }
362
363  // Populates the tab_id_ and details_ members.
364  EXTENSION_FUNCTION_VALIDATE(ExtractDataFromArguments());
365
366  // Find the WebContents that contains this tab id if one is required.
367  if (tab_id_ != ExtensionAction::kDefaultTabId) {
368    ExtensionTabUtil::GetTabById(tab_id_,
369                                 GetProfile(),
370                                 include_incognito(),
371                                 NULL,
372                                 NULL,
373                                 &contents_,
374                                 NULL);
375    if (!contents_) {
376      error_ = ErrorUtils::FormatErrorMessage(
377          kNoTabError, base::IntToString(tab_id_));
378      return false;
379    }
380  } else {
381    // Only browser actions and system indicators have a default tabId.
382    ActionInfo::Type action_type = extension_action_->action_type();
383    EXTENSION_FUNCTION_VALIDATE(
384        action_type == ActionInfo::TYPE_BROWSER ||
385        action_type == ActionInfo::TYPE_SYSTEM_INDICATOR);
386  }
387  return RunExtensionAction();
388}
389
390bool ExtensionActionFunction::ExtractDataFromArguments() {
391  // There may or may not be details (depends on the function).
392  // The tabId might appear in details (if it exists), as the first
393  // argument besides the action type (depends on the function), or be omitted
394  // entirely.
395  base::Value* first_arg = NULL;
396  if (!args_->Get(0, &first_arg))
397    return true;
398
399  switch (first_arg->GetType()) {
400    case base::Value::TYPE_INTEGER:
401      CHECK(first_arg->GetAsInteger(&tab_id_));
402      break;
403
404    case base::Value::TYPE_DICTIONARY: {
405      // Found the details argument.
406      details_ = static_cast<base::DictionaryValue*>(first_arg);
407      // Still need to check for the tabId within details.
408      base::Value* tab_id_value = NULL;
409      if (details_->Get("tabId", &tab_id_value)) {
410        switch (tab_id_value->GetType()) {
411          case base::Value::TYPE_NULL:
412            // OK; tabId is optional, leave it default.
413            return true;
414          case base::Value::TYPE_INTEGER:
415            CHECK(tab_id_value->GetAsInteger(&tab_id_));
416            return true;
417          default:
418            // Boom.
419            return false;
420        }
421      }
422      // Not found; tabId is optional, leave it default.
423      break;
424    }
425
426    case base::Value::TYPE_NULL:
427      // The tabId might be an optional argument.
428      break;
429
430    default:
431      return false;
432  }
433
434  return true;
435}
436
437void ExtensionActionFunction::NotifyChange() {
438  ExtensionActionAPI::Get(GetProfile())->NotifyChange(
439      extension_action_, contents_, GetProfile());
440}
441
442bool ExtensionActionFunction::SetVisible(bool visible) {
443  if (extension_action_->GetIsVisible(tab_id_) == visible)
444    return true;
445  extension_action_->SetIsVisible(tab_id_, visible);
446  NotifyChange();
447  return true;
448}
449
450bool ExtensionActionShowFunction::RunExtensionAction() {
451  return SetVisible(true);
452}
453
454bool ExtensionActionHideFunction::RunExtensionAction() {
455  return SetVisible(false);
456}
457
458bool ExtensionActionSetIconFunction::RunExtensionAction() {
459  EXTENSION_FUNCTION_VALIDATE(details_);
460
461  // setIcon can take a variant argument: either a dictionary of canvas
462  // ImageData, or an icon index.
463  base::DictionaryValue* canvas_set = NULL;
464  int icon_index;
465  if (details_->GetDictionary("imageData", &canvas_set)) {
466    gfx::ImageSkia icon;
467
468    EXTENSION_FUNCTION_VALIDATE(
469        ExtensionAction::ParseIconFromCanvasDictionary(*canvas_set, &icon));
470
471    extension_action_->SetIcon(tab_id_, gfx::Image(icon));
472  } else if (details_->GetInteger("iconIndex", &icon_index)) {
473    // Obsolete argument: ignore it.
474    return true;
475  } else {
476    EXTENSION_FUNCTION_VALIDATE(false);
477  }
478  NotifyChange();
479  return true;
480}
481
482bool ExtensionActionSetTitleFunction::RunExtensionAction() {
483  EXTENSION_FUNCTION_VALIDATE(details_);
484  std::string title;
485  EXTENSION_FUNCTION_VALIDATE(details_->GetString("title", &title));
486  extension_action_->SetTitle(tab_id_, title);
487  NotifyChange();
488  return true;
489}
490
491bool ExtensionActionSetPopupFunction::RunExtensionAction() {
492  EXTENSION_FUNCTION_VALIDATE(details_);
493  std::string popup_string;
494  EXTENSION_FUNCTION_VALIDATE(details_->GetString("popup", &popup_string));
495
496  GURL popup_url;
497  if (!popup_string.empty())
498    popup_url = extension()->GetResourceURL(popup_string);
499
500  extension_action_->SetPopupUrl(tab_id_, popup_url);
501  NotifyChange();
502  return true;
503}
504
505bool ExtensionActionSetBadgeTextFunction::RunExtensionAction() {
506  EXTENSION_FUNCTION_VALIDATE(details_);
507  std::string badge_text;
508  EXTENSION_FUNCTION_VALIDATE(details_->GetString("text", &badge_text));
509  extension_action_->SetBadgeText(tab_id_, badge_text);
510  NotifyChange();
511  return true;
512}
513
514bool ExtensionActionSetBadgeBackgroundColorFunction::RunExtensionAction() {
515  EXTENSION_FUNCTION_VALIDATE(details_);
516  base::Value* color_value = NULL;
517  EXTENSION_FUNCTION_VALIDATE(details_->Get("color", &color_value));
518  SkColor color = 0;
519  if (color_value->IsType(base::Value::TYPE_LIST)) {
520    base::ListValue* list = NULL;
521    EXTENSION_FUNCTION_VALIDATE(details_->GetList("color", &list));
522    EXTENSION_FUNCTION_VALIDATE(list->GetSize() == 4);
523
524    int color_array[4] = {0};
525    for (size_t i = 0; i < arraysize(color_array); ++i) {
526      EXTENSION_FUNCTION_VALIDATE(list->GetInteger(i, &color_array[i]));
527    }
528
529    color = SkColorSetARGB(color_array[3], color_array[0],
530                           color_array[1], color_array[2]);
531  } else if (color_value->IsType(base::Value::TYPE_STRING)) {
532    std::string color_string;
533    EXTENSION_FUNCTION_VALIDATE(details_->GetString("color", &color_string));
534    if (!image_util::ParseCSSColorString(color_string, &color))
535      return false;
536  }
537
538  extension_action_->SetBadgeBackgroundColor(tab_id_, color);
539  NotifyChange();
540  return true;
541}
542
543bool ExtensionActionGetTitleFunction::RunExtensionAction() {
544  SetResult(new base::StringValue(extension_action_->GetTitle(tab_id_)));
545  return true;
546}
547
548bool ExtensionActionGetPopupFunction::RunExtensionAction() {
549  SetResult(
550      new base::StringValue(extension_action_->GetPopupUrl(tab_id_).spec()));
551  return true;
552}
553
554bool ExtensionActionGetBadgeTextFunction::RunExtensionAction() {
555  SetResult(new base::StringValue(extension_action_->GetBadgeText(tab_id_)));
556  return true;
557}
558
559bool ExtensionActionGetBadgeBackgroundColorFunction::RunExtensionAction() {
560  base::ListValue* list = new base::ListValue();
561  SkColor color = extension_action_->GetBadgeBackgroundColor(tab_id_);
562  list->Append(
563      new base::FundamentalValue(static_cast<int>(SkColorGetR(color))));
564  list->Append(
565      new base::FundamentalValue(static_cast<int>(SkColorGetG(color))));
566  list->Append(
567      new base::FundamentalValue(static_cast<int>(SkColorGetB(color))));
568  list->Append(
569      new base::FundamentalValue(static_cast<int>(SkColorGetA(color))));
570  SetResult(list);
571  return true;
572}
573
574BrowserActionOpenPopupFunction::BrowserActionOpenPopupFunction()
575    : response_sent_(false) {
576}
577
578bool BrowserActionOpenPopupFunction::RunAsync() {
579  // We only allow the popup in the active window.
580  Browser* browser = chrome::FindLastActiveWithProfile(
581                         GetProfile(), chrome::GetActiveDesktop());
582
583  // If there's no active browser, or the Toolbar isn't visible, abort.
584  // Otherwise, try to open a popup in the active browser.
585  // TODO(justinlin): Remove toolbar check when http://crbug.com/308645 is
586  // fixed.
587  if (!browser ||
588      !browser->window()->IsActive() ||
589      !browser->window()->IsToolbarVisible() ||
590      !ExtensionActionAPI::Get(GetProfile())->ShowExtensionActionPopup(
591          extension_.get(), browser, false)) {
592    error_ = kOpenPopupError;
593    return false;
594  }
595
596  registrar_.Add(this,
597                 NOTIFICATION_EXTENSION_HOST_DID_STOP_LOADING,
598                 content::Source<Profile>(GetProfile()));
599
600  // Set a timeout for waiting for the notification that the popup is loaded.
601  // Waiting is required so that the popup view can be retrieved by the custom
602  // bindings for the response callback. It's also needed to keep this function
603  // instance around until a notification is observed.
604  base::MessageLoopForUI::current()->PostDelayedTask(
605      FROM_HERE,
606      base::Bind(&BrowserActionOpenPopupFunction::OpenPopupTimedOut, this),
607      base::TimeDelta::FromSeconds(10));
608  return true;
609}
610
611void BrowserActionOpenPopupFunction::OpenPopupTimedOut() {
612  if (response_sent_)
613    return;
614
615  DVLOG(1) << "chrome.browserAction.openPopup did not show a popup.";
616  error_ = kOpenPopupError;
617  SendResponse(false);
618  response_sent_ = true;
619}
620
621void BrowserActionOpenPopupFunction::Observe(
622    int type,
623    const content::NotificationSource& source,
624    const content::NotificationDetails& details) {
625  DCHECK_EQ(NOTIFICATION_EXTENSION_HOST_DID_STOP_LOADING, type);
626  if (response_sent_)
627    return;
628
629  ExtensionHost* host = content::Details<ExtensionHost>(details).ptr();
630  if (host->extension_host_type() != VIEW_TYPE_EXTENSION_POPUP ||
631      host->extension()->id() != extension_->id())
632    return;
633
634  SendResponse(true);
635  response_sent_ = true;
636  registrar_.RemoveAll();
637}
638
639}  // namespace extensions
640