blob: 2cc5d6c315ed6b79c101c94ffa380962de461bcc [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <limits>
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/containers/flat_map.h"
#include "base/containers/span.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted_memory.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
#include "content/browser/webui/web_ui_controller_factory_registry.h"
#include "content/browser/webui/web_ui_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui.h"
#include "content/public/browser/web_ui_controller.h"
#include "content/public/browser/web_ui_controller_interface_binder.h"
#include "content/public/browser/web_ui_data_source.h"
#include "content/public/common/bindings_policy.h"
#include "content/public/common/content_client.h"
#include "content/public/common/referrer.h"
#include "content/public/common/url_constants.h"
#include "content/public/common/url_utils.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/scoped_web_ui_controller_factory_registration.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/test/data/web_ui_test.test-mojom.h"
#include "content/test/data/web_ui_test_types.test-mojom.h"
#include "content/test/grit/web_ui_mojo_test_resources.h"
#include "content/test/grit/web_ui_mojo_test_resources_map.h"
#include "mojo/public/cpp/bindings/binder_map.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "third_party/blink/public/common/chrome_debug_urls.h"
namespace content {
namespace {
const char kMojoWebUiHost[] = "mojo-web-ui";
const char kDummyWebUiHost[] = "dummy-web-ui";
class WebUIMojoTestCacheImpl : public mojom::WebUIMojoTestCache {
public:
explicit WebUIMojoTestCacheImpl(
mojo::PendingReceiver<mojom::WebUIMojoTestCache> receiver)
: receiver_(this, std::move(receiver)) {}
~WebUIMojoTestCacheImpl() override = default;
// mojom::WebUIMojoTestCache overrides:
void Put(const GURL& url, const std::string& contents) override {
cache_[url] = contents;
}
void GetAll(GetAllCallback callback) override {
std::vector<mojom::CacheItemPtr> items;
for (const auto& entry : cache_)
items.push_back(mojom::CacheItem::New(entry.first, entry.second));
std::move(callback).Run(std::move(items));
}
private:
mojo::Receiver<mojom::WebUIMojoTestCache> receiver_;
std::map<GURL, std::string> cache_;
};
// WebUIController that sets up mojo bindings.
class TestWebUIController : public WebUIController {
public:
explicit TestWebUIController(WebUI* web_ui,
int bindings = BINDINGS_POLICY_MOJO_WEB_UI)
: WebUIController(web_ui) {
const base::span<const webui::ResourcePath> kMojoWebUiResources =
base::make_span(kWebUiMojoTestResources, kWebUiMojoTestResourcesSize);
web_ui->SetBindings(bindings);
{
WebUIDataSource* data_source = WebUIDataSource::CreateAndAdd(
web_ui->GetWebContents()->GetBrowserContext(), kMojoWebUiHost);
data_source->OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::ScriptSrc,
"script-src chrome://resources 'self' 'unsafe-eval';");
data_source->DisableTrustedTypesCSP();
data_source->AddResourcePaths(kMojoWebUiResources);
data_source->AddResourcePath("", IDR_WEB_UI_MOJO_HTML);
}
{
WebUIDataSource* data_source = WebUIDataSource::CreateAndAdd(
web_ui->GetWebContents()->GetBrowserContext(), kDummyWebUiHost);
data_source->SetRequestFilter(
base::BindRepeating([](const std::string& path) { return true; }),
base::BindRepeating([](const std::string& id,
WebUIDataSource::GotDataCallback callback) {
std::move(callback).Run(new base::RefCountedString);
}));
}
}
TestWebUIController(const TestWebUIController&) = delete;
TestWebUIController& operator=(const TestWebUIController&) = delete;
protected:
std::unique_ptr<WebUIMojoTestCacheImpl> cache_;
};
// TestWebUIController that can bind a WebUIMojoTestCache interface when
// requested by the page.
class CacheTestWebUIController : public TestWebUIController {
public:
explicit CacheTestWebUIController(WebUI* web_ui)
: TestWebUIController(web_ui) {}
~CacheTestWebUIController() override = default;
void BindInterface(
mojo::PendingReceiver<mojom::WebUIMojoTestCache> receiver) {
cache_ = std::make_unique<WebUIMojoTestCacheImpl>(std::move(receiver));
}
WEB_UI_CONTROLLER_TYPE_DECL();
};
WEB_UI_CONTROLLER_TYPE_IMPL(CacheTestWebUIController)
// WebUIControllerFactory that creates TestWebUIController.
class TestWebUIControllerFactory : public WebUIControllerFactory {
public:
TestWebUIControllerFactory()
: registered_controllers_(
{{"cache", base::BindRepeating(
&TestWebUIControllerFactory::CreateCacheController,
base::Unretained(this))},
{"hybrid", base::BindRepeating(
&TestWebUIControllerFactory::CreateHybridController,
base::Unretained(this))},
{"webui_bindings",
base::BindRepeating(
&TestWebUIControllerFactory::CreateWebUIController,
base::Unretained(this))}}) {}
TestWebUIControllerFactory(const TestWebUIControllerFactory&) = delete;
TestWebUIControllerFactory& operator=(const TestWebUIControllerFactory&) =
delete;
std::unique_ptr<WebUIController> CreateWebUIControllerForURL(
WebUI* web_ui,
const GURL& url) override {
if (!web_ui_enabled_ || !url.SchemeIs(kChromeUIScheme))
return nullptr;
auto it = registered_controllers_.find(url.query());
if (it != registered_controllers_.end())
return it->second.Run(web_ui);
return std::make_unique<TestWebUIController>(web_ui);
}
WebUI::TypeID GetWebUIType(BrowserContext* browser_context,
const GURL& url) override {
if (!web_ui_enabled_ || !url.SchemeIs(kChromeUIScheme))
return WebUI::kNoWebUI;
return reinterpret_cast<WebUI::TypeID>(1);
}
bool UseWebUIForURL(BrowserContext* browser_context,
const GURL& url) override {
return GetWebUIType(browser_context, url) != WebUI::kNoWebUI;
}
void set_web_ui_enabled(bool enabled) { web_ui_enabled_ = enabled; }
private:
std::unique_ptr<WebUIController> CreateCacheController(WebUI* web_ui) {
return std::make_unique<CacheTestWebUIController>(web_ui);
}
std::unique_ptr<WebUIController> CreateHybridController(WebUI* web_ui) {
return std::make_unique<TestWebUIController>(
web_ui, BINDINGS_POLICY_WEB_UI | BINDINGS_POLICY_MOJO_WEB_UI);
}
std::unique_ptr<WebUIController> CreateWebUIController(WebUI* web_ui) {
return std::make_unique<TestWebUIController>(web_ui,
BINDINGS_POLICY_WEB_UI);
}
bool web_ui_enabled_ = true;
const base::flat_map<
std::string,
base::RepeatingCallback<std::unique_ptr<WebUIController>(WebUI*)>>
registered_controllers_;
};
// Base for unit tests that need a ContentBrowserClient.
class TestWebUIContentBrowserClient : public ContentBrowserClient {
public:
TestWebUIContentBrowserClient() {}
TestWebUIContentBrowserClient(const TestWebUIContentBrowserClient&) = delete;
TestWebUIContentBrowserClient& operator=(
const TestWebUIContentBrowserClient&) = delete;
~TestWebUIContentBrowserClient() override {}
void RegisterBrowserInterfaceBindersForFrame(
RenderFrameHost* render_frame_host,
mojo::BinderMapWithContext<content::RenderFrameHost*>* map) override {
RegisterWebUIControllerInterfaceBinder<mojom::WebUIMojoTestCache,
CacheTestWebUIController>(map);
}
};
class WebUIMojoTest : public ContentBrowserTest {
public:
WebUIMojoTest() = default;
WebUIMojoTest(const WebUIMojoTest&) = delete;
WebUIMojoTest& operator=(const WebUIMojoTest&) = delete;
TestWebUIControllerFactory* factory() { return &factory_; }
void NavigateWithNewWebUI(const std::string& path) {
// Load a dummy WebUI URL first so that a new WebUI is set up when we load
// the URL we're actually interested in.
EXPECT_TRUE(NavigateToURL(shell(), GetWebUIURL(kDummyWebUiHost)));
EXPECT_TRUE(NavigateToURL(
shell(), GetWebUIURL(kMojoWebUiHost + std::string("/") + path)));
}
// Run |script| and return a boolean result.
bool RunBoolFunction(const std::string& script) {
return EvalJs(shell()->web_contents(), script).ExtractBool();
}
protected:
void SetUpOnMainThread() override {
original_client_ = SetBrowserClientForTesting(&client_);
}
void TearDownOnMainThread() override {
if (original_client_)
SetBrowserClientForTesting(original_client_);
}
private:
TestWebUIControllerFactory factory_;
content::ScopedWebUIControllerFactoryRegistration factory_registration_{
&factory_};
raw_ptr<ContentBrowserClient> original_client_ = nullptr;
TestWebUIContentBrowserClient client_;
};
// Loads a WebUI page that contains Mojo JS bindings and verifies a message
// round-trip between the page and the browser.
IN_PROC_BROWSER_TEST_F(WebUIMojoTest, EndToEndCommunication) {
GURL kTestUrl(GetWebUIURL(std::string(kMojoWebUiHost) + "/?cache"));
const std::string kTestScript = "runTest();";
EXPECT_TRUE(NavigateToURL(shell(), kTestUrl));
EXPECT_EQ(true, EvalJs(shell()->web_contents(), kTestScript,
EXECUTE_SCRIPT_USE_MANUAL_REPLY));
// Check that a second shell works correctly.
Shell* other_shell = CreateBrowser();
EXPECT_TRUE(NavigateToURL(other_shell, kTestUrl));
EXPECT_EQ(true, EvalJs(other_shell->web_contents(), kTestScript,
EXECUTE_SCRIPT_USE_MANUAL_REPLY));
// We expect two independent chrome://foo tabs/shells to use a separate
// process.
EXPECT_NE(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(),
other_shell->web_contents()->GetPrimaryMainFrame()->GetProcess());
// Close the second shell and wait until its process exits.
RenderProcessHostWatcher process_watcher(
other_shell->web_contents()->GetPrimaryMainFrame()->GetProcess(),
RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
other_shell->Close();
process_watcher.Wait();
// Check that a third shell works correctly, even if we force it to share a
// process with the first shell, by forcing an artificially low process
// limit.
RenderProcessHost::SetMaxRendererProcessCount(1);
other_shell = CreateBrowser();
EXPECT_TRUE(NavigateToURL(other_shell, kTestUrl));
EXPECT_EQ(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(),
other_shell->web_contents()->GetPrimaryMainFrame()->GetProcess());
EXPECT_EQ(true, EvalJs(other_shell->web_contents(), kTestScript,
EXECUTE_SCRIPT_USE_MANUAL_REPLY));
}
// Disabled due to flakiness: crbug.com/860385.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_NativeMojoAvailable DISABLED_NativeMojoAvailable
#else
#define MAYBE_NativeMojoAvailable NativeMojoAvailable
#endif
IN_PROC_BROWSER_TEST_F(WebUIMojoTest, MAYBE_NativeMojoAvailable) {
// Mojo bindings should be enabled.
NavigateWithNewWebUI("web_ui_mojo_native.html");
EXPECT_TRUE(RunBoolFunction("isNativeMojoAvailable()"));
// Now navigate again with normal WebUI bindings and ensure chrome.send is
// available.
NavigateWithNewWebUI("web_ui_mojo_native.html?webui_bindings");
EXPECT_FALSE(RunBoolFunction("isNativeMojoAvailable()"));
// Now navigate again both WebUI and Mojo bindings and ensure chrome.send is
// available.
NavigateWithNewWebUI("web_ui_mojo_native.html?hybrid");
EXPECT_TRUE(RunBoolFunction("isNativeMojoAvailable()"));
// Now navigate again with WebUI disabled and ensure the native bindings are
// not available.
factory()->set_web_ui_enabled(false);
NavigateWithNewWebUI("web_ui_mojo_native.html?hybrid");
EXPECT_FALSE(RunBoolFunction("isNativeMojoAvailable()"));
}
// Disabled due to flakiness: crbug.com/860385.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_ChromeSendAvailable DISABLED_ChromeSendAvailable
#else
#define MAYBE_ChromeSendAvailable ChromeSendAvailable
#endif
IN_PROC_BROWSER_TEST_F(WebUIMojoTest, MAYBE_ChromeSendAvailable) {
// chrome.send is not available on mojo-only WebUIs.
NavigateWithNewWebUI("web_ui_mojo_native.html");
EXPECT_FALSE(RunBoolFunction("isChromeSendAvailable()"));
// Now navigate again with normal WebUI bindings and ensure chrome.send is
// available.
NavigateWithNewWebUI("web_ui_mojo_native.html?webui_bindings");
EXPECT_TRUE(RunBoolFunction("isChromeSendAvailable()"));
// Now navigate again both WebUI and Mojo bindings and ensure chrome.send is
// available.
NavigateWithNewWebUI("web_ui_mojo_native.html?hybrid");
EXPECT_TRUE(RunBoolFunction("isChromeSendAvailable()"));
// Now navigate again with WebUI disabled and ensure that chrome.send is
// not available.
factory()->set_web_ui_enabled(false);
NavigateWithNewWebUI("web_ui_mojo_native.html?hybrid");
EXPECT_FALSE(RunBoolFunction("isChromeSendAvailable()"));
}
IN_PROC_BROWSER_TEST_F(WebUIMojoTest, ChromeSendAvailable_AfterCrash) {
GURL test_url(GetWebUIURL(std::string(kMojoWebUiHost) +
"/web_ui_mojo_native.html?webui_bindings"));
// Navigate with normal WebUI bindings and ensure chrome.send is available.
EXPECT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_TRUE(EvalJs(shell(), "isChromeSendAvailable()").ExtractBool());
WebUIImpl* web_ui = static_cast<WebUIImpl*>(
shell()->web_contents()->GetPrimaryMainFrame()->GetWebUI());
// Simulate a crash on the page.
content::ScopedAllowRendererCrashes allow_renderer_crashes(shell());
RenderProcessHostWatcher crash_observer(
shell()->web_contents(),
RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
shell()->web_contents()->GetController().LoadURL(
GURL(blink::kChromeUICrashURL), content::Referrer(),
ui::PAGE_TRANSITION_TYPED, std::string());
crash_observer.Wait();
EXPECT_FALSE(web_ui->GetRemoteForTest().is_bound());
// Now navigate again both WebUI and Mojo bindings and ensure chrome.send is
// available.
EXPECT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_TRUE(EvalJs(shell(), "isChromeSendAvailable()").ExtractBool());
// The RenderFrameHost has been replaced after the crash, so get web_ui again.
web_ui = static_cast<WebUIImpl*>(
shell()->web_contents()->GetPrimaryMainFrame()->GetWebUI());
EXPECT_TRUE(web_ui->GetRemoteForTest().is_bound());
}
} // namespace
} // namespace content