Allow restricting WebUI-enabled extension APIs to URL patterns.

BUG=391944
[email protected], [email protected]

Review URL: https://codereview.chromium.org/422433005

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@286564 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/chrome/browser/extensions/api/idle/idle_api_unittest.cc b/chrome/browser/extensions/api/idle/idle_api_unittest.cc
index cb49004..82779fe 100644
--- a/chrome/browser/extensions/api/idle/idle_api_unittest.cc
+++ b/chrome/browser/extensions/api/idle/idle_api_unittest.cc
@@ -107,16 +107,14 @@
                            const std::string& extension_id)
     : idle_manager_(idle_manager),
       extension_id_(extension_id) {
-  const EventListenerInfo details(idle::OnStateChanged::kEventName,
-                                  extension_id_,
-                                  NULL);
+  const EventListenerInfo details(
+      idle::OnStateChanged::kEventName, extension_id_, GURL(), NULL);
   idle_manager_->OnListenerAdded(details);
 }
 
 ScopedListen::~ScopedListen() {
-  const EventListenerInfo details(idle::OnStateChanged::kEventName,
-                                  extension_id_,
-                                  NULL);
+  const EventListenerInfo details(
+      idle::OnStateChanged::kEventName, extension_id_, GURL(), NULL);
   idle_manager_->OnListenerRemoved(details);
 }
 
diff --git a/chrome/browser/extensions/api/signed_in_devices/signed_in_devices_manager_unittest.cc b/chrome/browser/extensions/api/signed_in_devices/signed_in_devices_manager_unittest.cc
index 9621438..a7d59394 100644
--- a/chrome/browser/extensions/api/signed_in_devices/signed_in_devices_manager_unittest.cc
+++ b/chrome/browser/extensions/api/signed_in_devices/signed_in_devices_manager_unittest.cc
@@ -32,6 +32,7 @@
 
   EventListenerInfo info(api::signed_in_devices::OnDeviceInfoChange::kEventName,
                          "extension1",
+                         GURL(),
                          profile.get());
 
   // Add a listener.
