blob: 2af847e8e75c1507d3e42d10bc03e04ef527e631 [file] [log] [blame]
// Copyright (c) 2010 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/extension_popup_api.h"
#include "base/json/json_writer.h"
#include "base/string_util.h"
#include "base/stringprintf.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_dom_ui.h"
#include "chrome/browser/extensions/extension_event_router.h"
#include "chrome/browser/extensions/extension_host.h"
#include "chrome/browser/profile.h"
#include "chrome/browser/renderer_host/render_view_host.h"
#include "chrome/browser/renderer_host/render_view_host_delegate.h"
#include "chrome/browser/renderer_host/render_widget_host_view.h"
#include "chrome/browser/tab_contents/tab_contents.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/window_sizer.h"
#include "chrome/common/extensions/extension.h"
#include "chrome/common/notification_details.h"
#include "chrome/common/notification_service.h"
#include "chrome/common/notification_source.h"
#include "chrome/common/notification_type.h"
#include "chrome/common/url_constants.h"
#include "gfx/point.h"
#if defined(TOOLKIT_VIEWS)
#include "chrome/browser/views/bubble_border.h"
#include "chrome/browser/views/extensions/extension_popup.h"
#include "views/view.h"
#include "views/focus/focus_manager.h"
#endif // TOOLKIT_VIEWS
namespace extension_popup_module_events {
const char kOnPopupClosed[] = "experimental.popup.onClosed.%d";
} // namespace extension_popup_module_events
namespace {
// Errors.
const char kBadAnchorArgument[] = "Invalid anchor argument.";
const char kInvalidURLError[] = "Invalid URL.";
const char kNotAnExtension[] = "Not an extension view.";
const char kPopupsDisallowed[] =
"Popups are only supported from tab-contents views.";
// Keys.
const char kWidthKey[] = "width";
const char kHeightKey[] = "height";
const char kTopKey[] = "top";
const char kLeftKey[] = "left";
const char kGiveFocusKey[] = "giveFocus";
const char kDomAnchorKey[] = "domAnchor";
const char kBorderStyleKey[] = "borderStyle";
const char kMaxSizeKey[] = "maxSize";
// chrome enumeration values
const char kRectangleChrome[] = "rectangle";
#if defined(TOOLKIT_VIEWS)
// Returns an updated arrow location, conditioned on the type of intersection
// between the popup window, and the screen. |location| is the current position
// of the arrow on the popup. |intersection| is the rect representing the
// intersection between the popup view and its working screen. |popup_rect|
// is the rect of the popup window in screen space coordinates.
// The returned location will be horizontally or vertically inverted based on
// if the popup has been clipped horizontally or vertically.
BubbleBorder::ArrowLocation ToggleArrowLocation(
BubbleBorder::ArrowLocation location, const gfx::Rect& intersection,
const gfx::Rect& popup_rect) {
// If the popup has been clipped horizontally, flip the right-left position
// of the arrow.
if (intersection.right() != popup_rect.right() ||
intersection.x() != popup_rect.x()) {
location = BubbleBorder::horizontal_mirror(location);
}
// If the popup has been clipped vertically, flip the bottom-top position
// of the arrow.
if (intersection.y() != popup_rect.y() ||
intersection.bottom() != popup_rect.bottom()) {
location = BubbleBorder::vertical_mirror(location);
}
return location;
}
#endif // TOOLKIT_VIEWS
}; // namespace
#if defined(TOOLKIT_VIEWS)
// ExtensionPopupHost objects implement the environment necessary to host
// an ExtensionPopup views for the popup api. Its main job is to handle
// its lifetime and to fire the popup-closed event when the popup is closed.
// Because the close-on-focus-lost behavior is different from page action
// and browser action, it also manages its own focus change listening. The
// difference in close-on-focus-lost is that in the page action and browser
// action cases, the popup closes when the focus leaves the popup or any of its
// children. In this case, the popup closes when the focus leaves the popups
// containing view or any of *its* children.
class ExtensionPopupHost : public ExtensionPopup::Observer,
public views::WidgetFocusChangeListener,
public base::RefCounted<ExtensionPopupHost>,
public NotificationObserver {
public:
// Pass |max_popup_size| to specify the maximal size to which the popup
// will expand. A width or height of 0 will result in the popup making use
// of the default max width or height, respectively: ExtensionPopup:kMaxWidth,
// and ExtensionPopup::kMaxHeight.
explicit ExtensionPopupHost(ExtensionFunctionDispatcher* dispatcher,
const gfx::Size& max_popup_size)
: dispatcher_(dispatcher), popup_(NULL), max_popup_size_(max_popup_size) {
AddRef(); // Balanced in DispatchPopupClosedEvent().
views::FocusManager::GetWidgetFocusManager()->AddFocusChangeListener(this);
}
~ExtensionPopupHost() {
views::FocusManager::GetWidgetFocusManager()->
RemoveFocusChangeListener(this);
}
void set_popup(ExtensionPopup* popup) {
popup_ = popup;
// Now that a popup has been assigned, listen for subsequent popups being
// created in the same extension - we want to disallow more than one
// concurrently displayed popup windows.
registrar_.Add(
this,
NotificationType::EXTENSION_HOST_CREATED,
Source<ExtensionProcessManager>(
dispatcher_->profile()->GetExtensionProcessManager()));
registrar_.Add(
this,
NotificationType::RENDER_VIEW_HOST_WILL_CLOSE_RENDER_VIEW,
Source<RenderViewHost>(dispatcher_->render_view_host()));
registrar_.Add(
this,
NotificationType::EXTENSION_FUNCTION_DISPATCHER_DESTROYED,
Source<Profile>(dispatcher_->profile()));
}
// Overridden from ExtensionPopup::Observer
virtual void ExtensionPopupIsClosing(ExtensionPopup* popup) {
// Unregister the automation resource routing registered upon host
// creation.
AutomationResourceRoutingDelegate* router =
GetRoutingFromDispatcher(dispatcher_);
if (router)
router->UnregisterRenderViewHost(popup_->host()->render_view_host());
}
virtual void ExtensionPopupClosed(void* popup_token) {
if (popup_ == popup_token) {
popup_ = NULL;
DispatchPopupClosedEvent();
}
}
virtual void ExtensionHostCreated(ExtensionHost* host) {
// Pop-up views should share the same automation routing configuration as
// their hosting views, so register the RenderViewHost of the pop-up with
// the AutomationResourceRoutingDelegate interface of the dispatcher.
AutomationResourceRoutingDelegate* router =
GetRoutingFromDispatcher(dispatcher_);
if (router)
router->RegisterRenderViewHost(host->render_view_host());
// Extension hosts created for popup contents exist in the same tab
// contents as the ExtensionFunctionDispatcher that requested the popup.
// For example, '_blank' link navigation should be routed through the tab
// contents that requested the popup.
if (dispatcher_ && dispatcher_->delegate()) {
host->set_associated_tab_contents(
dispatcher_->delegate()->associated_tab_contents());
}
}
virtual void ExtensionPopupCreated(ExtensionPopup* popup) {
// The popup has been created, but not yet displayed, so install the max
// size overrides before the first positioning.
if (max_popup_size_.width())
popup->set_max_width(max_popup_size_.width());
if (max_popup_size_.height())
popup->set_max_height(max_popup_size_.height());
}
virtual void ExtensionPopupResized(ExtensionPopup* popup) {
// Reposition the location of the arrow on the popup so that the popup
// better fits on the working monitor.
gfx::Rect popup_rect = popup->GetOuterBounds();
if (popup_rect.IsEmpty())
return;
scoped_ptr<WindowSizer::MonitorInfoProvider> monitor_provider(
WindowSizer::CreateDefaultMonitorInfoProvider());
gfx::Rect monitor_bounds(
monitor_provider->GetMonitorWorkAreaMatching(popup_rect));
gfx::Rect intersection = monitor_bounds.Intersect(popup_rect);
// If the popup is totally out of the bounds of the monitor, then toggling
// the arrow location will not result in an un-clipped window.
if (intersection.IsEmpty())
return;
if (!intersection.Equals(popup_rect)) {
// The popup was clipped by the monitor. Toggle the arrow position
// to see if that improves visibility. Note: The assignment and
// re-assignment of the arrow-position will not trigger an intermittent
// display.
BubbleBorder::ArrowLocation previous_location = popup->arrow_position();
BubbleBorder::ArrowLocation flipped_location = ToggleArrowLocation(
previous_location, intersection, popup_rect);
popup->SetArrowPosition(flipped_location);
// Double check that toggling the position actually improved the
// situation - the popup will be contained entirely in its working monitor
// bounds.
gfx::Rect flipped_bounds = popup->GetOuterBounds();
gfx::Rect updated_monitor_bounds =
monitor_provider->GetMonitorWorkAreaMatching(flipped_bounds);
if (!updated_monitor_bounds.Contains(flipped_bounds))
popup->SetArrowPosition(previous_location);
}
}
// Overridden from views::WidgetFocusChangeListener
virtual void NativeFocusWillChange(gfx::NativeView focused_before,
gfx::NativeView focused_now) {
// If the popup doesn't exist, then do nothing.
if (!popup_)
return;
// If no view is to be focused, then Chrome was deactivated, so hide the
// popup.
if (focused_now) {
// On XP, the focus change handler may be invoked when the delegate has
// already been revoked.
// TODO(twiz@chromium.org): Resolve the trigger of this behaviour.
if (!dispatcher_ || !dispatcher_->delegate())
return;
gfx::NativeView host_view =
dispatcher_->delegate()->GetNativeViewOfHost();
// If the widget hosting the popup contains the newly focused view, then
// don't dismiss the pop-up.
ExtensionView* view = popup_->host()->view();
if (view) {
views::Widget* popup_root_widget = view->GetWidget();
if (popup_root_widget &&
popup_root_widget->ContainsNativeView(focused_now))
return;
}
// If the widget or RenderWidgetHostView hosting the extension that
// launched the pop-up is receiving focus, then don't dismiss the popup.
views::Widget* host_widget =
views::Widget::GetWidgetFromNativeView(host_view);
if (host_widget && host_widget->ContainsNativeView(focused_now))
return;
RenderWidgetHostView* render_host_view =
RenderWidgetHostView::GetRenderWidgetHostViewFromNativeView(
host_view);
if (render_host_view &&
render_host_view->ContainsNativeView(focused_now))
return;
}
// We are careful here to let the current event loop unwind before
// causing the popup to be closed.
MessageLoop::current()->PostTask(FROM_HERE, NewRunnableMethod(popup_,
&ExtensionPopup::Close));
}
// Overridden from NotificationObserver
virtual void Observe(NotificationType type,
const NotificationSource& source,
const NotificationDetails& details) {
if (NotificationType::EXTENSION_HOST_CREATED == type) {
Details<ExtensionHost> details_host(details);
// Disallow multiple pop-ups from the same extension, by closing
// the presently opened popup during construction of any new popups.
if (ViewType::EXTENSION_POPUP == details_host->GetRenderViewType() &&
popup_->host()->extension() == details_host->extension() &&
Details<ExtensionHost>(popup_->host()) != details) {
popup_->Close();
}
} else if (NotificationType::RENDER_VIEW_HOST_WILL_CLOSE_RENDER_VIEW ==
type) {
if (Source<RenderViewHost>(dispatcher_->render_view_host()) == source) {
// If the parent render view is about to be closed, signal closure
// of the popup.
popup_->Close();
}
} else if (NotificationType::EXTENSION_FUNCTION_DISPATCHER_DESTROYED ==
type) {
// Popups should not outlive the dispatchers that launched them.
// Normally, long-lived popups will be dismissed in response to the
// RENDER_VIEW_WILL_CLOSE_BY_RENDER_VIEW_HOST message. Unfortunately,
// if the hosting view invokes window.close(), there is no communication
// back to the browser until the entire view has been torn down, at which
// time the dispatcher will be invoked.
// Note: The onClosed event will not be fired, but because the hosting
// view has already been torn down, it is already too late to process it.
// TODO(twiz): Add a communication path between the renderer and browser
// for RenderView closure notifications initiatied within the renderer.
if (Details<ExtensionFunctionDispatcher>(dispatcher_) == details) {
dispatcher_ = NULL;
popup_->Close();
}
}
}
private:
// Returns the AutomationResourceRoutingDelegate interface for |dispatcher|.
static AutomationResourceRoutingDelegate*
GetRoutingFromDispatcher(ExtensionFunctionDispatcher* dispatcher) {
if (!dispatcher)
return NULL;
RenderViewHost* render_view_host = dispatcher->render_view_host();
RenderViewHostDelegate* delegate =
render_view_host ? render_view_host->delegate() : NULL;
return delegate ? delegate->GetAutomationResourceRoutingDelegate() : NULL;
}
void DispatchPopupClosedEvent() {
if (dispatcher_) {
PopupEventRouter::OnPopupClosed(
dispatcher_->profile(),
dispatcher_->render_view_host()->routing_id());
dispatcher_ = NULL;
}
Release(); // Balanced in ctor.
}
// A pointer to the dispatcher that handled the request that opened this
// popup view.
ExtensionFunctionDispatcher* dispatcher_;
// A pointer to the popup.
ExtensionPopup* popup_;
// The maximal size to which the popup is permitted to expand.
gfx::Size max_popup_size_;
NotificationRegistrar registrar_;
DISALLOW_COPY_AND_ASSIGN(ExtensionPopupHost);
};
#endif // TOOLKIT_VIEWS
PopupShowFunction::PopupShowFunction()
#if defined (TOOLKIT_VIEWS)
: popup_(NULL)
#endif
{}
void PopupShowFunction::Run() {
#if defined(TOOLKIT_VIEWS)
if (!RunImpl()) {
SendResponse(false);
} else {
// If the contents of the popup are already available, then immediately
// send the response. Otherwise wait for the EXTENSION_POPUP_VIEW_READY
// notification.
if (popup_->host() && popup_->host()->document_element_available()) {
SendResponse(true);
} else {
AddRef();
registrar_.Add(this, NotificationType::EXTENSION_POPUP_VIEW_READY,
NotificationService::AllSources());
registrar_.Add(this, NotificationType::EXTENSION_HOST_DESTROYED,
NotificationService::AllSources());
}
}
#else
SendResponse(false);
#endif
}
bool PopupShowFunction::RunImpl() {
// Popups may only be displayed from TAB_CONTENTS and EXTENSION_INFOBAR.
ViewType::Type view_type =
dispatcher()->render_view_host()->delegate()->GetRenderViewType();
if (ViewType::TAB_CONTENTS != view_type &&
ViewType::EXTENSION_INFOBAR != view_type) {
error_ = kPopupsDisallowed;
return false;
}
std::string url_string;
EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &url_string));
DictionaryValue* show_details = NULL;
EXTENSION_FUNCTION_VALIDATE(args_->GetDictionary(1, &show_details));
DictionaryValue* dom_anchor = NULL;
EXTENSION_FUNCTION_VALIDATE(show_details->GetDictionary(kDomAnchorKey,
&dom_anchor));
int dom_top, dom_left;
EXTENSION_FUNCTION_VALIDATE(dom_anchor->GetInteger(kTopKey,
&dom_top));
EXTENSION_FUNCTION_VALIDATE(dom_anchor->GetInteger(kLeftKey,
&dom_left));
int dom_width, dom_height;
EXTENSION_FUNCTION_VALIDATE(dom_anchor->GetInteger(kWidthKey,
&dom_width));
EXTENSION_FUNCTION_VALIDATE(dom_anchor->GetInteger(kHeightKey,
&dom_height));
EXTENSION_FUNCTION_VALIDATE(dom_top >= 0 && dom_left >= 0 &&
dom_width >= 0 && dom_height >= 0);
// The default behaviour is to give the focus to the pop-up window.
bool give_focus = true;
if (show_details->HasKey(kGiveFocusKey)) {
EXTENSION_FUNCTION_VALIDATE(show_details->GetBoolean(kGiveFocusKey,
&give_focus));
}
int max_width = 0;
int max_height = 0;
if (show_details->HasKey(kMaxSizeKey)) {
DictionaryValue* max_size = NULL;
EXTENSION_FUNCTION_VALIDATE(show_details->GetDictionary(kMaxSizeKey,
&max_size));
if (max_size->HasKey(kWidthKey))
EXTENSION_FUNCTION_VALIDATE(max_size->GetInteger(kWidthKey, &max_width));
if (max_size->HasKey(kHeightKey))
EXTENSION_FUNCTION_VALIDATE(max_size->GetInteger(kHeightKey,
&max_height));
}
#if defined(TOOLKIT_VIEWS)
// The default behaviour is to provide the bubble-chrome to the popup.
ExtensionPopup::PopupChrome chrome = ExtensionPopup::BUBBLE_CHROME;
if (show_details->HasKey(kBorderStyleKey)) {
std::string chrome_string;
EXTENSION_FUNCTION_VALIDATE(show_details->GetString(kBorderStyleKey,
&chrome_string));
if (chrome_string == kRectangleChrome)
chrome = ExtensionPopup::RECTANGLE_CHROME;
}
#endif
GURL url = dispatcher()->url().Resolve(url_string);
if (!url.is_valid()) {
error_ = kInvalidURLError;
return false;
}
// Disallow non-extension requests, or requests outside of the requesting
// extension view's extension.
const std::string& extension_id = url.host();
if (extension_id != GetExtension()->id() ||
!url.SchemeIs(chrome::kExtensionScheme)) {
error_ = kInvalidURLError;
return false;
}
gfx::Point origin(dom_left, dom_top);
if (!dispatcher()->render_view_host()->view()) {
error_ = kNotAnExtension;
return false;
}
gfx::Rect content_bounds =
dispatcher()->render_view_host()->view()->GetViewBounds();
origin.Offset(content_bounds.x(), content_bounds.y());
gfx::Rect rect(origin.x(), origin.y(), dom_width, dom_height);
// Get the correct native window to pass to ExtensionPopup.
// ExtensionFunctionDispatcher::Delegate may provide a custom implementation
// of this.
gfx::NativeWindow window =
dispatcher()->delegate()->GetCustomFrameNativeWindow();
if (!window)
window = GetCurrentBrowser()->window()->GetNativeHandle();
#if defined(TOOLKIT_VIEWS)
BubbleBorder::ArrowLocation arrow_location = BubbleBorder::TOP_LEFT;
// ExtensionPopupHost manages it's own lifetime.
ExtensionPopupHost* popup_host =
new ExtensionPopupHost(dispatcher(), gfx::Size(max_width, max_height));
popup_ = ExtensionPopup::Show(url,
GetCurrentBrowser(),
dispatcher()->profile(),
window,
rect,
arrow_location,
give_focus,
false, // inspect_with_devtools
chrome,
popup_host); // ExtensionPopup::Observer
// popup_host will handle focus change listening and close the popup when
// focus leaves the containing views hierarchy.
popup_->set_close_on_lost_focus(false);
popup_host->set_popup(popup_);
#endif // defined(TOOLKIT_VIEWS)
return true;
}
void PopupShowFunction::Observe(NotificationType type,
const NotificationSource& source,
const NotificationDetails& details) {
#if defined(TOOLKIT_VIEWS)
DCHECK(type == NotificationType::EXTENSION_POPUP_VIEW_READY ||
type == NotificationType::EXTENSION_HOST_DESTROYED);
DCHECK(popup_ != NULL);
// Wait for notification that the popup view is ready (and onload has been
// called), before completing the API call.
if (popup_ && type == NotificationType::EXTENSION_POPUP_VIEW_READY &&
Details<ExtensionHost>(popup_->host()) == details) {
SendResponse(true);
Release(); // Balanced in Run().
} else if (popup_ && type == NotificationType::EXTENSION_HOST_DESTROYED &&
Details<ExtensionHost>(popup_->host()) == details) {
// If the host was destroyed, then report failure, and release the remaining
// reference.
SendResponse(false);
Release(); // Balanced in Run().
}
#endif // defined(TOOLKIT_VIEWS)
}
// static
void PopupEventRouter::OnPopupClosed(Profile* profile,
int routing_id) {
std::string full_event_name = base::StringPrintf(
extension_popup_module_events::kOnPopupClosed,
routing_id);
profile->GetExtensionEventRouter()->DispatchEventToRenderers(
full_event_name, base::JSONWriter::kEmptyArray, profile, GURL());
}