summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsergeygs@chromium.org <sergeygs@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-09-10 09:11:39 +0000
committersergeygs@chromium.org <sergeygs@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-09-10 09:11:39 +0000
commit66376ed63378466b513cc88eb2a720c16e5d4b28 (patch)
treea6a2ea5a1be4e5d0ca540472fffdb57d9075d3f4
parent2fdd0064738e5ff5de14d562015660372c8f9a58 (diff)
downloadchromium_src-66376ed63378466b513cc88eb2a720c16e5d4b28.zip
chromium_src-66376ed63378466b513cc88eb2a720c16e5d4b28.tar.gz
chromium_src-66376ed63378466b513cc88eb2a720c16e5d4b28.tar.bz2
"Redirecting URLs to Packaged Apps" implementation: revised
Design proposal: https://docs.google.com/document/d/1r-RoOv2URfZBYrT_B6notQ6MeMqZRd1EP1AITuzJCAc/edit?usp=sharing * Support for url_handlers in manifest * New kind of onLaunched event with navigation info in launch data * Intercept/redirect top-level browser-initiated navigations (bookmarks, omnibox, etc.) * Intercept/redirect top-frame navigations in tabs and app windows. This is a rework of https://codereview.chromium.org/22944002/. BUG=111422 Review URL: https://chromiumcodereview.appspot.com/23847004 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@222235 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--apps/launcher.cc10
-rw-r--r--apps/launcher.h21
-rw-r--r--chrome/browser/apps/app_browsertest.cc2
-rw-r--r--chrome/browser/apps/app_browsertest_util.cc13
-rw-r--r--chrome/browser/apps/app_browsertest_util.h4
-rw-r--r--chrome/browser/apps/app_url_redirector.cc98
-rw-r--r--chrome/browser/apps/app_url_redirector.h32
-rw-r--r--chrome/browser/apps/app_url_redirector_browsertest.cc458
-rw-r--r--chrome/browser/extensions/api/app_runtime/app_runtime_api.cc18
-rw-r--r--chrome/browser/extensions/api/app_runtime/app_runtime_api.h18
-rw-r--r--chrome/browser/renderer_host/chrome_resource_dispatcher_host_delegate.cc20
-rw-r--r--chrome/chrome_browser_extensions.gypi2
-rw-r--r--chrome/chrome_common.gypi2
-rw-r--r--chrome/chrome_tests.gypi1
-rw-r--r--chrome/common/extensions/api/_manifest_features.json4
-rw-r--r--chrome/common/extensions/api/app_runtime.idl18
-rw-r--r--chrome/common/extensions/api/url_handlers/url_handlers_parser.cc170
-rw-r--r--chrome/common/extensions/api/url_handlers/url_handlers_parser.h71
-rw-r--r--chrome/common/extensions/chrome_manifest_handlers.cc2
-rw-r--r--chrome/common/extensions/extension_manifest_constants.cc8
-rw-r--r--chrome/common/extensions/extension_manifest_constants.h4
-rw-r--r--chrome/renderer/resources/extensions/app_runtime_custom_bindings.js17
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/common/mismatching_target.html16
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/common/target.html12
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/handlers/navigate_webview_to_url/main.html13
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/handlers/navigate_webview_to_url/manifest.json22
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/handlers/navigate_webview_to_url/test.js30
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/handlers/simple/main.html11
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/handlers/simple/manifest.json19
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/handlers/simple/test.js36
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/handlers/steal_xhr_target/main.html12
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/handlers/steal_xhr_target/manifest.json19
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/handlers/steal_xhr_target/test.js8
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_mismatching_window_open/main.html11
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_mismatching_window_open/main.js15
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_mismatching_window_open/manifest.json9
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_mismatching_window_open/test.js7
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_window_open/main.html11
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_window_open/main.js15
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_window_open/manifest.json9
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_window_open/test.js7
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_blank_link/main.html12
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_blank_link/main.js22
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_blank_link/manifest.json9
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_blank_link/test.js7
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_mismatching_blank_link/main.html12
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_mismatching_blank_link/main.js22
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_mismatching_blank_link/manifest.json9
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_mismatching_blank_link/test.js7
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launching_pages/call_window_open.html11
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launching_pages/click_blank_link.html12
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launching_pages/click_link.html12
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launching_pages/click_mismatching_link.html12
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/launching_pages/navigate.js35
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/xhr_downloader/download.js21
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/xhr_downloader/main.html12
-rw-r--r--chrome/test/data/extensions/platform_apps/url_handlers/xhr_downloader/target.html10
-rw-r--r--extensions/common/manifest_constants.cc2
-rw-r--r--extensions/common/manifest_constants.h2
59 files changed, 1480 insertions, 24 deletions
diff --git a/apps/launcher.cc b/apps/launcher.cc
index 933d37c..aae298f 100644
--- a/apps/launcher.cc
+++ b/apps/launcher.cc
@@ -31,6 +31,7 @@
#include "content/public/browser/web_contents.h"
#include "net/base/mime_util.h"
#include "net/base/net_util.h"
+#include "url/gurl.h"
#if defined(OS_CHROMEOS)
#include "chrome/browser/chromeos/drive/file_errors.h"
@@ -386,4 +387,13 @@ void RestartPlatformApp(Profile* profile, const Extension* extension) {
LaunchPlatformAppWithNoData(profile, extension);
}
+void LaunchPlatformAppWithUrl(Profile* profile,
+ const Extension* extension,
+ const std::string& handler_id,
+ const GURL& url,
+ const GURL& referrer_url) {
+ extensions::AppEventRouter::DispatchOnLaunchedEventWithUrl(
+ profile, extension, handler_id, url, referrer_url);
+}
+
} // namespace apps
diff --git a/apps/launcher.h b/apps/launcher.h
index 9412c11..8da998d 100644
--- a/apps/launcher.h
+++ b/apps/launcher.h
@@ -8,6 +8,7 @@
#include <string>
class CommandLine;
+class GURL;
class Profile;
namespace base {
@@ -29,8 +30,8 @@ void LaunchPlatformAppWithCommandLine(Profile* profile,
const CommandLine* command_line,
const base::FilePath& current_directory);
-// Launches the platform app |extension| with the contents of |file_path|
-// available through the launch data.
+// Launches the platform app |extension| by issuing an onLaunched event
+// with the contents of |file_path| available through the launch data.
void LaunchPlatformAppWithPath(Profile* profile,
const extensions::Extension* extension,
const base::FilePath& file_path);
@@ -39,13 +40,25 @@ void LaunchPlatformAppWithPath(Profile* profile,
void LaunchPlatformApp(Profile* profile,
const extensions::Extension* extension);
-// Launches the platform app |extension| with the contents of |file_path|
-// available through the launch data.
+// Launches the platform app |extension| with |handler_id| and the contents of
+// |file_path| available through the launch data. |handler_id| corresponds to
+// the id of the file_handlers item in the manifest that resulted in a match
+// that triggered this launch.
void LaunchPlatformAppWithFileHandler(Profile* profile,
const extensions::Extension* extension,
const std::string& handler_id,
const base::FilePath& file_path);
+// Launches the platform app |extension| with |handler_id|, |url| and
+// |referrer_url| available through the launch data. |handler_id| corresponds to
+// the id of the file_handlers item in the manifest that resulted in a match
+// that triggered this launch.
+void LaunchPlatformAppWithUrl(Profile* profile,
+ const extensions::Extension* extension,
+ const std::string& handler_id,
+ const GURL& url,
+ const GURL& referrer_url);
+
void RestartPlatformApp(Profile* profile,
const extensions::Extension* extension);
diff --git a/chrome/browser/apps/app_browsertest.cc b/chrome/browser/apps/app_browsertest.cc
index eef7244..70c3db9 100644
--- a/chrome/browser/apps/app_browsertest.cc
+++ b/chrome/browser/apps/app_browsertest.cc
@@ -13,8 +13,6 @@
#include "base/prefs/pref_service.h"
#include "base/stl_util.h"
#include "base/strings/utf_string_conversions.h"
-#include "base/test/test_timeouts.h"
-#include "base/threading/platform_thread.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/apps/app_browsertest_util.h"
#include "chrome/browser/automation/automation_util.h"
diff --git a/chrome/browser/apps/app_browsertest_util.cc b/chrome/browser/apps/app_browsertest_util.cc
index 7a2f9ce..afe32b0 100644
--- a/chrome/browser/apps/app_browsertest_util.cc
+++ b/chrome/browser/apps/app_browsertest_util.cc
@@ -60,15 +60,22 @@ const Extension* PlatformAppBrowserTest::LoadAndLaunchPlatformApp(
return extension;
}
+const Extension* PlatformAppBrowserTest::InstallPlatformApp(
+ const char* name) {
+ const Extension* extension = InstallExtension(
+ test_data_dir_.AppendASCII("platform_apps").AppendASCII(name), 1);
+ EXPECT_TRUE(extension);
+
+ return extension;
+}
+
const Extension* PlatformAppBrowserTest::InstallAndLaunchPlatformApp(
const char* name) {
content::WindowedNotificationObserver app_loaded_observer(
content::NOTIFICATION_LOAD_COMPLETED_MAIN_FRAME,
content::NotificationService::AllSources());
- const Extension* extension = InstallExtension(
- test_data_dir_.AppendASCII("platform_apps").AppendASCII(name), 1);
- EXPECT_TRUE(extension);
+ const Extension* extension = InstallPlatformApp(name);
chrome::OpenApplication(chrome::AppLaunchParams(browser()->profile(),
extension,
diff --git a/chrome/browser/apps/app_browsertest_util.h b/chrome/browser/apps/app_browsertest_util.h
index aa4efcd1..2b9d958 100644
--- a/chrome/browser/apps/app_browsertest_util.h
+++ b/chrome/browser/apps/app_browsertest_util.h
@@ -8,6 +8,7 @@
#include "apps/shell_window.h"
#include "chrome/browser/extensions/extension_apitest.h"
+#include "content/public/common/page_transition_types.h"
namespace content {
class WebContents;
@@ -29,6 +30,9 @@ class PlatformAppBrowserTest : public ExtensionApiTest {
// until it is launched.
const Extension* LoadAndLaunchPlatformApp(const char* name);
+ // Installs the app named |name| out of the platform_apps subdirectory.
+ const Extension* InstallPlatformApp(const char* name);
+
// Installs and runs the app named |name| out of the platform_apps
// subdirectory. Waits until it is launched.
const Extension* InstallAndLaunchPlatformApp(const char* name);
diff --git a/chrome/browser/apps/app_url_redirector.cc b/chrome/browser/apps/app_url_redirector.cc
new file mode 100644
index 0000000..d973fdd
--- /dev/null
+++ b/chrome/browser/apps/app_url_redirector.cc
@@ -0,0 +1,98 @@
+// Copyright 2013 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/apps/app_url_redirector.h"
+
+#include "apps/launcher.h"
+#include "base/bind.h"
+#include "base/logging.h"
+#include "chrome/browser/extensions/extension_info_map.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_io_data.h"
+#include "chrome/common/extensions/api/url_handlers/url_handlers_parser.h"
+#include "chrome/common/extensions/extension.h"
+#include "chrome/common/extensions/extension_messages.h"
+#include "chrome/common/extensions/extension_set.h"
+#include "components/navigation_interception/intercept_navigation_resource_throttle.h"
+#include "components/navigation_interception/navigation_params.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/render_view_host.h"
+#include "content/public/browser/resource_throttle.h"
+#include "content/public/browser/web_contents.h"
+#include "net/url_request/url_request.h"
+
+using content::BrowserThread;
+using content::WebContents;
+using extensions::Extension;
+using extensions::UrlHandlers;
+using extensions::UrlHandlerInfo;
+
+namespace {
+
+bool LaunchAppWithUrl(
+ const scoped_refptr<const Extension> app,
+ const std::string& handler_id,
+ content::RenderViewHost* source,
+ const navigation_interception::NavigationParams& params) {
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+
+ // Redirect top-level navigations only. This excludes iframes and webviews
+ // in particular.
+ if (source->IsSubframe())
+ return false;
+
+ // These are guaranteed by CreateThrottleFor below.
+ DCHECK(!params.is_post());
+ DCHECK(UrlHandlers::CanExtensionHandleUrl(app, params.url()));
+
+ WebContents* web_contents = WebContents::FromRenderViewHost(source);
+ if (!web_contents)
+ return false;
+
+ Profile* profile =
+ Profile::FromBrowserContext(web_contents->GetBrowserContext());
+
+ apps::LaunchPlatformAppWithUrl(
+ profile, app, handler_id, params.url(), params.referrer().url);
+
+ return true;
+}
+
+} // namespace
+
+// static
+content::ResourceThrottle*
+AppUrlRedirector::MaybeCreateThrottleFor(net::URLRequest* request,
+ ProfileIOData* profile_io_data) {
+ // Support only GET for now.
+ if (request->method() != "GET")
+ return NULL;
+
+ if (!request->url().SchemeIsHTTPOrHTTPS())
+ return NULL;
+
+ // Never redirect URLs to apps in incognito. Technically, apps are not
+ // supported in incognito, but that may change in future.
+ // See crbug.com/240879, which tracks incognito support for v2 apps.
+ if (profile_io_data->is_incognito())
+ return NULL;
+
+ const ExtensionSet& extensions =
+ profile_io_data->GetExtensionInfoMap()->extensions();
+ for (ExtensionSet::const_iterator iter = extensions.begin();
+ iter != extensions.end();
+ ++iter) {
+ const UrlHandlerInfo* handler =
+ UrlHandlers::FindMatchingUrlHandler(*iter, request->url());
+ if (handler) {
+ return new navigation_interception::InterceptNavigationResourceThrottle(
+ request,
+ base::Bind(&LaunchAppWithUrl,
+ scoped_refptr<const Extension>(*iter),
+ handler->id));
+ }
+ }
+
+ return NULL;
+}
diff --git a/chrome/browser/apps/app_url_redirector.h b/chrome/browser/apps/app_url_redirector.h
new file mode 100644
index 0000000..331d7b4
--- /dev/null
+++ b/chrome/browser/apps/app_url_redirector.h
@@ -0,0 +1,32 @@
+// Copyright 2013 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_APPS_APP_URL_REDIRECTOR_H_
+#define CHROME_BROWSER_APPS_APP_URL_REDIRECTOR_H_
+
+#include "base/basictypes.h"
+
+namespace content {
+class ResourceThrottle;
+}
+
+namespace net {
+class URLRequest;
+}
+
+class ProfileIOData;
+
+// This class creates resource throttles that redirect URLs to apps that
+// have a matching URL handler in the 'url_handlers' manifest key.
+class AppUrlRedirector {
+ public:
+ static content::ResourceThrottle* MaybeCreateThrottleFor(
+ net::URLRequest* request,
+ ProfileIOData* profile_io_data);
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(AppUrlRedirector);
+};
+
+#endif // CHROME_BROWSER_APPS_APP_URL_REDIRECTOR_H_
diff --git a/chrome/browser/apps/app_url_redirector_browsertest.cc b/chrome/browser/apps/app_url_redirector_browsertest.cc
new file mode 100644
index 0000000..efa10d0
--- /dev/null
+++ b/chrome/browser/apps/app_url_redirector_browsertest.cc
@@ -0,0 +1,458 @@
+// Copyright 2013 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 "base/strings/stringprintf.h"
+#include "base/strings/utf_string_conversions.h"
+#include "chrome/browser/apps/app_browsertest_util.h"
+#include "chrome/browser/chrome_notification_types.h"
+#include "chrome/browser/extensions/extension_test_message_listener.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "content/public/common/page_transition_types.h"
+#include "content/public/test/browser_test_base.h"
+#include "content/public/test/browser_test_utils.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+
+namespace extensions {
+
+class PlatformAppUrlRedirectorBrowserTest : public PlatformAppBrowserTest {
+ public:
+ virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE;
+
+ protected:
+ // Performs the following sequence:
+ // - installs the app |handler| (a relative path under the platform_apps
+ // subdirectory);
+ // - navigates the current tab to the HTML page |lancher_page| (ditto);
+ // - then waits for |handler| to launch and send back a |handler_ack_message|;
+ // - finally checks that the resulting shell window count is as expected.
+ // The |launcher_page| is supposed to trigger a navigation matching one of the
+ // url_handlers in the |handler|'s manifest, and thereby launch the |handler|.
+ void TestNavigationInTab(const char* launcher_page,
+ const char* handler,
+ const char* handler_start_message);
+
+ // The same as above, but does not expect the |handler| to launch. Verifies
+ // that it didn't, and that the navigation has opened in a new tab instead.
+ void TestMismatchingNavigationInTab(const char* launcher_page,
+ const char* success_tab_title,
+ const char* handler);
+
+ // - installs the app |handler|;
+ // - opens the page |xhr_opening_page| in the current tab;
+ // - the page sends an XHR to a local resouce, whose URL matches one of the
+ // url_handlers in |handler|;
+ // - waits until |xhr_opening_page| gets a response back and changes the tab's
+ // title to a value indicating success/failure of the XHR;
+ // - verifies that no shell windows have been opened, i.e. |handler| wasn't
+ // launched even though its url_handlers match the URL.
+ void TestNegativeXhrInTab(const char* xhr_opening_page,
+ const char* success_tab_title,
+ const char* failure_tab_title,
+ const char* handler);
+
+ // Performs the following sequence:
+ // - installs the app |handler| (a relative path under the platform_apps
+ // subdirectory);
+ // - loads and launches the app |launcher| (ditto);
+ // - waits for the |launcher| to launch and send back a |launcher_ack_message|
+ // (to make sure it's not the failing entity, if the test fails overall);
+ // - waits for the |handler| to launch and send back a |handler_ack_message|;
+ // - finally checks that the resulting shell window count is as expected.
+ // The |launcher| is supposed to trigger a navigation matching one of the
+ // url_handlers in the |handler|'s manifest, and thereby launch the |handler|.
+ void TestNavigationInApp(const char* launcher,
+ const char* launcher_done_message,
+ const char* handler,
+ const char* handler_start_message);
+
+ // The same as above, but does not expect the |handler| to launch. Verifies
+ // that it didn't, and that the navigation has opened in a new tab instead.
+ void TestMismatchingNavigationInApp(const char* launcher,
+ const char* launcher_done_message,
+ const char* handler);
+
+ // - installs the |handler| app;
+ // - loads and launches the |launcher| app;
+ // - waits until the |launcher| sends back a |launcher_done_message|;
+ // - the launcher performs a navigation to a URL that mismatches the
+ // |handler|'s url_handlers;
+ // - verifies that the |handler| hasn't been launched as a result of the
+ // navigation.
+ void TestNegativeNavigationInApp(const char* launcher,
+ const char* launcher_done_message,
+ const char* handler);
+
+ // - installs the app |handler|;
+ // - navigates the current tab to the HTML page |matching_target_page| with
+ // page transition |transition|;
+ // - waits for |handler| to launch and send back a |handler_start_message|;
+ // - finally checks that the resulting shell window count is as expected.
+ void TestNavigationInBrowser(const char* matching_target_page,
+ content::PageTransition transition,
+ const char* handler,
+ const char* handler_start_message);
+
+ // Same as above, but does not expect |handler| to launch. This is used, e.g.
+ // for form submissions, where the URL would normally match the url_handlers
+ // but should not launch it.
+ void TestNegativeNavigationInBrowser(const char* matching_target_page,
+ content::PageTransition transition,
+ const char* handler);
+
+ // Same as above, but expects the |mismatching_target_page| should not match
+ // any of the |handler|'s url_handlers, and therefor not launch the app.
+ void TestMismatchingNavigationInBrowser(const char* mismatching_target_page,
+ content::PageTransition transition,
+ const char* handler);
+};
+
+
+void PlatformAppUrlRedirectorBrowserTest::SetUpCommandLine(
+ CommandLine* command_line) {
+ PlatformAppBrowserTest::SetUpCommandLine(command_line);
+ command_line->AppendSwitch(::switches::kDisablePopupBlocking);
+}
+
+// TODO(sergeygs): Factor out common functionality from TestXyz,
+// TestNegativeXyz, and TestMismatchingXyz versions.
+
+// TODO(sergeys): Return testing::AssertionErrors from these methods to
+// preserve line numbers and (if applicable) failure messages.
+
+void PlatformAppUrlRedirectorBrowserTest::TestNavigationInTab(
+ const char* launcher_page,
+ const char* handler,
+ const char* handler_start_message) {
+ ASSERT_TRUE(StartEmbeddedTestServer());
+
+ InstallPlatformApp(handler);
+
+ ExtensionTestMessageListener handler_listener(handler_start_message, false);
+
+ ui_test_utils::NavigateToURLWithDisposition(
+ browser(),
+ embedded_test_server()->GetURL(base::StringPrintf(
+ "/extensions/platform_apps/%s", launcher_page)),
+ CURRENT_TAB,
+ ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION);
+
+ ASSERT_TRUE(handler_listener.WaitUntilSatisfied());
+
+ ASSERT_EQ(1U, GetShellWindowCount());
+}
+
+void PlatformAppUrlRedirectorBrowserTest::TestMismatchingNavigationInTab(
+ const char* launcher_page,
+ const char* success_tab_title,
+ const char* handler) {
+ ASSERT_TRUE(StartEmbeddedTestServer());
+
+ InstallPlatformApp(handler);
+
+ const string16 success_title = ASCIIToUTF16(success_tab_title);
+ content::WebContents* tab =
+ browser()->tab_strip_model()->GetActiveWebContents();
+ content::TitleWatcher title_watcher(tab, success_title);
+
+ ui_test_utils::NavigateToURLWithDisposition(
+ browser(),
+ embedded_test_server()->GetURL(base::StringPrintf(
+ "/extensions/platform_apps/%s", launcher_page)),
+ CURRENT_TAB,
+ ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION);
+
+ ASSERT_EQ(success_title, title_watcher.WaitAndGetTitle());
+ ASSERT_EQ(1, browser()->tab_strip_model()->count());
+ ASSERT_EQ(0U, GetShellWindowCount());
+}
+
+void PlatformAppUrlRedirectorBrowserTest::TestNegativeXhrInTab(
+ const char* launcher_page,
+ const char* success_tab_title,
+ const char* failure_tab_title,
+ const char* handler) {
+ ASSERT_TRUE(StartEmbeddedTestServer());
+
+ InstallPlatformApp(handler);
+
+ const string16 success_title = ASCIIToUTF16(success_tab_title);
+ const string16 failure_title = ASCIIToUTF16(failure_tab_title);
+ content::WebContents* tab =
+ browser()->tab_strip_model()->GetActiveWebContents();
+ content::TitleWatcher title_watcher(tab, success_title);
+ title_watcher.AlsoWaitForTitle(failure_title);
+
+ ui_test_utils::NavigateToURLWithDisposition(
+ browser(),
+ embedded_test_server()->GetURL(base::StringPrintf(
+ "/extensions/platform_apps/%s", launcher_page)),
+ CURRENT_TAB,
+ ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION);
+
+ ASSERT_EQ(success_title, title_watcher.WaitAndGetTitle());
+ ASSERT_EQ(1, browser()->tab_strip_model()->count());
+ ASSERT_EQ(0U, GetShellWindowCount());
+}
+
+void PlatformAppUrlRedirectorBrowserTest::TestNavigationInApp(
+ const char* launcher,
+ const char* launcher_done_message,
+ const char* handler,
+ const char* handler_start_message) {
+ ASSERT_TRUE(StartEmbeddedTestServer());
+
+ InstallPlatformApp(handler);
+
+ ExtensionTestMessageListener launcher_listener(launcher_done_message, false);
+ ExtensionTestMessageListener handler_listener(handler_start_message, false);
+
+ LoadAndLaunchPlatformApp(launcher);
+
+ ASSERT_TRUE(launcher_listener.WaitUntilSatisfied());
+ ASSERT_TRUE(handler_listener.WaitUntilSatisfied());
+
+ ASSERT_EQ(2U, GetShellWindowCount());
+}
+
+void PlatformAppUrlRedirectorBrowserTest::TestNegativeNavigationInApp(
+ const char* launcher,
+ const char* launcher_done_message,
+ const char* handler) {
+ ASSERT_TRUE(StartEmbeddedTestServer());
+
+ InstallPlatformApp(handler);
+
+ content::WindowedNotificationObserver observer(
+ chrome::NOTIFICATION_TAB_ADDED,
+ content::Source<content::WebContentsDelegate>(browser()));
+
+ ExtensionTestMessageListener launcher_done_listener(launcher_done_message,
+ false);
+ LoadAndLaunchPlatformApp(launcher);
+
+ ASSERT_TRUE(launcher_done_listener.WaitUntilSatisfied());
+
+ observer.Wait();
+
+ ASSERT_EQ(1U, GetShellWindowCount());
+}
+
+void PlatformAppUrlRedirectorBrowserTest::TestMismatchingNavigationInApp(
+ const char* launcher,
+ const char* launcher_done_message,
+ const char* handler) {
+ ASSERT_TRUE(StartEmbeddedTestServer());
+
+ InstallPlatformApp(handler);
+
+ content::WindowedNotificationObserver observer(
+ chrome::NOTIFICATION_TAB_ADDED,
+ content::Source<content::WebContentsDelegate>(browser()));
+
+ ExtensionTestMessageListener launcher_listener(launcher_done_message, false);
+ LoadAndLaunchPlatformApp(launcher);
+
+ ASSERT_TRUE(launcher_listener.WaitUntilSatisfied());
+
+ observer.Wait();
+ ASSERT_EQ(1U, GetShellWindowCount());
+ ASSERT_EQ(2, browser()->tab_strip_model()->count());
+}
+
+void PlatformAppUrlRedirectorBrowserTest::TestNavigationInBrowser(
+ const char* matching_target_page,
+ content::PageTransition transition,
+ const char* handler,
+ const char* handler_start_message) {
+ ASSERT_TRUE(StartEmbeddedTestServer());
+
+ InstallPlatformApp(handler);
+
+ ExtensionTestMessageListener handler_listener(handler_start_message, false);
+
+ chrome::NavigateParams params(
+ browser(),
+ embedded_test_server()->GetURL(base::StringPrintf(
+ "/extensions/platform_apps/%s", matching_target_page)),
+ transition);
+ ui_test_utils::NavigateToURL(&params);
+
+ ASSERT_TRUE(handler_listener.WaitUntilSatisfied());
+
+ ASSERT_EQ(1U, GetShellWindowCount());
+}
+
+void PlatformAppUrlRedirectorBrowserTest::TestNegativeNavigationInBrowser(
+ const char* matching_target_page,
+ content::PageTransition transition,
+ const char* handler) {
+ ASSERT_TRUE(StartEmbeddedTestServer());
+
+ InstallPlatformApp(handler);
+
+ chrome::NavigateParams params(
+ browser(),
+ embedded_test_server()->GetURL(base::StringPrintf(
+ "/extensions/platform_apps/%s", matching_target_page)),
+ transition);
+ ui_test_utils::NavigateToURL(&params);
+
+ ASSERT_EQ(0U, GetShellWindowCount());
+}
+
+void PlatformAppUrlRedirectorBrowserTest::TestMismatchingNavigationInBrowser(
+ const char* mismatching_target_page,
+ content::PageTransition transition,
+ const char* handler) {
+ TestNegativeNavigationInBrowser(mismatching_target_page, transition, handler);
+}
+
+// Test that a click on a regular link in a tab launches an app that has
+// matching url_handlers.
+IN_PROC_BROWSER_TEST_F(PlatformAppUrlRedirectorBrowserTest,
+ ClickInTabIntercepted) {
+ TestNavigationInTab(
+ "url_handlers/launching_pages/click_link.html",
+ "url_handlers/handlers/simple",
+ "Handler launched");
+}
+
+// Test that a click on a target='_blank' link in a tab launches an app that has
+// matching url_handlers.
+IN_PROC_BROWSER_TEST_F(PlatformAppUrlRedirectorBrowserTest,
+ BlankClickInTabIntercepted) {
+ TestNavigationInTab(
+ "url_handlers/launching_pages/click_blank_link.html",
+ "url_handlers/handlers/simple",
+ "Handler launched");
+}
+
+// Test that a call to window.open() in a tab launches an app that has
+// matching url_handlers.
+IN_PROC_BROWSER_TEST_F(PlatformAppUrlRedirectorBrowserTest,
+ WindowOpenInTabIntercepted) {
+ TestNavigationInTab(
+ "url_handlers/launching_pages/call_window_open.html",
+ "url_handlers/handlers/simple",
+ "Handler launched");
+}
+
+// Test that a click on a regular link in a tab launches an app that has
+// matching url_handlers.
+IN_PROC_BROWSER_TEST_F(PlatformAppUrlRedirectorBrowserTest,
+ MismatchingClickInTabNotIntercepted) {
+ TestMismatchingNavigationInTab(
+ "url_handlers/launching_pages/click_mismatching_link.html",
+ "Mismatching link target loaded",
+ "url_handlers/handlers/simple");
+}
+
+// Test that a click on target='_blank' link in an app's window launches
+// another app that has matching url_handlers.
+IN_PROC_BROWSER_TEST_F(PlatformAppUrlRedirectorBrowserTest,
+ BlankClickInAppIntercepted) {
+ TestNavigationInApp(
+ "url_handlers/launchers/click_blank_link",
+ "Launcher done",
+ "url_handlers/handlers/simple",
+ "Handler launched");
+}
+
+// Test that a call to window.open() in the app's foreground page launches
+// another app that has matching url_handlers.
+IN_PROC_BROWSER_TEST_F(PlatformAppUrlRedirectorBrowserTest,
+ WindowOpenInAppIntercepted) {
+ TestNavigationInApp(
+ "url_handlers/launchers/call_window_open",
+ "Launcher done",
+ "url_handlers/handlers/simple",
+ "Handler launched");
+}
+
+// Test that an app with url_handlers does not intercept a mismatching
+// click on a target='_blank' link in another app's window.
+IN_PROC_BROWSER_TEST_F(PlatformAppUrlRedirectorBrowserTest,
+ MismatchingWindowOpenInAppNotIntercepted) {
+ TestMismatchingNavigationInApp(
+ "url_handlers/launchers/call_mismatching_window_open",
+ "Launcher done",
+ "url_handlers/handlers/simple");
+}
+
+// Test that a webview in an app can be navigated to a URL without interception
+// even when there are other (or the same) apps that have matching url_handlers.
+IN_PROC_BROWSER_TEST_F(PlatformAppUrlRedirectorBrowserTest,
+ WebviewNavigationNotIntercepted) {
+ // The launcher clicks on a link, which gets intercepted and launches the
+ // handler. The handler also redirects an embedded webview to the URL. The
+ // webview should just navigate without creating an endless loop of
+ // navigate-intercept-launch sequences with multiplying handler's windows.
+ // There should be 2 windows only: launcher's and handler's.
+ TestNavigationInApp(
+ "url_handlers/launchers/click_blank_link",
+ "Launcher done",
+ "url_handlers/handlers/navigate_webview_to_url",
+ "Handler launched");
+}
+
+// Test that a webview in an app can be navigated to a URL without interception
+// even when there are other (or the same) apps that have matching url_handlers.
+IN_PROC_BROWSER_TEST_F(PlatformAppUrlRedirectorBrowserTest,
+ MismatchingBlankClickInAppNotIntercepted) {
+ // The launcher clicks on a link, which gets intercepted and launches the
+ // handler. The handler also redirects an embedded webview to the URL. The
+ // webview should just navigate without creating an endless loop of
+ // navigate-intercept-launch sequences with multiplying handler's windows.
+ // There should be 2 windows only: launcher's and handler's.
+ TestMismatchingNavigationInApp(
+ "url_handlers/launchers/click_mismatching_blank_link",
+ "Launcher done",
+ "url_handlers/handlers/simple");
+}
+
+// Test that a URL entry in the omnibar launches an app that has matching
+// url_handlers.
+IN_PROC_BROWSER_TEST_F(PlatformAppUrlRedirectorBrowserTest,
+ EntryInOmnibarIntercepted) {
+ TestNavigationInBrowser(
+ "url_handlers/common/target.html",
+ content::PAGE_TRANSITION_TYPED,
+ "url_handlers/handlers/simple",
+ "Handler launched");
+}
+
+// Test that an app with url_handlers does not intercept a mismatching
+// URL entry in the omnibar.
+IN_PROC_BROWSER_TEST_F(PlatformAppUrlRedirectorBrowserTest,
+ MismatchingEntryInOmnibarNotIntercepted) {
+ TestMismatchingNavigationInBrowser(
+ "url_handlers/common/mismatching_target.html",
+ content::PAGE_TRANSITION_TYPED,
+ "url_handlers/handlers/simple");
+}
+
+// Test that a form submission in a page is never subject to interception
+// by apps even with matching url_handlers.
+IN_PROC_BROWSER_TEST_F(PlatformAppUrlRedirectorBrowserTest,
+ FormSubmissionInTabNotIntercepted) {
+ TestNegativeNavigationInBrowser(
+ "url_handlers/common/target.html",
+ content::PAGE_TRANSITION_FORM_SUBMIT,
+ "url_handlers/handlers/simple");
+}
+
+// Test that a form submission in a page is never subject to interception
+// by apps even with matching url_handlers.
+IN_PROC_BROWSER_TEST_F(PlatformAppUrlRedirectorBrowserTest,
+ XhrInTabNotIntercepted) {
+ TestNegativeXhrInTab(
+ "url_handlers/xhr_downloader/main.html",
+ "XHR succeeded",
+ "XHR failed",
+ "url_handlers/handlers/steal_xhr_target");
+}
+
+} // namespace extensions
diff --git a/chrome/browser/extensions/api/app_runtime/app_runtime_api.cc b/chrome/browser/extensions/api/app_runtime/app_runtime_api.cc
index b976dc7..f48164d 100644
--- a/chrome/browser/extensions/api/app_runtime/app_runtime_api.cc
+++ b/chrome/browser/extensions/api/app_runtime/app_runtime_api.cc
@@ -61,6 +61,8 @@ void AppEventRouter::DispatchOnLaunchedEventWithFileEntry(
Profile* profile, const Extension* extension,
const std::string& handler_id, const std::string& mime_type,
const extensions::app_file_handler_util::GrantedFileEntry& file_entry) {
+ // TODO(sergeygs): Use the same way of creating an event (using the generated
+ // boilerplate) as below in DispatchOnLaunchedEventWithUrl.
scoped_ptr<base::ListValue> args(new base::ListValue());
base::DictionaryValue* launch_data = new base::DictionaryValue();
launch_data->SetString("id", handler_id);
@@ -76,4 +78,20 @@ void AppEventRouter::DispatchOnLaunchedEventWithFileEntry(
DispatchOnLaunchedEventImpl(extension->id(), args.Pass(), profile);
}
+// static.
+void AppEventRouter::DispatchOnLaunchedEventWithUrl(
+ Profile* profile,
+ const Extension* extension,
+ const std::string& handler_id,
+ const GURL& url,
+ const GURL& referrer_url) {
+ api::app_runtime::LaunchData launch_data;
+ launch_data.id.reset(new std::string(handler_id));
+ launch_data.url.reset(new std::string(url.spec()));
+ launch_data.referrer_url.reset(new std::string(referrer_url.spec()));
+ scoped_ptr<ListValue> args(new ListValue());
+ args->Append(launch_data.ToValue().release());
+ DispatchOnLaunchedEventImpl(extension->id(), args.Pass(), profile);
+}
+
} // namespace extensions
diff --git a/chrome/browser/extensions/api/app_runtime/app_runtime_api.h b/chrome/browser/extensions/api/app_runtime/app_runtime_api.h
index 220f0ca..c06a51b 100644
--- a/chrome/browser/extensions/api/app_runtime/app_runtime_api.h
+++ b/chrome/browser/extensions/api/app_runtime/app_runtime_api.h
@@ -28,6 +28,9 @@ class AppEventRouter {
// data.
static void DispatchOnLaunchedEvent(Profile* profile,
const Extension* extension);
+
+ // Dispatches the onRestarted event to the given app, providing a list of
+ // restored file entries from the previous run.
static void DispatchOnRestartedEvent(Profile* profile,
const Extension* extension);
@@ -43,15 +46,24 @@ class AppEventRouter {
// }
// }
- // launchData.intent.data and launchData.intent.postResults are created in a
- // custom dispatch event in javascript. The FileEntry is created from
- // |file_system_id| and |base_name|.
+ // The FileEntry is created from |file_system_id| and |base_name|.
+ // |handler_id| corresponds to the id of the file_handlers item in the
+ // manifest that resulted in a match which triggered this launch.
static void DispatchOnLaunchedEventWithFileEntry(
Profile* profile,
const Extension* extension,
const std::string& handler_id,
const std::string& mime_type,
const extensions::app_file_handler_util::GrantedFileEntry& file_entry);
+
+ // |handler_id| corresponds to the id of the url_handlers item
+ // in the manifest that resulted in a match which triggered this launch.
+ static void DispatchOnLaunchedEventWithUrl(
+ Profile* profile,
+ const Extension* extension,
+ const std::string& handler_id,
+ const GURL& url,
+ const GURL& referrer_url);
};
} // namespace extensions
diff --git a/chrome/browser/renderer_host/chrome_resource_dispatcher_host_delegate.cc b/chrome/browser/renderer_host/chrome_resource_dispatcher_host_delegate.cc
index 4e75cc9..063460e 100644
--- a/chrome/browser/renderer_host/chrome_resource_dispatcher_host_delegate.cc
+++ b/chrome/browser/renderer_host/chrome_resource_dispatcher_host_delegate.cc
@@ -65,6 +65,8 @@
#if defined(OS_ANDROID)
#include "chrome/browser/android/intercept_download_resource_throttle.h"
#include "components/navigation_interception/intercept_navigation_delegate.h"
+#else
+#include "chrome/browser/apps/app_url_redirector.h"
#endif
#if defined(OS_CHROMEOS)
@@ -263,12 +265,23 @@ void ChromeResourceDispatcherHostDelegate::RequestBeginning(
request->SetPriority(net::IDLE);
}
-#if defined(OS_ANDROID)
+ ProfileIOData* io_data = ProfileIOData::FromResourceContext(
+ resource_context);
+
if (!is_prerendering && resource_type == ResourceType::MAIN_FRAME) {
+#if defined(OS_ANDROID)
throttles->push_back(
InterceptNavigationDelegate::CreateThrottleFor(request));
- }
+#else
+ // Redirect some navigations to apps that have registered matching URL
+ // handlers ('url_handlers' in the manifest).
+ content::ResourceThrottle* url_to_app_throttle =
+ AppUrlRedirector::MaybeCreateThrottleFor(request, io_data);
+ if (url_to_app_throttle)
+ throttles->push_back(url_to_app_throttle);
#endif
+ }
+
#if defined(OS_CHROMEOS)
if (resource_type == ResourceType::MAIN_FRAME) {
// We check offline first, then check safe browsing so that we still can
@@ -289,8 +302,6 @@ void ChromeResourceDispatcherHostDelegate::RequestBeginning(
if (!request->is_pending()) {
net::HttpRequestHeaders headers;
headers.CopyFrom(request->extra_request_headers());
- ProfileIOData* io_data = ProfileIOData::FromResourceContext(
- resource_context);
bool incognito = io_data->is_incognito();
chrome_variations::VariationsHttpHeaderProvider::GetInstance()->
AppendHeaders(request->url(),
@@ -311,7 +322,6 @@ void ChromeResourceDispatcherHostDelegate::RequestBeginning(
resource_type,
throttles);
- ProfileIOData* io_data = ProfileIOData::FromResourceContext(resource_context);
if (io_data->resource_prefetch_predictor_observer()) {
io_data->resource_prefetch_predictor_observer()->OnRequestStarted(
request, resource_type, child_id, route_id);
diff --git a/chrome/chrome_browser_extensions.gypi b/chrome/chrome_browser_extensions.gypi
index 4f86fca..a5f65e9 100644
--- a/chrome/chrome_browser_extensions.gypi
+++ b/chrome/chrome_browser_extensions.gypi
@@ -64,6 +64,8 @@
'../extensions/browser/pref_names.h',
'../extensions/browser/view_type_utils.cc',
'../extensions/browser/view_type_utils.h',
+ 'browser/apps/app_url_redirector.cc',
+ 'browser/apps/app_url_redirector.h',
'browser/apps/shortcut_manager.cc',
'browser/apps/shortcut_manager.h',
'browser/apps/shortcut_manager_factory.cc',
diff --git a/chrome/chrome_common.gypi b/chrome/chrome_common.gypi
index c50836c..a7a9ae1 100644
--- a/chrome/chrome_common.gypi
+++ b/chrome/chrome_common.gypi
@@ -219,6 +219,8 @@
'common/extensions/api/storage/storage_schema_manifest_handler.h',
'common/extensions/api/system_indicator/system_indicator_handler.cc',
'common/extensions/api/system_indicator/system_indicator_handler.h',
+ 'common/extensions/api/url_handlers/url_handlers_parser.cc',
+ 'common/extensions/api/url_handlers/url_handlers_parser.h',
'common/extensions/background_info.cc',
'common/extensions/background_info.h',
'common/extensions/chrome_extensions_client.cc',
diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi
index c2d0ff6..0b6009e 100644
--- a/chrome/chrome_tests.gypi
+++ b/chrome/chrome_tests.gypi
@@ -1195,6 +1195,7 @@
'browser/apps/app_browsertest.cc',
'browser/apps/app_browsertest_util.cc',
'browser/apps/app_browsertest_util.h',
+ 'browser/apps/app_url_redirector_browsertest.cc',
'browser/apps/web_view_browsertest.cc',
'browser/apps/window_controls_browsertest.cc',
'browser/autocomplete/autocomplete_browsertest.cc',
diff --git a/chrome/common/extensions/api/_manifest_features.json b/chrome/common/extensions/api/_manifest_features.json
index 4565899..81a8427 100644
--- a/chrome/common/extensions/api/_manifest_features.json
+++ b/chrome/common/extensions/api/_manifest_features.json
@@ -361,6 +361,10 @@
"channel": "stable",
"extension_types": "all"
},
+ "url_handlers": {
+ "channel": "dev",
+ "extension_types": ["platform_app"]
+ },
"version": {
"channel": "stable",
"extension_types": "all"
diff --git a/chrome/common/extensions/api/app_runtime.idl b/chrome/common/extensions/api/app_runtime.idl
index da8ddb8..51b3862 100644
--- a/chrome/common/extensions/api/app_runtime.idl
+++ b/chrome/common/extensions/api/app_runtime.idl
@@ -15,12 +15,26 @@ namespace app.runtime {
DOMString type;
};
- // Optional data for the launch.
+ // Optional data for the launch. Either <code>items</code>, or
+ // the pair (<code>url, referrerUrl</code>) can be present for any given
+ // launch.
[inline_doc] dictionary LaunchData {
- // The id of the file handler that the app is being invoked with.
+ // The ID of the file or URL handler that the app is being invoked with.
+ // Handler IDs are the top-level keys in the <code>file_handlers</code>
+ // and/or <code>url_handlers</code> dictionaries in the manifest.
DOMString? id;
+ // The file entries for the <code>onLaunched</code> event triggered by a
+ // matching file handler in the <code>file_handlers</code> manifest key.
LaunchItem[]? items;
+
+ // The URL for the <code>onLaunched</code> event triggered by a matching
+ // URL handler in the <code>url_handlers</code> manifest key.
+ DOMString? url;
+
+ // The referrer URL for the <code>onLaunched</code> event triggered by a
+ // matching URL handler in the <code>url_handlers</code> manifest key.
+ DOMString? referrerUrl;
};
interface Events {
diff --git a/chrome/common/extensions/api/url_handlers/url_handlers_parser.cc b/chrome/common/extensions/api/url_handlers/url_handlers_parser.cc
new file mode 100644
index 0000000..68bd302
--- /dev/null
+++ b/chrome/common/extensions/api/url_handlers/url_handlers_parser.cc
@@ -0,0 +1,170 @@
+// Copyright 2013 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/common/extensions/api/url_handlers/url_handlers_parser.h"
+
+#include "base/command_line.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/values.h"
+#include "chrome/common/extensions/extension_manifest_constants.h"
+#include "chrome/common/extensions/manifest_handlers/offline_enabled_info.h"
+#include "extensions/common/error_utils.h"
+#include "extensions/common/switches.h"
+#include "net/base/network_change_notifier.h"
+#include "url/gurl.h"
+
+using base::ASCIIToUTF16;
+using net::NetworkChangeNotifier;
+
+namespace mkeys = extensions::manifest_keys;
+namespace merrors = extension_manifest_errors;
+
+// TODO(sergeygs): Use the same strategy that externally_connectable does for
+// parsing the manifest: declare a schema for the manifest entry in
+// manifest_types.json, then use it here.
+//
+// See:
+// chrome/common/extensions/api/manifest_types.json
+// chrome/common/extensions/manifest_handlers/externally_connectable.*
+//
+// Do the same in (at least) file_handlers_parser.cc as well.
+
+namespace extensions {
+
+UrlHandlerInfo::UrlHandlerInfo() {
+}
+
+UrlHandlerInfo::~UrlHandlerInfo() {
+}
+
+UrlHandlers::UrlHandlers() {
+}
+
+UrlHandlers::~UrlHandlers() {
+}
+
+// static
+const std::vector<UrlHandlerInfo>* UrlHandlers::GetUrlHandlers(
+ const Extension* extension) {
+ UrlHandlers* info = static_cast<UrlHandlers*>(
+ extension->GetManifestData(mkeys::kUrlHandlers));
+ return info ? &info->handlers : NULL;
+}
+
+// static
+bool UrlHandlers::CanExtensionHandleUrl(
+ const Extension* extension,
+ const GURL& url) {
+ return FindMatchingUrlHandler(extension, url) != NULL;
+}
+
+// static
+const UrlHandlerInfo* UrlHandlers::FindMatchingUrlHandler(
+ const Extension* extension,
+ const GURL& url) {
+ const std::vector<UrlHandlerInfo>* handlers = GetUrlHandlers(extension);
+ if (!handlers)
+ return NULL;
+
+ if (NetworkChangeNotifier::IsOffline() &&
+ !OfflineEnabledInfo::IsOfflineEnabled(extension))
+ return NULL;
+
+ for (std::vector<extensions::UrlHandlerInfo>::const_iterator it =
+ handlers->begin(); it != handlers->end(); it++) {
+ if (it->patterns.MatchesURL(url))
+ return &(*it);
+ }
+
+ return NULL;
+}
+
+UrlHandlersParser::UrlHandlersParser() {
+}
+
+UrlHandlersParser::~UrlHandlersParser() {
+}
+
+bool ParseUrlHandler(const std::string& handler_id,
+ const DictionaryValue& handler_info,
+ std::vector<UrlHandlerInfo>* url_handlers,
+ string16* error) {
+ DCHECK(error);
+
+ UrlHandlerInfo handler;
+ handler.id = handler_id;
+
+ if (!handler_info.GetString(mkeys::kUrlHandlerTitle, &handler.title)) {
+ *error = ASCIIToUTF16(merrors::kInvalidURLHandlerTitle);
+ return false;
+ }
+
+ const ListValue* manif_patterns = NULL;
+ if (!handler_info.GetList(mkeys::kMatches, &manif_patterns) ||
+ manif_patterns->GetSize() == 0) {
+ *error = ErrorUtils::FormatErrorMessageUTF16(
+ merrors::kInvalidURLHandlerPattern, handler_id);
+ return false;
+ }
+
+ for (ListValue::const_iterator it = manif_patterns->begin();
+ it != manif_patterns->end(); ++it) {
+ std::string str_pattern;
+ (*it)->GetAsString(&str_pattern);
+ // TODO(sergeygs): Limit this to non-top-level domains.
+ // TODO(sergeygs): Also add a verification to the CWS installer that the
+ // URL patterns claimed here belong to the app's author verified sites.
+ URLPattern pattern(URLPattern::SCHEME_HTTP |
+ URLPattern::SCHEME_HTTPS);
+ if (pattern.Parse(str_pattern) != URLPattern::PARSE_SUCCESS) {
+ *error = ErrorUtils::FormatErrorMessageUTF16(
+ merrors::kInvalidURLHandlerPatternElement, handler_id);
+ return false;
+ }
+ handler.patterns.AddPattern(pattern);
+ }
+
+ url_handlers->push_back(handler);
+
+ return true;
+}
+
+bool UrlHandlersParser::Parse(Extension* extension, string16* error) {
+ scoped_ptr<UrlHandlers> info(new UrlHandlers);
+ const DictionaryValue* all_handlers = NULL;
+ if (!extension->manifest()->GetDictionary(
+ mkeys::kUrlHandlers, &all_handlers)) {
+ *error = ASCIIToUTF16(merrors::kInvalidURLHandlers);
+ return false;
+ }
+
+ DCHECK(extension->is_platform_app());
+
+ for (DictionaryValue::Iterator iter(*all_handlers); !iter.IsAtEnd();
+ iter.Advance()) {
+ // A URL handler entry is a title and a list of URL patterns to handle.
+ const DictionaryValue* handler = NULL;
+ if (!iter.value().GetAsDictionary(&handler)) {
+ *error = ASCIIToUTF16(merrors::kInvalidURLHandlerPatternElement);
+ return false;
+ }
+
+ if (!ParseUrlHandler(iter.key(), *handler, &info->handlers, error)) {
+ // Text in |error| is set by ParseUrlHandler.
+ return false;
+ }
+ }
+
+ extension->SetManifestData(mkeys::kUrlHandlers, info.release());
+
+ return true;
+}
+
+const std::vector<std::string> UrlHandlersParser::Keys() const {
+ return SingleKey(mkeys::kUrlHandlers);
+}
+
+} // namespace extensions
diff --git a/chrome/common/extensions/api/url_handlers/url_handlers_parser.h b/chrome/common/extensions/api/url_handlers/url_handlers_parser.h
new file mode 100644
index 0000000..4b2ed4d
--- /dev/null
+++ b/chrome/common/extensions/api/url_handlers/url_handlers_parser.h
@@ -0,0 +1,71 @@
+// Copyright 2013 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_COMMON_EXTENSIONS_API_URL_HANDLERS_URL_HANDLERS_PARSER_H_
+#define CHROME_COMMON_EXTENSIONS_API_URL_HANDLERS_URL_HANDLERS_PARSER_H_
+
+#include <string>
+#include <vector>
+
+#include "chrome/common/extensions/extension.h"
+#include "chrome/common/extensions/manifest_handler.h"
+#include "extensions/common/url_pattern.h"
+
+class GURL;
+
+namespace extensions {
+
+struct UrlHandlerInfo {
+ UrlHandlerInfo();
+ ~UrlHandlerInfo();
+
+ // ID identifying this handler in the manifest.
+ std::string id;
+ // Handler title to display in all relevant UI.
+ std::string title;
+ // URL patterns associated with this handler.
+ URLPatternSet patterns;
+};
+
+struct UrlHandlers : public Extension::ManifestData {
+ UrlHandlers();
+ virtual ~UrlHandlers();
+
+ // Returns an array of URL handlers |extension| has defined in its manifest.
+ static const std::vector<UrlHandlerInfo>* GetUrlHandlers(
+ const Extension* extension);
+
+ // Determines whether |extension| has at least one URL handler that matches
+ // |url|.
+ static bool CanExtensionHandleUrl(
+ const Extension* extension,
+ const GURL& url);
+
+ // Finds a matching URL handler for |extension|, if any. Returns NULL in none
+ // are found.
+ static const UrlHandlerInfo* FindMatchingUrlHandler(
+ const Extension* extension,
+ const GURL& url);
+
+ std::vector<UrlHandlerInfo> handlers;
+};
+
+// Parses the "url_handlers" manifest key.
+class UrlHandlersParser : public ManifestHandler {
+ public:
+ UrlHandlersParser();
+ virtual ~UrlHandlersParser();
+
+ // ManifestHandler API
+ virtual bool Parse(Extension* extension, string16* error) OVERRIDE;
+
+ private:
+ virtual const std::vector<std::string> Keys() const OVERRIDE;
+
+ DISALLOW_COPY_AND_ASSIGN(UrlHandlersParser);
+};
+
+} // namespace extensions
+
+#endif // CHROME_COMMON_EXTENSIONS_API_URL_HANDLERS_URL_HANDLERS_PARSER_H_
diff --git a/chrome/common/extensions/chrome_manifest_handlers.cc b/chrome/common/extensions/chrome_manifest_handlers.cc
index 1881868..07137d9 100644
--- a/chrome/common/extensions/chrome_manifest_handlers.cc
+++ b/chrome/common/extensions/chrome_manifest_handlers.cc
@@ -23,6 +23,7 @@
#include "chrome/common/extensions/api/speech/tts_engine_manifest_handler.h"
#include "chrome/common/extensions/api/spellcheck/spellcheck_handler.h"
#include "chrome/common/extensions/api/system_indicator/system_indicator_handler.h"
+#include "chrome/common/extensions/api/url_handlers/url_handlers_parser.h"
#include "chrome/common/extensions/background_info.h"
#include "chrome/common/extensions/csp_handler.h"
#include "chrome/common/extensions/incognito_handler.h"
@@ -91,6 +92,7 @@ void RegisterChromeManifestHandlers() {
(new ThemeHandler)->Register();
(new TtsEngineManifestHandler)->Register();
(new UpdateURLHandler)->Register();
+ (new UrlHandlersParser)->Register();
(new URLOverridesHandler)->Register();
(new WebAccessibleResourcesHandler)->Register();
ManifestHandler::FinalizeRegistration();
diff --git a/chrome/common/extensions/extension_manifest_constants.cc b/chrome/common/extensions/extension_manifest_constants.cc
index 92c82c4..888aa69 100644
--- a/chrome/common/extensions/extension_manifest_constants.cc
+++ b/chrome/common/extensions/extension_manifest_constants.cc
@@ -410,6 +410,14 @@ const char kInvalidTtsVoicesVoiceName[] =
"Invalid value for 'tts_engine.voices[*].voice_name'.";
const char kInvalidUpdateURL[] =
"Invalid value for update url: '[*]'.";
+const char kInvalidURLHandlers[] =
+ "Invalid value for 'url_handlers'.";
+const char kInvalidURLHandlerPatternElement[] =
+ "Invalid value for 'url_handlers[*]'.";
+const char kInvalidURLHandlerTitle[] =
+ "Invalid value for 'url_handlers[*].title'.";
+const char kInvalidURLHandlerPattern[] =
+ "Invalid value for 'url_handlers[*].matches[*]'.";
const char kInvalidURLPatternError[] =
"Invalid url pattern '*'";
const char kInvalidVersion[] =
diff --git a/chrome/common/extensions/extension_manifest_constants.h b/chrome/common/extensions/extension_manifest_constants.h
index 37500da..d42714f 100644
--- a/chrome/common/extensions/extension_manifest_constants.h
+++ b/chrome/common/extensions/extension_manifest_constants.h
@@ -237,6 +237,10 @@ namespace extension_manifest_errors {
extern const char kInvalidTtsVoicesVoiceName[];
extern const char kInvalidUpdateURL[];
extern const char kInvalidURLPatternError[];
+ extern const char kInvalidURLHandlers[];
+ extern const char kInvalidURLHandlerPatternElement[];
+ extern const char kInvalidURLHandlerTitle[];
+ extern const char kInvalidURLHandlerPattern[];
extern const char kInvalidVersion[];
extern const char kInvalidWebAccessibleResourcesList[];
extern const char kInvalidWebAccessibleResource[];
diff --git a/chrome/renderer/resources/extensions/app_runtime_custom_bindings.js b/chrome/renderer/resources/extensions/app_runtime_custom_bindings.js
index 0411d32..b6af67f 100644
--- a/chrome/renderer/resources/extensions/app_runtime_custom_bindings.js
+++ b/chrome/renderer/resources/extensions/app_runtime_custom_bindings.js
@@ -19,8 +19,15 @@ eventBindings.registerArgumentMassager('app.runtime.onLaunched',
function(args, dispatch) {
var launchData = args[0];
- if (launchData && typeof launchData.id !== 'undefined') {
- // new-style dispatch.
+ if (!launchData) {
+ // An onLaunched corresponding to launching directly or from
+ // the app launcher or browser.
+ dispatch([]);
+ return;
+ }
+
+ if (launchData.items) {
+ // An onLaunched corresponding to file_handlers in the app's manifest.
var items = []
var numItems = launchData.items.length;
var itemLoaded = function(err, item) {
@@ -47,10 +54,10 @@ eventBindings.registerArgumentMassager('app.runtime.onLaunched',
itemLoaded(fileError);
});
});
- } else if (launchData) {
- dispatch([launchData]);
} else {
- dispatch([]);
+ // Default case. This currently covers an onLaunched corresponding to
+ // url_handlers in the app's manifest.
+ dispatch([launchData]);
}
});
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/common/mismatching_target.html b/chrome/test/data/extensions/platform_apps/url_handlers/common/mismatching_target.html
new file mode 100644
index 0000000..ecf52da
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/common/mismatching_target.html
@@ -0,0 +1,16 @@
+<!--
+ * Copyright (c) 2012 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.
+-->
+<html>
+<body>
+I am a target of a remote link.
+The handler app should NOT launch with me, even when it is installed,
+because my URL does not match its url_handlers.
+I should always be loaded in the current tab or opened in a new tab instead.
+<script>
+ document.title = "Mismatching link target loaded";
+</script>
+</body>
+</html>
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/common/target.html b/chrome/test/data/extensions/platform_apps/url_handlers/common/target.html
new file mode 100644
index 0000000..f1b3dfe
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/common/target.html
@@ -0,0 +1,12 @@
+<!--
+ * Copyright (c) 2012 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.
+-->
+<html>
+<body>
+I am a target of a remote link.
+The handler app should launch with me, when it is installed.
+I will be opened in a new tab, if it isn't or redirection fails.
+</body>
+</html>
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/handlers/navigate_webview_to_url/main.html b/chrome/test/data/extensions/platform_apps/url_handlers/handlers/navigate_webview_to_url/main.html
new file mode 100644
index 0000000..26218db
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/handlers/navigate_webview_to_url/main.html
@@ -0,0 +1,13 @@
+<!--
+ * Copyright 2013 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.
+-->
+<html>
+<body>
+ <div>I'm a URL handler. I'll try to redirect the webview below to the URL
+ I've been launched with:</div>
+ <div><a id='link' href='about:blank' target='_blank'></a><div>
+ <div><webview id='wv'></webview>
+</body>
+</html>
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/handlers/navigate_webview_to_url/manifest.json b/chrome/test/data/extensions/platform_apps/url_handlers/handlers/navigate_webview_to_url/manifest.json
new file mode 100644
index 0000000..797fb8e
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/handlers/navigate_webview_to_url/manifest.json
@@ -0,0 +1,22 @@
+{
+ "name": "Handler: navigate webview",
+ "version": "1",
+ "app": {
+ "background": {
+ "scripts": ["test.js"]
+ }
+ },
+
+ "permissions": [
+ "webview"
+ ],
+
+ "url_handlers": {
+ "my_doc_url": {
+ "title": "Open my doc",
+ "matches": [
+ "http://localhost:*/extensions/platform_apps/url_handlers/*"
+ ]
+ }
+ }
+}
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/handlers/navigate_webview_to_url/test.js b/chrome/test/data/extensions/platform_apps/url_handlers/handlers/navigate_webview_to_url/test.js
new file mode 100644
index 0000000..d18f596
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/handlers/navigate_webview_to_url/test.js
@@ -0,0 +1,30 @@
+// Copyright 2013 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.
+
+chrome.app.runtime.onLaunched.addListener(function (launchData) {
+ // Complete correctness of launchData is tested in another test.
+ chrome.test.assertTrue(typeof launchData !== 'undefined');
+
+ chrome.app.window.create(
+ "main.html",
+ {},
+ function(win) {
+ win.contentWindow.onload = function() {
+ // Redirect the embedded webview to the same URL we've been launched
+ // with. This should not create an endless loop of redirecting on
+ // ourselves with multiplying windows.
+ var webview = this.document.getElementById('wv');
+ webview.src = launchData.url;
+
+ webview.addEventListener("loadstop", function() {
+ // Give webview plenty of time to navigate to make sure that doesn't
+ // relaunch the handler.
+ setTimeout(function() {
+ chrome.test.sendMessage("Handler launched");
+ }, 500);
+ });
+ }
+ }.bind(this)
+ );
+});
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/handlers/simple/main.html b/chrome/test/data/extensions/platform_apps/url_handlers/handlers/simple/main.html
new file mode 100644
index 0000000..29e3b1ae
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/handlers/simple/main.html
@@ -0,0 +1,11 @@
+<!--
+ * Copyright 2013 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.
+-->
+<html>
+<body>
+ <div>I'm a URL handler. I've been launched with:</div>
+ <div><a id='link' href='about:blank' target='_blank'></a><div>
+</body>
+</html>
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/handlers/simple/manifest.json b/chrome/test/data/extensions/platform_apps/url_handlers/handlers/simple/manifest.json
new file mode 100644
index 0000000..60c6ee8
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/handlers/simple/manifest.json
@@ -0,0 +1,19 @@
+{
+ "name": "Handler: simple",
+ "version": "1",
+ "app": {
+ "background": {
+ "scripts": ["test.js"]
+ }
+ },
+
+ "url_handlers": {
+ "my_doc_url": {
+ "title": "Open my doc",
+ "matches": [
+ "http://localhost:*/extensions/platform_apps/url_handlers/*/target.html",
+ "http://127.0.0.1:*/extensions/platform_apps/url_handlers/*/target.html"
+ ]
+ }
+ }
+}
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/handlers/simple/test.js b/chrome/test/data/extensions/platform_apps/url_handlers/handlers/simple/test.js
new file mode 100644
index 0000000..fd27aa9
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/handlers/simple/test.js
@@ -0,0 +1,36 @@
+// Copyright 2013 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.
+
+chrome.app.runtime.onLaunched.addListener(function (launchData) {
+ // Test that the id and items fields in FileEntry can be read.
+ chrome.test.runTests([
+ function testUrlHandler() {
+ chrome.test.assertTrue(typeof launchData != 'undefined', "No launchData");
+ chrome.test.assertEq(launchData.id, "my_doc_url",
+ "launchData.id incorrect");
+ chrome.test.assertTrue(typeof launchData.items == 'undefined',
+ "Launched for file_handlers, not url_handlers");
+ chrome.test.assertTrue(typeof launchData.url != 'undefined',
+ "No url in launchData");
+ chrome.test.assertTrue(typeof launchData.referrerUrl != 'undefined',
+ "No referrerUrl in launchData");
+ chrome.test.assertFalse(
+ !launchData.url.match(
+ /http:\/\/.*:.*\/extensions\/platform_apps\/url_handlers\/.*/),
+ "Wrong launchData.url");
+ }
+ ]);
+
+ chrome.app.window.create(
+ "main.html",
+ { },
+ function(win) {
+ win.contentWindow.onload = function() {
+ var link = this.document.querySelector('#link');
+ link.href = launchData.url;
+ link.innerText = launchData.url;
+ chrome.test.sendMessage("Handler launched");
+ }
+ });
+});
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/handlers/steal_xhr_target/main.html b/chrome/test/data/extensions/platform_apps/url_handlers/handlers/steal_xhr_target/main.html
new file mode 100644
index 0000000..0bdcbac
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/handlers/steal_xhr_target/main.html
@@ -0,0 +1,12 @@
+<!--
+ * Copyright 2013 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.
+-->
+<html>
+<body>
+I'm a URL handler.
+I'm attempting to steal URLs that the launcher loads via XHR.
+I should never succeed in that.
+</body>
+</html>
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/handlers/steal_xhr_target/manifest.json b/chrome/test/data/extensions/platform_apps/url_handlers/handlers/steal_xhr_target/manifest.json
new file mode 100644
index 0000000..fa101c4
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/handlers/steal_xhr_target/manifest.json
@@ -0,0 +1,19 @@
+{
+ "name": "Handler: steal XHR targets",
+ "version": "1",
+ "app": {
+ "background": {
+ "scripts": ["test.js"]
+ }
+ },
+
+ "url_handlers": {
+ "my_doc_url": {
+ "title": "Steal somebody else's doc",
+ "matches": [
+ "http://localhost:*/extensions/platform_apps/url_handlers/*/target.html",
+ "http://127.0.0.1:*/extensions/platform_apps/url_handlers/*/target.html"
+ ]
+ }
+ }
+}
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/handlers/steal_xhr_target/test.js b/chrome/test/data/extensions/platform_apps/url_handlers/handlers/steal_xhr_target/test.js
new file mode 100644
index 0000000..a112d6c
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/handlers/steal_xhr_target/test.js
@@ -0,0 +1,8 @@
+// Copyright 2013 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.
+
+chrome.app.runtime.onLaunched.addListener(function (launchData) {
+ chrome.app.window.create("main.html", {}, function() {});
+ chrome.test.fail("This handler shouldn't have launched");
+});
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_mismatching_window_open/main.html b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_mismatching_window_open/main.html
new file mode 100644
index 0000000..701aeb2
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_mismatching_window_open/main.html
@@ -0,0 +1,11 @@
+<!--
+ * Copyright 2013 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.
+-->
+<html>
+<body>
+<div>I'm the app's main window page. I will click the link below.</div>
+<script src="main.js"></script>
+</body>
+</html>
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_mismatching_window_open/main.js b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_mismatching_window_open/main.js
new file mode 100644
index 0000000..cd13c7a
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_mismatching_window_open/main.js
@@ -0,0 +1,15 @@
+// Copyright 2013 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.
+
+chrome.test.getConfig(function(config) {
+ var port = config ? config.testServer.port : '0';
+ var href =
+ 'http://localhost:' + port +
+ '/extensions/platform_apps/url_handlers/common/mismatching_target.html';
+ // This click should be intercepted and redirected to the handler app
+ // (pre-installed by the CPP side of the test before launching this).
+ window.open(href);
+
+ chrome.test.sendMessage("Launcher done");
+});
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_mismatching_window_open/manifest.json b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_mismatching_window_open/manifest.json
new file mode 100644
index 0000000..ecf8266
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_mismatching_window_open/manifest.json
@@ -0,0 +1,9 @@
+{
+ "name": "Launcher: mismatching blank link",
+ "version": "1",
+ "app": {
+ "background": {
+ "scripts": ["test.js"]
+ }
+ }
+}
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_mismatching_window_open/test.js b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_mismatching_window_open/test.js
new file mode 100644
index 0000000..2f9f855
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_mismatching_window_open/test.js
@@ -0,0 +1,7 @@
+// Copyright 2013 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.
+
+chrome.app.runtime.onLaunched.addListener(function() {
+ chrome.app.window.create('main.html', {}, function () {});
+});
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_window_open/main.html b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_window_open/main.html
new file mode 100644
index 0000000..701aeb2
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_window_open/main.html
@@ -0,0 +1,11 @@
+<!--
+ * Copyright 2013 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.
+-->
+<html>
+<body>
+<div>I'm the app's main window page. I will click the link below.</div>
+<script src="main.js"></script>
+</body>
+</html>
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_window_open/main.js b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_window_open/main.js
new file mode 100644
index 0000000..ad66fed
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_window_open/main.js
@@ -0,0 +1,15 @@
+// Copyright 2013 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.
+
+chrome.test.getConfig(function(config) {
+ var port = config ? config.testServer.port : '0';
+ var href =
+ 'http://localhost:' + port +
+ '/extensions/platform_apps/url_handlers/common/target.html';
+ // This click should be intercepted and redirected to the handler app
+ // (pre-installed by the CPP side of the test before launching this).
+ window.open(href);
+
+ chrome.test.sendMessage("Launcher done");
+});
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_window_open/manifest.json b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_window_open/manifest.json
new file mode 100644
index 0000000..52fcb8d
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_window_open/manifest.json
@@ -0,0 +1,9 @@
+{
+ "name": "Launcher: window.open()",
+ "version": "1",
+ "app": {
+ "background": {
+ "scripts": ["test.js"]
+ }
+ }
+}
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_window_open/test.js b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_window_open/test.js
new file mode 100644
index 0000000..2f9f855
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/call_window_open/test.js
@@ -0,0 +1,7 @@
+// Copyright 2013 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.
+
+chrome.app.runtime.onLaunched.addListener(function() {
+ chrome.app.window.create('main.html', {}, function () {});
+});
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_blank_link/main.html b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_blank_link/main.html
new file mode 100644
index 0000000..683d70c
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_blank_link/main.html
@@ -0,0 +1,12 @@
+<!--
+ * Copyright 2013 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.
+-->
+<html>
+<body>
+<div>I'm the app's main window page. I will click the link below.</div>
+<div><a id="link" target="_blank">Blank link</a></div>
+<script src="main.js"></script>
+</body>
+</html>
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_blank_link/main.js b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_blank_link/main.js
new file mode 100644
index 0000000..df891ba
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_blank_link/main.js
@@ -0,0 +1,22 @@
+// Copyright 2013 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.
+
+chrome.test.getConfig(function(config) {
+ var port = config ? config.testServer.port : '0';
+
+ var link = document.getElementById('link');
+ link.href =
+ 'http://localhost:' + port +
+ '/extensions/platform_apps/url_handlers/common/target.html';
+
+ // This click should be intercepted and redirected to the handler app
+ // (pre-installed by the CPP side of the test before launching this).
+ var clickEvent = document.createEvent('MouseEvents');
+ clickEvent.initMouseEvent('click', true, true, window,
+ 0, 0, 0, 0, 0, false, false,
+ false, false, 0, null);
+ link.dispatchEvent(clickEvent);
+
+ chrome.test.sendMessage("Launcher done");
+});
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_blank_link/manifest.json b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_blank_link/manifest.json
new file mode 100644
index 0000000..764a60f
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_blank_link/manifest.json
@@ -0,0 +1,9 @@
+{
+ "name": "Launcher: blank link",
+ "version": "1",
+ "app": {
+ "background": {
+ "scripts": ["test.js"]
+ }
+ }
+}
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_blank_link/test.js b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_blank_link/test.js
new file mode 100644
index 0000000..2f9f855
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_blank_link/test.js
@@ -0,0 +1,7 @@
+// Copyright 2013 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.
+
+chrome.app.runtime.onLaunched.addListener(function() {
+ chrome.app.window.create('main.html', {}, function () {});
+});
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_mismatching_blank_link/main.html b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_mismatching_blank_link/main.html
new file mode 100644
index 0000000..2b53a63
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_mismatching_blank_link/main.html
@@ -0,0 +1,12 @@
+<!--
+ * Copyright 2013 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.
+-->
+<html>
+<body>
+<div>I'm the app's main window page. I will click the link below. It should open in a tab.</div>
+<div><a id="mismatching_link" target="_blank">Mismatching blank link</a></div>
+<script src="main.js"></script>
+</body>
+</html>
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_mismatching_blank_link/main.js b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_mismatching_blank_link/main.js
new file mode 100644
index 0000000..1c106b5
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_mismatching_blank_link/main.js
@@ -0,0 +1,22 @@
+// Copyright 2013 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.
+
+chrome.test.getConfig(function(config) {
+ var port = config ? config.testServer.port : '0';
+
+ var link = document.getElementById('mismatching_link');
+ link.href =
+ 'http://localhost:' + port +
+ '/extensions/platform_apps/url_handlers/common/mismatching_target.html';
+
+ // This click should be intercepted and redirected to the handler app
+ // (pre-installed by the CPP side of the test before launching this).
+ var clickEvent = document.createEvent('MouseEvents');
+ clickEvent.initMouseEvent('click', true, true, window,
+ 0, 0, 0, 0, 0, false, false,
+ false, false, 0, null);
+ link.dispatchEvent(clickEvent);
+
+ chrome.test.sendMessage("Launcher done");
+});
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_mismatching_blank_link/manifest.json b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_mismatching_blank_link/manifest.json
new file mode 100644
index 0000000..764a60f
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_mismatching_blank_link/manifest.json
@@ -0,0 +1,9 @@
+{
+ "name": "Launcher: blank link",
+ "version": "1",
+ "app": {
+ "background": {
+ "scripts": ["test.js"]
+ }
+ }
+}
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_mismatching_blank_link/test.js b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_mismatching_blank_link/test.js
new file mode 100644
index 0000000..2f9f855
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launchers/click_mismatching_blank_link/test.js
@@ -0,0 +1,7 @@
+// Copyright 2013 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.
+
+chrome.app.runtime.onLaunched.addListener(function() {
+ chrome.app.window.create('main.html', {}, function () {});
+});
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launching_pages/call_window_open.html b/chrome/test/data/extensions/platform_apps/url_handlers/launching_pages/call_window_open.html
new file mode 100644
index 0000000..7ce197d
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launching_pages/call_window_open.html
@@ -0,0 +1,11 @@
+<!--
+ * Copyright 2013 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.
+-->
+<html>
+<body>
+I'm a page that calls window.open().
+<script src="navigate.js"></script>
+</body>
+</html>
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launching_pages/click_blank_link.html b/chrome/test/data/extensions/platform_apps/url_handlers/launching_pages/click_blank_link.html
new file mode 100644
index 0000000..fdbfe17
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launching_pages/click_blank_link.html
@@ -0,0 +1,12 @@
+<!--
+ * Copyright 2013 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.
+-->
+<html>
+<body>
+I'm a page that clicks a target='_blank' link.
+<div><a id="link" target="_blank">Blank link</a></div>
+<script src="navigate.js"></script>
+</body>
+</html>
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launching_pages/click_link.html b/chrome/test/data/extensions/platform_apps/url_handlers/launching_pages/click_link.html
new file mode 100644
index 0000000..358bdd4
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launching_pages/click_link.html
@@ -0,0 +1,12 @@
+<!--
+ * Copyright 2013 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.
+-->
+<html>
+<body>
+I'm a page that click a regular link.
+<div><a id="link">Regular link</a></div>
+<script src="navigate.js"></script>
+</body>
+</html>
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launching_pages/click_mismatching_link.html b/chrome/test/data/extensions/platform_apps/url_handlers/launching_pages/click_mismatching_link.html
new file mode 100644
index 0000000..42fc7f5
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launching_pages/click_mismatching_link.html
@@ -0,0 +1,12 @@
+<!--
+ * Copyright 2013 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.
+-->
+<html>
+<body>
+I'm a page that click a mismatching regular link.
+<div><a id="mismatching_link">Mismatching regular link</a></div>
+<script src="navigate.js"></script>
+</body>
+</html>
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/launching_pages/navigate.js b/chrome/test/data/extensions/platform_apps/url_handlers/launching_pages/navigate.js
new file mode 100644
index 0000000..865b91e
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/launching_pages/navigate.js
@@ -0,0 +1,35 @@
+// Copyright 2013 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.
+
+(function() {
+ var target_dir = '/extensions/platform_apps/url_handlers/common/';
+ var link = document.getElementById('link');
+ var mismatching_link = document.getElementById('mismatching_link');
+
+ if (link || mismatching_link) {
+ var clickEvent = document.createEvent('MouseEvents');
+ clickEvent.initMouseEvent('click', true, true, window,
+ 0, 0, 0, 0, 0, false, false,
+ false, false, 0, null);
+ if (link) {
+ console.log("Clicking a matching link");
+ link.href = target_dir + 'target.html';
+ // This click should open the handler app (pre-installed in the browser by
+ // the CPP test before launching this) with link.href.
+ link.dispatchEvent(clickEvent);
+ }
+
+ if (mismatching_link) {
+ console.log("Clicking a mismatching link");
+ mismatching_link.href = target_dir + 'mismatching_target.html';
+ // This click should NOT open the handler app, because the URL does not
+ // match the url_handlers of the app. It should open the link in a new
+ // tab instead.
+ mismatching_link.dispatchEvent(clickEvent);
+ }
+ } else {
+ console.log("Calling window.open()");
+ window.open(target_dir + 'target.html');
+ }
+})();
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/xhr_downloader/download.js b/chrome/test/data/extensions/platform_apps/url_handlers/xhr_downloader/download.js
new file mode 100644
index 0000000..38ff074
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/xhr_downloader/download.js
@@ -0,0 +1,21 @@
+// Copyright 2013 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.
+
+(function() {
+ var href =
+ '/extensions/platform_apps/url_handlers/xhr_downloader/target.html';
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function() {
+ document.querySelector('body').innerText =
+ 'XHR succeeded:\n' + xhr.responseText;
+ document.title = "XHR succeeded";
+ };
+ xhr.onerror = function() {
+ document.querySelector('body').innerText =
+ 'XHR failed with status ' + xhr.status;
+ document.title = "XHR failed";
+ }
+ xhr.open('GET', href, true);
+ xhr.send();
+})();
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/xhr_downloader/main.html b/chrome/test/data/extensions/platform_apps/url_handlers/xhr_downloader/main.html
new file mode 100644
index 0000000..6021e82
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/xhr_downloader/main.html
@@ -0,0 +1,12 @@
+<!--
+ * Copyright 2013 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.
+-->
+<html>
+<body>
+<div>I'm the app's main window page.
+I will download an HTML page using XMLHttpRequest.</div>
+<script src="download.js"></script>
+</body>
+</html>
diff --git a/chrome/test/data/extensions/platform_apps/url_handlers/xhr_downloader/target.html b/chrome/test/data/extensions/platform_apps/url_handlers/xhr_downloader/target.html
new file mode 100644
index 0000000..fc8a565
--- /dev/null
+++ b/chrome/test/data/extensions/platform_apps/url_handlers/xhr_downloader/target.html
@@ -0,0 +1,10 @@
+<!--
+ * Copyright 2013 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.
+-->
+<html>
+<body>
+<div>I'm a page to be downloaded via XHR</div>
+</body>
+</html>
diff --git a/extensions/common/manifest_constants.cc b/extensions/common/manifest_constants.cc
index 0402c40..cc73f02 100644
--- a/extensions/common/manifest_constants.cc
+++ b/extensions/common/manifest_constants.cc
@@ -143,6 +143,8 @@ const char kTtsVoicesLang[] = "lang";
const char kTtsVoicesVoiceName[] = "voice_name";
const char kType[] = "type";
const char kUpdateURL[] = "update_url";
+const char kUrlHandlers[] = "url_handlers";
+const char kUrlHandlerTitle[] = "title";
const char kVersion[] = "version";
const char kWebAccessibleResources[] = "web_accessible_resources";
const char kWebURLs[] = "app.urls";
diff --git a/extensions/common/manifest_constants.h b/extensions/common/manifest_constants.h
index e08348e..de60410 100644
--- a/extensions/common/manifest_constants.h
+++ b/extensions/common/manifest_constants.h
@@ -148,6 +148,8 @@ namespace manifest_keys {
extern const char kTtsVoicesVoiceName[];
extern const char kType[];
extern const char kUpdateURL[];
+ extern const char kUrlHandlers[];
+ extern const char kUrlHandlerTitle[];
extern const char kVersion[];
extern const char kWebAccessibleResources[];
extern const char kWebURLs[];