diff --git a/chrome/browser/extensions/extension_webui_apitest.cc b/chrome/browser/extensions/extension_webui_apitest.cc
index 1eca94d..dcd40d3 100644
--- a/chrome/browser/extensions/extension_webui_apitest.cc
+++ b/chrome/browser/extensions/extension_webui_apitest.cc
@@ -40,7 +40,10 @@
 // Tests running extension APIs on WebUI.
 class ExtensionWebUITest : public ExtensionApiTest {
  protected:
-  testing::AssertionResult RunTest(const char* name) {
+  testing::AssertionResult RunTest(const char* name,
+                                   const GURL& page_url,
+                                   const GURL& frame_url,
+                                   bool expected_result) {
     // Tests are located in chrome/test/data/extensions/webui/$(name).
     base::FilePath path;
     PathService::Get(chrome::DIR_TEST_DATA, &path);
@@ -55,38 +58,64 @@
     script = "(function(){'use strict';" + script + "}());";
 
     // Run the test.
-    bool result = false;
-    content::RenderFrameHost* webui = NavigateToWebUI();
+    bool actual_result = false;
+    content::RenderFrameHost* webui = NavigateToWebUI(page_url, frame_url);
     if (!webui)
       return testing::AssertionFailure() << "Failed to navigate to WebUI";
-    CHECK(content::ExecuteScriptAndExtractBool(webui, script, &result));
-    return result ? testing::AssertionSuccess()
-                  : (testing::AssertionFailure() << "Check console output");
+    CHECK(content::ExecuteScriptAndExtractBool(webui, script, &actual_result));
+    return (expected_result == actual_result)
+               ? testing::AssertionSuccess()
+               : (testing::AssertionFailure() << "Check console output");
   }
 
- private:
-  // Navigates the browser to a WebUI page and returns the RenderFrameHost for
-  // that page.
-  content::RenderFrameHost* NavigateToWebUI() {
-    // Use the chrome://extensions page, cos, why not.
-    ui_test_utils::NavigateToURL(browser(), GURL("chrome://extensions/"));
-
+  testing::AssertionResult RunTestOnExtensions(const char* name) {
     // In the current design the URL of the chrome://extensions page it's
     // actually chrome://extensions-frame/ -- and it's important we find it,
     // because the top-level frame doesn't execute any code, so a script
     // context is never created, so the bindings are never set up, and
     // apparently the call to ExecuteScriptAndExtractString doesn't adequately
     // set them up either.
+    return RunTest(name,
+                   GURL("chrome://extensions"),
+                   GURL("chrome://extensions-frame"),
+                   true);  // tests on chrome://extensions should succeed
+  }
+
+  testing::AssertionResult RunTestOnAbout(const char* name) {
+    // chrome://about is an innocuous page that doesn't have any bindings.
+    // Tests should fail.
+    return RunTest(name,
+                   GURL("chrome://about"),
+                   GURL("chrome://about"),
+                   false);  // tests on chrome://about should fail
+  }
+
+ private:
+  // Navigates the browser to a WebUI page and returns the RenderFrameHost for
+  // that page.
+  content::RenderFrameHost* NavigateToWebUI(const GURL& page_url,
+                                            const GURL& frame_url) {
+    ui_test_utils::NavigateToURL(browser(), page_url);
+
+    content::WebContents* active_web_contents =
+        browser()->tab_strip_model()->GetActiveWebContents();
+
+    if (active_web_contents->GetLastCommittedURL() == frame_url)
+      return active_web_contents->GetMainFrame();
+
     content::RenderFrameHost* frame_host = NULL;
-    browser()->tab_strip_model()->GetActiveWebContents()->ForEachFrame(
-        base::Bind(
-            &FindFrame, GURL("chrome://extensions-frame/"), &frame_host));
+    active_web_contents->ForEachFrame(
+        base::Bind(&FindFrame, frame_url, &frame_host));
     return frame_host;
   }
 };
 
 IN_PROC_BROWSER_TEST_F(ExtensionWebUITest, SanityCheckAvailableAPIs) {
-  ASSERT_TRUE(RunTest("sanity_check_available_apis.js"));
+  ASSERT_TRUE(RunTestOnExtensions("sanity_check_available_apis.js"));
+}
+
+IN_PROC_BROWSER_TEST_F(ExtensionWebUITest, SanityCheckUnavailableAPIs) {
+  ASSERT_TRUE(RunTestOnAbout("sanity_check_available_apis.js"));
 }
 
 // Tests chrome.test.sendMessage, which exercises WebUI making a
@@ -95,7 +124,7 @@
   scoped_ptr<ExtensionTestMessageListener> listener(
       new ExtensionTestMessageListener("ping", true));
 
-  ASSERT_TRUE(RunTest("send_message.js"));
+  ASSERT_TRUE(RunTestOnExtensions("send_message.js"));
 
   ASSERT_TRUE(listener->WaitUntilSatisfied());
   listener->Reply("pong");
@@ -108,7 +137,7 @@
 // Tests chrome.runtime.onMessage, which exercises WebUI registering and
 // receiving an event.
 IN_PROC_BROWSER_TEST_F(ExtensionWebUITest, OnMessage) {
-  ASSERT_TRUE(RunTest("on_message.js"));
+  ASSERT_TRUE(RunTestOnExtensions("on_message.js"));
 
   OnMessage::Info info;
   info.data = "hi";
@@ -128,7 +157,7 @@
   scoped_ptr<ExtensionTestMessageListener> listener(
       new ExtensionTestMessageListener("ping", true));
 
-  ASSERT_TRUE(RunTest("runtime_last_error.js"));
+  ASSERT_TRUE(RunTestOnExtensions("runtime_last_error.js"));
 
   ASSERT_TRUE(listener->WaitUntilSatisfied());
   listener->ReplyWithError("unknown host");
diff --git a/chrome/test/data/extensions/webui/on_message.js b/chrome/test/data/extensions/webui/on_message.js
index e564224..48464e5 100644
--- a/chrome/test/data/extensions/webui/on_message.js
+++ b/chrome/test/data/extensions/webui/on_message.js
@@ -4,6 +4,13 @@
 
 // out/Debug/browser_tests --gtest_filter=ExtensionWebUITest.OnMessage
 
+if (!chrome || !chrome.test || !chrome.test.onMessage) {
+  console.error('chrome.test.onMessage is unavailable on ' +
+                document.location.href);
+  domAutomationController.send(false);
+  return;
+}
+
 chrome.test.listenOnce(chrome.test.onMessage, function(args) {
   if (args.data != 'hi') {
     console.error('Expected "hi", Actual ' + JSON.stringify(args.data));
diff --git a/chrome/test/data/extensions/webui/sanity_check_available_apis.js b/chrome/test/data/extensions/webui/sanity_check_available_apis.js
index a57b832b..3d53055 100644
--- a/chrome/test/data/extensions/webui/sanity_check_available_apis.js
+++ b/chrome/test/data/extensions/webui/sanity_check_available_apis.js
@@ -22,10 +22,14 @@
 ];
 var actual = Object.keys(chrome).sort();
 
-if (!chrome.test.checkDeepEq(expected, actual)) {
+var isEqual = expected.length == actual.length;
+for (var i = 0; i < expected.length && isEqual; i++) {
+  if (expected[i] != actual[i])
+    isEqual = false;
+}
+
+if (!isEqual) {
   console.error('Expected: ' + JSON.stringify(expected) + ', ' +
                 'Actual: ' + JSON.stringify(actual));
-  domAutomationController.send(false);
-} else {
-  domAutomationController.send(true);
 }
+domAutomationController.send(isEqual);
diff --git a/chrome/test/data/extensions/webui/send_message.js b/chrome/test/data/extensions/webui/send_message.js
index 69ab3142..2524e2c 100644
--- a/chrome/test/data/extensions/webui/send_message.js
+++ b/chrome/test/data/extensions/webui/send_message.js
@@ -4,6 +4,13 @@
 
 // out/Debug/browser_tests --gtest_filter=ExtensionWebUITest.SendMessage
 
+if (!chrome || !chrome.test || !chrome.test.sendMessage) {
+  console.error('chrome.test.sendMessage is unavailable on ' +
+                document.location.href);
+  domAutomationController.send(false);
+  return;
+}
+
 chrome.test.sendMessage('ping', function(reply) {
   if (reply != 'pong') {
     console.error('Expected "pong", Actual ' + JSON.stringify(reply));
diff --git a/extensions/browser/event_listener_map.cc b/extensions/browser/event_listener_map.cc
index 5d90e980..d6e7d2d8 100644
--- a/extensions/browser/event_listener_map.cc
+++ b/extensions/browser/event_listener_map.cc
@@ -8,6 +8,7 @@
 #include "content/public/browser/render_process_host.h"
 #include "extensions/browser/event_router.h"
 #include "ipc/ipc_message.h"
+#include "url/gurl.h"
 
 using base::DictionaryValue;
 
@@ -15,15 +16,24 @@
 
 typedef EventFilter::MatcherID MatcherID;
 
-EventListener::EventListener(const std::string& event_name,
-                             const std::string& extension_id,
-                             content::RenderProcessHost* process,
-                             scoped_ptr<DictionaryValue> filter)
-    : event_name_(event_name),
-      extension_id_(extension_id),
-      process_(process),
-      filter_(filter.Pass()),
-      matcher_id_(-1) {
+// static
+scoped_ptr<EventListener> EventListener::ForExtension(
+    const std::string& event_name,
+    const std::string& extension_id,
+    content::RenderProcessHost* process,
+    scoped_ptr<base::DictionaryValue> filter) {
+  return make_scoped_ptr(new EventListener(
+      event_name, extension_id, GURL(), process, filter.Pass()));
+}
+
+// static
+scoped_ptr<EventListener> EventListener::ForURL(
+    const std::string& event_name,
+    const GURL& listener_url,
+    content::RenderProcessHost* process,
+    scoped_ptr<base::DictionaryValue> filter) {
+  return make_scoped_ptr(
+      new EventListener(event_name, "", listener_url, process, filter.Pass()));
 }
 
 EventListener::~EventListener() {}
@@ -33,7 +43,8 @@
   // filter that hasn't been added to EventFilter to match one that is
   // equivalent but has.
   return event_name_ == other->event_name_ &&
-         extension_id_ == other->extension_id_ && process_ == other->process_ &&
+         extension_id_ == other->extension_id_ &&
+         listener_url_ == other->listener_url_ && process_ == other->process_ &&
          ((!!filter_.get()) == (!!other->filter_.get())) &&
          (!filter_.get() || filter_->Equals(other->filter_.get()));
 }
@@ -43,7 +54,7 @@
   if (filter_)
     filter_copy.reset(filter_->DeepCopy());
   return scoped_ptr<EventListener>(new EventListener(
-      event_name_, extension_id_, process_, filter_copy.Pass()));
+      event_name_, extension_id_, listener_url_, process_, filter_copy.Pass()));
 }
 
 bool EventListener::IsLazy() const {
@@ -58,6 +69,19 @@
   return process_ ? process_->GetBrowserContext() : NULL;
 }
 
+EventListener::EventListener(const std::string& event_name,
+                             const std::string& extension_id,
+                             const GURL& listener_url,
+                             content::RenderProcessHost* process,
+                             scoped_ptr<DictionaryValue> filter)
+    : event_name_(event_name),
+      extension_id_(extension_id),
+      listener_url_(listener_url),
+      process_(process),
+      filter_(filter.Pass()),
+      matcher_id_(-1) {
+}
+
 EventListenerMap::EventListenerMap(Delegate* delegate)
     : delegate_(delegate) {
 }
@@ -86,7 +110,7 @@
 scoped_ptr<EventMatcher> EventListenerMap::ParseEventMatcher(
     DictionaryValue* filter_dict) {
   return scoped_ptr<EventMatcher>(new EventMatcher(
-      scoped_ptr<DictionaryValue>(filter_dict->DeepCopy()), MSG_ROUTING_NONE));
+      make_scoped_ptr(filter_dict->DeepCopy()), MSG_ROUTING_NONE));
 }
 
 bool EventListenerMap::RemoveListener(const EventListener* listener) {
@@ -173,8 +197,8 @@
     const std::set<std::string>& event_names) {
   for (std::set<std::string>::const_iterator it = event_names.begin();
        it != event_names.end(); ++it) {
-    AddListener(scoped_ptr<EventListener>(new EventListener(
-        *it, extension_id, NULL, scoped_ptr<DictionaryValue>())));
+    AddListener(EventListener::ForExtension(
+        *it, extension_id, NULL, scoped_ptr<DictionaryValue>()));
   }
 }
 
@@ -190,9 +214,8 @@
       const DictionaryValue* filter = NULL;
       if (!filter_list->GetDictionary(i, &filter))
         continue;
-      AddListener(scoped_ptr<EventListener>(new EventListener(
-          it.key(), extension_id, NULL,
-          scoped_ptr<DictionaryValue>(filter->DeepCopy()))));
+      AddListener(EventListener::ForExtension(
+          it.key(), extension_id, NULL, make_scoped_ptr(filter->DeepCopy())));
     }
   }
 }
diff --git a/extensions/browser/event_listener_map.h b/extensions/browser/event_listener_map.h
index 163fc8f..6f5cea9 100644
--- a/extensions/browser/event_listener_map.h
+++ b/extensions/browser/event_listener_map.h
@@ -12,6 +12,7 @@
 
 #include "base/memory/scoped_ptr.h"
 #include "extensions/common/event_filter.h"
+#include "url/gurl.h"
 
 namespace base {
 class DictionaryValue;
@@ -37,9 +38,10 @@
 // is listening to the event. It is associated with no process, so to dispatch
 // an event to a lazy listener one must start a process running the associated
 // extension and dispatch the event to that.
-//
 class EventListener {
  public:
+  // Constructs EventListeners for either an Extension or a URL.
+  //
   // |filter| represents a generic filter structure that EventFilter knows how
   // to filter events with. A typical filter instance will look like
   //
@@ -47,10 +49,17 @@
   //   url: [{hostSuffix: 'google.com'}],
   //   tabId: 5
   // }
-  EventListener(const std::string& event_name,
-                const std::string& extension_id,
-                content::RenderProcessHost* process,
-                scoped_ptr<base::DictionaryValue> filter);
+  static scoped_ptr<EventListener> ForExtension(
+      const std::string& event_name,
+      const std::string& extension_id,
+      content::RenderProcessHost* process,
+      scoped_ptr<base::DictionaryValue> filter);
+  static scoped_ptr<EventListener> ForURL(
+      const std::string& event_name,
+      const GURL& listener_url,
+      content::RenderProcessHost* process,
+      scoped_ptr<base::DictionaryValue> filter);
+
   ~EventListener();
 
   bool Equals(const EventListener* other) const;
@@ -67,16 +76,24 @@
   // IsLazy.
   content::BrowserContext* GetBrowserContext() const;
 
-  const std::string event_name() const { return event_name_; }
-  const std::string extension_id() const { return extension_id_; }
+  const std::string& event_name() const { return event_name_; }
+  const std::string& extension_id() const { return extension_id_; }
+  const GURL& listener_url() const { return listener_url_; }
   content::RenderProcessHost* process() const { return process_; }
   base::DictionaryValue* filter() const { return filter_.get(); }
   EventFilter::MatcherID matcher_id() const { return matcher_id_; }
   void set_matcher_id(EventFilter::MatcherID id) { matcher_id_ = id; }
 
  private:
+  EventListener(const std::string& event_name,
+                const std::string& extension_id,
+                const GURL& listener_url,
+                content::RenderProcessHost* process,
+                scoped_ptr<base::DictionaryValue> filter);
+
   const std::string event_name_;
   const std::string extension_id_;
+  const GURL listener_url_;
   content::RenderProcessHost* process_;
   scoped_ptr<base::DictionaryValue> filter_;
   EventFilter::MatcherID matcher_id_;  // -1 if unset.
diff --git a/extensions/browser/event_listener_map_unittest.cc b/extensions/browser/event_listener_map_unittest.cc
index 9f4c5c3..3a45c68 100644
--- a/extensions/browser/event_listener_map_unittest.cc
+++ b/extensions/browser/event_listener_map_unittest.cc
@@ -4,10 +4,12 @@
 
 #include "extensions/browser/event_listener_map.h"
 
+#include "base/bind.h"
 #include "content/public/test/mock_render_process_host.h"
 #include "content/public/test/test_browser_context.h"
 #include "extensions/browser/event_router.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
 
 using base::DictionaryValue;
 using base::ListValue;
@@ -21,20 +23,26 @@
 const char kExt2Id[] = "extension_2";
 const char kEvent1Name[] = "event1";
 const char kEvent2Name[] = "event2";
+const char kURL[] = "https://google.com/some/url";
+
+typedef base::Callback<scoped_ptr<EventListener>(
+    const std::string&,           // event_name
+    content::RenderProcessHost*,  // process
+    base::DictionaryValue*        // filter (takes ownership)
+    )> EventListenerConstructor;
 
 class EmptyDelegate : public EventListenerMap::Delegate {
   virtual void OnListenerAdded(const EventListener* listener) OVERRIDE {};
   virtual void OnListenerRemoved(const EventListener* listener) OVERRIDE {};
 };
 
-class EventListenerMapUnittest : public testing::Test {
+class EventListenerMapTest : public testing::Test {
  public:
-  EventListenerMapUnittest()
-    : delegate_(new EmptyDelegate),
-      listeners_(new EventListenerMap(delegate_.get())),
-      browser_context_(new content::TestBrowserContext),
-      process_(new content::MockRenderProcessHost(browser_context_.get())) {
-  }
+  EventListenerMapTest()
+      : delegate_(new EmptyDelegate),
+        listeners_(new EventListenerMap(delegate_.get())),
+        browser_context_(new content::TestBrowserContext),
+        process_(new content::MockRenderProcessHost(browser_context_.get())) {}
 
   scoped_ptr<DictionaryValue> CreateHostSuffixFilter(
       const std::string& suffix) {
@@ -64,27 +72,64 @@
   }
 
  protected:
+  void TestUnfilteredEventsGoToAllListeners(
+      const EventListenerConstructor& constructor);
+  void TestRemovingByProcess(const EventListenerConstructor& constructor);
+  void TestRemovingByListener(const EventListenerConstructor& constructor);
+  void TestAddExistingUnfilteredListener(
+      const EventListenerConstructor& constructor);
+  void TestHasListenerForEvent(const EventListenerConstructor& constructor);
+
   scoped_ptr<EventListenerMap::Delegate> delegate_;
   scoped_ptr<EventListenerMap> listeners_;
   scoped_ptr<content::TestBrowserContext> browser_context_;
   scoped_ptr<content::MockRenderProcessHost> process_;
 };
 
-TEST_F(EventListenerMapUnittest, UnfilteredEventsGoToAllListeners) {
-  listeners_->AddListener(scoped_ptr<EventListener>(new EventListener(
-      kEvent1Name, kExt1Id, NULL, scoped_ptr<DictionaryValue>())));
-
-  scoped_ptr<Event> event(CreateNamedEvent(kEvent1Name));
-  std::set<const EventListener*> targets(listeners_->GetEventListeners(*event));
-  ASSERT_EQ(1u, targets.size());
+scoped_ptr<EventListener> CreateEventListenerForExtension(
+    const std::string& extension_id,
+    const std::string& event_name,
+    content::RenderProcessHost* process,
+    base::DictionaryValue* filter) {
+  return EventListener::ForExtension(
+      event_name, extension_id, process, make_scoped_ptr(filter));
 }
 
-TEST_F(EventListenerMapUnittest, FilteredEventsGoToAllMatchingListeners) {
-  listeners_->AddListener(scoped_ptr<EventListener>(new EventListener(
-      kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com"))));
-  listeners_->AddListener(scoped_ptr<EventListener>(new EventListener(
-      kEvent1Name, kExt1Id, NULL, scoped_ptr<DictionaryValue>(
-      new DictionaryValue))));
+scoped_ptr<EventListener> CreateEventListenerForURL(
+    const GURL& listener_url,
+    const std::string& event_name,
+    content::RenderProcessHost* process,
+    base::DictionaryValue* filter) {
+  return EventListener::ForURL(
+      event_name, listener_url, process, make_scoped_ptr(filter));
+}
+
+void EventListenerMapTest::TestUnfilteredEventsGoToAllListeners(
+    const EventListenerConstructor& constructor) {
+  listeners_->AddListener(
+      constructor.Run(kEvent1Name, NULL, new DictionaryValue()));
+  scoped_ptr<Event> event(CreateNamedEvent(kEvent1Name));
+  ASSERT_EQ(1u, listeners_->GetEventListeners(*event).size());
+}
+
+TEST_F(EventListenerMapTest, UnfilteredEventsGoToAllListenersForExtensions) {
+  TestUnfilteredEventsGoToAllListeners(
+      base::Bind(&CreateEventListenerForExtension, kExt1Id));
+}
+
+TEST_F(EventListenerMapTest, UnfilteredEventsGoToAllListenersForURLs) {
+  TestUnfilteredEventsGoToAllListeners(
+      base::Bind(&CreateEventListenerForURL, GURL(kURL)));
+}
+
+TEST_F(EventListenerMapTest, FilteredEventsGoToAllMatchingListeners) {
+  listeners_->AddListener(EventListener::ForExtension(
+      kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com")));
+  listeners_->AddListener(EventListener::ForExtension(
+      kEvent1Name,
+      kExt1Id,
+      NULL,
+      scoped_ptr<DictionaryValue>(new DictionaryValue)));
 
   scoped_ptr<Event> event(CreateNamedEvent(kEvent1Name));
   event->filter_info.SetURL(GURL("http://www.google.com"));
@@ -92,11 +137,11 @@
   ASSERT_EQ(2u, targets.size());
 }
 
-TEST_F(EventListenerMapUnittest, FilteredEventsOnlyGoToMatchingListeners) {
-  listeners_->AddListener(scoped_ptr<EventListener>(new EventListener(
-      kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com"))));
-  listeners_->AddListener(scoped_ptr<EventListener>(new EventListener(
-      kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("yahoo.com"))));
+TEST_F(EventListenerMapTest, FilteredEventsOnlyGoToMatchingListeners) {
+  listeners_->AddListener(EventListener::ForExtension(
+      kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com")));
+  listeners_->AddListener(EventListener::ForExtension(
+      kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("yahoo.com")));
 
   scoped_ptr<Event> event(CreateNamedEvent(kEvent1Name));
   event->filter_info.SetURL(GURL("http://www.google.com"));
@@ -104,13 +149,15 @@
   ASSERT_EQ(1u, targets.size());
 }
 
-TEST_F(EventListenerMapUnittest, LazyAndUnlazyListenersGetReturned) {
-  listeners_->AddListener(scoped_ptr<EventListener>(new EventListener(
-      kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com"))));
+TEST_F(EventListenerMapTest, LazyAndUnlazyListenersGetReturned) {
+  listeners_->AddListener(EventListener::ForExtension(
+      kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com")));
 
-  listeners_->AddListener(scoped_ptr<EventListener>(new EventListener(
-      kEvent1Name, kExt1Id, process_.get(),
-      CreateHostSuffixFilter("google.com"))));
+  listeners_->AddListener(
+      EventListener::ForExtension(kEvent1Name,
+                                  kExt1Id,
+                                  process_.get(),
+                                  CreateHostSuffixFilter("google.com")));
 
   scoped_ptr<Event> event(CreateNamedEvent(kEvent1Name));
   event->filter_info.SetURL(GURL("http://www.google.com"));
@@ -118,48 +165,68 @@
   ASSERT_EQ(2u, targets.size());
 }
 
-TEST_F(EventListenerMapUnittest, TestRemovingByProcess) {
-  listeners_->AddListener(scoped_ptr<EventListener>(new EventListener(
-      kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com"))));
+void EventListenerMapTest::TestRemovingByProcess(
+    const EventListenerConstructor& constructor) {
+  listeners_->AddListener(constructor.Run(
+      kEvent1Name, NULL, CreateHostSuffixFilter("google.com").release()));
 
-  listeners_->AddListener(scoped_ptr<EventListener>(new EventListener(
-      kEvent1Name, kExt1Id, process_.get(),
-      CreateHostSuffixFilter("google.com"))));
+  listeners_->AddListener(
+      constructor.Run(kEvent1Name,
+                      process_.get(),
+                      CreateHostSuffixFilter("google.com").release()));
 
   listeners_->RemoveListenersForProcess(process_.get());
 
   scoped_ptr<Event> event(CreateNamedEvent(kEvent1Name));
   event->filter_info.SetURL(GURL("http://www.google.com"));
-  std::set<const EventListener*> targets(listeners_->GetEventListeners(*event));
-  ASSERT_EQ(1u, targets.size());
+  ASSERT_EQ(1u, listeners_->GetEventListeners(*event).size());
 }
 
-TEST_F(EventListenerMapUnittest, TestRemovingByListener) {
-  listeners_->AddListener(scoped_ptr<EventListener>(new EventListener(
-      kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com"))));
+TEST_F(EventListenerMapTest, TestRemovingByProcessForExtension) {
+  TestRemovingByProcess(base::Bind(&CreateEventListenerForExtension, kExt1Id));
+}
 
-  listeners_->AddListener(scoped_ptr<EventListener>(new EventListener(
-      kEvent1Name, kExt1Id, process_.get(),
-      CreateHostSuffixFilter("google.com"))));
+TEST_F(EventListenerMapTest, TestRemovingByProcessForURL) {
+  TestRemovingByProcess(base::Bind(&CreateEventListenerForURL, GURL(kURL)));
+}
 
-  scoped_ptr<EventListener> listener(new EventListener(kEvent1Name, kExt1Id,
-      process_.get(), CreateHostSuffixFilter("google.com")));
+void EventListenerMapTest::TestRemovingByListener(
+    const EventListenerConstructor& constructor) {
+  listeners_->AddListener(constructor.Run(
+      kEvent1Name, NULL, CreateHostSuffixFilter("google.com").release()));
+
+  listeners_->AddListener(
+      constructor.Run(kEvent1Name,
+                      process_.get(),
+                      CreateHostSuffixFilter("google.com").release()));
+
+  scoped_ptr<EventListener> listener(
+      constructor.Run(kEvent1Name,
+                      process_.get(),
+                      CreateHostSuffixFilter("google.com").release()));
   listeners_->RemoveListener(listener.get());
 
   scoped_ptr<Event> event(CreateNamedEvent(kEvent1Name));
   event->filter_info.SetURL(GURL("http://www.google.com"));
-  std::set<const EventListener*> targets(listeners_->GetEventListeners(*event));
-  ASSERT_EQ(1u, targets.size());
+  ASSERT_EQ(1u, listeners_->GetEventListeners(*event).size());
 }
 
-TEST_F(EventListenerMapUnittest, TestLazyDoubleAddIsUndoneByRemove) {
-  listeners_->AddListener(scoped_ptr<EventListener>(new EventListener(
-      kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com"))));
-  listeners_->AddListener(scoped_ptr<EventListener>(new EventListener(
-      kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com"))));
+TEST_F(EventListenerMapTest, TestRemovingByListenerForExtension) {
+  TestRemovingByListener(base::Bind(&CreateEventListenerForExtension, kExt1Id));
+}
 
-  scoped_ptr<EventListener> listener(new EventListener(
-        kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com")));
+TEST_F(EventListenerMapTest, TestRemovingByListenerForURL) {
+  TestRemovingByListener(base::Bind(&CreateEventListenerForURL, GURL(kURL)));
+}
+
+TEST_F(EventListenerMapTest, TestLazyDoubleAddIsUndoneByRemove) {
+  listeners_->AddListener(EventListener::ForExtension(
+      kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com")));
+  listeners_->AddListener(EventListener::ForExtension(
+      kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com")));
+
+  scoped_ptr<EventListener> listener(EventListener::ForExtension(
+      kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com")));
   listeners_->RemoveListener(listener.get());
 
   scoped_ptr<Event> event(CreateNamedEvent(kEvent1Name));
@@ -168,17 +235,17 @@
   ASSERT_EQ(0u, targets.size());
 }
 
-TEST_F(EventListenerMapUnittest, HostSuffixFilterEquality) {
+TEST_F(EventListenerMapTest, HostSuffixFilterEquality) {
   scoped_ptr<DictionaryValue> filter1(CreateHostSuffixFilter("google.com"));
   scoped_ptr<DictionaryValue> filter2(CreateHostSuffixFilter("google.com"));
   ASSERT_TRUE(filter1->Equals(filter2.get()));
 }
 
-TEST_F(EventListenerMapUnittest, RemoveLazyListenersForExtension) {
-  listeners_->AddListener(scoped_ptr<EventListener>(new EventListener(
-      kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com"))));
-  listeners_->AddListener(scoped_ptr<EventListener>(new EventListener(
-      kEvent2Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com"))));
+TEST_F(EventListenerMapTest, RemoveLazyListeners) {
+  listeners_->AddListener(EventListener::ForExtension(
+      kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com")));
+  listeners_->AddListener(EventListener::ForExtension(
+      kEvent2Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com")));
 
   listeners_->RemoveLazyListenersForExtension(kExt1Id);
 
@@ -192,29 +259,25 @@
   ASSERT_EQ(0u, targets.size());
 }
 
-TEST_F(EventListenerMapUnittest, AddExistingFilteredListener) {
-  bool first_new = listeners_->AddListener(scoped_ptr<EventListener>(
-      new EventListener(kEvent1Name, kExt1Id, NULL,
-                        CreateHostSuffixFilter("google.com"))));
-  bool second_new = listeners_->AddListener(scoped_ptr<EventListener>(
-      new EventListener(kEvent1Name, kExt1Id, NULL,
-                        CreateHostSuffixFilter("google.com"))));
+TEST_F(EventListenerMapTest, AddExistingFilteredListener) {
+  bool first_new = listeners_->AddListener(EventListener::ForExtension(
+      kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com")));
+  bool second_new = listeners_->AddListener(EventListener::ForExtension(
+      kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com")));
 
   ASSERT_TRUE(first_new);
   ASSERT_FALSE(second_new);
 }
 
-TEST_F(EventListenerMapUnittest, AddExistingUnfilteredListener) {
-  bool first_add = listeners_->AddListener(scoped_ptr<EventListener>(
-      new EventListener(kEvent1Name, kExt1Id, NULL,
-                        scoped_ptr<DictionaryValue>())));
-  bool second_add = listeners_->AddListener(scoped_ptr<EventListener>(
-      new EventListener(kEvent1Name, kExt1Id, NULL,
-                        scoped_ptr<DictionaryValue>())));
+void EventListenerMapTest::TestAddExistingUnfilteredListener(
+    const EventListenerConstructor& constructor) {
+  bool first_add = listeners_->AddListener(
+      constructor.Run(kEvent1Name, NULL, new DictionaryValue()));
+  bool second_add = listeners_->AddListener(
+      constructor.Run(kEvent1Name, NULL, new DictionaryValue()));
 
   scoped_ptr<EventListener> listener(
-        new EventListener(kEvent1Name, kExt1Id, NULL,
-                          scoped_ptr<DictionaryValue>()));
+      constructor.Run(kEvent1Name, NULL, new DictionaryValue()));
   bool first_remove = listeners_->RemoveListener(listener.get());
   bool second_remove = listeners_->RemoveListener(listener.get());
 
@@ -224,20 +287,31 @@
   ASSERT_FALSE(second_remove);
 }
 
-TEST_F(EventListenerMapUnittest, RemovingRouters) {
-  listeners_->AddListener(scoped_ptr<EventListener>(
-      new EventListener(kEvent1Name, kExt1Id, process_.get(),
-                        scoped_ptr<DictionaryValue>())));
+TEST_F(EventListenerMapTest, AddExistingUnfilteredListenerForExtensions) {
+  TestAddExistingUnfilteredListener(
+      base::Bind(&CreateEventListenerForExtension, kExt1Id));
+}
+
+TEST_F(EventListenerMapTest, AddExistingUnfilteredListenerForURLs) {
+  TestAddExistingUnfilteredListener(
+      base::Bind(&CreateEventListenerForURL, GURL(kURL)));
+}
+
+TEST_F(EventListenerMapTest, RemovingRouters) {
+  listeners_->AddListener(EventListener::ForExtension(
+      kEvent1Name, kExt1Id, process_.get(), scoped_ptr<DictionaryValue>()));
+  listeners_->AddListener(EventListener::ForURL(
+      kEvent1Name, GURL(kURL), process_.get(), scoped_ptr<DictionaryValue>()));
   listeners_->RemoveListenersForProcess(process_.get());
   ASSERT_FALSE(listeners_->HasListenerForEvent(kEvent1Name));
 }
 
-TEST_F(EventListenerMapUnittest, HasListenerForEvent) {
+void EventListenerMapTest::TestHasListenerForEvent(
+    const EventListenerConstructor& constructor) {
   ASSERT_FALSE(listeners_->HasListenerForEvent(kEvent1Name));
 
-  listeners_->AddListener(scoped_ptr<EventListener>(
-      new EventListener(kEvent1Name, kExt1Id, process_.get(),
-                        scoped_ptr<DictionaryValue>())));
+  listeners_->AddListener(
+      constructor.Run(kEvent1Name, process_.get(), new DictionaryValue()));
 
   ASSERT_FALSE(listeners_->HasListenerForEvent(kEvent2Name));
   ASSERT_TRUE(listeners_->HasListenerForEvent(kEvent1Name));
@@ -245,17 +319,24 @@
   ASSERT_FALSE(listeners_->HasListenerForEvent(kEvent1Name));
 }
 
-TEST_F(EventListenerMapUnittest, HasListenerForExtension) {
+TEST_F(EventListenerMapTest, HasListenerForEventForExtension) {
+  TestHasListenerForEvent(
+      base::Bind(&CreateEventListenerForExtension, kExt1Id));
+}
+
+TEST_F(EventListenerMapTest, HasListenerForEventForURL) {
+  TestHasListenerForEvent(base::Bind(&CreateEventListenerForURL, GURL(kURL)));
+}
+
+TEST_F(EventListenerMapTest, HasListenerForExtension) {
   ASSERT_FALSE(listeners_->HasListenerForExtension(kExt1Id, kEvent1Name));
 
   // Non-lazy listener.
-  listeners_->AddListener(scoped_ptr<EventListener>(
-      new EventListener(kEvent1Name, kExt1Id, process_.get(),
-                        scoped_ptr<DictionaryValue>())));
+  listeners_->AddListener(EventListener::ForExtension(
+      kEvent1Name, kExt1Id, process_.get(), scoped_ptr<DictionaryValue>()));
   // Lazy listener.
-  listeners_->AddListener(scoped_ptr<EventListener>(
-      new EventListener(kEvent1Name, kExt1Id, NULL,
-                        scoped_ptr<DictionaryValue>())));
+  listeners_->AddListener(EventListener::ForExtension(
+      kEvent1Name, kExt1Id, NULL, scoped_ptr<DictionaryValue>()));
 
   ASSERT_FALSE(listeners_->HasListenerForExtension(kExt1Id, kEvent2Name));
   ASSERT_TRUE(listeners_->HasListenerForExtension(kExt1Id, kEvent1Name));
@@ -266,7 +347,7 @@
   ASSERT_FALSE(listeners_->HasListenerForExtension(kExt1Id, kEvent1Name));
 }
 
-TEST_F(EventListenerMapUnittest, AddLazyListenersFromPreferences) {
+TEST_F(EventListenerMapTest, AddLazyListenersFromPreferences) {
   scoped_ptr<DictionaryValue> filter1(CreateHostSuffixFilter("google.com"));
   scoped_ptr<DictionaryValue> filter2(CreateHostSuffixFilter("yahoo.com"));
 
@@ -283,12 +364,12 @@
                           GURL("http://www.google.com")));
   std::set<const EventListener*> targets(listeners_->GetEventListeners(*event));
   ASSERT_EQ(1u, targets.size());
-  scoped_ptr<EventListener> listener(new EventListener(kEvent1Name, kExt1Id,
-      NULL, CreateHostSuffixFilter("google.com")));
+  scoped_ptr<EventListener> listener(EventListener::ForExtension(
+      kEvent1Name, kExt1Id, NULL, CreateHostSuffixFilter("google.com")));
   ASSERT_TRUE((*targets.begin())->Equals(listener.get()));
 }
 
-TEST_F(EventListenerMapUnittest, CorruptedExtensionPrefsShouldntCrash) {
+TEST_F(EventListenerMapTest, CorruptedExtensionPrefsShouldntCrash) {
   scoped_ptr<DictionaryValue> filter1(CreateHostSuffixFilter("google.com"));
 
   DictionaryValue filtered_listeners;
diff --git a/extensions/browser/event_router.cc b/extensions/browser/event_router.cc
index 0efddb7..21985b1 100644
--- a/extensions/browser/event_router.cc
+++ b/extensions/browser/event_router.cc
@@ -188,16 +188,31 @@
 void EventRouter::AddEventListener(const std::string& event_name,
                                    content::RenderProcessHost* process,
                                    const std::string& extension_id) {
-  listeners_.AddListener(scoped_ptr<EventListener>(new EventListener(
-      event_name, extension_id, process, scoped_ptr<DictionaryValue>())));
+  listeners_.AddListener(EventListener::ForExtension(
+      event_name, extension_id, process, scoped_ptr<DictionaryValue>()));
 }
 
 void EventRouter::RemoveEventListener(const std::string& event_name,
                                       content::RenderProcessHost* process,
                                       const std::string& extension_id) {
-  EventListener listener(event_name, extension_id, process,
-                         scoped_ptr<DictionaryValue>());
-  listeners_.RemoveListener(&listener);
+  scoped_ptr<EventListener> listener = EventListener::ForExtension(
+      event_name, extension_id, process, scoped_ptr<DictionaryValue>());
+  listeners_.RemoveListener(listener.get());
+}
+
+void EventRouter::AddEventListenerForURL(const std::string& event_name,
+                                         content::RenderProcessHost* process,
+                                         const GURL& listener_url) {
+  listeners_.AddListener(EventListener::ForURL(
+      event_name, listener_url, process, scoped_ptr<DictionaryValue>()));
+}
+
+void EventRouter::RemoveEventListenerForURL(const std::string& event_name,
+                                            content::RenderProcessHost* process,
+                                            const GURL& listener_url) {
+  scoped_ptr<EventListener> listener = EventListener::ForURL(
+      event_name, listener_url, process, scoped_ptr<DictionaryValue>());
+  listeners_.RemoveListener(listener.get());
 }
 
 void EventRouter::RegisterObserver(Observer* observer,
@@ -221,6 +236,7 @@
 void EventRouter::OnListenerAdded(const EventListener* listener) {
   const EventListenerInfo details(listener->event_name(),
                                   listener->extension_id(),
+                                  listener->listener_url(),
                                   listener->GetBrowserContext());
   std::string base_event_name = GetBaseEventName(listener->event_name());
   ObserverMap::iterator observer = observers_.find(base_event_name);
@@ -231,6 +247,7 @@
 void EventRouter::OnListenerRemoved(const EventListener* listener) {
   const EventListenerInfo details(listener->event_name(),
                                   listener->extension_id(),
+                                  listener->listener_url(),
                                   listener->GetBrowserContext());
   std::string base_event_name = GetBaseEventName(listener->event_name());
   ObserverMap::iterator observer = observers_.find(base_event_name);
@@ -240,9 +257,8 @@
 
 void EventRouter::AddLazyEventListener(const std::string& event_name,
                                        const std::string& extension_id) {
-  scoped_ptr<EventListener> listener(new EventListener(
+  bool is_new = listeners_.AddListener(EventListener::ForExtension(
       event_name, extension_id, NULL, scoped_ptr<DictionaryValue>()));
-  bool is_new = listeners_.AddListener(listener.Pass());
 
   if (is_new) {
     std::set<std::string> events = GetRegisteredEvents(extension_id);
@@ -254,9 +270,9 @@
 
 void EventRouter::RemoveLazyEventListener(const std::string& event_name,
                                           const std::string& extension_id) {
-  EventListener listener(event_name, extension_id, NULL,
-                         scoped_ptr<DictionaryValue>());
-  bool did_exist = listeners_.RemoveListener(&listener);
+  scoped_ptr<EventListener> listener = EventListener::ForExtension(
+      event_name, extension_id, NULL, scoped_ptr<DictionaryValue>());
+  bool did_exist = listeners_.RemoveListener(listener.get());
 
   if (did_exist) {
     std::set<std::string> events = GetRegisteredEvents(extension_id);
@@ -271,14 +287,18 @@
                                            const std::string& extension_id,
                                            const base::DictionaryValue& filter,
                                            bool add_lazy_listener) {
-  listeners_.AddListener(scoped_ptr<EventListener>(new EventListener(
-      event_name, extension_id, process,
-      scoped_ptr<DictionaryValue>(filter.DeepCopy()))));
+  listeners_.AddListener(EventListener::ForExtension(
+      event_name,
+      extension_id,
+      process,
+      scoped_ptr<DictionaryValue>(filter.DeepCopy())));
 
   if (add_lazy_listener) {
-    bool added = listeners_.AddListener(scoped_ptr<EventListener>(
-        new EventListener(event_name, extension_id, NULL,
-        scoped_ptr<DictionaryValue>(filter.DeepCopy()))));
+    bool added = listeners_.AddListener(EventListener::ForExtension(
+        event_name,
+        extension_id,
+        NULL,
+        scoped_ptr<DictionaryValue>(filter.DeepCopy())));
 
     if (added)
       AddFilterToEvent(event_name, extension_id, &filter);
@@ -291,14 +311,17 @@
     const std::string& extension_id,
     const base::DictionaryValue& filter,
     bool remove_lazy_listener) {
-  EventListener listener(event_name, extension_id, process,
-                         scoped_ptr<DictionaryValue>(filter.DeepCopy()));
+  scoped_ptr<EventListener> listener = EventListener::ForExtension(
+      event_name,
+      extension_id,
+      process,
+      scoped_ptr<DictionaryValue>(filter.DeepCopy()));
 
-  listeners_.RemoveListener(&listener);
+  listeners_.RemoveListener(listener.get());
 
   if (remove_lazy_listener) {
-    listener.MakeLazy();
-    bool removed = listeners_.RemoveListener(&listener);
+    listener->MakeLazy();
+    bool removed = listeners_.RemoveListener(listener.get());
 
     if (removed)
       RemoveFilterFromEvent(event_name, extension_id, &filter);
@@ -471,8 +494,10 @@
         EventDispatchIdentifier dispatch_id(listener->GetBrowserContext(),
                                             listener->extension_id());
         if (!ContainsKey(already_dispatched, dispatch_id)) {
-          DispatchEventToProcess(
-              listener->extension_id(), listener->process(), event);
+          DispatchEventToProcess(listener->extension_id(),
+                                 listener->listener_url(),
+                                 listener->process(),
+                                 event);
         }
       }
     }
@@ -511,6 +536,7 @@
 }
 
 void EventRouter::DispatchEventToProcess(const std::string& extension_id,
+                                         const GURL& listener_url,
                                          content::RenderProcessHost* process,
                                          const linked_ptr<Event>& event) {
   BrowserContext* listener_context = process->GetBrowserContext();
@@ -556,7 +582,7 @@
                  ->HasWebUIBindings(process->GetID())) {
     // Dispatching event to WebUI.
     if (!ExtensionAPI::GetSharedInstance()->IsAvailableToWebUI(
-            event->event_name)) {
+            event->event_name, listener_url)) {
       return;
     }
   } else {
@@ -691,8 +717,9 @@
 
   if (listeners_.HasProcessListener(host->render_process_host(),
                                     host->extension()->id())) {
-    DispatchEventToProcess(host->extension()->id(),
-                           host->render_process_host(), event);
+    // URL events cannot be lazy therefore can't be pending, hence the GURL().
+    DispatchEventToProcess(
+        host->extension()->id(), GURL(), host->render_process_host(), event);
   }
 }
 
@@ -800,9 +827,12 @@
 
 EventListenerInfo::EventListenerInfo(const std::string& event_name,
                                      const std::string& extension_id,
+                                     const GURL& listener_url,
                                      content::BrowserContext* browser_context)
     : event_name(event_name),
       extension_id(extension_id),
-      browser_context(browser_context) {}
+      listener_url(listener_url),
+      browser_context(browser_context) {
+}
 
 }  // namespace extensions
diff --git a/extensions/browser/event_router.h b/extensions/browser/event_router.h
index a69d0422..c8c5226 100644
--- a/extensions/browser/event_router.h
+++ b/extensions/browser/event_router.h
@@ -13,7 +13,6 @@
 #include "base/callback.h"
 #include "base/compiler_specific.h"
 #include "base/containers/hash_tables.h"
-#include "base/gtest_prod_util.h"
 #include "base/memory/linked_ptr.h"
 #include "base/memory/ref_counted.h"
 #include "base/values.h"
@@ -93,7 +92,8 @@
               ExtensionPrefs* extension_prefs);
   virtual ~EventRouter();
 
-  // Add or remove the process/extension pair as a listener for |event_name|.
+  // Add or remove an extension as an event listener for |event_name|.
+  //
   // Note that multiple extensions can share a process due to process
   // collapsing. Also, a single extension can have 2 processes if it is a split
   // mode extension.
@@ -104,6 +104,14 @@
                            content::RenderProcessHost* process,
                            const std::string& extension_id);
 
+  // Add or remove a URL as an event listener for |event_name|.
+  void AddEventListenerForURL(const std::string& event_name,
+                              content::RenderProcessHost* process,
+                              const GURL& listener_url);
+  void RemoveEventListenerForURL(const std::string& event_name,
+                                 content::RenderProcessHost* process,
+                                 const GURL& listener_url);
+
   EventListenerMap& listeners() { return listeners_; }
 
   // Registers an observer to be notified when an event listener for
@@ -171,7 +179,7 @@
                   const std::string& extension_id);
 
  private:
-  FRIEND_TEST_ALL_PREFIXES(EventRouterTest, EventRouterObserver);
+  friend class EventRouterTest;
 
   // The extension and process that contains the event listener for a given
   // event.
@@ -221,8 +229,10 @@
                          const linked_ptr<Event>& event,
                          std::set<EventDispatchIdentifier>* already_dispatched);
 
-  // Dispatches the event to the specified extension running in |process|.
+  // Dispatches the event to the specified extension or URL running in
+  // |process|.
   void DispatchEventToProcess(const std::string& extension_id,
+                              const GURL& listener_url,
                               content::RenderProcessHost* process,
                               const linked_ptr<Event>& event);
 
@@ -357,12 +367,14 @@
 struct EventListenerInfo {
   EventListenerInfo(const std::string& event_name,
                     const std::string& extension_id,
+                    const GURL& listener_url,
                     content::BrowserContext* browser_context);
   // The event name including any sub-event, e.g. "runtime.onStartup" or
   // "webRequest.onCompleted/123".
   const std::string event_name;
 
   const std::string extension_id;
+  const GURL listener_url;
   content::BrowserContext* browser_context;
 };
 
diff --git a/extensions/browser/event_router_unittest.cc b/extensions/browser/event_router_unittest.cc
index c47b7f20..aa068ced 100644
--- a/extensions/browser/event_router_unittest.cc
+++ b/extensions/browser/event_router_unittest.cc
@@ -6,6 +6,7 @@
 
 #include <string>
 
+#include "base/bind.h"
 #include "base/compiler_specific.h"
 #include "base/memory/scoped_ptr.h"
 #include "base/values.h"
@@ -53,9 +54,37 @@
   DISALLOW_COPY_AND_ASSIGN(MockEventRouterObserver);
 };
 
+typedef base::Callback<scoped_ptr<EventListener>(
+    const std::string&,           // event_name
+    content::RenderProcessHost*,  // process
+    base::DictionaryValue*        // filter (takes ownership)
+    )> EventListenerConstructor;
+
+scoped_ptr<EventListener> CreateEventListenerForExtension(
+    const std::string& extension_id,
+    const std::string& event_name,
+    content::RenderProcessHost* process,
+    base::DictionaryValue* filter) {
+  return EventListener::ForExtension(
+      event_name, extension_id, process, make_scoped_ptr(filter));
+}
+
+scoped_ptr<EventListener> CreateEventListenerForURL(
+    const GURL& listener_url,
+    const std::string& event_name,
+    content::RenderProcessHost* process,
+    base::DictionaryValue* filter) {
+  return EventListener::ForURL(
+      event_name, listener_url, process, make_scoped_ptr(filter));
+}
+
 }  // namespace
 
-typedef testing::Test EventRouterTest;
+class EventRouterTest : public testing::Test {
+ protected:
+  // Tests adding and removing observers from EventRouter.
+  void RunEventRouterObserverTest(const EventListenerConstructor& constructor);
+};
 
 TEST_F(EventRouterTest, GetBaseEventName) {
   // Normal event names are passed through unchanged.
@@ -66,14 +95,15 @@
 }
 
 // Tests adding and removing observers from EventRouter.
-TEST_F(EventRouterTest, EventRouterObserver) {
+void EventRouterTest::RunEventRouterObserverTest(
+    const EventListenerConstructor& constructor) {
   EventRouter router(NULL, NULL);
-  EventListener listener(
-      "event_name", "extension_id", NULL, scoped_ptr<base::DictionaryValue>());
+  scoped_ptr<EventListener> listener =
+      constructor.Run("event_name", NULL, new base::DictionaryValue());
 
   // Add/remove works without any observers.
-  router.OnListenerAdded(&listener);
-  router.OnListenerRemoved(&listener);
+  router.OnListenerAdded(listener.get());
+  router.OnListenerRemoved(listener.get());
 
   // Register observers that both match and don't match the event above.
   MockEventRouterObserver matching_observer;
@@ -82,43 +112,51 @@
   router.RegisterObserver(&non_matching_observer, "other");
 
   // Adding a listener notifies the appropriate observers.
-  router.OnListenerAdded(&listener);
+  router.OnListenerAdded(listener.get());
   EXPECT_EQ(1, matching_observer.listener_added_count());
   EXPECT_EQ(0, non_matching_observer.listener_added_count());
 
   // Removing a listener notifies the appropriate observers.
-  router.OnListenerRemoved(&listener);
+  router.OnListenerRemoved(listener.get());
   EXPECT_EQ(1, matching_observer.listener_removed_count());
   EXPECT_EQ(0, non_matching_observer.listener_removed_count());
 
   // Adding the listener again notifies again.
-  router.OnListenerAdded(&listener);
+  router.OnListenerAdded(listener.get());
   EXPECT_EQ(2, matching_observer.listener_added_count());
   EXPECT_EQ(0, non_matching_observer.listener_added_count());
 
   // Removing the listener again notifies again.
-  router.OnListenerRemoved(&listener);
+  router.OnListenerRemoved(listener.get());
   EXPECT_EQ(2, matching_observer.listener_removed_count());
   EXPECT_EQ(0, non_matching_observer.listener_removed_count());
 
   // Adding a listener with a sub-event notifies the main observer with
   // proper details.
   matching_observer.Reset();
-  EventListener sub_event_listener("event_name/1",
-                                   "extension_id",
-                                   NULL,
-                                   scoped_ptr<base::DictionaryValue>());
-  router.OnListenerAdded(&sub_event_listener);
+  scoped_ptr<EventListener> sub_event_listener =
+      constructor.Run("event_name/1", NULL, new base::DictionaryValue());
+  router.OnListenerAdded(sub_event_listener.get());
   EXPECT_EQ(1, matching_observer.listener_added_count());
   EXPECT_EQ(0, matching_observer.listener_removed_count());
   EXPECT_EQ("event_name/1", matching_observer.last_event_name());
 
   // Ditto for removing the listener.
   matching_observer.Reset();
-  router.OnListenerRemoved(&sub_event_listener);
+  router.OnListenerRemoved(sub_event_listener.get());
   EXPECT_EQ(0, matching_observer.listener_added_count());
   EXPECT_EQ(1, matching_observer.listener_removed_count());
   EXPECT_EQ("event_name/1", matching_observer.last_event_name());
 }
 
+TEST_F(EventRouterTest, EventRouterObserverForExtensions) {
+  RunEventRouterObserverTest(
+      base::Bind(&CreateEventListenerForExtension, "extension_id"));
+}
+
+TEST_F(EventRouterTest, EventRouterObserverForURLs) {
+  RunEventRouterObserverTest(
+      base::Bind(&CreateEventListenerForURL, GURL("http://google.com/path")));
+}
+
 }  // namespace extensions
diff --git a/extensions/browser/extension_function_dispatcher.cc b/extensions/browser/extension_function_dispatcher.cc
index 9701f03..ee9651c 100644
--- a/extensions/browser/extension_function_dispatcher.cc
+++ b/extensions/browser/extension_function_dispatcher.cc
@@ -486,7 +486,7 @@
   } else if (content::ChildProcessSecurityPolicy::GetInstance()
                  ->HasWebUIBindings(requesting_process_id)) {
     // WebUI is calling this API.
-    if (!api->IsAvailableToWebUI(params.name)) {
+    if (!api->IsAvailableToWebUI(params.name, params.source_url)) {
       disallowed_reason = "WebUI can only call webui-enabled APIs";
     }
   } else {
diff --git a/extensions/browser/extension_message_filter.cc b/extensions/browser/extension_message_filter.cc
index f7e794b..c9c6c9b 100644
--- a/extensions/browser/extension_message_filter.cc
+++ b/extensions/browser/extension_message_filter.cc
@@ -13,6 +13,7 @@
 #include "extensions/browser/extension_system.h"
 #include "extensions/browser/info_map.h"
 #include "extensions/browser/process_manager.h"
+#include "extensions/common/extension.h"
 #include "extensions/common/extension_messages.h"
 #include "ipc/ipc_message_macros.h"
 
@@ -95,6 +96,7 @@
 
 void ExtensionMessageFilter::OnExtensionAddListener(
     const std::string& extension_id,
+    const GURL& listener_url,
     const std::string& event_name) {
   RenderProcessHost* process = RenderProcessHost::FromID(render_process_id_);
   if (!process)
@@ -102,11 +104,20 @@
   EventRouter* router = EventRouter::Get(browser_context_);
   if (!router)
     return;
-  router->AddEventListener(event_name, process, extension_id);
+
+  if (Extension::IdIsValid(extension_id)) {
+    router->AddEventListener(event_name, process, extension_id);
+  } else if (listener_url.is_valid()) {
+    router->AddEventListenerForURL(event_name, process, listener_url);
+  } else {
+    NOTREACHED() << "Tried to add an event listener without a valid "
+                 << "extension ID nor listener URL";
+  }
 }
 
 void ExtensionMessageFilter::OnExtensionRemoveListener(
     const std::string& extension_id,
+    const GURL& listener_url,
     const std::string& event_name) {
   RenderProcessHost* process = RenderProcessHost::FromID(render_process_id_);
   if (!process)
@@ -114,7 +125,15 @@
   EventRouter* router = EventRouter::Get(browser_context_);
   if (!router)
     return;
-  router->RemoveEventListener(event_name, process, extension_id);
+
+  if (Extension::IdIsValid(extension_id)) {
+    router->RemoveEventListener(event_name, process, extension_id);
+  } else if (listener_url.is_valid()) {
+    router->RemoveEventListenerForURL(event_name, process, listener_url);
+  } else {
+    NOTREACHED() << "Tried to remove an event listener without a valid "
+                 << "extension ID nor listener URL";
+  }
 }
 
 void ExtensionMessageFilter::OnExtensionAddLazyListener(
diff --git a/extensions/browser/extension_message_filter.h b/extensions/browser/extension_message_filter.h
index 1ed785e5..c45fb03 100644
--- a/extensions/browser/extension_message_filter.h
+++ b/extensions/browser/extension_message_filter.h
@@ -12,6 +12,7 @@
 #include "base/macros.h"
 #include "base/memory/weak_ptr.h"
 #include "content/public/browser/browser_message_filter.h"
+#include "url/gurl.h"
 
 struct ExtensionHostMsg_Request_Params;
 
@@ -52,8 +53,10 @@
 
   // Message handlers on the UI thread.
   void OnExtensionAddListener(const std::string& extension_id,
+                              const GURL& listener_url,
                               const std::string& event_name);
   void OnExtensionRemoveListener(const std::string& extension_id,
+                                 const GURL& listener_url,
                                  const std::string& event_name);
   void OnExtensionAddLazyListener(const std::string& extension_id,
                                   const std::string& event_name);
diff --git a/extensions/common/api/_api_features.json b/extensions/common/api/_api_features.json
index fbabf29..4555a10 100644
--- a/extensions/common/api/_api_features.json
+++ b/extensions/common/api/_api_features.json
@@ -122,18 +122,21 @@
     "dependencies": ["permission:storage"],
     "contexts": ["blessed_extension", "unblessed_extension", "content_script"]
   },
-  "test": {
+  "test": [{
     "internal": true,
     "channel": "stable",
     "extension_types": "all",
-    // Everything except web pages.
-    "contexts": [
-      "blessed_extension",
-      "content_script",
-      "unblessed_extension",
-      "webui"
+    // Everything except web pages and WebUI. WebUI is declared in a separate
+    // rule to keep the "matches" property isolated.
+    "contexts": ["blessed_extension", "content_script", "unblessed_extension"]
+  }, {
+    "internal": true,
+    "channel": "stable",
+    "contexts": ["webui"],
+    "matches": [
+      "chrome://extensions-frame/*"
     ]
-  },
+  }],
   "types": {
     "channel": "stable",
     "extension_types": ["extension", "legacy_packaged_app", "platform_app"],
diff --git a/extensions/common/extension_api.cc b/extensions/common/extension_api.cc
index 3f17488..d10084c 100644
--- a/extensions/common/extension_api.cc
+++ b/extensions/common/extension_api.cc
@@ -303,8 +303,9 @@
              .is_available();
 }
 
-bool ExtensionAPI::IsAvailableToWebUI(const std::string& name) {
-  return IsAvailable(name, NULL, Feature::WEBUI_CONTEXT, GURL()).is_available();
+bool ExtensionAPI::IsAvailableToWebUI(const std::string& name,
+                                      const GURL& url) {
+  return IsAvailable(name, NULL, Feature::WEBUI_CONTEXT, url).is_available();
 }
 
 const base::DictionaryValue* ExtensionAPI::GetSchema(
diff --git a/extensions/common/extension_api.h b/extensions/common/extension_api.h
index 8ce0245..d03c339 100644
--- a/extensions/common/extension_api.h
+++ b/extensions/common/extension_api.h
@@ -109,8 +109,8 @@
   bool IsAvailableInUntrustedContext(const std::string& name,
                                      const Extension* extension);
 
-  // Returns true if |name| is available to webui contexts.
-  bool IsAvailableToWebUI(const std::string& name);
+  // Returns true if |name| is available to WebUI contexts on |url|.
+  bool IsAvailableToWebUI(const std::string& name, const GURL& url);
 
   // Gets the schema for the extension API with namespace |full_name|.
   // Ownership remains with this object.
diff --git a/extensions/common/extension_api_stub.cc b/extensions/common/extension_api_stub.cc
index 439b7e1..4b44f97 100644
--- a/extensions/common/extension_api_stub.cc
+++ b/extensions/common/extension_api_stub.cc
@@ -8,6 +8,7 @@
 #include "extensions/common/extension_api.h"
 
 #include "extensions/common/features/feature.h"
+#include "url/gurl.h"
 
 namespace extensions {
 
@@ -41,7 +42,8 @@
   return false;
 }
 
-bool ExtensionAPI::IsAvailableToWebUI(const std::string& name) {
+bool ExtensionAPI::IsAvailableToWebUI(const std::string& name,
+                                      const GURL& url) {
   return false;
 }
 
diff --git a/extensions/common/extension_messages.h b/extensions/common/extension_messages.h
index e3154ca..5878603 100644
--- a/extensions/common/extension_messages.h
+++ b/extensions/common/extension_messages.h
@@ -507,14 +507,16 @@
                      ExtensionHostMsg_Request_Params)
 
 // Notify the browser that the given extension added a listener to an event.
-IPC_MESSAGE_CONTROL2(ExtensionHostMsg_AddListener,
+IPC_MESSAGE_CONTROL3(ExtensionHostMsg_AddListener,
                      std::string /* extension_id */,
+                     GURL /* listener_url */,
                      std::string /* name */)
 
 // Notify the browser that the given extension removed a listener from an
 // event.
-IPC_MESSAGE_CONTROL2(ExtensionHostMsg_RemoveListener,
+IPC_MESSAGE_CONTROL3(ExtensionHostMsg_RemoveListener,
                      std::string /* extension_id */,
+                     GURL /* listener_url */,
                      std::string /* name */)
 
 // Notify the browser that the given extension added a listener to an event from
diff --git a/extensions/common/features/simple_feature.cc b/extensions/common/features/simple_feature.cc
index bdf8fae..e623bcf 100644
--- a/extensions/common/features/simple_feature.cc
+++ b/extensions/common/features/simple_feature.cc
@@ -291,8 +291,8 @@
                     &component_extensions_auto_granted_);
 
   // NOTE: ideally we'd sanity check that "matches" can be specified if and
-  // only if there's a "web_page" context, but without (Simple)Features being
-  // aware of their own heirarchy this is impossible.
+  // only if there's a "web_page" or "webui" context, but without
+  // (Simple)Features being aware of their own heirarchy this is impossible.
   //
   // For example, we might have feature "foo" available to "web_page" context
   // and "matches" google.com/*. Then a sub-feature "foo.bar" might override
@@ -402,8 +402,13 @@
   if (!contexts_.empty() && contexts_.find(context) == contexts_.end())
     return CreateAvailability(INVALID_CONTEXT, context);
 
-  if (context == WEB_PAGE_CONTEXT && !matches_.MatchesURL(url))
+  // TODO(kalman): Consider checking |matches_| regardless of context type.
+  // Fewer surprises, and if the feature configuration wants to isolate
+  // "matches" from say "blessed_extension" then they can use complex features.
+  if ((context == WEB_PAGE_CONTEXT || context == WEBUI_CONTEXT) &&
+      !matches_.MatchesURL(url)) {
     return CreateAvailability(INVALID_URL, url);
+  }
 
   for (FilterList::const_iterator filter_iter = filters_.begin();
        filter_iter != filters_.end();
@@ -414,6 +419,8 @@
       return availability;
   }
 
+  // TODO(kalman): Assert that if the context was a webpage or WebUI context
+  // then at some point a "matches" restriction was checked.
   return CheckDependencies(base::Bind(
       &IsAvailableToContextForBind, extension, context, url, platform));
 }
diff --git a/extensions/renderer/event_bindings.cc b/extensions/renderer/event_bindings.cc
index 331e6512..948d3ded 100644
--- a/extensions/renderer/event_bindings.cc
+++ b/extensions/renderer/event_bindings.cc
@@ -150,8 +150,8 @@
   std::string extension_id = context()->GetExtensionID();
   EventListenerCounts& listener_counts = g_listener_counts.Get()[extension_id];
   if (++listener_counts[event_name] == 1) {
-    content::RenderThread::Get()->Send(
-        new ExtensionHostMsg_AddListener(extension_id, event_name));
+    content::RenderThread::Get()->Send(new ExtensionHostMsg_AddListener(
+        extension_id, context()->GetURL(), event_name));
   }
 
   // This is called the first time the page has added a listener. Since
@@ -177,8 +177,8 @@
   EventListenerCounts& listener_counts = g_listener_counts.Get()[extension_id];
 
   if (--listener_counts[event_name] == 0) {
-    content::RenderThread::Get()->Send(
-        new ExtensionHostMsg_RemoveListener(extension_id, event_name));
+    content::RenderThread::Get()->Send(new ExtensionHostMsg_RemoveListener(
+        extension_id, context()->GetURL(), event_name));
   }
 
   // DetachEvent is called when the last listener for the context is
diff --git a/extensions/renderer/script_context.cc b/extensions/renderer/script_context.cc
index 97e6c76..0f7a77c5 100644
--- a/extensions/renderer/script_context.cc
+++ b/extensions/renderer/script_context.cc
@@ -169,8 +169,7 @@
   blink::WebDataSource* data_source = frame->provisionalDataSource()
                                           ? frame->provisionalDataSource()
                                           : frame->dataSource();
-  CHECK(data_source);
-  return GURL(data_source->request().url());
+  return data_source ? GURL(data_source->request().url()) : GURL();
 }
 
 // static
diff --git a/extensions/renderer/script_context.h b/extensions/renderer/script_context.h
index 7b19ac1..db7130a2 100644
--- a/extensions/renderer/script_context.h
+++ b/extensions/renderer/script_context.h
@@ -104,6 +104,7 @@
 
   // Utility to get the URL we will match against for a frame. If the frame has
   // committed, this is the commited URL. Otherwise it is the provisional URL.
+  // The returned URL may be invalid.
   static GURL GetDataSourceURLForFrame(const blink::WebFrame* frame);
 
   // Returns the first non-about:-URL in the document hierarchy above and