1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
|
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/views/extensions/extension_popup.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "chrome/browser/devtools/devtools_window.h"
#include "chrome/browser/extensions/extension_view_host.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/views/extensions/extensions_dialogs_utils.h"
#include "chrome/browser/ui/views/extensions/security_dialog_tracker.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "components/javascript_dialogs/app_modal_dialog_queue.h"
#include "components/web_modal/web_modal_utils.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/views/border.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/controls/native/native_view_host.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/style/platform_style.h"
#include "ui/views/widget/widget.h"
#if defined(USE_AURA)
#include "ui/aura/window.h"
#include "ui/wm/core/window_animations.h"
#endif
#if BUILDFLAG(IS_OZONE)
#include "ui/ozone/public/ozone_platform.h"
#endif
#if BUILDFLAG(IS_MAC)
#include "base/message_loop/message_pump_apple.h"
#endif
constexpr gfx::Size ExtensionPopup::kMinSize;
constexpr gfx::Size ExtensionPopup::kMaxSize;
// The most recently constructed popup; used for testing purposes.
ExtensionPopup* g_last_popup_for_testing = nullptr;
// A helper class to scope the observation of DevToolsAgentHosts. We can't just
// use base::ScopedObservation here because that requires a specific source
// object, where as DevToolsAgentHostObservers are added to a singleton list.
// The `observer_` passed into this object will be registered as an observer
// for this object's lifetime.
class ExtensionPopup::ScopedDevToolsAgentHostObservation {
public:
explicit ScopedDevToolsAgentHostObservation(
content::DevToolsAgentHostObserver* observer)
: observer_(observer) {
content::DevToolsAgentHost::AddObserver(observer_);
}
ScopedDevToolsAgentHostObservation(
const ScopedDevToolsAgentHostObservation&) = delete;
ScopedDevToolsAgentHostObservation& operator=(
const ScopedDevToolsAgentHostObservation&) = delete;
~ScopedDevToolsAgentHostObservation() {
content::DevToolsAgentHost::RemoveObserver(observer_);
}
private:
raw_ptr<content::DevToolsAgentHostObserver> observer_;
};
// static
ExtensionPopup* ExtensionPopup::last_popup_for_testing() {
return g_last_popup_for_testing;
}
// static
void ExtensionPopup::ShowPopup(
Browser* browser,
std::unique_ptr<extensions::ExtensionViewHost> host,
views::View* anchor_view,
views::BubbleBorder::Arrow arrow,
PopupShowAction show_action,
ShowPopupCallback callback) {
auto* popup = new ExtensionPopup(browser, std::move(host), anchor_view, arrow,
show_action, std::move(callback));
views::BubbleDialogDelegateView::CreateBubble(popup);
// Check that the preferred adjustment is set to mirror to match
// the assumption in the logic to calculate max bounds.
DCHECK_EQ(popup->GetBubbleFrameView()->GetPreferredArrowAdjustment(),
views::BubbleFrameView::PreferredArrowAdjustment::kMirror);
#if defined(USE_AURA)
gfx::NativeView native_view = popup->GetWidget()->GetNativeView();
wm::SetWindowVisibilityAnimationType(
native_view, wm::WINDOW_VISIBILITY_ANIMATION_TYPE_VERTICAL);
wm::SetWindowVisibilityAnimationVerticalPosition(native_view, -3.0f);
#endif
}
ExtensionPopup::~ExtensionPopup() {
// The ExtensionPopup may close before it was ever shown. If so, indicate such
// through the callback.
if (shown_callback_) {
std::move(shown_callback_).Run(nullptr);
}
if (g_last_popup_for_testing == this) {
g_last_popup_for_testing = nullptr;
}
}
gfx::Size ExtensionPopup::CalculatePreferredSize(
const views::SizeBounds& available_size) const {
// Constrain the size to popup min/max.
gfx::Size sz = views::View::CalculatePreferredSize(available_size);
sz.SetToMax(kMinSize);
sz.SetToMin(kMaxSize);
return sz;
}
void ExtensionPopup::AddedToWidget() {
BubbleDialogDelegateView::AddedToWidget();
const gfx::RoundedCornersF& radii = GetBubbleFrameView()->GetRoundedCorners();
CHECK_EQ(radii.upper_left(), radii.upper_right());
CHECK_EQ(radii.lower_left(), radii.lower_right());
const bool contents_has_rounded_corners =
extension_view_->holder()->SetCornerRadii(radii);
SetBorder(views::CreateEmptyBorder(gfx::Insets::TLBR(
contents_has_rounded_corners ? 0 : radii.upper_left(), 0,
contents_has_rounded_corners ? 0 : radii.lower_left(), 0)));
}
void ExtensionPopup::OnWidgetDestroying(views::Widget* widget) {
BubbleDialogDelegateView::OnWidgetDestroying(widget);
anchor_widget_observation_.Reset();
}
void ExtensionPopup::OnWidgetTreeActivated(views::Widget* root_widget,
views::Widget* active_widget) {
// The widget is shown asynchronously and may take a long time to appear, so
// only close if it's actually been shown.
if (!GetWidget()->IsVisible()) {
return;
}
// Close the popup on the activation of any widget in the anchor widget tree,
// unless if the extension is blocked by DevTools inspection or JS dialogs.
// We cannot close the popup on deactivation because the user may want to
// leave the popup open to look at the info there while working on other
// apps or browser windows.
// TODO(crbug.com/326681253): don't show the popup if it might cover
// security-sensitive UIs.
if (active_widget != GetWidget()) {
CloseUnlessBlockedByInspectionOrJSDialog();
}
}
gfx::Size ExtensionPopup::GetMinBounds() {
return kMinSize;
}
gfx::Size ExtensionPopup::GetMaxBounds() {
#if BUILDFLAG(IS_OZONE)
// Some platforms like wayland don't allow clients to know the global
// coordinates of the window. This means in those platforms we have no way to
// calculate exact space available based on the position of the parent window.
// So simply fall back on default max.
if (!ui::OzonePlatform::GetInstance()
->GetPlatformProperties()
.supports_global_screen_coordinates) {
return kMaxSize;
}
#endif
gfx::Size max_size = kMaxSize;
max_size.SetToMin(
BubbleDialogDelegate::GetMaxAvailableScreenSpaceToPlaceBubble(
GetAnchorView(), arrow(), adjust_if_offscreen(),
views::BubbleFrameView::PreferredArrowAdjustment::kMirror));
max_size.SetToMax(kMinSize);
return max_size;
}
void ExtensionPopup::OnExtensionUnloaded(
content::BrowserContext* browser_context,
const extensions::Extension* extension,
extensions::UnloadedExtensionReason reason) {
CHECK(host_);
if (extension->id() == host_->extension_id()) {
// To ensure |extension_view_| cannot receive any messages that cause it to
// try to access the host during Widget closure, destroy it immediately.
RemoveChildViewT(extension_view_.get());
// Note: it's important that we unregister the devtools observation *before*
// we destroy `host_`. Otherwise, destroying `host_` can synchronously cause
// the associated WebContents to be destroyed, which will cause devtools to
// detach, which will notify our observer, where we rely on `host_` - all
// synchronously.
scoped_devtools_observation_.reset();
host_.reset();
// Stop observing the registry immediately to prevent any subsequent
// notifications, since Widget::Close is asynchronous.
DCHECK(extension_registry_observation_.IsObserving());
extension_registry_observation_.Reset();
CloseDeferredIfNecessary();
}
}
void ExtensionPopup::DocumentOnLoadCompletedInPrimaryMainFrame() {
// Show when the content finishes loading and its width is computed.
ShowBubble();
Observe(nullptr);
}
void ExtensionPopup::OnTabStripModelChanged(
TabStripModel* tab_strip_model,
const TabStripModelChange& change,
const TabStripSelectionChange& selection) {
if (!tab_strip_model->empty() && selection.active_tab_changed()) {
CloseDeferredIfNecessary();
}
}
void ExtensionPopup::DevToolsAgentHostAttached(
content::DevToolsAgentHost* agent_host) {
DCHECK(host_);
if (host_->host_contents() == agent_host->GetWebContents()) {
show_action_ = PopupShowAction::kShowAndInspect;
}
}
void ExtensionPopup::DevToolsAgentHostDetached(
content::DevToolsAgentHost* agent_host) {
DCHECK(host_);
if (host_->host_contents() == agent_host->GetWebContents()) {
show_action_ = PopupShowAction::kShow;
}
}
ExtensionPopup::ExtensionPopup(
Browser* browser,
std::unique_ptr<extensions::ExtensionViewHost> host,
views::View* anchor_view,
views::BubbleBorder::Arrow arrow,
PopupShowAction show_action,
ShowPopupCallback callback)
: BubbleDialogDelegateView(anchor_view,
arrow,
views::BubbleBorder::STANDARD_SHADOW,
/*autosize=*/true),
browser_(browser),
host_(std::move(host)),
show_action_(show_action),
shown_callback_(std::move(callback)),
deferred_close_weak_ptr_factory_(this) {
g_last_popup_for_testing = this;
SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone));
set_use_round_corners(false);
set_margins(gfx::Insets());
SetLayoutManager(std::make_unique<views::FillLayout>());
// Set the default value before initializing |extension_view_| to use
// the correct value while calculating max bounds.
set_adjust_if_offscreen(views::PlatformStyle::kAdjustBubbleIfOffscreen);
extension_view_ = AddChildView(
std::make_unique<ExtensionViewViews>(browser_->profile(), host_.get()));
extension_view_->SetContainer(this);
extension_view_->Init();
// See comments in OnWidgetActivationChanged().
set_close_on_deactivate(false);
scoped_devtools_observation_ =
std::make_unique<ScopedDevToolsAgentHostObservation>(this);
browser_->tab_strip_model()->AddObserver(this);
CHECK(anchor_widget());
anchor_widget_observation_.Observe(anchor_widget()->GetPrimaryWindowWidget());
// Handle the containing view calling window.close();
// The base::Unretained() below is safe because this object owns `host_`, so
// the callback will never fire if `this` is deleted.
host_->SetCloseHandler(base::BindOnce(
&ExtensionPopup::HandleCloseExtensionHost, base::Unretained(this)));
extension_registry_observation_.Observe(
extensions::ExtensionRegistry::Get(host_->browser_context()));
// If the host had somehow finished loading, then we'd miss the notification
// and not show. This seems to happen in single-process mode.
if (host_->has_loaded_once()) {
ShowBubble();
} else {
// Wait to show the popup until the contained host finishes loading.
Observe(host_->host_contents());
}
}
void ExtensionPopup::ShowBubble() {
// Don't show the popup if there are visible security dialogs. This protects
// the security dialogs from spoofing.
if (extensions::SecurityDialogTracker::GetInstance()
->BrowserHasVisibleSecurityDialogs(browser_)) {
CloseDeferredIfNecessary();
return;
}
GetWidget()->Show();
// Focus on the host contents when the bubble is first shown.
host_->host_contents()->Focus();
if (show_action_ == PopupShowAction::kShowAndInspect) {
DevToolsWindow::OpenDevToolsWindow(
host_->host_contents(), DevToolsToggleAction::ShowConsolePanel(),
DevToolsOpenedByAction::kContextMenuInspect);
}
if (shown_callback_) {
std::move(shown_callback_).Run(host_.get());
}
}
void ExtensionPopup::CloseUnlessBlockedByInspectionOrJSDialog() {
// Don't close if the extension page is under inspection.
if (show_action_ == PopupShowAction::kShowAndInspect) {
return;
}
// Don't close if an app modal dialog is showing.
javascript_dialogs::AppModalDialogQueue* app_modal_queue =
javascript_dialogs::AppModalDialogQueue::GetInstance();
CHECK(app_modal_queue);
if (app_modal_queue->HasActiveDialog()) {
return;
}
// Don't close if a web modal dialog (e.g. webauthn security key dialog) is
// showing.
if (web_modal::WebContentsHasActiveWebModal(host_->host_contents())) {
return;
}
CloseDeferredIfNecessary(views::Widget::ClosedReason::kLostFocus);
}
void ExtensionPopup::CloseDeferredIfNecessary(
views::Widget::ClosedReason reason) {
#if BUILDFLAG(IS_MAC)
// On Mac, defer close if we're in a nested run loop (for example, showing a
// context menu) to avoid messaging deallocated objects.
if (base::message_pump_apple::IsHandlingSendEvent()) {
deferred_close_weak_ptr_factory_.InvalidateWeakPtrs();
auto weak_ptr = deferred_close_weak_ptr_factory_.GetWeakPtr();
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^{
if (weak_ptr) {
weak_ptr->GetWidget()->CloseWithReason(reason);
}
});
return;
}
#endif // BUILDFLAG(IS_MAC)
GetWidget()->CloseWithReason(reason);
}
void ExtensionPopup::HandleCloseExtensionHost(extensions::ExtensionHost* host) {
DCHECK_EQ(host, host_.get());
CloseDeferredIfNecessary();
}
BEGIN_METADATA(ExtensionPopup)
END_METADATA
|