// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/extensions/api/extension_action/extension_action_api.h" #include #include #include "base/lazy_instance.h" #include "base/location.h" #include "base/macros.h" #include "base/single_thread_task_runner.h" #include "base/strings/string_number_conversions.h" #include "base/thread_task_runner_handle.h" #include "base/values.h" #include "chrome/browser/extensions/extension_action_manager.h" #include "chrome/browser/extensions/extension_action_runner.h" #include "chrome/browser/extensions/extension_tab_util.h" #include "chrome/browser/extensions/extension_util.h" #include "chrome/browser/extensions/tab_helper.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/sessions/session_tab_helper.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_finder.h" #include "chrome/browser/ui/browser_window.h" #include "chrome/browser/ui/location_bar/location_bar.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/browser/ui/toolbar/toolbar_actions_bar.h" #include "chrome/common/extensions/api/extension_action/action_info.h" #include "content/public/browser/notification_service.h" #include "extensions/browser/event_router.h" #include "extensions/browser/extension_function_registry.h" #include "extensions/browser/extension_host.h" #include "extensions/browser/extension_registry.h" #include "extensions/browser/notification_types.h" #include "extensions/common/error_utils.h" #include "extensions/common/feature_switch.h" #include "extensions/common/image_util.h" #include "ui/gfx/image/image.h" #include "ui/gfx/image/image_skia.h" using content::WebContents; namespace extensions { namespace { // Whether the browser action is visible in the toolbar. const char kBrowserActionVisible[] = "browser_action_visible"; // Errors. const char kNoExtensionActionError[] = "This extension has no action specified."; const char kNoTabError[] = "No tab with id: *."; const char kOpenPopupError[] = "Failed to show popup either because there is an existing popup or another " "error occurred."; const char kInvalidColorError[] = "The color specification could not be parsed."; } // namespace // // ExtensionActionAPI::Observer // void ExtensionActionAPI::Observer::OnExtensionActionUpdated( ExtensionAction* extension_action, content::WebContents* web_contents, content::BrowserContext* browser_context) { } void ExtensionActionAPI::Observer::OnExtensionActionVisibilityChanged( const std::string& extension_id, bool is_now_visible) { } void ExtensionActionAPI::Observer::OnPageActionsUpdated( content::WebContents* web_contents) { } void ExtensionActionAPI::Observer::OnExtensionActionAPIShuttingDown() { } ExtensionActionAPI::Observer::~Observer() { } // // ExtensionActionAPI // static base::LazyInstance > g_factory = LAZY_INSTANCE_INITIALIZER; ExtensionActionAPI::ExtensionActionAPI(content::BrowserContext* context) : browser_context_(context), extension_prefs_(nullptr) { ExtensionFunctionRegistry* registry = ExtensionFunctionRegistry::GetInstance(); // Browser Actions registry->RegisterFunction(); registry->RegisterFunction(); registry->RegisterFunction(); registry->RegisterFunction(); registry->RegisterFunction(); registry->RegisterFunction(); registry->RegisterFunction(); registry->RegisterFunction(); registry->RegisterFunction(); registry->RegisterFunction(); registry->RegisterFunction(); registry->RegisterFunction(); // Page Actions registry->RegisterFunction(); registry->RegisterFunction(); registry->RegisterFunction(); registry->RegisterFunction(); registry->RegisterFunction(); registry->RegisterFunction(); registry->RegisterFunction(); } ExtensionActionAPI::~ExtensionActionAPI() { } // static BrowserContextKeyedAPIFactory* ExtensionActionAPI::GetFactoryInstance() { return g_factory.Pointer(); } // static ExtensionActionAPI* ExtensionActionAPI::Get(content::BrowserContext* context) { return BrowserContextKeyedAPIFactory::Get(context); } void ExtensionActionAPI::AddObserver(Observer* observer) { observers_.AddObserver(observer); } void ExtensionActionAPI::RemoveObserver(Observer* observer) { observers_.RemoveObserver(observer); } bool ExtensionActionAPI::GetBrowserActionVisibility( const std::string& extension_id) { bool visible = false; ExtensionPrefs* prefs = GetExtensionPrefs(); if (!prefs || !prefs->ReadPrefAsBoolean(extension_id, kBrowserActionVisible, &visible)) { return true; } return visible; } void ExtensionActionAPI::SetBrowserActionVisibility( const std::string& extension_id, bool visible) { if (GetBrowserActionVisibility(extension_id) == visible) return; GetExtensionPrefs()->UpdateExtensionPref(extension_id, kBrowserActionVisible, new base::FundamentalValue(visible)); FOR_EACH_OBSERVER(Observer, observers_, OnExtensionActionVisibilityChanged( extension_id, visible)); } bool ExtensionActionAPI::ShowExtensionActionPopup( const Extension* extension, Browser* browser, bool grant_active_tab_permissions) { ExtensionAction* extension_action = ExtensionActionManager::Get(browser_context_)->GetExtensionAction( *extension); if (!extension_action) return false; if (extension_action->action_type() == ActionInfo::TYPE_PAGE && !FeatureSwitch::extension_action_redesign()->IsEnabled()) { // We show page actions in the location bar unless the new toolbar is // enabled. return browser->window()->GetLocationBar()->ShowPageActionPopup( extension, grant_active_tab_permissions); } // Don't support showing action popups in a popup window. if (!browser->SupportsWindowFeature(Browser::FEATURE_TOOLBAR)) return false; ToolbarActionsBar* toolbar_actions_bar = browser->window()->GetToolbarActionsBar(); // ToolbarActionsBar could be null if, e.g., this is a popup window with no // toolbar. return toolbar_actions_bar && toolbar_actions_bar->ShowToolbarActionPopup( extension->id(), grant_active_tab_permissions); } void ExtensionActionAPI::NotifyChange(ExtensionAction* extension_action, content::WebContents* web_contents, content::BrowserContext* context) { FOR_EACH_OBSERVER( Observer, observers_, OnExtensionActionUpdated(extension_action, web_contents, context)); if (extension_action->action_type() == ActionInfo::TYPE_PAGE) NotifyPageActionsChanged(web_contents); } void ExtensionActionAPI::DispatchExtensionActionClicked( const ExtensionAction& extension_action, WebContents* web_contents) { events::HistogramValue histogram_value = events::UNKNOWN; const char* event_name = NULL; switch (extension_action.action_type()) { case ActionInfo::TYPE_BROWSER: histogram_value = events::BROWSER_ACTION_ON_CLICKED; event_name = "browserAction.onClicked"; break; case ActionInfo::TYPE_PAGE: histogram_value = events::PAGE_ACTION_ON_CLICKED; event_name = "pageAction.onClicked"; break; case ActionInfo::TYPE_SYSTEM_INDICATOR: // The System Indicator handles its own clicks. NOTREACHED(); break; } if (event_name) { scoped_ptr args(new base::ListValue()); args->Append( ExtensionTabUtil::CreateTabObject(web_contents)->ToValue().release()); DispatchEventToExtension(web_contents->GetBrowserContext(), extension_action.extension_id(), histogram_value, event_name, std::move(args)); } } void ExtensionActionAPI::ClearAllValuesForTab( content::WebContents* web_contents) { DCHECK(web_contents); int tab_id = SessionTabHelper::IdForTab(web_contents); content::BrowserContext* browser_context = web_contents->GetBrowserContext(); const ExtensionSet& enabled_extensions = ExtensionRegistry::Get(browser_context_)->enabled_extensions(); ExtensionActionManager* action_manager = ExtensionActionManager::Get(browser_context_); for (ExtensionSet::const_iterator iter = enabled_extensions.begin(); iter != enabled_extensions.end(); ++iter) { ExtensionAction* extension_action = action_manager->GetExtensionAction(**iter); if (extension_action) { extension_action->ClearAllValuesForTab(tab_id); NotifyChange(extension_action, web_contents, browser_context); } } } ExtensionPrefs* ExtensionActionAPI::GetExtensionPrefs() { // This lazy initialization is more than just an optimization, because it // allows tests to associate a new ExtensionPrefs with the browser context // before we access it. if (!extension_prefs_) extension_prefs_ = ExtensionPrefs::Get(browser_context_); return extension_prefs_; } void ExtensionActionAPI::DispatchEventToExtension( content::BrowserContext* context, const std::string& extension_id, events::HistogramValue histogram_value, const std::string& event_name, scoped_ptr event_args) { if (!EventRouter::Get(context)) return; scoped_ptr event( new Event(histogram_value, event_name, std::move(event_args))); event->restrict_to_browser_context = context; event->user_gesture = EventRouter::USER_GESTURE_ENABLED; EventRouter::Get(context) ->DispatchEventToExtension(extension_id, std::move(event)); } void ExtensionActionAPI::NotifyPageActionsChanged( content::WebContents* web_contents) { Browser* browser = chrome::FindBrowserWithWebContents(web_contents); if (!browser) return; LocationBar* location_bar = browser->window() ? browser->window()->GetLocationBar() : NULL; if (!location_bar) return; location_bar->UpdatePageActions(); FOR_EACH_OBSERVER(Observer, observers_, OnPageActionsUpdated(web_contents)); } void ExtensionActionAPI::Shutdown() { FOR_EACH_OBSERVER(Observer, observers_, OnExtensionActionAPIShuttingDown()); } // // ExtensionActionFunction // ExtensionActionFunction::ExtensionActionFunction() : details_(NULL), tab_id_(ExtensionAction::kDefaultTabId), contents_(NULL), extension_action_(NULL) { } ExtensionActionFunction::~ExtensionActionFunction() { } bool ExtensionActionFunction::RunSync() { ExtensionActionManager* manager = ExtensionActionManager::Get(GetProfile()); if (base::StartsWith(name(), "systemIndicator.", base::CompareCase::INSENSITIVE_ASCII)) { extension_action_ = manager->GetSystemIndicator(*extension()); } else { extension_action_ = manager->GetBrowserAction(*extension()); if (!extension_action_) { extension_action_ = manager->GetPageAction(*extension()); } } if (!extension_action_) { // TODO(kalman): ideally the browserAction/pageAction APIs wouldn't event // exist for extensions that don't have one declared. This should come as // part of the Feature system. error_ = kNoExtensionActionError; return false; } // Populates the tab_id_ and details_ members. EXTENSION_FUNCTION_VALIDATE(ExtractDataFromArguments()); // Find the WebContents that contains this tab id if one is required. if (tab_id_ != ExtensionAction::kDefaultTabId) { ExtensionTabUtil::GetTabById(tab_id_, GetProfile(), include_incognito(), NULL, NULL, &contents_, NULL); if (!contents_) { error_ = ErrorUtils::FormatErrorMessage( kNoTabError, base::IntToString(tab_id_)); return false; } } else { // Only browser actions and system indicators have a default tabId. ActionInfo::Type action_type = extension_action_->action_type(); EXTENSION_FUNCTION_VALIDATE( action_type == ActionInfo::TYPE_BROWSER || action_type == ActionInfo::TYPE_SYSTEM_INDICATOR); } return RunExtensionAction(); } bool ExtensionActionFunction::ExtractDataFromArguments() { // There may or may not be details (depends on the function). // The tabId might appear in details (if it exists), as the first // argument besides the action type (depends on the function), or be omitted // entirely. base::Value* first_arg = NULL; if (!args_->Get(0, &first_arg)) return true; switch (first_arg->GetType()) { case base::Value::TYPE_INTEGER: CHECK(first_arg->GetAsInteger(&tab_id_)); break; case base::Value::TYPE_DICTIONARY: { // Found the details argument. details_ = static_cast(first_arg); // Still need to check for the tabId within details. base::Value* tab_id_value = NULL; if (details_->Get("tabId", &tab_id_value)) { switch (tab_id_value->GetType()) { case base::Value::TYPE_NULL: // OK; tabId is optional, leave it default. return true; case base::Value::TYPE_INTEGER: CHECK(tab_id_value->GetAsInteger(&tab_id_)); return true; default: // Boom. return false; } } // Not found; tabId is optional, leave it default. break; } case base::Value::TYPE_NULL: // The tabId might be an optional argument. break; default: return false; } return true; } void ExtensionActionFunction::NotifyChange() { ExtensionActionAPI::Get(GetProfile())->NotifyChange( extension_action_, contents_, GetProfile()); } bool ExtensionActionFunction::SetVisible(bool visible) { if (extension_action_->GetIsVisible(tab_id_) == visible) return true; extension_action_->SetIsVisible(tab_id_, visible); NotifyChange(); return true; } bool ExtensionActionShowFunction::RunExtensionAction() { return SetVisible(true); } bool ExtensionActionHideFunction::RunExtensionAction() { return SetVisible(false); } bool ExtensionActionSetIconFunction::RunExtensionAction() { EXTENSION_FUNCTION_VALIDATE(details_); // setIcon can take a variant argument: either a dictionary of canvas // ImageData, or an icon index. base::DictionaryValue* canvas_set = NULL; int icon_index; if (details_->GetDictionary("imageData", &canvas_set)) { gfx::ImageSkia icon; EXTENSION_FUNCTION_VALIDATE( ExtensionAction::ParseIconFromCanvasDictionary(*canvas_set, &icon)); if (icon.isNull()) { error_ = "Icon invalid."; return false; } extension_action_->SetIcon(tab_id_, gfx::Image(icon)); } else if (details_->GetInteger("iconIndex", &icon_index)) { // Obsolete argument: ignore it. return true; } else { EXTENSION_FUNCTION_VALIDATE(false); } NotifyChange(); return true; } bool ExtensionActionSetTitleFunction::RunExtensionAction() { EXTENSION_FUNCTION_VALIDATE(details_); std::string title; EXTENSION_FUNCTION_VALIDATE(details_->GetString("title", &title)); extension_action_->SetTitle(tab_id_, title); NotifyChange(); return true; } bool ExtensionActionSetPopupFunction::RunExtensionAction() { EXTENSION_FUNCTION_VALIDATE(details_); std::string popup_string; EXTENSION_FUNCTION_VALIDATE(details_->GetString("popup", &popup_string)); GURL popup_url; if (!popup_string.empty()) popup_url = extension()->GetResourceURL(popup_string); extension_action_->SetPopupUrl(tab_id_, popup_url); NotifyChange(); return true; } bool ExtensionActionSetBadgeTextFunction::RunExtensionAction() { EXTENSION_FUNCTION_VALIDATE(details_); std::string badge_text; EXTENSION_FUNCTION_VALIDATE(details_->GetString("text", &badge_text)); extension_action_->SetBadgeText(tab_id_, badge_text); NotifyChange(); return true; } bool ExtensionActionSetBadgeBackgroundColorFunction::RunExtensionAction() { EXTENSION_FUNCTION_VALIDATE(details_); base::Value* color_value = NULL; EXTENSION_FUNCTION_VALIDATE(details_->Get("color", &color_value)); SkColor color = 0; if (color_value->IsType(base::Value::TYPE_LIST)) { base::ListValue* list = NULL; EXTENSION_FUNCTION_VALIDATE(details_->GetList("color", &list)); EXTENSION_FUNCTION_VALIDATE(list->GetSize() == 4); int color_array[4] = {0}; for (size_t i = 0; i < arraysize(color_array); ++i) { EXTENSION_FUNCTION_VALIDATE(list->GetInteger(i, &color_array[i])); } color = SkColorSetARGB(color_array[3], color_array[0], color_array[1], color_array[2]); } else if (color_value->IsType(base::Value::TYPE_STRING)) { std::string color_string; EXTENSION_FUNCTION_VALIDATE(details_->GetString("color", &color_string)); if (!image_util::ParseCssColorString(color_string, &color)) { error_ = kInvalidColorError; return false; } } extension_action_->SetBadgeBackgroundColor(tab_id_, color); NotifyChange(); return true; } bool ExtensionActionGetTitleFunction::RunExtensionAction() { SetResult(new base::StringValue(extension_action_->GetTitle(tab_id_))); return true; } bool ExtensionActionGetPopupFunction::RunExtensionAction() { SetResult( new base::StringValue(extension_action_->GetPopupUrl(tab_id_).spec())); return true; } bool ExtensionActionGetBadgeTextFunction::RunExtensionAction() { SetResult(new base::StringValue(extension_action_->GetBadgeText(tab_id_))); return true; } bool ExtensionActionGetBadgeBackgroundColorFunction::RunExtensionAction() { base::ListValue* list = new base::ListValue(); SkColor color = extension_action_->GetBadgeBackgroundColor(tab_id_); list->Append( new base::FundamentalValue(static_cast(SkColorGetR(color)))); list->Append( new base::FundamentalValue(static_cast(SkColorGetG(color)))); list->Append( new base::FundamentalValue(static_cast(SkColorGetB(color)))); list->Append( new base::FundamentalValue(static_cast(SkColorGetA(color)))); SetResult(list); return true; } BrowserActionOpenPopupFunction::BrowserActionOpenPopupFunction() : response_sent_(false) { } bool BrowserActionOpenPopupFunction::RunAsync() { // We only allow the popup in the active window. Profile* profile = GetProfile(); Browser* browser = chrome::FindLastActiveWithProfile(profile); // It's possible that the last active browser actually corresponds to the // associated incognito profile, and this won't be returned by // FindLastActiveWithProfile. If the browser we found isn't active and the // extension can operate incognito, then check the last active incognito, too. if ((!browser || !browser->window()->IsActive()) && util::IsIncognitoEnabled(extension()->id(), profile) && profile->HasOffTheRecordProfile()) { browser = chrome::FindLastActiveWithProfile(profile->GetOffTheRecordProfile()); } // If there's no active browser, or the Toolbar isn't visible, abort. // Otherwise, try to open a popup in the active browser. // TODO(justinlin): Remove toolbar check when http://crbug.com/308645 is // fixed. if (!browser || !browser->window()->IsActive() || !browser->window()->IsToolbarVisible() || !ExtensionActionAPI::Get(GetProfile())->ShowExtensionActionPopup( extension_.get(), browser, false)) { error_ = kOpenPopupError; return false; } // Even if this is for an incognito window, we want to use the normal profile. // If the extension is spanning, then extension hosts are created with the // original profile, and if it's split, then we know the api call came from // the right profile. registrar_.Add(this, NOTIFICATION_EXTENSION_HOST_DID_STOP_FIRST_LOAD, content::Source(profile)); // Set a timeout for waiting for the notification that the popup is loaded. // Waiting is required so that the popup view can be retrieved by the custom // bindings for the response callback. It's also needed to keep this function // instance around until a notification is observed. base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( FROM_HERE, base::Bind(&BrowserActionOpenPopupFunction::OpenPopupTimedOut, this), base::TimeDelta::FromSeconds(10)); return true; } void BrowserActionOpenPopupFunction::OpenPopupTimedOut() { if (response_sent_) return; DVLOG(1) << "chrome.browserAction.openPopup did not show a popup."; error_ = kOpenPopupError; SendResponse(false); response_sent_ = true; } void BrowserActionOpenPopupFunction::Observe( int type, const content::NotificationSource& source, const content::NotificationDetails& details) { DCHECK_EQ(NOTIFICATION_EXTENSION_HOST_DID_STOP_FIRST_LOAD, type); if (response_sent_) return; ExtensionHost* host = content::Details(details).ptr(); if (host->extension_host_type() != VIEW_TYPE_EXTENSION_POPUP || host->extension()->id() != extension_->id()) return; SendResponse(true); response_sent_ = true; registrar_.RemoveAll(); } } // namespace extensions