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