summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorrdevlin.cronin <rdevlin.cronin@chromium.org>2016-03-23 17:02:29 -0700
committerCommit bot <commit-bot@chromium.org>2016-03-24 00:03:51 +0000
commit4a78c48bcf51dda7cb915ba1866fdefcffa9bc0c (patch)
tree1506b62bbcd37454d87a78d7bb55fb36518834eb
parentde73d1be8f921f7155a606a558452b5f89c48fb2 (diff)
downloadchromium_src-4a78c48bcf51dda7cb915ba1866fdefcffa9bc0c.zip
chromium_src-4a78c48bcf51dda7cb915ba1866fdefcffa9bc0c.tar.gz
chromium_src-4a78c48bcf51dda7cb915ba1866fdefcffa9bc0c.tar.bz2
[Extensions] Show a "refresh" bubble when needed with click-to-script
When an extension wants to inject scripts at document start or use webRequest, it needs to act on the page before the page completes loading. This is a problem with click-to-script, which makes extensions wait for user action. For now, add a bubble that prompts the user to refresh the page in order to allow the extension to run on the page if the user triggers it and it had blocked actions that need to run at document_start. BUG=460306 Review URL: https://codereview.chromium.org/1809813002 Cr-Commit-Position: refs/heads/master@{#382981}
-rw-r--r--chrome/app/generated_resources.grd16
-rw-r--r--chrome/browser/extensions/api/web_request/web_request_apitest.cc17
-rw-r--r--chrome/browser/extensions/extension_action_runner.cc78
-rw-r--r--chrome/browser/extensions/extension_action_runner.h30
-rw-r--r--chrome/browser/extensions/extension_action_runner_browsertest.cc107
-rw-r--r--chrome/browser/extensions/extension_action_runner_unittest.cc12
-rw-r--r--chrome/browser/ui/cocoa/extensions/browser_actions_controller.mm51
-rw-r--r--chrome/browser/ui/cocoa/extensions/extension_message_bubble_bridge.h1
-rw-r--r--chrome/browser/ui/cocoa/extensions/extension_message_bubble_bridge.mm5
-rw-r--r--chrome/browser/ui/extensions/blocked_action_bubble_delegate.cc53
-rw-r--r--chrome/browser/ui/extensions/blocked_action_bubble_delegate.h38
-rw-r--r--chrome/browser/ui/extensions/extension_action_view_controller.cc2
-rw-r--r--chrome/browser/ui/extensions/extension_toolbar_icon_surfacing_bubble_delegate.cc4
-rw-r--r--chrome/browser/ui/extensions/extension_toolbar_icon_surfacing_bubble_delegate.h1
-rw-r--r--chrome/browser/ui/toolbar/test_toolbar_actions_bar_bubble_delegate.cc1
-rw-r--r--chrome/browser/ui/toolbar/toolbar_actions_bar.cc12
-rw-r--r--chrome/browser/ui/toolbar/toolbar_actions_bar.h13
-rw-r--r--chrome/browser/ui/toolbar/toolbar_actions_bar_bubble_delegate.h6
-rw-r--r--chrome/browser/ui/toolbar/toolbar_actions_bar_delegate.h5
-rw-r--r--chrome/browser/ui/views/toolbar/browser_actions_container.cc19
-rw-r--r--chrome/browser/ui/views/toolbar/browser_actions_container.h2
-rw-r--r--chrome/chrome_browser_ui.gypi2
-rw-r--r--chrome/test/data/extensions/blocked_actions/content_scripts/manifest.json11
-rw-r--r--chrome/test/data/extensions/blocked_actions/content_scripts/script.js9
24 files changed, 470 insertions, 25 deletions
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index 222cbd5..518cf5f 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -3618,8 +3618,8 @@ Even if you have downloaded files from this website before, the website might ha
<ph name="EXTENSION_NAME">$1<ex>Adblock</ex></ph> was installed remotely
</message>
- <!-- Extension toolbar bubble -->
<if expr="enable_extensions">
+ <!-- Extension toolbar bubble -->
<message name="IDS_EXTENSION_TOOLBAR_BUBBLE_HEADING" desc="Heading of the bubble to tell users that all extensions are now visible in the toolbar (the extension toolbar info bubble).">
All your extensions are here
</message>
@@ -3629,6 +3629,20 @@ Even if you have downloaded files from this website before, the website might ha
<message name="IDS_EXTENSION_TOOLBAR_BUBBLE_OK" desc="Button label for the extension toolbar info bubble.">
Got it
</message>
+
+ <!-- Extension blocked action bubble -->
+ <message name="IDS_EXTENSION_BLOCKED_ACTION_BUBBLE_HEADING" desc="Heading of the bubble to tell users that in order to run an extension, they'll need to refresh the page.">
+ Refresh required
+ </message>
+ <message name="IDS_EXTENSION_BLOCKED_ACTION_BUBBLE_CONTENT" desc="The content of the bubble to tell users that in order to run an extension, they'll need to refresh the page.">
+ In order to run this extension, you need to refresh the page. You can run this extension automatically on this site by right-clicking on the extension icon.
+ </message>
+ <message name="IDS_EXTENSION_BLOCKED_ACTION_BUBBLE_OK_BUTTON" desc="The text of the button to proceed with the page refresh for running an extension.">
+ OK, refresh
+ </message>
+ <message name="IDS_EXTENSION_BLOCKED_ACTION_BUBBLE_CANCEL_BUTTON" desc="The text of the button to close the bubble and not refresh the page or run the extension.">
+ Never mind
+ </message>
</if>
<!-- Extension install prompt -->
diff --git a/chrome/browser/extensions/api/web_request/web_request_apitest.cc b/chrome/browser/extensions/api/web_request/web_request_apitest.cc
index 15458a5..8522337 100644
--- a/chrome/browser/extensions/api/web_request/web_request_apitest.cc
+++ b/chrome/browser/extensions/api/web_request/web_request_apitest.cc
@@ -527,10 +527,22 @@ IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest,
// Grant activeTab permission, and perform another XHR. The extension should
// receive the event.
EXPECT_EQ(BLOCKED_ACTION_WEB_REQUEST, runner->GetBlockedActions(extension));
+ runner->set_default_bubble_close_action_for_testing(
+ make_scoped_ptr(new ToolbarActionsBarBubbleDelegate::CloseAction(
+ ToolbarActionsBarBubbleDelegate::CLOSE_EXECUTE)));
runner->RunAction(extension, true);
+ base::RunLoop().RunUntilIdle();
+ EXPECT_TRUE(content::WaitForLoadStop(web_contents));
+ // The runner will have refreshed the page...
EXPECT_EQ(BLOCKED_ACTION_NONE, runner->GetBlockedActions(extension));
+ int xhr_count = GetWebRequestCountFromBackgroundPage(extension, profile());
+ // ... which means that we should have a non-zero xhr count.
+ EXPECT_GT(xhr_count, 0);
+ // And the extension should receive future events.
PerformXhrInPage(web_contents, kHost, port, kXhrPath);
- EXPECT_EQ(1, GetWebRequestCountFromBackgroundPage(extension, profile()));
+ ++xhr_count;
+ EXPECT_EQ(xhr_count,
+ GetWebRequestCountFromBackgroundPage(extension, profile()));
// If we revoke the extension's tab permissions, it should no longer receive
// webRequest events.
@@ -540,7 +552,8 @@ IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest,
granter->RevokeForTesting();
base::RunLoop().RunUntilIdle();
PerformXhrInPage(web_contents, kHost, port, kXhrPath);
- EXPECT_EQ(1, GetWebRequestCountFromBackgroundPage(extension, profile()));
+ EXPECT_EQ(xhr_count,
+ GetWebRequestCountFromBackgroundPage(extension, profile()));
EXPECT_EQ(BLOCKED_ACTION_WEB_REQUEST, runner->GetBlockedActions(extension));
}
diff --git a/chrome/browser/extensions/extension_action_runner.cc b/chrome/browser/extensions/extension_action_runner.cc
index 8fa7f01..8be229b 100644
--- a/chrome/browser/extensions/extension_action_runner.cc
+++ b/chrome/browser/extensions/extension_action_runner.cc
@@ -4,6 +4,7 @@
#include "chrome/browser/extensions/extension_action_runner.h"
+#include "base/auto_reset.h"
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/memory/scoped_ptr.h"
@@ -17,6 +18,10 @@
#include "chrome/browser/extensions/tab_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sessions/session_tab_helper.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/browser_window.h"
+#include "chrome/browser/ui/extensions/blocked_action_bubble_delegate.h"
+#include "chrome/browser/ui/toolbar/toolbar_actions_bar.h"
#include "chrome/common/extensions/api/extension_action/action_info.h"
#include "components/crx_file/id_util.h"
#include "content/public/browser/navigation_controller.h"
@@ -35,6 +40,13 @@
namespace extensions {
+namespace {
+
+// The blocked actions that require a page refresh to run.
+const int kRefreshRequiredActionsMask =
+ BLOCKED_ACTION_WEB_REQUEST | BLOCKED_ACTION_SCRIPT_AT_START;
+}
+
ExtensionActionRunner::PendingScript::PendingScript(
UserScript::RunLocation run_location,
const base::Closure& permit_script)
@@ -50,7 +62,9 @@ ExtensionActionRunner::ExtensionActionRunner(content::WebContents* web_contents)
num_page_requests_(0),
browser_context_(web_contents->GetBrowserContext()),
was_used_on_page_(false),
- extension_registry_observer_(this) {
+ ignore_active_tab_granted_(false),
+ extension_registry_observer_(this),
+ weak_factory_(this) {
CHECK(web_contents);
extension_registry_observer_.Add(ExtensionRegistry::Get(browser_context_));
}
@@ -71,15 +85,19 @@ ExtensionActionRunner* ExtensionActionRunner::GetForWebContents(
ExtensionAction::ShowAction ExtensionActionRunner::RunAction(
const Extension* extension,
bool grant_tab_permissions) {
- bool has_pending_scripts = WantsToRun(extension);
if (grant_tab_permissions) {
+ int blocked = GetBlockedActions(extension);
+ if ((blocked & kRefreshRequiredActionsMask) != 0) {
+ ShowBlockedActionBubble(extension);
+ return ExtensionAction::ACTION_NONE;
+ }
TabHelper::FromWebContents(web_contents())
->active_tab_permission_granter()
->GrantIfRequested(extension);
// If the extension had blocked actions, granting active tab will have
// run the extension. Don't execute further since clicking should run
// blocked actions *or* the normal extension action, not both.
- if (has_pending_scripts)
+ if (blocked != BLOCKED_ACTION_NONE)
return ExtensionAction::ACTION_NONE;
}
@@ -123,7 +141,7 @@ void ExtensionActionRunner::RunBlockedActions(const Extension* extension) {
void ExtensionActionRunner::OnActiveTabPermissionGranted(
const Extension* extension) {
- if (WantsToRun(extension))
+ if (!ignore_active_tab_granted_ && WantsToRun(extension))
RunBlockedActions(extension);
}
@@ -162,6 +180,14 @@ bool ExtensionActionRunner::WantsToRun(const Extension* extension) {
return GetBlockedActions(extension) != BLOCKED_ACTION_NONE;
}
+void ExtensionActionRunner::RunForTesting(const Extension* extension) {
+ if (WantsToRun(extension)) {
+ TabHelper::FromWebContents(web_contents())
+ ->active_tab_permission_granter()
+ ->GrantIfRequested(extension);
+ }
+}
+
PermissionsData::AccessType
ExtensionActionRunner::RequiresUserConsentForScriptInjection(
const Extension* extension,
@@ -312,6 +338,49 @@ void ExtensionActionRunner::LogUMA() const {
}
}
+void ExtensionActionRunner::ShowBlockedActionBubble(
+ const Extension* extension) {
+ Browser* browser = chrome::FindBrowserWithWebContents(web_contents());
+ ToolbarActionsBar* toolbar_actions_bar =
+ browser ? browser->window()->GetToolbarActionsBar() : nullptr;
+ if (toolbar_actions_bar) {
+ auto callback =
+ base::Bind(&ExtensionActionRunner::OnBlockedActionBubbleClosed,
+ weak_factory_.GetWeakPtr(), extension->id());
+ if (default_bubble_close_action_for_testing_) {
+ base::MessageLoop::current()->PostTask(
+ FROM_HERE,
+ base::Bind(callback, *default_bubble_close_action_for_testing_));
+ } else {
+ toolbar_actions_bar->ShowToolbarActionBubble(make_scoped_ptr(
+ new BlockedActionBubbleDelegate(callback, extension->id())));
+ }
+ }
+}
+
+void ExtensionActionRunner::OnBlockedActionBubbleClosed(
+ const std::string& extension_id,
+ ToolbarActionsBarBubbleDelegate::CloseAction action) {
+ // If the user agreed to refresh the page, do so.
+ if (action == ToolbarActionsBarBubbleDelegate::CLOSE_EXECUTE) {
+ const Extension* extension = ExtensionRegistry::Get(browser_context_)
+ ->enabled_extensions()
+ .GetByID(extension_id);
+ if (!extension)
+ return;
+ {
+ // Ignore the active tab permission being granted because we don't want
+ // to run scripts right before we refresh the page.
+ base::AutoReset<bool> ignore_active_tab(&ignore_active_tab_granted_,
+ true);
+ TabHelper::FromWebContents(web_contents())
+ ->active_tab_permission_granter()
+ ->GrantIfRequested(extension);
+ }
+ web_contents()->GetController().Reload(false);
+ }
+}
+
bool ExtensionActionRunner::OnMessageReceived(
const IPC::Message& message,
content::RenderFrameHost* render_frame_host) {
@@ -336,6 +405,7 @@ void ExtensionActionRunner::DidNavigateMainFrame(
pending_scripts_.clear();
web_request_blocked_.clear();
was_used_on_page_ = false;
+ weak_factory_.InvalidateWeakPtrs();
}
void ExtensionActionRunner::OnExtensionUnloaded(
diff --git a/chrome/browser/extensions/extension_action_runner.h b/chrome/browser/extensions/extension_action_runner.h
index d985a28..d260ee0 100644
--- a/chrome/browser/extensions/extension_action_runner.h
+++ b/chrome/browser/extensions/extension_action_runner.h
@@ -15,8 +15,10 @@
#include "base/callback.h"
#include "base/compiler_specific.h"
#include "base/macros.h"
+#include "base/memory/weak_ptr.h"
#include "base/scoped_observer.h"
#include "chrome/browser/extensions/extension_action.h"
+#include "chrome/browser/ui/toolbar/toolbar_actions_bar_bubble_delegate.h"
#include "content/public/browser/web_contents_observer.h"
#include "extensions/browser/blocked_action_type.h"
#include "extensions/browser/extension_registry_observer.h"
@@ -76,8 +78,17 @@ class ExtensionActionRunner : public content::WebContentsObserver,
// Returns true if the given |extension| has any blocked actions.
bool WantsToRun(const Extension* extension);
+ // Runs any blocked actions the extension has, but does not handle any page
+ // refreshes for document_start/webRequest.
+ void RunForTesting(const Extension* extension);
+
int num_page_requests() const { return num_page_requests_; }
+ void set_default_bubble_close_action_for_testing(
+ scoped_ptr<ToolbarActionsBarBubbleDelegate::CloseAction> action) {
+ default_bubble_close_action_for_testing_ = std::move(action);
+ }
+
#if defined(UNIT_TEST)
// Only used in tests.
PermissionsData::AccessType RequiresUserConsentForScriptInjectionForTesting(
@@ -141,6 +152,15 @@ class ExtensionActionRunner : public content::WebContentsObserver,
// Log metrics.
void LogUMA() const;
+ // Shows the bubble to prompt the user to refresh the page to run the blocked
+ // actions for the given |extension|.
+ void ShowBlockedActionBubble(const Extension* extension);
+
+ // Called when the blocked actions bubble is closed.
+ void OnBlockedActionBubbleClosed(
+ const std::string& extension_id,
+ ToolbarActionsBarBubbleDelegate::CloseAction action);
+
// content::WebContentsObserver implementation.
bool OnMessageReceived(const IPC::Message& message,
content::RenderFrameHost* render_frame_host) override;
@@ -177,9 +197,19 @@ class ExtensionActionRunner : public content::WebContentsObserver,
// should incorporate more fully with ActiveTab.
std::set<std::string> permitted_extensions_;
+ // If true, ignore active tab being granted rather than running pending
+ // actions.
+ bool ignore_active_tab_granted_;
+
+ // If non-null, the bubble action to simulate for testing.
+ scoped_ptr<ToolbarActionsBarBubbleDelegate::CloseAction>
+ default_bubble_close_action_for_testing_;
+
ScopedObserver<ExtensionRegistry, ExtensionRegistryObserver>
extension_registry_observer_;
+ base::WeakPtrFactory<ExtensionActionRunner> weak_factory_;
+
DISALLOW_COPY_AND_ASSIGN(ExtensionActionRunner);
};
diff --git a/chrome/browser/extensions/extension_action_runner_browsertest.cc b/chrome/browser/extensions/extension_action_runner_browsertest.cc
index f55091d..5428c3c 100644
--- a/chrome/browser/extensions/extension_action_runner_browsertest.cc
+++ b/chrome/browser/extensions/extension_action_runner_browsertest.cc
@@ -20,6 +20,8 @@
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/ui_test_utils.h"
+#include "content/public/browser/navigation_entry.h"
+#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/common/feature_switch.h"
#include "extensions/common/switches.h"
@@ -63,6 +65,22 @@ bool RunAllPendingInRenderer(content::WebContents* web_contents) {
return content::ExecuteScript(web_contents, "1 == 1;");
}
+// For use with blocked actions browsertests that put the result in
+// window.localStorage. Returns the result or "undefined" if the result is not
+// set.
+std::string GetValue(content::WebContents* web_contents) {
+ std::string out;
+ if (!content::ExecuteScriptAndExtractString(
+ web_contents,
+ "var res = window.localStorage.getItem('extResult') || 'undefined';"
+ "window.localStorage.removeItem('extResult');"
+ "window.domAutomationController.send(res);",
+ &out)) {
+ out = "Failed to inject script";
+ }
+ return out;
+}
+
} // namespace
class ExtensionActionRunnerBrowserTest : public ExtensionBrowserTest {
@@ -119,7 +137,7 @@ const Extension* ExtensionActionRunnerBrowserTest::CreateExtension(
" {"
" \"matches\": [\"%s\"],"
" \"js\": [\"script.js\"],"
- " \"run_at\": \"document_start\""
+ " \"run_at\": \"document_end\""
" }"
"]",
permission_scheme);
@@ -427,6 +445,93 @@ IN_PROC_BROWSER_TEST_F(ExtensionActionRunnerBrowserTest,
EXPECT_FALSE(inject_success_listener.was_satisfied());
}
+IN_PROC_BROWSER_TEST_F(ExtensionActionRunnerBrowserTest,
+ BlockedActionBrowserTest) {
+ // Load an extension that wants to run on every page at document start, and
+ // load a test page.
+ ASSERT_TRUE(embedded_test_server()->Start());
+ const GURL url = embedded_test_server()->GetURL("/simple.html");
+ const Extension* extension = LoadExtension(
+ test_data_dir_.AppendASCII("blocked_actions/content_scripts"));
+ ASSERT_TRUE(extension);
+ ui_test_utils::NavigateToURL(browser(), url);
+ content::WebContents* web_contents =
+ browser()->tab_strip_model()->GetActiveWebContents();
+ EXPECT_TRUE(content::WaitForLoadStop(web_contents));
+
+ // The extension should want to run on the page, and should not have
+ // injected.
+ ExtensionActionRunner* runner =
+ ExtensionActionRunner::GetForWebContents(web_contents);
+ ASSERT_TRUE(runner);
+ EXPECT_TRUE(runner->WantsToRun(extension));
+ EXPECT_EQ("undefined", GetValue(web_contents));
+
+ // Wire up the runner to automatically accept the bubble to prompt for page
+ // refresh.
+ runner->set_default_bubble_close_action_for_testing(
+ make_scoped_ptr(new ToolbarActionsBarBubbleDelegate::CloseAction(
+ ToolbarActionsBarBubbleDelegate::CLOSE_EXECUTE)));
+
+ content::NavigationEntry* entry =
+ web_contents->GetController().GetLastCommittedEntry();
+ ASSERT_TRUE(entry);
+ const int first_nav_id = entry->GetUniqueID();
+
+ // Run the extension action, which should cause a page refresh (since we
+ // automatically accepted the bubble prompting us), and the extension should
+ // have injected at document start.
+ runner->RunAction(extension, true);
+ base::RunLoop().RunUntilIdle();
+ EXPECT_TRUE(content::WaitForLoadStop(web_contents));
+ entry = web_contents->GetController().GetLastCommittedEntry();
+ ASSERT_TRUE(entry);
+ // Confirm that we refreshed the page.
+ EXPECT_GE(entry->GetUniqueID(), first_nav_id);
+ EXPECT_EQ("success", GetValue(web_contents));
+ EXPECT_FALSE(runner->WantsToRun(extension));
+
+ // Revoke permission and reload to try different bubble options.
+ ActiveTabPermissionGranter* active_tab_granter =
+ TabHelper::FromWebContents(web_contents)->active_tab_permission_granter();
+ ASSERT_TRUE(active_tab_granter);
+ active_tab_granter->RevokeForTesting();
+ web_contents->GetController().Reload(true);
+ EXPECT_TRUE(content::WaitForLoadStop(web_contents));
+
+ // The extension should again want to run. Automatically dismiss the bubble
+ // that pops up prompting for page refresh.
+ EXPECT_TRUE(runner->WantsToRun(extension));
+ EXPECT_EQ("undefined", GetValue(web_contents));
+ const int next_nav_id =
+ web_contents->GetController().GetLastCommittedEntry()->GetUniqueID();
+ runner->set_default_bubble_close_action_for_testing(
+ make_scoped_ptr(new ToolbarActionsBarBubbleDelegate::CloseAction(
+ ToolbarActionsBarBubbleDelegate::CLOSE_DISMISS_USER_ACTION)));
+
+ // Try running the extension. Nothing should happen, because the user
+ // didn't agree to refresh the page. The extension should still want to run.
+ runner->RunAction(extension, true);
+ base::RunLoop().RunUntilIdle();
+ EXPECT_TRUE(content::WaitForLoadStop(web_contents));
+ EXPECT_EQ("undefined", GetValue(web_contents));
+ EXPECT_EQ(
+ next_nav_id,
+ web_contents->GetController().GetLastCommittedEntry()->GetUniqueID());
+
+ // Repeat with a dismissal from bubble deactivation - same story.
+ runner->set_default_bubble_close_action_for_testing(
+ make_scoped_ptr(new ToolbarActionsBarBubbleDelegate::CloseAction(
+ ToolbarActionsBarBubbleDelegate::CLOSE_DISMISS_DEACTIVATION)));
+ runner->RunAction(extension, true);
+ base::RunLoop().RunUntilIdle();
+ EXPECT_TRUE(content::WaitForLoadStop(web_contents));
+ EXPECT_EQ("undefined", GetValue(web_contents));
+ EXPECT_EQ(
+ next_nav_id,
+ web_contents->GetController().GetLastCommittedEntry()->GetUniqueID());
+}
+
// A version of the test with the flag off, in order to test that everything
// still works as expected.
class FlagOffExtensionActionRunnerBrowserTest
diff --git a/chrome/browser/extensions/extension_action_runner_unittest.cc b/chrome/browser/extensions/extension_action_runner_unittest.cc
index 2aaff8c..edacb84 100644
--- a/chrome/browser/extensions/extension_action_runner_unittest.cc
+++ b/chrome/browser/extensions/extension_action_runner_unittest.cc
@@ -209,7 +209,7 @@ TEST_F(ExtensionActionRunnerUnitTest, RequestPermissionAndExecute) {
EXPECT_EQ(0u, GetExecutionCountForExtension(extension->id()));
// Click to accept the extension executing.
- runner()->RunAction(extension, true);
+ runner()->RunForTesting(extension);
// The extension should execute, and the extension shouldn't want to run.
EXPECT_EQ(1u, GetExecutionCountForExtension(extension->id()));
@@ -234,7 +234,7 @@ TEST_F(ExtensionActionRunnerUnitTest, RequestPermissionAndExecute) {
// Grant access.
RequestInjection(extension);
- runner()->RunAction(extension, true);
+ runner()->RunForTesting(extension);
EXPECT_EQ(2u, GetExecutionCountForExtension(extension->id()));
EXPECT_FALSE(runner()->WantsToRun(extension));
@@ -266,7 +266,7 @@ TEST_F(ExtensionActionRunnerUnitTest, PendingInjectionsRemovedAtNavigation) {
// Request and accept a new injection.
RequestInjection(extension);
- runner()->RunAction(extension, true);
+ runner()->RunForTesting(extension);
// The extension should only have executed once, even though a grand total
// of two executions were requested.
@@ -290,7 +290,7 @@ TEST_F(ExtensionActionRunnerUnitTest, MultiplePendingInjection) {
EXPECT_EQ(0u, GetExecutionCountForExtension(extension->id()));
- runner()->RunAction(extension, true);
+ runner()->RunForTesting(extension);
// All pending injections should have executed.
EXPECT_EQ(kNumInjections, GetExecutionCountForExtension(extension->id()));
@@ -388,7 +388,7 @@ TEST_F(ExtensionActionRunnerUnitTest, TestAlwaysRun) {
// Allow the extension to always run on this origin.
ScriptingPermissionsModifier modifier(profile(), extension);
modifier.GrantHostPermission(web_contents()->GetLastCommittedURL());
- runner()->RunAction(extension, true);
+ runner()->RunForTesting(extension);
// The extension should execute, and the extension shouldn't want to run.
EXPECT_EQ(1u, GetExecutionCountForExtension(extension->id()));
@@ -444,7 +444,7 @@ TEST_F(ExtensionActionRunnerUnitTest, TestDifferentScriptRunLocations) {
EXPECT_EQ(BLOCKED_ACTION_SCRIPT_AT_START | BLOCKED_ACTION_SCRIPT_OTHER,
runner()->GetBlockedActions(extension));
- runner()->RunAction(extension, true);
+ runner()->RunForTesting(extension);
EXPECT_EQ(BLOCKED_ACTION_NONE, runner()->GetBlockedActions(extension));
}
diff --git a/chrome/browser/ui/cocoa/extensions/browser_actions_controller.mm b/chrome/browser/ui/cocoa/extensions/browser_actions_controller.mm
index 3f97173..d6b7bb5 100644
--- a/chrome/browser/ui/cocoa/extensions/browser_actions_controller.mm
+++ b/chrome/browser/ui/cocoa/extensions/browser_actions_controller.mm
@@ -167,11 +167,11 @@ const CGFloat kBrowserActionBubbleYOffset = 3.0;
// Returns the associated ToolbarController.
- (ToolbarController*)toolbarController;
-// Creates a message bubble anchored to the given |anchorAction|, or the app
-// menu if no |anchorAction| is null.
+// Creates a message bubble with the given |delegate| that is anchored to the
+// given |anchorView|.
- (ToolbarActionsBarBubbleMac*)createMessageBubble:
(scoped_ptr<ToolbarActionsBarBubbleDelegate>)delegate
- anchorToSelf:(BOOL)anchorToSelf;
+ anchorView:(NSView*)anchorView;
// Called when the window for the active bubble is closing, and sets the active
// bubble to nil.
@@ -223,6 +223,8 @@ class ToolbarActionsBarBridge : public ToolbarActionsBarDelegate {
void ShowExtensionMessageBubble(
scoped_ptr<extensions::ExtensionMessageBubbleController> controller,
ToolbarActionViewController* anchor_action) override;
+ void ShowToolbarActionBubble(
+ scoped_ptr<ToolbarActionsBarBubbleDelegate> bubble) override;
// The owning BrowserActionsController; weak.
BrowserActionsController* controller_;
@@ -297,6 +299,19 @@ int ToolbarActionsBarBridge::GetChevronWidth() const {
void ToolbarActionsBarBridge::ShowExtensionMessageBubble(
scoped_ptr<extensions::ExtensionMessageBubbleController> bubble_controller,
ToolbarActionViewController* anchor_action) {
+ NSView* anchorView = nil;
+ BOOL anchoredToAction = NO;
+ if (anchor_action) {
+ BrowserActionButton* actionButton =
+ [controller_ buttonForId:anchor_action->GetId()];
+ if (actionButton && [actionButton superview]) {
+ anchorView = actionButton;
+ anchoredToAction = YES;
+ }
+ }
+ if (!anchorView)
+ anchorView = [[controller_ toolbarController] appMenuButton];
+
// This goop is a by-product of needing to wire together abstract classes,
// C++/Cocoa bridges, and ExtensionMessageBubbleController's somewhat strange
// Show() interface. It's ugly, but it's pretty confined, so it's probably
@@ -305,14 +320,32 @@ void ToolbarActionsBarBridge::ShowExtensionMessageBubble(
bubble_controller.get();
scoped_ptr<ExtensionMessageBubbleBridge> bridge(
new ExtensionMessageBubbleBridge(std::move(bubble_controller),
- anchor_action != nullptr));
+ anchoredToAction));
ToolbarActionsBarBubbleMac* bubble =
[controller_ createMessageBubble:std::move(bridge)
- anchorToSelf:anchor_action != nil];
+ anchorView:anchorView];
weak_controller->OnShown();
[bubble showWindow:nil];
}
+void ToolbarActionsBarBridge::ShowToolbarActionBubble(
+ scoped_ptr<ToolbarActionsBarBubbleDelegate> bubble) {
+ NSView* anchorView = nil;
+ if (!bubble->GetAnchorActionId().empty()) {
+ BrowserActionButton* button =
+ [controller_ buttonForId:bubble->GetAnchorActionId()];
+ anchorView = button && [button superview] ? button :
+ [[controller_ toolbarController] appMenuButton];
+ } else {
+ anchorView = [controller_ containerView];
+ }
+
+ ToolbarActionsBarBubbleMac* bubbleView =
+ [controller_ createMessageBubble:std::move(bubble)
+ anchorView:anchorView];
+ [bubbleView showWindow:nil];
+}
+
} // namespace
@implementation BrowserActionsController
@@ -826,7 +859,8 @@ void ToolbarActionsBarBridge::ShowExtensionMessageBubble(
scoped_ptr<ToolbarActionsBarBubbleDelegate> delegate(
new ExtensionToolbarIconSurfacingBubbleDelegate(browser_->profile()));
ToolbarActionsBarBubbleMac* bubble =
- [self createMessageBubble:std::move(delegate) anchorToSelf:YES];
+ [self createMessageBubble:std::move(delegate)
+ anchorView:containerView_];
[bubble showWindow:nil];
}
[containerView_ setTrackingEnabled:NO];
@@ -1042,10 +1076,9 @@ void ToolbarActionsBarBridge::ShowExtensionMessageBubble(
- (ToolbarActionsBarBubbleMac*)createMessageBubble:
(scoped_ptr<ToolbarActionsBarBubbleDelegate>)delegate
- anchorToSelf:(BOOL)anchorToSelf {
+ anchorView:(NSView*)anchorView {
+ DCHECK(anchorView);
DCHECK_GE([buttons_ count], 0u);
- NSView* anchorView =
- anchorToSelf ? containerView_ : [[self toolbarController] appMenuButton];
NSPoint anchor = [self popupPointForView:anchorView
withBounds:[anchorView bounds]];
diff --git a/chrome/browser/ui/cocoa/extensions/extension_message_bubble_bridge.h b/chrome/browser/ui/cocoa/extensions/extension_message_bubble_bridge.h
index 5a86977..ab925a4 100644
--- a/chrome/browser/ui/cocoa/extensions/extension_message_bubble_bridge.h
+++ b/chrome/browser/ui/cocoa/extensions/extension_message_bubble_bridge.h
@@ -32,6 +32,7 @@ class ExtensionMessageBubbleBridge : public ToolbarActionsBarBubbleDelegate {
base::string16 GetActionButtonText() override;
base::string16 GetDismissButtonText() override;
base::string16 GetLearnMoreButtonText() override;
+ std::string GetAnchorActionId() override;
void OnBubbleShown() override;
void OnBubbleClosed(CloseAction action) override;
diff --git a/chrome/browser/ui/cocoa/extensions/extension_message_bubble_bridge.mm b/chrome/browser/ui/cocoa/extensions/extension_message_bubble_bridge.mm
index f7cb1f5..ae34ee2 100644
--- a/chrome/browser/ui/cocoa/extensions/extension_message_bubble_bridge.mm
+++ b/chrome/browser/ui/cocoa/extensions/extension_message_bubble_bridge.mm
@@ -44,6 +44,11 @@ base::string16 ExtensionMessageBubbleBridge::GetLearnMoreButtonText() {
return controller_->delegate()->GetLearnMoreLabel();
}
+std::string ExtensionMessageBubbleBridge::GetAnchorActionId() {
+ return controller_->GetExtensionIdList().size() == 1u ?
+ controller_->GetExtensionIdList()[0] : std::string();
+}
+
void ExtensionMessageBubbleBridge::OnBubbleShown() {
}
diff --git a/chrome/browser/ui/extensions/blocked_action_bubble_delegate.cc b/chrome/browser/ui/extensions/blocked_action_bubble_delegate.cc
new file mode 100644
index 0000000..288ffef
--- /dev/null
+++ b/chrome/browser/ui/extensions/blocked_action_bubble_delegate.cc
@@ -0,0 +1,53 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/extensions/blocked_action_bubble_delegate.h"
+
+#include "base/callback_helpers.h"
+#include "base/strings/string16.h"
+#include "grit/generated_resources.h"
+#include "ui/base/l10n/l10n_util.h"
+
+BlockedActionBubbleDelegate::BlockedActionBubbleDelegate(
+ const base::Callback<void(CloseAction)>& callback,
+ const std::string& extension_id)
+ : callback_(callback), extension_id_(extension_id) {}
+
+BlockedActionBubbleDelegate::~BlockedActionBubbleDelegate() {}
+
+base::string16 BlockedActionBubbleDelegate::GetHeadingText() {
+ return l10n_util::GetStringUTF16(IDS_EXTENSION_BLOCKED_ACTION_BUBBLE_HEADING);
+}
+
+base::string16 BlockedActionBubbleDelegate::GetBodyText() {
+ return l10n_util::GetStringUTF16(IDS_EXTENSION_BLOCKED_ACTION_BUBBLE_CONTENT);
+}
+
+base::string16 BlockedActionBubbleDelegate::GetItemListText() {
+ return base::string16(); // No item list.
+}
+
+base::string16 BlockedActionBubbleDelegate::GetActionButtonText() {
+ return l10n_util::GetStringUTF16(
+ IDS_EXTENSION_BLOCKED_ACTION_BUBBLE_OK_BUTTON);
+}
+
+base::string16 BlockedActionBubbleDelegate::GetDismissButtonText() {
+ return l10n_util::GetStringUTF16(
+ IDS_EXTENSION_BLOCKED_ACTION_BUBBLE_CANCEL_BUTTON);
+}
+
+base::string16 BlockedActionBubbleDelegate::GetLearnMoreButtonText() {
+ return base::string16(); // No learn more link.
+}
+
+std::string BlockedActionBubbleDelegate::GetAnchorActionId() {
+ return extension_id_;
+}
+
+void BlockedActionBubbleDelegate::OnBubbleShown() {}
+
+void BlockedActionBubbleDelegate::OnBubbleClosed(CloseAction action) {
+ base::ResetAndReturn(&callback_).Run(action);
+}
diff --git a/chrome/browser/ui/extensions/blocked_action_bubble_delegate.h b/chrome/browser/ui/extensions/blocked_action_bubble_delegate.h
new file mode 100644
index 0000000..8e2200e
--- /dev/null
+++ b/chrome/browser/ui/extensions/blocked_action_bubble_delegate.h
@@ -0,0 +1,38 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_EXTENSIONS_BLOCKED_ACTION_BUBBLE_DELEGATE_H_
+#define CHROME_BROWSER_UI_EXTENSIONS_BLOCKED_ACTION_BUBBLE_DELEGATE_H_
+
+#include "base/callback.h"
+#include "base/macros.h"
+#include "chrome/browser/ui/toolbar/toolbar_actions_bar_bubble_delegate.h"
+
+// The delegate for the bubble to ask the user if they want to refresh the page
+// in order to run any blocked actions the extension may have.
+class BlockedActionBubbleDelegate : public ToolbarActionsBarBubbleDelegate {
+ public:
+ BlockedActionBubbleDelegate(const base::Callback<void(CloseAction)>& callback,
+ const std::string& extension_id);
+ ~BlockedActionBubbleDelegate() override;
+
+ private:
+ // ToolbarActionsBarBubbleDelegate:
+ base::string16 GetHeadingText() override;
+ base::string16 GetBodyText() override;
+ base::string16 GetItemListText() override;
+ base::string16 GetActionButtonText() override;
+ base::string16 GetDismissButtonText() override;
+ base::string16 GetLearnMoreButtonText() override;
+ std::string GetAnchorActionId() override;
+ void OnBubbleShown() override;
+ void OnBubbleClosed(CloseAction action) override;
+
+ base::Callback<void(CloseAction)> callback_;
+ std::string extension_id_;
+
+ DISALLOW_COPY_AND_ASSIGN(BlockedActionBubbleDelegate);
+};
+
+#endif // CHROME_BROWSER_UI_EXTENSIONS_BLOCKED_ACTION_BUBBLE_DELEGATE_H_
diff --git a/chrome/browser/ui/extensions/extension_action_view_controller.cc b/chrome/browser/ui/extensions/extension_action_view_controller.cc
index bdbb532..4b9f123 100644
--- a/chrome/browser/ui/extensions/extension_action_view_controller.cc
+++ b/chrome/browser/ui/extensions/extension_action_view_controller.cc
@@ -216,7 +216,7 @@ bool ExtensionActionViewController::ExecuteAction(PopupShowAction show_action,
if (action_runner->RunAction(extension(), grant_tab_permissions) ==
ExtensionAction::ACTION_SHOW_POPUP) {
GURL popup_url = extension_action_->GetPopupUrl(
- SessionTabHelper::IdForTab(view_delegate_->GetCurrentWebContents()));
+ SessionTabHelper::IdForTab(web_contents));
return GetPreferredPopupViewController()
->TriggerPopupWithUrl(show_action, popup_url, grant_tab_permissions);
}
diff --git a/chrome/browser/ui/extensions/extension_toolbar_icon_surfacing_bubble_delegate.cc b/chrome/browser/ui/extensions/extension_toolbar_icon_surfacing_bubble_delegate.cc
index a2c8ffc..1151155 100644
--- a/chrome/browser/ui/extensions/extension_toolbar_icon_surfacing_bubble_delegate.cc
+++ b/chrome/browser/ui/extensions/extension_toolbar_icon_surfacing_bubble_delegate.cc
@@ -91,6 +91,10 @@ ExtensionToolbarIconSurfacingBubbleDelegate::GetLearnMoreButtonText() {
return base::string16(); // No learn more link.
}
+std::string ExtensionToolbarIconSurfacingBubbleDelegate::GetAnchorActionId() {
+ return std::string(); // Point to the whole set of actions.
+}
+
void ExtensionToolbarIconSurfacingBubbleDelegate::OnBubbleShown() {
// Record the last time the bubble was shown.
profile_->GetPrefs()->SetInt64(
diff --git a/chrome/browser/ui/extensions/extension_toolbar_icon_surfacing_bubble_delegate.h b/chrome/browser/ui/extensions/extension_toolbar_icon_surfacing_bubble_delegate.h
index 527201a..85e4caf 100644
--- a/chrome/browser/ui/extensions/extension_toolbar_icon_surfacing_bubble_delegate.h
+++ b/chrome/browser/ui/extensions/extension_toolbar_icon_surfacing_bubble_delegate.h
@@ -29,6 +29,7 @@ class ExtensionToolbarIconSurfacingBubbleDelegate
base::string16 GetActionButtonText() override;
base::string16 GetDismissButtonText() override;
base::string16 GetLearnMoreButtonText() override;
+ std::string GetAnchorActionId() override;
void OnBubbleShown() override;
void OnBubbleClosed(CloseAction action) override;
diff --git a/chrome/browser/ui/toolbar/test_toolbar_actions_bar_bubble_delegate.cc b/chrome/browser/ui/toolbar/test_toolbar_actions_bar_bubble_delegate.cc
index 957ad6f..fa13f07 100644
--- a/chrome/browser/ui/toolbar/test_toolbar_actions_bar_bubble_delegate.cc
+++ b/chrome/browser/ui/toolbar/test_toolbar_actions_bar_bubble_delegate.cc
@@ -23,6 +23,7 @@ class TestToolbarActionsBarBubbleDelegate::DelegateImpl
base::string16 GetLearnMoreButtonText() override {
return parent_->learn_more_;
}
+ std::string GetAnchorActionId() override { return std::string(); }
void OnBubbleShown() override {
CHECK(!parent_->shown_);
parent_->shown_ = true;
diff --git a/chrome/browser/ui/toolbar/toolbar_actions_bar.cc b/chrome/browser/ui/toolbar/toolbar_actions_bar.cc
index 546acd4..cb62c0c 100644
--- a/chrome/browser/ui/toolbar/toolbar_actions_bar.cc
+++ b/chrome/browser/ui/toolbar/toolbar_actions_bar.cc
@@ -493,6 +493,8 @@ void ToolbarActionsBar::OnAnimationEnded() {
// message bubble, or to show a popup.
if (pending_extension_bubble_controller_) {
MaybeShowExtensionBubble(std::move(pending_extension_bubble_controller_));
+ } else if (pending_toolbar_bubble_controller_) {
+ ShowToolbarActionBubble(std::move(pending_toolbar_bubble_controller_));
} else if (!popped_out_closure_.is_null()) {
popped_out_closure_.Run();
popped_out_closure_.Reset();
@@ -575,6 +577,16 @@ void ToolbarActionsBar::RemoveObserver(ToolbarActionsBarObserver* observer) {
observers_.RemoveObserver(observer);
}
+void ToolbarActionsBar::ShowToolbarActionBubble(
+ scoped_ptr<ToolbarActionsBarBubbleDelegate> bubble) {
+ DCHECK(bubble->GetAnchorActionId().empty() ||
+ GetActionForId(bubble->GetAnchorActionId()));
+ if (delegate_->IsAnimating())
+ pending_toolbar_bubble_controller_ = std::move(bubble);
+ else
+ delegate_->ShowToolbarActionBubble(std::move(bubble));
+}
+
void ToolbarActionsBar::MaybeShowExtensionBubble(
scoped_ptr<extensions::ExtensionMessageBubbleController> controller) {
controller->HighlightExtensionsIfNecessary(); // Safe to call multiple times.
diff --git a/chrome/browser/ui/toolbar/toolbar_actions_bar.h b/chrome/browser/ui/toolbar/toolbar_actions_bar.h
index 1c04dae..871db82 100644
--- a/chrome/browser/ui/toolbar/toolbar_actions_bar.h
+++ b/chrome/browser/ui/toolbar/toolbar_actions_bar.h
@@ -211,6 +211,10 @@ class ToolbarActionsBar : public ToolbarActionsModel::Observer {
void AddObserver(ToolbarActionsBarObserver* observer);
void RemoveObserver(ToolbarActionsBarObserver* observer);
+ // Displays the given |bubble| once the toolbar is no longer animating.
+ void ShowToolbarActionBubble(
+ scoped_ptr<ToolbarActionsBarBubbleDelegate> bubble);
+
// Returns the underlying toolbar actions, but does not order them. Primarily
// for use in testing.
const std::vector<ToolbarActionViewController*>& toolbar_actions_unordered()
@@ -336,10 +340,17 @@ class ToolbarActionsBar : public ToolbarActionsModel::Observer {
// it is fully popped out.
base::Closure popped_out_closure_;
- // The controller of the bubble to show once animation finishes, if any.
+ // The controller of the extension message bubble to show once animation
+ // finishes, if any. This has priority over
+ // |pending_toolbar_bubble_controller_|.
scoped_ptr<extensions::ExtensionMessageBubbleController>
pending_extension_bubble_controller_;
+ // The controller for the toolbar action bubble to show once animation
+ // finishes, if any.
+ scoped_ptr<ToolbarActionsBarBubbleDelegate>
+ pending_toolbar_bubble_controller_;
+
base::ObserverList<ToolbarActionsBarObserver> observers_;
base::WeakPtrFactory<ToolbarActionsBar> weak_ptr_factory_;
diff --git a/chrome/browser/ui/toolbar/toolbar_actions_bar_bubble_delegate.h b/chrome/browser/ui/toolbar/toolbar_actions_bar_bubble_delegate.h
index 508371e..d07dc71 100644
--- a/chrome/browser/ui/toolbar/toolbar_actions_bar_bubble_delegate.h
+++ b/chrome/browser/ui/toolbar/toolbar_actions_bar_bubble_delegate.h
@@ -5,6 +5,8 @@
#ifndef CHROME_BROWSER_UI_TOOLBAR_TOOLBAR_ACTIONS_BAR_BUBBLE_DELEGATE_H_
#define CHROME_BROWSER_UI_TOOLBAR_TOOLBAR_ACTIONS_BAR_BUBBLE_DELEGATE_H_
+#include <string>
+
#include "base/strings/string16.h"
// A delegate for a generic bubble that hangs off the toolbar actions bar.
@@ -43,6 +45,10 @@ class ToolbarActionsBarBubbleDelegate {
// string, no button will be added.
virtual base::string16 GetLearnMoreButtonText() = 0;
+ // Returns the id of the action to point to, or the empty string if the
+ // bubble should point to the center of the actions container.
+ virtual std::string GetAnchorActionId() = 0;
+
// Called when the bubble is shown.
virtual void OnBubbleShown() = 0;
diff --git a/chrome/browser/ui/toolbar/toolbar_actions_bar_delegate.h b/chrome/browser/ui/toolbar/toolbar_actions_bar_delegate.h
index 584a4b3..4c31025 100644
--- a/chrome/browser/ui/toolbar/toolbar_actions_bar_delegate.h
+++ b/chrome/browser/ui/toolbar/toolbar_actions_bar_delegate.h
@@ -12,6 +12,7 @@
#include "ui/gfx/geometry/size.h"
class ToolbarActionViewController;
+class ToolbarActionsBarBubbleDelegate;
namespace extensions {
class ExtensionMessageBubbleController;
@@ -70,6 +71,10 @@ class ToolbarActionsBarDelegate {
virtual void ShowExtensionMessageBubble(
scoped_ptr<extensions::ExtensionMessageBubbleController> controller,
ToolbarActionViewController* anchor_action) = 0;
+
+ // Shows the given |bubble| if no other bubbles are showing.
+ virtual void ShowToolbarActionBubble(
+ scoped_ptr<ToolbarActionsBarBubbleDelegate> bubble) = 0;
};
#endif // CHROME_BROWSER_UI_TOOLBAR_TOOLBAR_ACTIONS_BAR_DELEGATE_H_
diff --git a/chrome/browser/ui/views/toolbar/browser_actions_container.cc b/chrome/browser/ui/views/toolbar/browser_actions_container.cc
index 5ac758e..2d704d4 100644
--- a/chrome/browser/ui/views/toolbar/browser_actions_container.cc
+++ b/chrome/browser/ui/views/toolbar/browser_actions_container.cc
@@ -319,6 +319,25 @@ int BrowserActionsContainer::GetChevronWidth() const {
chevron_->GetPreferredSize().width() + GetChevronSpacing() : 0;
}
+void BrowserActionsContainer::ShowToolbarActionBubble(
+ scoped_ptr<ToolbarActionsBarBubbleDelegate> controller) {
+ // The container shouldn't be asked to show a bubble if it's animating.
+ DCHECK(!animating());
+ views::View* anchor_view = nullptr;
+ if (!controller->GetAnchorActionId().empty()) {
+ ToolbarActionView* action_view =
+ GetViewForId(controller->GetAnchorActionId());
+ anchor_view =
+ action_view->visible() ? action_view : GetOverflowReferenceView();
+ } else {
+ anchor_view = this;
+ }
+ ToolbarActionsBarBubbleViews* bubble =
+ new ToolbarActionsBarBubbleViews(anchor_view, std::move(controller));
+ views::BubbleDelegateView::CreateBubble(bubble);
+ bubble->Show();
+}
+
void BrowserActionsContainer::ShowExtensionMessageBubble(
scoped_ptr<extensions::ExtensionMessageBubbleController> controller,
ToolbarActionViewController* anchor_action) {
diff --git a/chrome/browser/ui/views/toolbar/browser_actions_container.h b/chrome/browser/ui/views/toolbar/browser_actions_container.h
index 14517aa..f77f7cb 100644
--- a/chrome/browser/ui/views/toolbar/browser_actions_container.h
+++ b/chrome/browser/ui/views/toolbar/browser_actions_container.h
@@ -239,6 +239,8 @@ class BrowserActionsContainer : public views::View,
bool IsAnimating() const override;
void StopAnimating() override;
int GetChevronWidth() const override;
+ void ShowToolbarActionBubble(
+ scoped_ptr<ToolbarActionsBarBubbleDelegate> controller) override;
void ShowExtensionMessageBubble(
scoped_ptr<extensions::ExtensionMessageBubbleController> controller,
ToolbarActionViewController* anchor_action) override;
diff --git a/chrome/chrome_browser_ui.gypi b/chrome/chrome_browser_ui.gypi
index 3239cda..75fa4985 100644
--- a/chrome/chrome_browser_ui.gypi
+++ b/chrome/chrome_browser_ui.gypi
@@ -2769,6 +2769,8 @@
'browser/ui/extensions/extension_message_bubble_factory.h',
'browser/ui/extensions/extension_toolbar_icon_surfacing_bubble_delegate.cc',
'browser/ui/extensions/extension_toolbar_icon_surfacing_bubble_delegate.h',
+ 'browser/ui/extensions/blocked_action_bubble_delegate.cc',
+ 'browser/ui/extensions/blocked_action_bubble_delegate.h',
'browser/ui/extensions/hosted_app_browser_controller.cc',
'browser/ui/extensions/hosted_app_browser_controller.h',
'browser/ui/extensions/icon_with_badge_image_source.cc',
diff --git a/chrome/test/data/extensions/blocked_actions/content_scripts/manifest.json b/chrome/test/data/extensions/blocked_actions/content_scripts/manifest.json
new file mode 100644
index 0000000..70d666b
--- /dev/null
+++ b/chrome/test/data/extensions/blocked_actions/content_scripts/manifest.json
@@ -0,0 +1,11 @@
+{
+ "name": "Blocked Actions Doc Start",
+ "description": "An extension that wants to run at document start on all urls",
+ "version": "0.1",
+ "manifest_version": 2,
+ "content_scripts": [{
+ "matches": ["<all_urls>"],
+ "run_at": "document_start",
+ "js": ["script.js"]
+ }]
+}
diff --git a/chrome/test/data/extensions/blocked_actions/content_scripts/script.js b/chrome/test/data/extensions/blocked_actions/content_scripts/script.js
new file mode 100644
index 0000000..c54b92c
--- /dev/null
+++ b/chrome/test/data/extensions/blocked_actions/content_scripts/script.js
@@ -0,0 +1,9 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// If the script was really injected at document_start, then document.body will
+// be null. If it's not null, then we didn't inject at document_start.
+// Store the result in window.localStorage so that it's accessible from the
+// main world script context in the browsertest.
+window.localStorage.setItem('extResult', document.body ? 'failure' : 'success');