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