1 // Copyright (c) 2012 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/extensions/extension_context_menu_model.h"
6 
7 #include "base/bind.h"
8 #include "base/macros.h"
9 #include "base/metrics/histogram_macros.h"
10 #include "base/metrics/user_metrics.h"
11 #include "base/metrics/user_metrics_action.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "chrome/app/chrome_command_ids.h"
14 #include "chrome/browser/extensions/chrome_extension_browser_constants.h"
15 #include "chrome/browser/extensions/context_menu_matcher.h"
16 #include "chrome/browser/extensions/extension_action_runner.h"
17 #include "chrome/browser/extensions/extension_management.h"
18 #include "chrome/browser/extensions/extension_tab_util.h"
19 #include "chrome/browser/extensions/extension_uninstall_dialog.h"
20 #include "chrome/browser/extensions/extension_util.h"
21 #include "chrome/browser/extensions/menu_manager.h"
22 #include "chrome/browser/extensions/scripting_permissions_modifier.h"
23 #include "chrome/browser/profiles/profile.h"
24 #include "chrome/browser/ui/browser.h"
25 #include "chrome/browser/ui/browser_window.h"
26 #include "chrome/browser/ui/chrome_pages.h"
27 #include "chrome/browser/ui/tabs/tab_strip_model.h"
28 #include "chrome/browser/ui/toolbar/toolbar_actions_model.h"
29 #include "chrome/browser/ui/ui_features.h"
30 #include "chrome/common/extensions/extension_constants.h"
31 #include "chrome/common/pref_names.h"
32 #include "chrome/common/url_constants.h"
33 #include "chrome/grit/chromium_strings.h"
34 #include "chrome/grit/generated_resources.h"
35 #include "components/prefs/pref_service.h"
36 #include "components/sessions/content/session_tab_helper.h"
37 #include "components/url_formatter/url_formatter.h"
38 #include "components/vector_icons/vector_icons.h"
39 #include "content/public/browser/context_menu_params.h"
40 #include "content/public/browser/web_contents.h"
41 #include "extensions/browser/extension_action.h"
42 #include "extensions/browser/extension_action_manager.h"
43 #include "extensions/browser/extension_registry.h"
44 #include "extensions/browser/extension_system.h"
45 #include "extensions/browser/management_policy.h"
46 #include "extensions/browser/uninstall_reason.h"
47 #include "extensions/common/extension.h"
48 #include "extensions/common/manifest_handlers/options_page_info.h"
49 #include "extensions/common/manifest_url_handlers.h"
50 #include "ui/base/l10n/l10n_util.h"
51 #include "ui/base/models/image_model.h"
52 #include "ui/base/models/menu_separator_types.h"
53 #include "ui/base/resource/resource_bundle.h"
54 #include "ui/gfx/color_palette.h"
55 #include "ui/gfx/image/image.h"
56 #include "ui/gfx/paint_vector_icon.h"
57 
58 namespace extensions {
59 
60 namespace {
61 
62 // Returns true if the given |item| is of the given |type|.
MenuItemMatchesAction(const base::Optional<ActionInfo::Type> action_type,const MenuItem * item)63 bool MenuItemMatchesAction(const base::Optional<ActionInfo::Type> action_type,
64                            const MenuItem* item) {
65   if (!action_type)
66     return false;
67 
68   const MenuItem::ContextList& contexts = item->contexts();
69 
70   if (contexts.Contains(MenuItem::ALL))
71     return true;
72   if (contexts.Contains(MenuItem::PAGE_ACTION) &&
73       (*action_type == ActionInfo::TYPE_PAGE)) {
74     return true;
75   }
76   if (contexts.Contains(MenuItem::BROWSER_ACTION) &&
77       (*action_type == ActionInfo::TYPE_BROWSER)) {
78     return true;
79   }
80   if (contexts.Contains(MenuItem::ACTION) &&
81       (*action_type == ActionInfo::TYPE_ACTION)) {
82     return true;
83   }
84 
85   return false;
86 }
87 
88 // Returns true if the given |extension| is required to remain pinned/visible in
89 // the toolbar by policy.
IsExtensionForcePinned(const Extension & extension,Profile * profile)90 bool IsExtensionForcePinned(const Extension& extension, Profile* profile) {
91   auto* management = ExtensionManagementFactory::GetForBrowserContext(profile);
92   return base::Contains(management->GetForcePinnedList(), extension.id());
93 }
94 
95 // Returns the id for the visibility command for the given |extension|.
GetVisibilityStringId(Profile * profile,const Extension * extension,ExtensionContextMenuModel::ButtonVisibility button_visibility)96 int GetVisibilityStringId(
97     Profile* profile,
98     const Extension* extension,
99     ExtensionContextMenuModel::ButtonVisibility button_visibility) {
100   if (base::FeatureList::IsEnabled(features::kExtensionsToolbarMenu)) {
101     if (IsExtensionForcePinned(*extension, profile))
102       return IDS_EXTENSIONS_PINNED_BY_ADMIN;
103     if (button_visibility == ExtensionContextMenuModel::PINNED)
104       return IDS_EXTENSIONS_UNPIN_FROM_TOOLBAR;
105     return IDS_EXTENSIONS_PIN_TO_TOOLBAR;
106   }
107   DCHECK(profile);
108   int string_id = -1;
109   // We display "show" or "hide" based on the icon's visibility, and can have
110   // "transitively shown" buttons that are shown only while the button has a
111   // popup or menu visible.
112   switch (button_visibility) {
113     case (ExtensionContextMenuModel::PINNED):
114       string_id = IDS_EXTENSIONS_HIDE_BUTTON_IN_MENU;
115       break;
116     case (ExtensionContextMenuModel::TRANSITIVELY_VISIBLE):
117       string_id = IDS_EXTENSIONS_KEEP_BUTTON_IN_TOOLBAR;
118       break;
119     case (ExtensionContextMenuModel::UNPINNED):
120       string_id = IDS_EXTENSIONS_SHOW_BUTTON_IN_TOOLBAR;
121       break;
122   }
123 
124   return string_id;
125 }
126 
127 // Returns true if the given |extension| is required to remain installed by
128 // policy.
IsExtensionRequiredByPolicy(const Extension * extension,Profile * profile)129 bool IsExtensionRequiredByPolicy(const Extension* extension,
130                                  Profile* profile) {
131   ManagementPolicy* policy = ExtensionSystem::Get(profile)->management_policy();
132   return !policy->UserMayModifySettings(extension, nullptr) ||
133          policy->MustRemainInstalled(extension, nullptr);
134 }
135 
CommandIdToContextMenuAction(int command_id)136 ExtensionContextMenuModel::ContextMenuAction CommandIdToContextMenuAction(
137     int command_id) {
138   using ContextMenuAction = ExtensionContextMenuModel::ContextMenuAction;
139 
140   switch (command_id) {
141     case ExtensionContextMenuModel::HOME_PAGE:
142       return ContextMenuAction::kHomePage;
143     case ExtensionContextMenuModel::OPTIONS:
144       return ContextMenuAction::kOptions;
145     case ExtensionContextMenuModel::TOGGLE_VISIBILITY:
146       return ContextMenuAction::kToggleVisibility;
147     case ExtensionContextMenuModel::UNINSTALL:
148       return ContextMenuAction::kUninstall;
149     case ExtensionContextMenuModel::MANAGE_EXTENSIONS:
150       return ContextMenuAction::kManageExtensions;
151     case ExtensionContextMenuModel::INSPECT_POPUP:
152       return ContextMenuAction::kInspectPopup;
153     case ExtensionContextMenuModel::PAGE_ACCESS_RUN_ON_CLICK:
154       return ContextMenuAction::kPageAccessRunOnClick;
155     case ExtensionContextMenuModel::PAGE_ACCESS_RUN_ON_SITE:
156       return ContextMenuAction::kPageAccessRunOnSite;
157     case ExtensionContextMenuModel::PAGE_ACCESS_RUN_ON_ALL_SITES:
158       return ContextMenuAction::kPageAccessRunOnAllSites;
159     case ExtensionContextMenuModel::PAGE_ACCESS_LEARN_MORE:
160       return ContextMenuAction::kPageAccessLearnMore;
161     case ExtensionContextMenuModel::PAGE_ACCESS_CANT_ACCESS:
162     case ExtensionContextMenuModel::PAGE_ACCESS_SUBMENU:
163       NOTREACHED();
164       break;
165     default:
166       break;
167   }
168   NOTREACHED();
169   return ContextMenuAction::kNoAction;
170 }
171 
172 // A stub for the uninstall dialog.
173 // TODO(devlin): Ideally, we would just have the uninstall dialog take a
174 // base::Callback, but that's a bunch of churn.
175 class UninstallDialogHelper : public ExtensionUninstallDialog::Delegate {
176  public:
177   // Kicks off the asynchronous process to confirm and uninstall the given
178   // |extension|.
UninstallExtension(Browser * browser,const Extension * extension)179   static void UninstallExtension(Browser* browser, const Extension* extension) {
180     UninstallDialogHelper* helper = new UninstallDialogHelper();
181     helper->BeginUninstall(browser, extension);
182   }
183 
184  private:
185   // This class handles its own lifetime.
UninstallDialogHelper()186   UninstallDialogHelper() {}
~UninstallDialogHelper()187   ~UninstallDialogHelper() override {}
188 
BeginUninstall(Browser * browser,const Extension * extension)189   void BeginUninstall(Browser* browser, const Extension* extension) {
190     uninstall_dialog_ = ExtensionUninstallDialog::Create(
191         browser->profile(), browser->window()->GetNativeWindow(), this);
192     uninstall_dialog_->ConfirmUninstall(extension,
193                                         UNINSTALL_REASON_USER_INITIATED,
194                                         UNINSTALL_SOURCE_TOOLBAR_CONTEXT_MENU);
195   }
196 
197   // ExtensionUninstallDialog::Delegate:
OnExtensionUninstallDialogClosed(bool did_start_uninstall,const base::string16 & error)198   void OnExtensionUninstallDialogClosed(bool did_start_uninstall,
199                                         const base::string16& error) override {
200     delete this;
201   }
202 
203   std::unique_ptr<ExtensionUninstallDialog> uninstall_dialog_;
204 
205   DISALLOW_COPY_AND_ASSIGN(UninstallDialogHelper);
206 };
207 
208 }  // namespace
209 
ExtensionContextMenuModel(const Extension * extension,Browser * browser,ButtonVisibility button_visibility,PopupDelegate * delegate,bool can_show_icon_in_toolbar)210 ExtensionContextMenuModel::ExtensionContextMenuModel(
211     const Extension* extension,
212     Browser* browser,
213     ButtonVisibility button_visibility,
214     PopupDelegate* delegate,
215     bool can_show_icon_in_toolbar)
216     : SimpleMenuModel(this),
217       extension_id_(extension->id()),
218       is_component_(Manifest::IsComponentLocation(extension->location())),
219       browser_(browser),
220       profile_(browser->profile()),
221       delegate_(delegate),
222       button_visibility_(button_visibility),
223       can_show_icon_in_toolbar_(can_show_icon_in_toolbar) {
224   InitMenu(extension, button_visibility);
225 }
226 
IsCommandIdChecked(int command_id) const227 bool ExtensionContextMenuModel::IsCommandIdChecked(int command_id) const {
228   const Extension* extension = GetExtension();
229   if (!extension)
230     return false;
231 
232   if (ContextMenuMatcher::IsExtensionsCustomCommandId(command_id))
233     return extension_items_->IsCommandIdChecked(command_id);
234 
235   if (command_id == PAGE_ACCESS_RUN_ON_CLICK ||
236       command_id == PAGE_ACCESS_RUN_ON_SITE ||
237       command_id == PAGE_ACCESS_RUN_ON_ALL_SITES) {
238     content::WebContents* web_contents = GetActiveWebContents();
239     return web_contents &&
240            GetCurrentPageAccess(extension, web_contents) == command_id;
241   }
242 
243   return false;
244 }
245 
IsCommandIdVisible(int command_id) const246 bool ExtensionContextMenuModel::IsCommandIdVisible(int command_id) const {
247   const Extension* extension = GetExtension();
248   if (!extension)
249     return false;
250   if (ContextMenuMatcher::IsExtensionsCustomCommandId(command_id)) {
251     return extension_items_->IsCommandIdVisible(command_id);
252   }
253 
254   // The command is hidden in app windows because they don't
255   // support showing extensions in the app window frame.
256   if (command_id == TOGGLE_VISIBILITY)
257     return can_show_icon_in_toolbar_;
258 
259   // Standard menu items are visible.
260   return true;
261 }
262 
IsCommandIdEnabled(int command_id) const263 bool ExtensionContextMenuModel::IsCommandIdEnabled(int command_id) const {
264   const Extension* extension = GetExtension();
265   if (!extension)
266     return false;
267 
268   if (ContextMenuMatcher::IsExtensionsCustomCommandId(command_id))
269     return extension_items_->IsCommandIdEnabled(command_id);
270 
271   switch (command_id) {
272     case HOME_PAGE:
273       // The HOME_PAGE links to the Homepage URL. If the extension doesn't have
274       // a homepage, we just disable this menu item. We also disable for
275       // component extensions, because it doesn't make sense to link to a
276       // webstore page or chrome://extensions.
277       return ManifestURL::GetHomepageURL(extension).is_valid() &&
278              !is_component_;
279     case OPTIONS:
280       return OptionsPageInfo::HasOptionsPage(extension);
281     case INSPECT_POPUP: {
282       content::WebContents* web_contents = GetActiveWebContents();
283       return web_contents && extension_action_ &&
284              extension_action_->HasPopup(
285                  sessions::SessionTabHelper::IdForTab(web_contents).id());
286     }
287     case UNINSTALL:
288       return !IsExtensionRequiredByPolicy(extension, profile_);
289     case PAGE_ACCESS_CANT_ACCESS:
290     case PAGE_ACCESS_SUBMENU:
291     case PAGE_ACCESS_RUN_ON_CLICK:
292     case PAGE_ACCESS_RUN_ON_SITE:
293     case PAGE_ACCESS_RUN_ON_ALL_SITES:
294     case PAGE_ACCESS_LEARN_MORE: {
295       content::WebContents* web_contents = GetActiveWebContents();
296       if (!web_contents)
297         return false;
298       // TODO(devlin): This can lead to some fun race-like conditions, where the
299       // menu is constructed during navigation. Since we get the URL both here
300       // and in execution of the command, there's a chance we'll find two
301       // different URLs. This would be solved if we maintained the URL that the
302       // menu was showing for.
303       const GURL& url = web_contents->GetLastCommittedURL();
304       return IsPageAccessCommandEnabled(*extension, url, command_id);
305     }
306     // Extension pinning/unpinning is not available for Incognito as this leaves
307     // a trace of user activity.
308     case TOGGLE_VISIBILITY:
309       return (base::FeatureList::IsEnabled(features::kExtensionsToolbarMenu) &&
310               !browser_->profile()->IsOffTheRecord() &&
311               !IsExtensionForcePinned(*extension, profile_));
312     // Manage extensions is always enabled.
313     case MANAGE_EXTENSIONS:
314       return true;
315     default:
316       NOTREACHED() << "Unknown command" << command_id;
317   }
318   return true;
319 }
320 
ExecuteCommand(int command_id,int event_flags)321 void ExtensionContextMenuModel::ExecuteCommand(int command_id,
322                                                int event_flags) {
323   const Extension* extension = GetExtension();
324   if (!extension)
325     return;
326 
327   if (ContextMenuMatcher::IsExtensionsCustomCommandId(command_id)) {
328     DCHECK(extension_items_);
329     extension_items_->ExecuteCommand(command_id, GetActiveWebContents(),
330                                      nullptr, content::ContextMenuParams());
331     action_taken_ = ContextMenuAction::kCustomCommand;
332     return;
333   }
334 
335   action_taken_ = CommandIdToContextMenuAction(command_id);
336 
337   switch (command_id) {
338     case HOME_PAGE: {
339       content::OpenURLParams params(ManifestURL::GetHomepageURL(extension),
340                                     content::Referrer(),
341                                     WindowOpenDisposition::NEW_FOREGROUND_TAB,
342                                     ui::PAGE_TRANSITION_LINK, false);
343       browser_->OpenURL(params);
344       break;
345     }
346     case OPTIONS:
347       DCHECK(OptionsPageInfo::HasOptionsPage(extension));
348       ExtensionTabUtil::OpenOptionsPage(extension, browser_);
349       break;
350     case TOGGLE_VISIBILITY: {
351       bool currently_visible = button_visibility_ == PINNED;
352       ToolbarActionsModel::Get(browser_->profile())
353           ->SetActionVisibility(extension->id(), !currently_visible);
354       break;
355     }
356     case UNINSTALL: {
357       UninstallDialogHelper::UninstallExtension(browser_, extension);
358       break;
359     }
360     case MANAGE_EXTENSIONS: {
361       chrome::ShowExtensions(browser_, extension->id());
362       break;
363     }
364     case INSPECT_POPUP: {
365       delegate_->InspectPopup();
366       break;
367     }
368     case PAGE_ACCESS_RUN_ON_CLICK:
369     case PAGE_ACCESS_RUN_ON_SITE:
370     case PAGE_ACCESS_RUN_ON_ALL_SITES:
371     case PAGE_ACCESS_LEARN_MORE:
372       HandlePageAccessCommand(command_id, extension);
373       break;
374     default:
375      NOTREACHED() << "Unknown option";
376      break;
377   }
378 }
379 
OnMenuWillShow(ui::SimpleMenuModel * menu)380 void ExtensionContextMenuModel::OnMenuWillShow(ui::SimpleMenuModel* menu) {
381   action_taken_ = ContextMenuAction::kNoAction;
382 }
383 
MenuClosed(ui::SimpleMenuModel * menu)384 void ExtensionContextMenuModel::MenuClosed(ui::SimpleMenuModel* menu) {
385   if (action_taken_) {
386     ContextMenuAction action = *action_taken_;
387     UMA_HISTOGRAM_ENUMERATION("Extensions.ContextMenuAction", action);
388     action_taken_ = base::nullopt;
389   }
390 }
391 
~ExtensionContextMenuModel()392 ExtensionContextMenuModel::~ExtensionContextMenuModel() {}
393 
InitMenu(const Extension * extension,ButtonVisibility button_visibility)394 void ExtensionContextMenuModel::InitMenu(const Extension* extension,
395                                          ButtonVisibility button_visibility) {
396   DCHECK(extension);
397 
398   base::Optional<ActionInfo::Type> action_type;
399   extension_action_ =
400       ExtensionActionManager::Get(profile_)->GetExtensionAction(*extension);
401   if (extension_action_)
402     action_type = extension_action_->action_type();
403 
404   extension_items_.reset(new ContextMenuMatcher(
405       profile_, this, this,
406       base::BindRepeating(MenuItemMatchesAction, action_type)));
407 
408   std::string extension_name = extension->name();
409   // Ampersands need to be escaped to avoid being treated like
410   // mnemonics in the menu.
411   base::ReplaceChars(extension_name, "&", "&&", &extension_name);
412   AddItem(HOME_PAGE, base::UTF8ToUTF16(extension_name));
413   AppendExtensionItems();
414   AddSeparator(ui::NORMAL_SEPARATOR);
415 
416   CreatePageAccessSubmenu(extension);
417 
418   if (!is_component_ || OptionsPageInfo::HasOptionsPage(extension))
419     AddItemWithStringId(OPTIONS, IDS_EXTENSIONS_OPTIONS_MENU_ITEM);
420 
421   if (!is_component_) {
422     bool is_required_by_policy =
423         IsExtensionRequiredByPolicy(extension, profile_);
424     int message_id = is_required_by_policy ?
425         IDS_EXTENSIONS_INSTALLED_BY_ADMIN : IDS_EXTENSIONS_UNINSTALL;
426     AddItem(UNINSTALL, l10n_util::GetStringUTF16(message_id));
427     if (is_required_by_policy) {
428       int uninstall_index = GetIndexOfCommandId(UNINSTALL);
429       // TODO (kylixrd): Investigate the usage of the hard-coded color.
430       SetIcon(uninstall_index,
431               ui::ImageModel::FromVectorIcon(vector_icons::kBusinessIcon,
432                                              gfx::kChromeIconGrey, 16));
433     }
434   }
435 
436   // Add a toggle visibility (show/hide) if the extension icon is shown on the
437   // toolbar.
438   int visibility_string_id =
439       GetVisibilityStringId(profile_, extension, button_visibility);
440   DCHECK_NE(-1, visibility_string_id);
441   AddItemWithStringId(TOGGLE_VISIBILITY, visibility_string_id);
442   if (IsExtensionForcePinned(*extension, profile_)) {
443     int toggle_visibility_index = GetIndexOfCommandId(TOGGLE_VISIBILITY);
444     SetIcon(toggle_visibility_index,
445             ui::ImageModel::FromVectorIcon(vector_icons::kBusinessIcon,
446                                            gfx::kChromeIconGrey, 16));
447   }
448 
449   if (!is_component_) {
450     AddSeparator(ui::NORMAL_SEPARATOR);
451     AddItemWithStringId(MANAGE_EXTENSIONS, IDS_MANAGE_EXTENSION);
452   }
453 
454   const ActionInfo* action_info = ActionInfo::GetExtensionActionInfo(extension);
455   if (delegate_ && !is_component_ && action_info && !action_info->synthesized &&
456       profile_->GetPrefs()->GetBoolean(prefs::kExtensionsUIDeveloperMode)) {
457     AddSeparator(ui::NORMAL_SEPARATOR);
458     AddItemWithStringId(INSPECT_POPUP, IDS_EXTENSION_ACTION_INSPECT_POPUP);
459   }
460 }
461 
GetExtension() const462 const Extension* ExtensionContextMenuModel::GetExtension() const {
463   return ExtensionRegistry::Get(profile_)->enabled_extensions().GetByID(
464       extension_id_);
465 }
466 
AppendExtensionItems()467 void ExtensionContextMenuModel::AppendExtensionItems() {
468   MenuManager* menu_manager = MenuManager::Get(profile_);
469   if (!menu_manager ||  // Null in unit tests
470       !menu_manager->MenuItems(MenuItem::ExtensionKey(extension_id_)))
471     return;
472 
473   AddSeparator(ui::NORMAL_SEPARATOR);
474 
475   int index = 0;
476   extension_items_->AppendExtensionItems(MenuItem::ExtensionKey(extension_id_),
477                                          base::string16(), &index,
478                                          true);  // is_action_menu
479 }
480 
481 ExtensionContextMenuModel::MenuEntries
GetCurrentPageAccess(const Extension * extension,content::WebContents * web_contents) const482 ExtensionContextMenuModel::GetCurrentPageAccess(
483     const Extension* extension,
484     content::WebContents* web_contents) const {
485   DCHECK(web_contents);
486   ScriptingPermissionsModifier modifier(profile_, extension);
487   DCHECK(modifier.CanAffectExtension());
488   ScriptingPermissionsModifier::SiteAccess site_access =
489       modifier.GetSiteAccess(web_contents->GetLastCommittedURL());
490   if (site_access.has_all_sites_access)
491     return PAGE_ACCESS_RUN_ON_ALL_SITES;
492   if (site_access.has_site_access)
493     return PAGE_ACCESS_RUN_ON_SITE;
494   return PAGE_ACCESS_RUN_ON_CLICK;
495 }
496 
IsPageAccessCommandEnabled(const Extension & extension,const GURL & url,int command_id) const497 bool ExtensionContextMenuModel::IsPageAccessCommandEnabled(
498     const Extension& extension,
499     const GURL& url,
500     int command_id) const {
501   // The "Can't access this site" entry is, by design, always disabled.
502   if (command_id == PAGE_ACCESS_CANT_ACCESS)
503     return false;
504 
505   ScriptingPermissionsModifier modifier(profile_, &extension);
506   DCHECK(modifier.CanAffectExtension());
507 
508   ScriptingPermissionsModifier::SiteAccess site_access =
509       modifier.GetSiteAccess(url);
510 
511   // Verify the extension wants access to the page - that's the only time these
512   // commands should be shown.
513   DCHECK(site_access.has_site_access || site_access.withheld_site_access ||
514          extension.permissions_data()->HasAPIPermission(
515              APIPermission::kActiveTab));
516 
517   switch (command_id) {
518     case PAGE_ACCESS_SUBMENU:
519     case PAGE_ACCESS_LEARN_MORE:
520     case PAGE_ACCESS_RUN_ON_CLICK:
521       // These are always enabled.
522       return true;
523     case PAGE_ACCESS_RUN_ON_SITE:
524       // The "on this site" option is only enabled if the extension wants to
525       // always run on the site without user interaction.
526       return site_access.has_site_access || site_access.withheld_site_access;
527     case PAGE_ACCESS_RUN_ON_ALL_SITES:
528       // The "on all sites" option is only enabled if the extension wants to be
529       // able to run everywhere.
530       return site_access.has_all_sites_access ||
531              site_access.withheld_all_sites_access;
532     default:
533       break;
534   }
535 
536   NOTREACHED() << "Unexpected command id: " << command_id;
537   return false;
538 }
539 
CreatePageAccessSubmenu(const Extension * extension)540 void ExtensionContextMenuModel::CreatePageAccessSubmenu(
541     const Extension* extension) {
542   content::WebContents* web_contents = GetActiveWebContents();
543   if (!web_contents)
544     return;
545 
546   ScriptingPermissionsModifier modifier(profile_, extension);
547   if (!modifier.CanAffectExtension())
548     return;
549 
550   const GURL& url = web_contents->GetLastCommittedURL();
551   ScriptingPermissionsModifier::SiteAccess site_access =
552       modifier.GetSiteAccess(url);
553 
554   bool has_active_tab = extension->permissions_data()->HasAPIPermission(
555       APIPermission::kActiveTab);
556   bool wants_site_access =
557       site_access.has_site_access || site_access.withheld_site_access;
558   if (!wants_site_access && !has_active_tab) {
559     AddItemWithStringId(PAGE_ACCESS_CANT_ACCESS,
560                         IDS_EXTENSIONS_CONTEXT_MENU_CANT_ACCESS_PAGE);
561     return;
562   }
563 
564   const int kRadioGroup = 0;
565   page_access_submenu_ = std::make_unique<ui::SimpleMenuModel>(this);
566 
567   // Add the three options for "on click", "on this site", "on all sites".
568   // Though we always add these three, some may be disabled.
569   page_access_submenu_->AddRadioItemWithStringId(
570       PAGE_ACCESS_RUN_ON_CLICK,
571       IDS_EXTENSIONS_CONTEXT_MENU_PAGE_ACCESS_RUN_ON_CLICK, kRadioGroup);
572   page_access_submenu_->AddRadioItem(
573       PAGE_ACCESS_RUN_ON_SITE,
574       l10n_util::GetStringFUTF16(
575           IDS_EXTENSIONS_CONTEXT_MENU_PAGE_ACCESS_RUN_ON_SITE,
576           url_formatter::IDNToUnicode(url_formatter::StripWWW(
577               web_contents->GetLastCommittedURL().host()))),
578       kRadioGroup);
579   page_access_submenu_->AddRadioItemWithStringId(
580       PAGE_ACCESS_RUN_ON_ALL_SITES,
581       IDS_EXTENSIONS_CONTEXT_MENU_PAGE_ACCESS_RUN_ON_ALL_SITES, kRadioGroup);
582 
583   // Add the learn more link.
584   page_access_submenu_->AddSeparator(ui::NORMAL_SEPARATOR);
585   page_access_submenu_->AddItemWithStringId(
586       PAGE_ACCESS_LEARN_MORE,
587       IDS_EXTENSIONS_CONTEXT_MENU_PAGE_ACCESS_LEARN_MORE);
588 
589   AddSubMenuWithStringId(PAGE_ACCESS_SUBMENU,
590                          IDS_EXTENSIONS_CONTEXT_MENU_PAGE_ACCESS,
591                          page_access_submenu_.get());
592 }
593 
HandlePageAccessCommand(int command_id,const Extension * extension) const594 void ExtensionContextMenuModel::HandlePageAccessCommand(
595     int command_id,
596     const Extension* extension) const {
597   content::WebContents* web_contents = GetActiveWebContents();
598   if (!web_contents)
599     return;
600 
601   LogPageAccessAction(command_id);
602 
603   if (command_id == PAGE_ACCESS_LEARN_MORE) {
604     content::OpenURLParams params(
605         GURL(chrome_extension_constants::kRuntimeHostPermissionsHelpURL),
606         content::Referrer(), WindowOpenDisposition::NEW_FOREGROUND_TAB,
607         ui::PAGE_TRANSITION_LINK, false);
608     browser_->OpenURL(params);
609     return;
610   }
611 
612   MenuEntries current_access = GetCurrentPageAccess(extension, web_contents);
613   if (command_id == current_access)
614     return;
615 
616   auto convert_page_access = [](int command_id) {
617     switch (command_id) {
618       case PAGE_ACCESS_RUN_ON_CLICK:
619         return ExtensionActionRunner::PageAccess::RUN_ON_CLICK;
620       case PAGE_ACCESS_RUN_ON_SITE:
621         return ExtensionActionRunner::PageAccess::RUN_ON_SITE;
622       case PAGE_ACCESS_RUN_ON_ALL_SITES:
623         return ExtensionActionRunner::PageAccess::RUN_ON_ALL_SITES;
624     }
625     NOTREACHED();
626     return ExtensionActionRunner::PageAccess::RUN_ON_CLICK;
627   };
628 
629   ExtensionActionRunner* runner =
630       ExtensionActionRunner::GetForWebContents(web_contents);
631   if (runner)
632     runner->HandlePageAccessModified(extension,
633                                      convert_page_access(current_access),
634                                      convert_page_access(command_id));
635 }
636 
LogPageAccessAction(int command_id) const637 void ExtensionContextMenuModel::LogPageAccessAction(int command_id) const {
638   switch (command_id) {
639     case PAGE_ACCESS_LEARN_MORE:
640       base::RecordAction(base::UserMetricsAction(
641           "Extensions.ContextMenu.Hosts.LearnMoreClicked"));
642       break;
643     case PAGE_ACCESS_RUN_ON_CLICK:
644       base::RecordAction(base::UserMetricsAction(
645           "Extensions.ContextMenu.Hosts.OnClickClicked"));
646       break;
647     case PAGE_ACCESS_RUN_ON_SITE:
648       base::RecordAction(base::UserMetricsAction(
649           "Extensions.ContextMenu.Hosts.OnSiteClicked"));
650       break;
651     case PAGE_ACCESS_RUN_ON_ALL_SITES:
652       base::RecordAction(base::UserMetricsAction(
653           "Extensions.ContextMenu.Hosts.OnAllSitesClicked"));
654       break;
655     default:
656       NOTREACHED() << "Unknown option: " << command_id;
657       break;
658   }
659 }
660 
GetActiveWebContents() const661 content::WebContents* ExtensionContextMenuModel::GetActiveWebContents() const {
662   return browser_->tab_strip_model()->GetActiveWebContents();
663 }
664 
665 }  // namespace extensions
666