1 // Copyright 2014 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/ui/extensions/extension_action_view_controller.h"
6
7 #include <memory>
8 #include <string>
9 #include <utility>
10
11 #include "base/bind.h"
12 #include "base/check_op.h"
13 #include "base/metrics/histogram_functions.h"
14 #include "base/strings/strcat.h"
15 #include "base/strings/utf_string_conversions.h"
16 #include "chrome/browser/extensions/api/commands/command_service.h"
17 #include "chrome/browser/extensions/api/extension_action/extension_action_api.h"
18 #include "chrome/browser/extensions/extension_action_runner.h"
19 #include "chrome/browser/extensions/extension_util.h"
20 #include "chrome/browser/extensions/extension_view.h"
21 #include "chrome/browser/extensions/extension_view_host.h"
22 #include "chrome/browser/extensions/extension_view_host_factory.h"
23 #include "chrome/browser/profiles/profile.h"
24 #include "chrome/browser/ui/browser.h"
25 #include "chrome/browser/ui/extensions/extension_action_platform_delegate.h"
26 #include "chrome/browser/ui/extensions/extensions_container.h"
27 #include "chrome/browser/ui/extensions/icon_with_badge_image_source.h"
28 #include "chrome/browser/ui/toolbar/toolbar_action_view_delegate.h"
29 #include "chrome/browser/ui/ui_features.h"
30 #include "chrome/grit/generated_resources.h"
31 #include "components/sessions/content/session_tab_helper.h"
32 #include "extensions/browser/extension_action.h"
33 #include "extensions/browser/extension_registry.h"
34 #include "extensions/common/api/extension_action/action_info.h"
35 #include "extensions/common/extension.h"
36 #include "extensions/common/extension_features.h"
37 #include "extensions/common/manifest_constants.h"
38 #include "extensions/common/permissions/api_permission.h"
39 #include "ui/base/l10n/l10n_util.h"
40 #include "ui/gfx/image/image_skia.h"
41 #include "ui/gfx/image/image_skia_operations.h"
42
43 using extensions::ActionInfo;
44 using extensions::CommandService;
45 using extensions::ExtensionActionRunner;
46
ExtensionActionViewController(const extensions::Extension * extension,Browser * browser,extensions::ExtensionAction * extension_action,ExtensionsContainer * extensions_container,bool in_overflow_mode)47 ExtensionActionViewController::ExtensionActionViewController(
48 const extensions::Extension* extension,
49 Browser* browser,
50 extensions::ExtensionAction* extension_action,
51 ExtensionsContainer* extensions_container,
52 bool in_overflow_mode)
53 : extension_(extension),
54 browser_(browser),
55 in_overflow_mode_(in_overflow_mode),
56 extension_action_(extension_action),
57 extensions_container_(extensions_container),
58 popup_host_(nullptr),
59 view_delegate_(nullptr),
60 platform_delegate_(ExtensionActionPlatformDelegate::Create(this)),
61 icon_factory_(browser->profile(), extension, extension_action, this),
62 extension_registry_(
63 extensions::ExtensionRegistry::Get(browser_->profile())) {
64 DCHECK(extensions_container);
65 DCHECK(extension_action);
66 DCHECK(extension);
67 }
68
~ExtensionActionViewController()69 ExtensionActionViewController::~ExtensionActionViewController() {
70 DCHECK(!IsShowingPopup());
71 }
72
GetId() const73 std::string ExtensionActionViewController::GetId() const {
74 return extension_->id();
75 }
76
SetDelegate(ToolbarActionViewDelegate * delegate)77 void ExtensionActionViewController::SetDelegate(
78 ToolbarActionViewDelegate* delegate) {
79 DCHECK((delegate == nullptr) ^ (view_delegate_ == nullptr));
80 if (delegate) {
81 view_delegate_ = delegate;
82 platform_delegate_->OnDelegateSet();
83 } else {
84 HidePopup();
85 platform_delegate_.reset();
86 view_delegate_ = nullptr;
87 }
88 }
89
GetIcon(content::WebContents * web_contents,const gfx::Size & size)90 gfx::Image ExtensionActionViewController::GetIcon(
91 content::WebContents* web_contents,
92 const gfx::Size& size) {
93 if (!ExtensionIsValid())
94 return gfx::Image();
95
96 return gfx::Image(
97 gfx::ImageSkia(GetIconImageSource(web_contents, size), size));
98 }
99
GetActionName() const100 base::string16 ExtensionActionViewController::GetActionName() const {
101 if (!ExtensionIsValid())
102 return base::string16();
103
104 return base::UTF8ToUTF16(extension_->name());
105 }
106
GetAccessibleName(content::WebContents * web_contents) const107 base::string16 ExtensionActionViewController::GetAccessibleName(
108 content::WebContents* web_contents) const {
109 if (!ExtensionIsValid())
110 return base::string16();
111
112 // GetAccessibleName() can (surprisingly) be called during browser
113 // teardown. Handle this gracefully.
114 if (!web_contents)
115 return base::UTF8ToUTF16(extension()->name());
116
117 std::string title = extension_action()->GetTitle(
118 sessions::SessionTabHelper::IdForTab(web_contents).id());
119
120 base::string16 title_utf16 =
121 base::UTF8ToUTF16(title.empty() ? extension()->name() : title);
122
123 // Include a "host access" portion of the tooltip if the extension has or
124 // wants access to the site.
125 PageInteractionStatus interaction_status =
126 GetPageInteractionStatus(web_contents);
127 int interaction_status_description_id = -1;
128 switch (interaction_status) {
129 case PageInteractionStatus::kNone:
130 // No string for neither having nor wanting access.
131 break;
132 case PageInteractionStatus::kPending:
133 interaction_status_description_id = IDS_EXTENSIONS_WANTS_ACCESS_TO_SITE;
134 break;
135 case PageInteractionStatus::kActive:
136 interaction_status_description_id = IDS_EXTENSIONS_HAS_ACCESS_TO_SITE;
137 break;
138 }
139
140 if (interaction_status_description_id != -1) {
141 title_utf16 = base::StrCat(
142 {title_utf16, base::UTF8ToUTF16("\n"),
143 l10n_util::GetStringUTF16(interaction_status_description_id)});
144 }
145
146 return title_utf16;
147 }
148
GetTooltip(content::WebContents * web_contents) const149 base::string16 ExtensionActionViewController::GetTooltip(
150 content::WebContents* web_contents) const {
151 return GetAccessibleName(web_contents);
152 }
153
IsEnabled(content::WebContents * web_contents) const154 bool ExtensionActionViewController::IsEnabled(
155 content::WebContents* web_contents) const {
156 if (!ExtensionIsValid())
157 return false;
158
159 return extension_action_->GetIsVisible(
160 sessions::SessionTabHelper::IdForTab(web_contents).id()) ||
161 GetPageInteractionStatus(web_contents) ==
162 PageInteractionStatus::kPending;
163 }
164
HasPopup(content::WebContents * web_contents) const165 bool ExtensionActionViewController::HasPopup(
166 content::WebContents* web_contents) const {
167 if (!ExtensionIsValid())
168 return false;
169
170 SessionID tab_id = sessions::SessionTabHelper::IdForTab(web_contents);
171 return tab_id.is_valid() ? extension_action_->HasPopup(tab_id.id()) : false;
172 }
173
IsShowingPopup() const174 bool ExtensionActionViewController::IsShowingPopup() const {
175 return popup_host_ != nullptr;
176 }
177
HidePopup()178 void ExtensionActionViewController::HidePopup() {
179 if (IsShowingPopup()) {
180 popup_host_->Close();
181 // We need to do these actions synchronously (instead of closing and then
182 // performing the rest of the cleanup in OnExtensionHostDestroyed()) because
183 // the extension host may close asynchronously, and we need to keep the view
184 // delegate up to date.
185 if (popup_host_)
186 OnPopupClosed();
187 }
188 }
189
GetPopupNativeView()190 gfx::NativeView ExtensionActionViewController::GetPopupNativeView() {
191 return popup_host_ ? popup_host_->view()->GetNativeView() : nullptr;
192 }
193
GetContextMenu()194 ui::MenuModel* ExtensionActionViewController::GetContextMenu() {
195 if (!ExtensionIsValid())
196 return nullptr;
197
198 ToolbarActionViewController* const action =
199 extensions_container_->GetActionForId(GetId());
200 extensions::ExtensionContextMenuModel::ButtonVisibility visibility =
201 extensions_container_->GetActionVisibility(action);
202
203 // Reconstruct the menu every time because the menu's contents are dynamic.
204 context_menu_model_ = std::make_unique<extensions::ExtensionContextMenuModel>(
205 extension(), browser_, visibility, this,
206 view_delegate_->CanShowIconInToolbar());
207 return context_menu_model_.get();
208 }
209
OnContextMenuShown()210 void ExtensionActionViewController::OnContextMenuShown() {
211 extensions_container_->OnContextMenuShown(this);
212 }
213
OnContextMenuClosed()214 void ExtensionActionViewController::OnContextMenuClosed() {
215 if (base::FeatureList::IsEnabled(features::kExtensionsToolbarMenu)) {
216 extensions_container_->OnContextMenuClosed(this);
217 return;
218 }
219
220 if (extensions_container_->GetPoppedOutAction() == this && !IsShowingPopup())
221 extensions_container_->UndoPopOut();
222 }
223
ExecuteAction(bool by_user,InvocationSource source)224 bool ExtensionActionViewController::ExecuteAction(bool by_user,
225 InvocationSource source) {
226 if (!ExtensionIsValid())
227 return false;
228
229 if (!IsEnabled(view_delegate_->GetCurrentWebContents())) {
230 if (DisabledClickOpensMenu())
231 GetPreferredPopupViewController()->platform_delegate_->ShowContextMenu();
232 return false;
233 }
234
235 base::UmaHistogramEnumeration("Extensions.Toolbar.InvocationSource", source);
236 return ExecuteAction(SHOW_POPUP, by_user);
237 }
238
UpdateState()239 void ExtensionActionViewController::UpdateState() {
240 if (!ExtensionIsValid())
241 return;
242
243 view_delegate_->UpdateState();
244 }
245
ExecuteAction(PopupShowAction show_action,bool grant_tab_permissions)246 bool ExtensionActionViewController::ExecuteAction(PopupShowAction show_action,
247 bool grant_tab_permissions) {
248 if (!ExtensionIsValid())
249 return false;
250
251 content::WebContents* web_contents = view_delegate_->GetCurrentWebContents();
252 ExtensionActionRunner* action_runner =
253 ExtensionActionRunner::GetForWebContents(web_contents);
254 if (!action_runner)
255 return false;
256
257 if (base::FeatureList::IsEnabled(features::kExtensionsToolbarMenu))
258 extensions_container_->CloseOverflowMenuIfOpen();
259
260 if (action_runner->RunAction(extension(), grant_tab_permissions) ==
261 extensions::ExtensionAction::ACTION_SHOW_POPUP) {
262 GURL popup_url = extension_action_->GetPopupUrl(
263 sessions::SessionTabHelper::IdForTab(web_contents).id());
264 return GetPreferredPopupViewController()
265 ->TriggerPopupWithUrl(show_action, popup_url, grant_tab_permissions);
266 }
267 return false;
268 }
269
RegisterCommand()270 void ExtensionActionViewController::RegisterCommand() {
271 if (!ExtensionIsValid())
272 return;
273
274 platform_delegate_->RegisterCommand();
275 }
276
UnregisterCommand()277 void ExtensionActionViewController::UnregisterCommand() {
278 platform_delegate_->UnregisterCommand();
279 }
280
DisabledClickOpensMenu() const281 bool ExtensionActionViewController::DisabledClickOpensMenu() const {
282 return true;
283 }
284
InspectPopup()285 void ExtensionActionViewController::InspectPopup() {
286 ExecuteAction(SHOW_POPUP_AND_INSPECT, true);
287 }
288
OnIconUpdated()289 void ExtensionActionViewController::OnIconUpdated() {
290 // We update the view first, so that if the observer relies on its UI it can
291 // be ready.
292 if (view_delegate_)
293 view_delegate_->UpdateState();
294 }
295
OnExtensionHostDestroyed(extensions::ExtensionHost * host)296 void ExtensionActionViewController::OnExtensionHostDestroyed(
297 extensions::ExtensionHost* host) {
298 OnPopupClosed();
299 }
300
301 ExtensionActionViewController::PageInteractionStatus
GetPageInteractionStatus(content::WebContents * web_contents) const302 ExtensionActionViewController::GetPageInteractionStatus(
303 content::WebContents* web_contents) const {
304 // The |web_contents| can be null, if TabStripModel::GetActiveWebContents()
305 // returns null. In that case, default to kNone.
306 if (!web_contents)
307 return PageInteractionStatus::kNone;
308
309 const int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
310 const GURL& url = web_contents->GetLastCommittedURL();
311 extensions::PermissionsData::PageAccess page_access =
312 extension_->permissions_data()->GetPageAccess(url, tab_id,
313 /*error=*/nullptr);
314 extensions::PermissionsData::PageAccess script_access =
315 extension_->permissions_data()->GetContentScriptAccess(url, tab_id,
316 /*error=*/nullptr);
317 if (page_access == extensions::PermissionsData::PageAccess::kAllowed ||
318 script_access == extensions::PermissionsData::PageAccess::kAllowed) {
319 return PageInteractionStatus::kActive;
320 }
321 // TODO(tjudkins): Investigate if we need to check HasBeenBlocked() for this
322 // case. We do know that extensions that have been blocked should always be
323 // marked pending, but those cases should be covered by the withheld page
324 // access checks.
325 if (page_access == extensions::PermissionsData::PageAccess::kWithheld ||
326 script_access == extensions::PermissionsData::PageAccess::kWithheld ||
327 HasBeenBlocked(web_contents) || HasActiveTabAndCanAccess(url)) {
328 return PageInteractionStatus::kPending;
329 }
330
331 return PageInteractionStatus::kNone;
332 }
333
ExtensionIsValid() const334 bool ExtensionActionViewController::ExtensionIsValid() const {
335 return extension_registry_->enabled_extensions().Contains(extension_->id());
336 }
337
GetExtensionCommand(extensions::Command * command) const338 bool ExtensionActionViewController::GetExtensionCommand(
339 extensions::Command* command) const {
340 DCHECK(command);
341 if (!ExtensionIsValid())
342 return false;
343
344 CommandService* command_service = CommandService::Get(browser_->profile());
345 return command_service->GetExtensionActionCommand(
346 extension_->id(), extension_action_->action_type(),
347 CommandService::ACTIVE, command, nullptr);
348 }
349
CanHandleAccelerators() const350 bool ExtensionActionViewController::CanHandleAccelerators() const {
351 if (!ExtensionIsValid())
352 return false;
353
354 #if DCHECK_IS_ON()
355 {
356 extensions::Command command;
357 DCHECK(GetExtensionCommand(&command));
358 }
359 #endif
360
361 // Page action accelerators are enabled if and only if the page action is
362 // enabled ("visible" in legacy terms) on the given tab. Other actions can
363 // always accept accelerators.
364 // TODO(devlin): Have all actions behave similarly; this should likely mean
365 // always checking IsEnabled(). It's weird to use a keyboard shortcut on a
366 // disabled action (in most cases, this will result in opening the context
367 // menu).
368 if (extension_action_->action_type() == extensions::ActionInfo::TYPE_PAGE)
369 return IsEnabled(view_delegate_->GetCurrentWebContents());
370 return true;
371 }
372
373 std::unique_ptr<IconWithBadgeImageSource>
GetIconImageSourceForTesting(content::WebContents * web_contents,const gfx::Size & size)374 ExtensionActionViewController::GetIconImageSourceForTesting(
375 content::WebContents* web_contents,
376 const gfx::Size& size) {
377 return GetIconImageSource(web_contents, size);
378 }
379
HasBeenBlockedForTesting(content::WebContents * web_contents) const380 bool ExtensionActionViewController::HasBeenBlockedForTesting(
381 content::WebContents* web_contents) const {
382 return HasBeenBlocked(web_contents);
383 }
384
385 ExtensionActionViewController*
GetPreferredPopupViewController()386 ExtensionActionViewController::GetPreferredPopupViewController() {
387 return static_cast<ExtensionActionViewController*>(
388 extensions_container_->GetActionForId(GetId()));
389 }
390
TriggerPopupWithUrl(PopupShowAction show_action,const GURL & popup_url,bool grant_tab_permissions)391 bool ExtensionActionViewController::TriggerPopupWithUrl(
392 PopupShowAction show_action,
393 const GURL& popup_url,
394 bool grant_tab_permissions) {
395 DCHECK(!in_overflow_mode_)
396 << "Only the main bar's extensions should ever try to show a popup";
397 if (!ExtensionIsValid())
398 return false;
399
400 // Always hide the current popup, even if it's not owned by this extension.
401 // Only one popup should be visible at a time.
402 extensions_container_->HideActivePopup();
403
404 std::unique_ptr<extensions::ExtensionViewHost> host =
405 extensions::ExtensionViewHostFactory::CreatePopupHost(popup_url,
406 browser_);
407 if (!host)
408 return false;
409
410 popup_host_ = host.get();
411 popup_host_observer_.Add(popup_host_);
412 extensions_container_->SetPopupOwner(this);
413
414 if (!extensions_container_->IsActionVisibleOnToolbar(this) ||
415 base::FeatureList::IsEnabled(features::kExtensionsToolbarMenu)) {
416 extensions_container_->CloseOverflowMenuIfOpen();
417 extensions_container_->PopOutAction(
418 this, show_action == SHOW_POPUP_AND_INSPECT,
419 base::Bind(&ExtensionActionViewController::ShowPopup,
420 weak_factory_.GetWeakPtr(), base::Passed(std::move(host)),
421 grant_tab_permissions, show_action));
422 } else {
423 ShowPopup(std::move(host), grant_tab_permissions, show_action);
424 }
425
426 return true;
427 }
428
ShowPopup(std::unique_ptr<extensions::ExtensionViewHost> popup_host,bool grant_tab_permissions,PopupShowAction show_action)429 void ExtensionActionViewController::ShowPopup(
430 std::unique_ptr<extensions::ExtensionViewHost> popup_host,
431 bool grant_tab_permissions,
432 PopupShowAction show_action) {
433 // It's possible that the popup should be closed before it finishes opening
434 // (since it can open asynchronously). Check before proceeding.
435 if (!popup_host_)
436 return;
437 platform_delegate_->ShowPopup(std::move(popup_host), grant_tab_permissions,
438 show_action);
439 view_delegate_->OnPopupShown(grant_tab_permissions);
440 }
441
OnPopupClosed()442 void ExtensionActionViewController::OnPopupClosed() {
443 popup_host_observer_.Remove(popup_host_);
444 popup_host_ = nullptr;
445 extensions_container_->SetPopupOwner(nullptr);
446 if (extensions_container_->GetPoppedOutAction() == this &&
447 (base::FeatureList::IsEnabled(features::kExtensionsToolbarMenu) ||
448 !view_delegate_->IsMenuRunning())) {
449 extensions_container_->UndoPopOut();
450 }
451 view_delegate_->OnPopupClosed();
452 }
453
454 std::unique_ptr<IconWithBadgeImageSource>
GetIconImageSource(content::WebContents * web_contents,const gfx::Size & size)455 ExtensionActionViewController::GetIconImageSource(
456 content::WebContents* web_contents,
457 const gfx::Size& size) {
458 int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
459 std::unique_ptr<IconWithBadgeImageSource> image_source(
460 new IconWithBadgeImageSource(size));
461
462 image_source->SetIcon(icon_factory_.GetIcon(tab_id));
463
464 std::unique_ptr<IconWithBadgeImageSource::Badge> badge;
465 std::string badge_text = extension_action_->GetDisplayBadgeText(tab_id);
466 if (!badge_text.empty()) {
467 badge = std::make_unique<IconWithBadgeImageSource::Badge>(
468 badge_text, extension_action_->GetBadgeTextColor(tab_id),
469 extension_action_->GetBadgeBackgroundColor(tab_id));
470 }
471 image_source->SetBadge(std::move(badge));
472
473 bool grayscale = false;
474 bool was_blocked = false;
475 bool action_is_visible = extension_action_->GetIsVisible(tab_id);
476 PageInteractionStatus interaction_status =
477 GetPageInteractionStatus(web_contents);
478 // We only grayscale the icon if it cannot interact with the page and the icon
479 // is disabled.
480 grayscale =
481 interaction_status == PageInteractionStatus::kNone && !action_is_visible;
482 was_blocked = HasBeenBlocked(web_contents);
483
484 image_source->set_grayscale(grayscale);
485 image_source->set_paint_blocked_actions_decoration(was_blocked);
486
487 // If the action has an active page action on the web contents and is also
488 // overflowed, we add a decoration so that the user can see which overflowed
489 // action wants to run (since they wouldn't be able to see the change from
490 // grayscale to color).
491 image_source->set_paint_page_action_decoration(
492 !was_blocked && in_overflow_mode_ && PageActionWantsToRun(web_contents));
493
494 return image_source;
495 }
496
PageActionWantsToRun(content::WebContents * web_contents) const497 bool ExtensionActionViewController::PageActionWantsToRun(
498 content::WebContents* web_contents) const {
499 return extension_action_->action_type() ==
500 extensions::ActionInfo::TYPE_PAGE &&
501 extension_action_->GetIsVisible(
502 sessions::SessionTabHelper::IdForTab(web_contents).id());
503 }
504
HasActiveTabAndCanAccess(const GURL & url) const505 bool ExtensionActionViewController::HasActiveTabAndCanAccess(
506 const GURL& url) const {
507 return extension_->permissions_data()->HasAPIPermission(
508 extensions::APIPermission::kActiveTab) &&
509 !extension_->permissions_data()->IsRestrictedUrl(url,
510 /*error=*/nullptr) &&
511 (!url.SchemeIsFile() || extensions::util::AllowFileAccess(
512 extension_->id(), browser_->profile()));
513 }
514
HasBeenBlocked(content::WebContents * web_contents) const515 bool ExtensionActionViewController::HasBeenBlocked(
516 content::WebContents* web_contents) const {
517 ExtensionActionRunner* action_runner =
518 ExtensionActionRunner::GetForWebContents(web_contents);
519 return action_runner && action_runner->WantsToRun(extension());
520 }
521