| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/command_line.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/ref_counted_memory.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/bind.h" |
| #include "base/test/simple_test_tick_clock.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "content/browser/fenced_frame/fenced_frame.h" |
| #include "content/browser/renderer_host/navigation_controller_impl.h" |
| #include "content/browser/renderer_host/navigation_entry_impl.h" |
| #include "content/browser/renderer_host/navigation_entry_restore_context_impl.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/browser/webui/web_ui_controller_factory_registry.h" |
| #include "content/browser/webui/web_ui_impl.h" |
| #include "content/common/content_navigation_policy.h" |
| #include "content/public/browser/child_process_security_policy.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_ui_controller.h" |
| #include "content/public/browser/web_ui_data_source.h" |
| #include "content/public/browser/web_ui_message_handler.h" |
| #include "content/public/browser/webui_config_map.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/common/isolated_world_ids.h" |
| #include "content/public/common/url_constants.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/fenced_frame_test_util.h" |
| #include "content/public/test/scoped_web_ui_controller_factory_registration.h" |
| #include "content/public/test/test_frame_navigation_observer.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "content/public/test/test_utils.h" |
| #include "content/public/test/web_ui_browsertest_util.h" |
| #include "content/shell/browser/shell.h" |
| #include "content/test/content_browser_test_utils_internal.h" |
| #include "testing/gmock/include/gmock/gmock-matchers.h" |
| #include "third_party/blink/public/common/input/web_mouse_event.h" |
| #include "third_party/blink/public/common/input/web_mouse_wheel_event.h" |
| #include "ui/events/base_event_utils.h" |
| #include "ui/webui/untrusted_web_ui_browsertest_util.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| using WebUIImplBrowserTest = ContentBrowserTest; |
| |
| // TODO(crbug.com/40290702): Shared workers are not available on Android. |
| #if !BUILDFLAG(IS_ANDROID) |
| const char kLoadSharedWorkerScript[] = R"( |
| new Promise((resolve) => { |
| const sharedWorker = new SharedWorker($1); |
| sharedWorker.port.onmessage = (event) => { |
| resolve(event.data === 'pong'); |
| }; |
| sharedWorker.port.postMessage('ping'); |
| }); |
| )"; |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| const char kLoadDedicatedWorkerScript[] = R"( |
| new Promise((resolve) => { |
| const worker = new Worker($1); |
| worker.onmessage = (event) => { |
| resolve(event.data === 'pong'); |
| }; |
| worker.postMessage('ping'); |
| }); |
| )"; |
| |
| class TestWebUIMessageHandler : public WebUIMessageHandler { |
| public: |
| void RegisterMessages() override { |
| web_ui()->RegisterMessageCallback( |
| "messageRequiringGesture", |
| base::BindRepeating(&TestWebUIMessageHandler::OnMessageRequiringGesture, |
| base::Unretained(this))); |
| web_ui()->RegisterMessageCallback( |
| "notifyFinish", |
| base::BindRepeating(&TestWebUIMessageHandler::OnNotifyFinish, |
| base::Unretained(this))); |
| web_ui()->RegisterMessageCallback( |
| "sendMessage", |
| base::BindRepeating(&TestWebUIMessageHandler::OnSendMessage, |
| base::Unretained(this))); |
| } |
| |
| void set_finish_closure(base::RepeatingClosure closure) { |
| finish_closure_ = std::move(closure); |
| } |
| |
| int message_requiring_gesture_count() const { |
| return message_requiring_gesture_count_; |
| } |
| |
| void set_send_message_closure(base::OnceClosure closure) { |
| send_message_closure_ = std::move(closure); |
| } |
| |
| private: |
| void OnMessageRequiringGesture(const base::Value::List& args) { |
| ++message_requiring_gesture_count_; |
| } |
| |
| void OnNotifyFinish(const base::Value::List& args) { |
| if (finish_closure_) |
| finish_closure_.Run(); |
| } |
| |
| void OnSendMessage(const base::Value::List& args) { |
| // This message will be invoked when WebContents changes the main RFH |
| // and the old main RFH is still alive during navigating from WebUI page |
| // to cross-site. WebUI message should be handled with old main RFH. |
| |
| if (send_message_closure_) |
| std::move(send_message_closure_).Run(); |
| |
| // AllowJavascript should not have a CHECK crash. |
| AllowJavascript(); |
| |
| // WebUI::CallJavascriptFunctionUnsafe should be run with old main RFH. |
| web_ui()->CallJavascriptFunctionUnsafe("test"); |
| |
| if (finish_closure_) |
| std::move(finish_closure_).Run(); |
| } |
| |
| int message_requiring_gesture_count_ = 0; |
| base::RepeatingClosure finish_closure_; |
| base::OnceClosure send_message_closure_; |
| }; |
| |
| class WebUIRequiringGestureBrowserTest : public ContentBrowserTest { |
| public: |
| WebUIRequiringGestureBrowserTest() { |
| clock_.SetNowTicks(base::TimeTicks::Now()); |
| ui::SetEventTickClockForTesting(&clock_); |
| } |
| |
| ~WebUIRequiringGestureBrowserTest() override { |
| ui::SetEventTickClockForTesting(nullptr); |
| } |
| |
| void SetUpOnMainThread() override { |
| ASSERT_TRUE(NavigateToURL(web_contents(), GetWebUIURL(kChromeUIGpuHost))); |
| auto test_handler = std::make_unique<TestWebUIMessageHandler>(); |
| test_handler_ = test_handler.get(); |
| web_contents()->GetWebUI()->AddMessageHandler(std::move(test_handler)); |
| } |
| void TearDownOnMainThread() override { test_handler_ = nullptr; } |
| |
| protected: |
| void SendMessageAndWaitForFinish() { |
| main_rfh()->ExecuteJavaScriptForTests( |
| u"chrome.send('messageRequiringGesture');" |
| u"chrome.send('notifyFinish');", |
| base::NullCallback(), ISOLATED_WORLD_ID_GLOBAL); |
| base::RunLoop run_loop; |
| test_handler()->set_finish_closure(run_loop.QuitClosure()); |
| run_loop.Run(); |
| } |
| |
| void AdvanceClock(base::TimeDelta delta) { clock_.Advance(delta); } |
| |
| WebContents* web_contents() { return shell()->web_contents(); } |
| RenderFrameHost* main_rfh() { return web_contents()->GetPrimaryMainFrame(); } |
| |
| TestWebUIMessageHandler* test_handler() { return test_handler_; } |
| |
| private: |
| base::SimpleTestTickClock clock_; |
| |
| // Owned by the WebUI associated with the WebContents. |
| raw_ptr<TestWebUIMessageHandler> test_handler_ = nullptr; |
| }; |
| |
| } // namespace |
| |
| // Tests that navigating between WebUIs of different types results in |
| // SiteInstance swap when running in process-per-tab process model. |
| IN_PROC_BROWSER_TEST_F(WebUIImplBrowserTest, ForceSwapOnDifferenteWebUITypes) { |
| base::CommandLine::ForCurrentProcess()->AppendSwitch( |
| switches::kProcessPerTab); |
| WebContents* web_contents = shell()->web_contents(); |
| |
| const GURL web_ui_url(GetWebUIURL(kChromeUIHistogramHost)); |
| EXPECT_TRUE(WebUIControllerFactoryRegistry::GetInstance()->UseWebUIForURL( |
| web_contents->GetBrowserContext(), web_ui_url)); |
| ASSERT_TRUE(NavigateToURL(web_contents, web_ui_url)); |
| EXPECT_TRUE(ChildProcessSecurityPolicy::GetInstance()->HasWebUIBindings( |
| web_contents->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID())); |
| |
| // Capture the SiteInstance before navigating for later comparison. |
| scoped_refptr<SiteInstance> orig_site_instance( |
| web_contents->GetSiteInstance()); |
| auto orig_browsing_instance_id = orig_site_instance->GetBrowsingInstanceId(); |
| |
| // Navigate to a different WebUI type and ensure that the SiteInstance |
| // has changed and the new process also has WebUI bindings. |
| const GURL web_ui_url2(GetWebUIURL(kChromeUIGpuHost)); |
| EXPECT_TRUE(WebUIConfigMap::GetInstance().GetConfig( |
| web_contents->GetBrowserContext(), web_ui_url2)); |
| ASSERT_TRUE(NavigateToURL(web_contents, web_ui_url2)); |
| auto* new_site_instance = web_contents->GetSiteInstance(); |
| EXPECT_NE(orig_site_instance, new_site_instance); |
| EXPECT_NE(orig_browsing_instance_id, |
| new_site_instance->GetBrowsingInstanceId()); |
| EXPECT_TRUE(ChildProcessSecurityPolicy::GetInstance()->HasWebUIBindings( |
| web_contents->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID())); |
| } |
| |
| // Tests that a WebUI page will stay in the initial RenderFrameHost and its |
| // SiteInstance when we navigate to it from the initial blank page. |
| // |
| // While WebUI navigations require a BrowsingInstance swap and hence typically |
| // force a RenderFrameHost swap, the initial RenderFrameHost is in an unused |
| // process and unassigned SiteInstance, and so it effectively satisfies the |
| // BrowsingInstance swap requirement. So, it should be reused without requiring |
| // an extra RenderFrameHost swap. |
| IN_PROC_BROWSER_TEST_F(WebUIImplBrowserTest, |
| ReuseInitialRenderFrameHostOnFirstNavigation) { |
| WebContents* web_contents = shell()->web_contents(); |
| scoped_refptr<SiteInstance> orig_site_instance( |
| web_contents->GetSiteInstance()); |
| EXPECT_FALSE( |
| static_cast<SiteInstanceImpl*>(orig_site_instance.get())->HasSite()); |
| RenderFrameHostWrapper initial_rfh( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| EXPECT_TRUE(initial_rfh->GetProcess()->IsUnused()); |
| |
| // Navigate from the initial blank page to the WebUI URL. |
| const GURL web_ui_url(GetWebUIURL(kChromeUIHistogramHost)); |
| EXPECT_TRUE(WebUIControllerFactoryRegistry::GetInstance()->UseWebUIForURL( |
| web_contents->GetBrowserContext(), web_ui_url)); |
| ASSERT_TRUE(NavigateToURL(web_contents, web_ui_url)); |
| |
| EXPECT_TRUE(ChildProcessSecurityPolicy::GetInstance()->HasWebUIBindings( |
| web_contents->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID())); |
| auto* new_site_instance = web_contents->GetSiteInstance(); |
| EXPECT_EQ(orig_site_instance, new_site_instance); |
| EXPECT_TRUE( |
| static_cast<SiteInstanceImpl*>(orig_site_instance.get())->HasSite()); |
| EXPECT_EQ(initial_rfh.get(), shell()->web_contents()->GetPrimaryMainFrame()); |
| ASSERT_TRUE(initial_rfh); |
| EXPECT_FALSE(initial_rfh->GetProcess()->IsUnused()); |
| } |
| |
| // Check that starting a WebUI navigation while an about:blank navigation in an |
| // initial RenderFrameHost is pending commit does not crash. See |
| // https://crbug.com/1492076. |
| IN_PROC_BROWSER_TEST_F(WebUIImplBrowserTest, |
| StartWebUINavigationWhileAboutBlankIsPendingCommit) { |
| WebContents* web_contents = shell()->web_contents(); |
| RenderFrameHostImplWrapper initial_rfh(web_contents->GetPrimaryMainFrame()); |
| EXPECT_TRUE(initial_rfh->GetProcess()->IsUnused()); |
| EXPECT_FALSE(initial_rfh->has_committed_any_navigation()); |
| EXPECT_TRUE(initial_rfh->is_initial_empty_document()); |
| |
| // Start an about:blank navigation, but don't wait for commit. Since |
| // about:blank doesn't run through the typical navigation throttles, it |
| // proceeds straight to CommitNavigation, so when LoadURL() returns, |
| // about:blank is pending commit and waiting for DidCommitNavigation. |
| shell()->LoadURL(GURL(url::kAboutBlankURL)); |
| |
| // Because has_committed_any_navigation() is modified in CommitNavigation(), |
| // it should have become true now. is_initial_empty_document() is modified in |
| // DidCommitNavigation, so it hasn't been updated yet. |
| EXPECT_TRUE(initial_rfh->has_committed_any_navigation()); |
| EXPECT_TRUE(initial_rfh->is_initial_empty_document()); |
| |
| // Now, start a navigation to a WebUI URL. This used to crash in |
| // https://crbug.com/1492076. |
| const GURL web_ui_url(GetWebUIURL(kChromeUIHistogramHost)); |
| // Note: NavigateToURL() does a WaitForLoadStop() before starting a |
| // navigation, so start the navigation via LoadURL instead. |
| TestNavigationManager manager(web_contents, web_ui_url); |
| shell()->LoadURL(web_ui_url); |
| |
| // After the WebUI navigation finishes, check that it didn't reuse the initial |
| // RFH, since it was already considered to have a committed/committing |
| // navigation (to about:blank) when the WebUI navigation started. |
| EXPECT_TRUE(manager.WaitForNavigationFinished()); |
| RenderFrameHostImplWrapper final_rfh(web_contents->GetPrimaryMainFrame()); |
| EXPECT_NE(initial_rfh.get(), final_rfh.get()); |
| EXPECT_TRUE(final_rfh->has_committed_any_navigation()); |
| EXPECT_FALSE(final_rfh->is_initial_empty_document()); |
| } |
| |
| // Check that navigating to a WebUI page from a crashed about:blank page will |
| // work correctly, properly granting WebUI bindings and avoiding the reuse of |
| // the initial SiteInstance and process. |
| IN_PROC_BROWSER_TEST_F(WebUIImplBrowserTest, NavigateFromCrashedAboutBlank) { |
| WebContents* web_contents = shell()->web_contents(); |
| scoped_refptr<SiteInstance> orig_site_instance( |
| web_contents->GetSiteInstance()); |
| EXPECT_FALSE( |
| static_cast<SiteInstanceImpl*>(orig_site_instance.get())->HasSite()); |
| RenderFrameHostImplWrapper initial_rfh(web_contents->GetPrimaryMainFrame()); |
| EXPECT_TRUE(initial_rfh->GetProcess()->IsUnused()); |
| EXPECT_TRUE(initial_rfh->is_initial_empty_document()); |
| EXPECT_FALSE(initial_rfh->has_committed_any_navigation()); |
| |
| // Explicitly navigate to about:blank. Note that this stays in the initial |
| // RenderFrameHost but resets is_initial_empty_document() to false. |
| ASSERT_TRUE(NavigateToURL(web_contents, GURL(url::kAboutBlankURL))); |
| ASSERT_TRUE(initial_rfh.get()); |
| EXPECT_FALSE(initial_rfh->is_initial_empty_document()); |
| EXPECT_TRUE(initial_rfh->has_committed_any_navigation()); |
| EXPECT_TRUE(initial_rfh->GetProcess()->IsUnused()); |
| |
| // Crash the initial process. Note that this resets |
| // has_committed_any_navigation(), but is_initial_empty_document() is still |
| // false. |
| RenderProcessHostWatcher crash_observer( |
| initial_rfh->GetProcess(), |
| RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); |
| EXPECT_TRUE(initial_rfh->GetProcess()->Shutdown(0)); |
| crash_observer.Wait(); |
| EXPECT_FALSE(initial_rfh->is_initial_empty_document()); |
| EXPECT_FALSE(initial_rfh->has_committed_any_navigation()); |
| |
| // Navigate from the crashed blank page to a WebUI URL. |
| const GURL web_ui_url(GetWebUIURL(kChromeUIHistogramHost)); |
| EXPECT_TRUE(WebUIControllerFactoryRegistry::GetInstance()->UseWebUIForURL( |
| web_contents->GetBrowserContext(), web_ui_url)); |
| ASSERT_TRUE(NavigateToURL(web_contents, web_ui_url)); |
| |
| // The explicit about:blank commit should prevent subsequent WebUI |
| // navigations from reusing the current RenderFrameHost, as it's no longer |
| // the initial one. Crashing the about:blank page shouldn't affect this, so |
| // the WebUI navigation should end up in a fresh SiteInstance and process. |
| EXPECT_NE(orig_site_instance, web_contents->GetSiteInstance()); |
| EXPECT_NE(orig_site_instance->GetOrCreateProcessForTesting(), |
| web_contents->GetPrimaryMainFrame()->GetProcess()); |
| |
| // Check that the resulting WebUI page has bindings, and its process is |
| // marked as used. |
| EXPECT_TRUE(ChildProcessSecurityPolicy::GetInstance()->HasWebUIBindings( |
| web_contents->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID())); |
| EXPECT_FALSE(web_contents->GetPrimaryMainFrame()->GetProcess()->IsUnused()); |
| } |
| |
| // Check that when over the process limit, a WebUI navigation in one tab does |
| // not reuse an initial RFH's unused process in another unrelated tab. |
| IN_PROC_BROWSER_TEST_F(WebUIImplBrowserTest, |
| DoNotReuseInitialRenderFrameHostForDifferentTab) { |
| // Force all SiteInstances to always try to reuse an available process. |
| RenderProcessHost::SetMaxRendererProcessCount(1); |
| |
| // Ensure the initial RenderFrameHost has an unused process to start with. |
| WebContents* web_contents = shell()->web_contents(); |
| scoped_refptr<SiteInstance> orig_site_instance( |
| web_contents->GetSiteInstance()); |
| RenderFrameHostWrapper initial_rfh( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| EXPECT_TRUE(initial_rfh->GetProcess()->IsUnused()); |
| |
| // Create a new WebUI window in an unrelated SiteInstance. When the new |
| // SiteInstance obtains its process (as part of creating the initial RFH in |
| // the new WebContents), it should *not* reuse the initial process from the |
| // first window. Despite being unused, that first process should only be |
| // allowed to be used by navigations in `shell()` and not other windows. In |
| // practice, this matters for scenarios like chrome://*.top-chrome/ WebUI |
| // URLs, which should all share a single process, rather than reusing |
| // arbitrary unused processes from unrelated windows. |
| const GURL web_ui_url(GetWebUIURL(kChromeUIHistogramHost)); |
| Shell* new_shell = Shell::CreateNewWindow( |
| shell()->web_contents()->GetBrowserContext(), GURL(), |
| SiteInstanceImpl::CreateForURL( |
| shell()->web_contents()->GetBrowserContext(), web_ui_url), |
| gfx::Size()); |
| ASSERT_TRUE(NavigateToURL(new_shell->web_contents(), web_ui_url)); |
| EXPECT_NE(initial_rfh->GetProcess(), |
| new_shell->web_contents()->GetPrimaryMainFrame()->GetProcess()); |
| EXPECT_TRUE(initial_rfh->GetProcess()->IsUnused()); |
| |
| // Create a third window and ensure it shares a process with the second |
| // window, rather than reusing the still-unused process from the first window. |
| Shell* new_shell2 = Shell::CreateNewWindow( |
| shell()->web_contents()->GetBrowserContext(), GURL(), |
| SiteInstanceImpl::CreateForURL( |
| shell()->web_contents()->GetBrowserContext(), web_ui_url), |
| gfx::Size()); |
| ASSERT_TRUE(NavigateToURL(new_shell2->web_contents(), web_ui_url)); |
| EXPECT_NE(initial_rfh->GetProcess(), |
| new_shell2->web_contents()->GetPrimaryMainFrame()->GetProcess()); |
| EXPECT_EQ(new_shell->web_contents()->GetPrimaryMainFrame()->GetProcess(), |
| new_shell2->web_contents()->GetPrimaryMainFrame()->GetProcess()); |
| EXPECT_TRUE(initial_rfh->GetProcess()->IsUnused()); |
| } |
| |
| // Check that if two initial RenderFrameHosts in different tabs share a |
| // process, and that process becomes locked to a normal web site after a |
| // navigation in the first tab, then that process should not be reused for a |
| // WebUI navigation in the second tab, even though it's still the initial |
| // RFH's process in that tab. |
| IN_PROC_BROWSER_TEST_F(WebUIImplBrowserTest, |
| DoNotReuseInitialRenderFrameHostWithUsedProcess) { |
| // Force all SiteInstances to always try to reuse an available process. |
| RenderProcessHost::SetMaxRendererProcessCount(1); |
| |
| // The test starts with an unused process in the initial RFH. |
| EXPECT_TRUE( |
| shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()->IsUnused()); |
| |
| // Create a new tab with an unassigned SiteInstance. That SiteInstance |
| // should reuse the available unused process from the first tab. |
| Shell* new_shell = Shell::CreateNewWindow( |
| shell()->web_contents()->GetBrowserContext(), GURL(), |
| SiteInstanceImpl::Create(shell()->web_contents()->GetBrowserContext()), |
| gfx::Size()); |
| EXPECT_EQ(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(), |
| new_shell->web_contents()->GetPrimaryMainFrame()->GetProcess()); |
| EXPECT_TRUE( |
| shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()->IsUnused()); |
| |
| // Navigate first tab to a normal web URL. This should mark the corresponding |
| // process as used, but the process is still shared with new_shell's RFH. |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_TRUE( |
| NavigateToURL(shell()->web_contents(), |
| embedded_test_server()->GetURL("/simple_page.html"))); |
| EXPECT_FALSE(new_shell->web_contents() |
| ->GetPrimaryMainFrame() |
| ->GetProcess() |
| ->IsUnused()); |
| EXPECT_EQ(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(), |
| new_shell->web_contents()->GetPrimaryMainFrame()->GetProcess()); |
| EXPECT_FALSE(ChildProcessSecurityPolicy::GetInstance()->HasWebUIBindings( |
| shell() |
| ->web_contents() |
| ->GetPrimaryMainFrame() |
| ->GetProcess() |
| ->GetDeprecatedID())); |
| |
| // Navigate the second tab to a WebUI URL. This should not reuse the |
| // initial RFH's process and should end up in a new process. |
| const GURL web_ui_url(GetWebUIURL(kChromeUIHistogramHost)); |
| ASSERT_TRUE(NavigateToURL(new_shell->web_contents(), web_ui_url)); |
| EXPECT_NE(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(), |
| new_shell->web_contents()->GetPrimaryMainFrame()->GetProcess()); |
| EXPECT_TRUE(ChildProcessSecurityPolicy::GetInstance()->HasWebUIBindings( |
| new_shell->web_contents() |
| ->GetPrimaryMainFrame() |
| ->GetProcess() |
| ->GetDeprecatedID())); |
| } |
| |
| // Check that if two initial RenderFrameHosts in different tabs share a |
| // process, then that process can be reused for a WebUI navigation in one of |
| // these tabs. |
| IN_PROC_BROWSER_TEST_F(WebUIImplBrowserTest, |
| ReuseInitialProcessSharedByMultipleTabs) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // Force all SiteInstances to always try to reuse an available process. |
| RenderProcessHost::SetMaxRendererProcessCount(1); |
| |
| // The test starts with an unused process in the initial RFH. |
| EXPECT_TRUE( |
| shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()->IsUnused()); |
| |
| // Create a new tab with an unassigned SiteInstance. That SiteInstance |
| // should reuse the available unused process from the first tab. |
| Shell* new_shell = Shell::CreateNewWindow( |
| shell()->web_contents()->GetBrowserContext(), GURL(), |
| SiteInstanceImpl::Create(shell()->web_contents()->GetBrowserContext()), |
| gfx::Size()); |
| EXPECT_EQ(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(), |
| new_shell->web_contents()->GetPrimaryMainFrame()->GetProcess()); |
| EXPECT_TRUE( |
| shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()->IsUnused()); |
| |
| // Create a third tab, also with an unassigned SiteInstance and also reusing |
| // the first tab's initial process. |
| Shell* third_shell = Shell::CreateNewWindow( |
| shell()->web_contents()->GetBrowserContext(), GURL(), |
| SiteInstanceImpl::Create(shell()->web_contents()->GetBrowserContext()), |
| gfx::Size()); |
| EXPECT_EQ(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(), |
| third_shell->web_contents()->GetPrimaryMainFrame()->GetProcess()); |
| |
| // Navigate the second tab to a WebUI URL. This should reuse the |
| // initial process, lock it to the WebUI site, and mark it as used. |
| const GURL web_ui_url(GetWebUIURL(kChromeUIHistogramHost)); |
| ASSERT_TRUE(NavigateToURL(new_shell->web_contents(), web_ui_url)); |
| EXPECT_EQ(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(), |
| new_shell->web_contents()->GetPrimaryMainFrame()->GetProcess()); |
| EXPECT_FALSE( |
| shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()->IsUnused()); |
| EXPECT_TRUE(ChildProcessSecurityPolicy::GetInstance()->HasWebUIBindings( |
| shell() |
| ->web_contents() |
| ->GetPrimaryMainFrame() |
| ->GetProcess() |
| ->GetDeprecatedID())); |
| |
| // Navigate the first tab to a normal web URL. This should not stay in the |
| // the initial process, which is now used by WebUI. |
| EXPECT_TRUE( |
| NavigateToURL(shell()->web_contents(), |
| embedded_test_server()->GetURL("/simple_page.html"))); |
| EXPECT_NE(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(), |
| new_shell->web_contents()->GetPrimaryMainFrame()->GetProcess()); |
| EXPECT_FALSE(ChildProcessSecurityPolicy::GetInstance()->HasWebUIBindings( |
| shell() |
| ->web_contents() |
| ->GetPrimaryMainFrame() |
| ->GetProcess() |
| ->GetDeprecatedID())); |
| EXPECT_TRUE(ChildProcessSecurityPolicy::GetInstance()->HasWebUIBindings( |
| new_shell->web_contents() |
| ->GetPrimaryMainFrame() |
| ->GetProcess() |
| ->GetDeprecatedID())); |
| |
| // Navigate the third tab to the same WebUI URL. This should stay in the |
| // third tab's initial process which is shared with the second tab, since the |
| // second and third tab's WebUI URLs are same-site. |
| ASSERT_TRUE(NavigateToURL(third_shell->web_contents(), web_ui_url)); |
| EXPECT_EQ(new_shell->web_contents()->GetPrimaryMainFrame()->GetProcess(), |
| third_shell->web_contents()->GetPrimaryMainFrame()->GetProcess()); |
| } |
| |
| // Check that when a tab with a WebUI page is cloned, the new tab reuses the old |
| // tab's SiteInstance and process when it loads its copy of the WebUI page. |
| IN_PROC_BROWSER_TEST_F(WebUIImplBrowserTest, ReuseProcessInClonedTab) { |
| // Load a normal page and then a WebUI page in the initial tab. |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_TRUE( |
| NavigateToURL(shell()->web_contents(), |
| embedded_test_server()->GetURL("/simple_page.html"))); |
| |
| const GURL web_ui_url(GetWebUIURL(kChromeUIHistogramHost)); |
| ASSERT_TRUE(NavigateToURL(shell()->web_contents(), web_ui_url)); |
| |
| // Clone the tab with these two NavigationEntries. |
| std::unique_ptr<WebContents> cloned_tab = shell()->web_contents()->Clone(); |
| WebContentsImpl* cloned_tab_impl = |
| static_cast<WebContentsImpl*>(cloned_tab.get()); |
| NavigationController& new_controller = cloned_tab_impl->GetController(); |
| EXPECT_TRUE(new_controller.IsInitialNavigation()); |
| EXPECT_TRUE(new_controller.NeedsReload()); |
| EXPECT_EQ(2, new_controller.GetEntryCount()); |
| EXPECT_EQ(1, new_controller.GetLastCommittedEntryIndex()); |
| |
| // The cloned WebContents will use the old tab's current SiteInstance for its |
| // initial RFH. That means its initial SiteInstance should already have a |
| // site, and it should keep the same process (from the old tab). |
| // TODO(crbug.com/40277187): these expectations may change in the future if |
| // duplicating tabs stops inheriting the old tab's SiteInstance. |
| EXPECT_EQ(shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance(), |
| cloned_tab->GetPrimaryMainFrame()->GetSiteInstance()); |
| EXPECT_TRUE( |
| cloned_tab_impl->GetPrimaryMainFrame()->GetSiteInstance()->HasSite()); |
| EXPECT_EQ(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(), |
| cloned_tab->GetPrimaryMainFrame()->GetProcess()); |
| EXPECT_FALSE( |
| cloned_tab_impl->GetPrimaryMainFrame()->GetProcess()->IsUnused()); |
| |
| // Load the cloned tab. This should reuse the old tab's WebUI process. |
| // TODO(crbug.com/40277187): this expectation may change in the future if |
| // duplicating tabs stops inheriting the old tab's SiteInstance. |
| { |
| TestNavigationObserver clone_observer(cloned_tab_impl); |
| new_controller.LoadIfNecessary(); |
| clone_observer.Wait(); |
| } |
| EXPECT_EQ(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(), |
| cloned_tab->GetPrimaryMainFrame()->GetProcess()); |
| } |
| |
| // Check that doing a session restore of a WebUI NavigationEntry in a new tab |
| // can reuse the new tab's initial RFH. |
| IN_PROC_BROWSER_TEST_F(WebUIImplBrowserTest, ReuseInitialRFHInRestoredTab) { |
| // Load a WebUI URL. |
| const GURL web_ui_url(GetWebUIURL(kChromeUIHistogramHost)); |
| ASSERT_TRUE(NavigateToURL(shell()->web_contents(), web_ui_url)); |
| NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>( |
| shell()->web_contents()->GetController()); |
| NavigationEntryImpl* entry = controller.GetLastCommittedEntry(); |
| |
| // Create a NavigationEntry with the same PageState as the current entry. |
| std::unique_ptr<NavigationEntryImpl> restored_entry = |
| NavigationEntryImpl::FromNavigationEntry( |
| NavigationController::CreateNavigationEntry( |
| web_ui_url, Referrer(), std::nullopt /* initiator_origin= */, |
| /* initiator_base_url= */ std::nullopt, |
| ui::PAGE_TRANSITION_RELOAD, false, std::string(), |
| controller.GetBrowserContext(), |
| nullptr /* blob_url_loader_factory */)); |
| std::unique_ptr<NavigationEntryRestoreContextImpl> context = |
| std::make_unique<NavigationEntryRestoreContextImpl>(); |
| restored_entry->SetPageState(entry->GetPageState(), context.get()); |
| |
| // Create a new shell for session restore. At this point, since we haven't |
| // loaded anything yet, the restored shell's initial RFH should still be in an |
| // unassigned SiteInstance and an unused process. |
| Shell* restore_shell = Shell::CreateNewWindow(controller.GetBrowserContext(), |
| GURL(), nullptr, gfx::Size()); |
| WebContentsImpl* restore_contents = |
| static_cast<WebContentsImpl*>(restore_shell->web_contents()); |
| RenderFrameHostWrapper restore_rfh(restore_contents->GetPrimaryMainFrame()); |
| scoped_refptr<SiteInstance> restore_site_instance( |
| restore_rfh->GetSiteInstance()); |
| EXPECT_TRUE(restore_rfh->GetProcess()->IsUnused()); |
| EXPECT_FALSE( |
| static_cast<SiteInstanceImpl*>(restore_site_instance.get())->HasSite()); |
| EXPECT_FALSE(ChildProcessSecurityPolicy::GetInstance()->HasWebUIBindings( |
| restore_rfh->GetProcess()->GetDeprecatedID())); |
| EXPECT_NE(shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance(), |
| restore_site_instance); |
| |
| // Restore and load the WebUI entry in the new shell. |
| std::vector<std::unique_ptr<NavigationEntry>> entries; |
| entries.push_back(std::move(restored_entry)); |
| NavigationControllerImpl& restore_controller = |
| restore_contents->GetController(); |
| restore_controller.Restore(entries.size() - 1, RestoreType::kRestored, |
| &entries); |
| ASSERT_EQ(0u, entries.size()); |
| EXPECT_EQ(1, restore_controller.GetEntryCount()); |
| { |
| TestNavigationObserver restore_observer(restore_contents); |
| restore_controller.LoadIfNecessary(); |
| restore_observer.Wait(); |
| } |
| // The initial RFH should be reused by the restored WebUI navigation, its |
| // SiteInstance should now have a site, and its process should now be marked |
| // as used and gain WebUI bindings. Note that SiteInstances and |
| // BrowsingInstances are not currently persisted in session history, so this |
| // does not load in the original tab's SiteInstance and BrowsingInstance. This |
| // could change in the future. |
| EXPECT_EQ(restore_contents->GetPrimaryMainFrame()->GetSiteInstance(), |
| restore_site_instance); |
| EXPECT_TRUE( |
| static_cast<SiteInstanceImpl*>(restore_site_instance.get())->HasSite()); |
| EXPECT_EQ(restore_contents->GetPrimaryMainFrame(), restore_rfh.get()); |
| ASSERT_TRUE(restore_rfh.get()); |
| EXPECT_FALSE(restore_rfh->GetProcess()->IsUnused()); |
| EXPECT_TRUE(ChildProcessSecurityPolicy::GetInstance()->HasWebUIBindings( |
| restore_rfh->GetProcess()->GetDeprecatedID())); |
| } |
| |
| // Tests that navigating from chrome:// to chrome-untrusted:// results in |
| // SiteInstance swap. |
| IN_PROC_BROWSER_TEST_F(WebUIImplBrowserTest, ForceSwapOnFromChromeToUntrusted) { |
| WebContents* web_contents = shell()->web_contents(); |
| content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig( |
| std::make_unique<ui::TestUntrustedWebUIConfig>("test-host")); |
| |
| const GURL web_ui_url(GetWebUIURL(kChromeUIHistogramHost)); |
| EXPECT_TRUE(WebUIControllerFactoryRegistry::GetInstance()->UseWebUIForURL( |
| web_contents->GetBrowserContext(), web_ui_url)); |
| |
| ASSERT_TRUE(NavigateToURL(web_contents, web_ui_url)); |
| EXPECT_TRUE(ChildProcessSecurityPolicy::GetInstance()->HasWebUIBindings( |
| web_contents->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID())); |
| |
| // Capture the SiteInstance before navigating for later comparison. |
| scoped_refptr<SiteInstance> orig_site_instance( |
| web_contents->GetSiteInstance()); |
| auto orig_browsing_instance_id = orig_site_instance->GetBrowsingInstanceId(); |
| |
| // Navigate to chrome-untrusted:// and ensure that the SiteInstance |
| // has changed and the new process has no WebUI bindings. |
| ASSERT_TRUE(NavigateToURL(web_contents, |
| GetChromeUntrustedUIURL("test-host/title1.html"))); |
| auto* new_site_instance = web_contents->GetSiteInstance(); |
| EXPECT_NE(orig_site_instance, new_site_instance); |
| EXPECT_NE(orig_browsing_instance_id, |
| new_site_instance->GetBrowsingInstanceId()); |
| EXPECT_FALSE(ChildProcessSecurityPolicy::GetInstance()->HasWebUIBindings( |
| web_contents->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID())); |
| } |
| |
| // Tests that navigating from chrome-untrusted:// to chrome:// results in |
| // SiteInstance swap. |
| IN_PROC_BROWSER_TEST_F(WebUIImplBrowserTest, ForceSwapOnFromUntrustedToChrome) { |
| WebContents* web_contents = shell()->web_contents(); |
| content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig( |
| std::make_unique<ui::TestUntrustedWebUIConfig>("test-host")); |
| |
| ASSERT_TRUE(NavigateToURL(web_contents, |
| GetChromeUntrustedUIURL("test-host/title1.html"))); |
| EXPECT_FALSE(ChildProcessSecurityPolicy::GetInstance()->HasWebUIBindings( |
| web_contents->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID())); |
| |
| // Capture the SiteInstance before navigating for later comparison. |
| scoped_refptr<SiteInstance> orig_site_instance( |
| web_contents->GetSiteInstance()); |
| auto orig_browsing_instance_id = orig_site_instance->GetBrowsingInstanceId(); |
| |
| // Navigate to a WebUI and ensure that the SiteInstance has changed and the |
| // new process has WebUI bindings. |
| const GURL web_ui_url(GetWebUIURL(kChromeUIHistogramHost)); |
| EXPECT_TRUE(WebUIControllerFactoryRegistry::GetInstance()->UseWebUIForURL( |
| web_contents->GetBrowserContext(), web_ui_url)); |
| |
| ASSERT_TRUE(NavigateToURL(web_contents, web_ui_url)); |
| auto* new_site_instance = web_contents->GetSiteInstance(); |
| EXPECT_NE(orig_site_instance, new_site_instance); |
| EXPECT_NE(orig_browsing_instance_id, |
| new_site_instance->GetBrowsingInstanceId()); |
| EXPECT_TRUE(ChildProcessSecurityPolicy::GetInstance()->HasWebUIBindings( |
| web_contents->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID())); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebUIImplBrowserTest, SameDocumentNavigationsAndReload) { |
| auto* web_contents = shell()->web_contents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, GetWebUIURL(kChromeUIHistogramHost))); |
| |
| auto owned_test_handler = std::make_unique<TestWebUIMessageHandler>(); |
| auto* test_handler = owned_test_handler.get(); |
| web_contents->GetWebUI()->AddMessageHandler(std::move(owned_test_handler)); |
| test_handler->AllowJavascriptForTesting(); |
| |
| // Push onto window.history. Back should now be an in-page navigation. |
| ASSERT_TRUE( |
| ExecJs(web_contents, |
| "window.history.pushState({}, '', location.href + '#foo')")); |
| shell()->GoBackOrForward(-1); |
| EXPECT_TRUE(WaitForLoadStop(web_contents)); |
| |
| // Test handler should still have JavaScript allowed after in-page navigation. |
| EXPECT_TRUE(test_handler->IsJavascriptAllowed()); |
| |
| shell()->Reload(); |
| EXPECT_TRUE(WaitForLoadStop(web_contents)); |
| |
| // Verify that after a reload, the test handler has been disallowed. |
| if (!ShouldCreateNewHostForAllFrames()) { |
| EXPECT_FALSE(test_handler->IsJavascriptAllowed()); |
| } else { |
| // If the RenderFrameHost and WebUI objects changed after navigation, the |
| // `TestWebUIMessageHandler` will point to a stale WebUI and we can't check |
| // the `IsJavascriptAllowed()` value there. So use a new handler here and |
| // check the value on the new handler instead. |
| auto owned_test_handler2 = std::make_unique<TestWebUIMessageHandler>(); |
| auto* test_handler2 = owned_test_handler2.get(); |
| web_contents->GetWebUI()->AddMessageHandler(std::move(owned_test_handler2)); |
| EXPECT_FALSE(test_handler2->IsJavascriptAllowed()); |
| } |
| } |
| |
| // A WebUI message that should require a user gesture is ignored if there is no |
| // recent input event. |
| IN_PROC_BROWSER_TEST_F(WebUIRequiringGestureBrowserTest, |
| MessageRequiringGestureIgnoredIfNoGesture) { |
| SendMessageAndWaitForFinish(); |
| EXPECT_EQ(0, test_handler()->message_requiring_gesture_count()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebUIRequiringGestureBrowserTest, |
| MessageRequiringGestureIgnoresRendererOnlyGesture) { |
| // Note: this doesn't use SendMessageAndWaitForFinish() since this test needs |
| // to use a test-only helper to instantiate a scoped user gesture in the |
| // renderer. |
| main_rfh()->ExecuteJavaScriptWithUserGestureForTests( |
| u"chrome.send('messageRequiringGesture');" |
| u"chrome.send('notifyFinish');", |
| base::NullCallback(), ISOLATED_WORLD_ID_GLOBAL); |
| base::RunLoop run_loop; |
| test_handler()->set_finish_closure(run_loop.QuitClosure()); |
| run_loop.Run(); |
| EXPECT_EQ(0, test_handler()->message_requiring_gesture_count()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebUIRequiringGestureBrowserTest, |
| MessageRequiringGestureIgnoresNonInteractiveEvents) { |
| // Mouse enter / mouse move / mouse leave should not be considered input |
| // events that interact with the page. |
| content::SimulateMouseEvent(web_contents(), |
| blink::WebInputEvent::Type::kMouseEnter, |
| gfx::Point(50, 50)); |
| content::SimulateMouseEvent(web_contents(), |
| blink::WebInputEvent::Type::kMouseMove, |
| gfx::Point(50, 50)); |
| content::SimulateMouseEvent(web_contents(), |
| blink::WebInputEvent::Type::kMouseLeave, |
| gfx::Point(50, 50)); |
| // Nor should mouse wheel. |
| content::SimulateMouseWheelEvent(web_contents(), gfx::Point(50, 50), |
| gfx::Vector2d(0, 100), |
| blink::WebMouseWheelEvent::kPhaseBegan); |
| SendMessageAndWaitForFinish(); |
| EXPECT_EQ(0, test_handler()->message_requiring_gesture_count()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F( |
| WebUIRequiringGestureBrowserTest, |
| // TODO(crbug.com/40851657): Re-enable this test |
| DISABLED_MessageRequiringGestureAllowedWithInteractiveEvent) { |
| // Simulate a click at Now. |
| content::SimulateMouseClick(web_contents(), 0, |
| blink::WebMouseEvent::Button::kLeft); |
| |
| // Now+0 should be allowed. |
| SendMessageAndWaitForFinish(); |
| EXPECT_EQ(1, test_handler()->message_requiring_gesture_count()); |
| |
| // Now+5 seconds should be allowed. |
| AdvanceClock(base::Seconds(5)); |
| SendMessageAndWaitForFinish(); |
| EXPECT_EQ(2, test_handler()->message_requiring_gesture_count()); |
| |
| // Anything after that should be disallowed though. |
| AdvanceClock(base::Microseconds(1)); |
| SendMessageAndWaitForFinish(); |
| EXPECT_EQ(2, test_handler()->message_requiring_gesture_count()); |
| } |
| |
| // Verify that we can successfully navigate to a chrome-untrusted:// URL. |
| IN_PROC_BROWSER_TEST_F(WebUIImplBrowserTest, UntrustedSchemeLoads) { |
| content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig( |
| std::make_unique<ui::TestUntrustedWebUIConfig>("test-host")); |
| |
| const GURL untrusted_url(GetChromeUntrustedUIURL("test-host/title2.html")); |
| auto* web_contents = shell()->web_contents(); |
| EXPECT_TRUE(NavigateToURL(web_contents, untrusted_url)); |
| EXPECT_EQ(u"Title Of Awesomeness", web_contents->GetTitle()); |
| } |
| |
| // Verify that we can successfully navigate to a chrome-untrusted:// URL |
| // without a crash while WebUI::Send is being performed. |
| // TODO(crbug.com/40773523): Enable this test once a root cause is identified. |
| IN_PROC_BROWSER_TEST_F(WebUIImplBrowserTest, DISABLED_NavigateWhileWebUISend) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| auto* web_contents = shell()->web_contents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, GetWebUIURL(kChromeUIGpuHost))); |
| |
| auto owned_test_handler = std::make_unique<TestWebUIMessageHandler>(); |
| auto* test_handler = owned_test_handler.get(); |
| web_contents->GetWebUI()->AddMessageHandler(std::move(owned_test_handler)); |
| |
| auto* webui = static_cast<WebUIImpl*>(web_contents->GetWebUI()); |
| EXPECT_EQ(web_contents->GetPrimaryMainFrame(), webui->GetRenderFrameHost()); |
| |
| test_handler->set_finish_closure(base::BindLambdaForTesting([&]() { |
| EXPECT_NE(web_contents->GetPrimaryMainFrame(), webui->GetRenderFrameHost()); |
| })); |
| |
| bool received_send_message = false; |
| test_handler->set_send_message_closure( |
| base::BindLambdaForTesting([&]() { received_send_message = true; })); |
| |
| base::RunLoop run_loop; |
| web_contents->GetPrimaryMainFrame()->ExecuteJavaScriptForTests( |
| u"onunload=function() { chrome.send('sendMessage')}", |
| base::BindOnce([](base::OnceClosure callback, |
| base::Value) { std::move(callback).Run(); }, |
| run_loop.QuitClosure()), |
| ISOLATED_WORLD_ID_GLOBAL); |
| run_loop.Run(); |
| |
| RenderFrameDeletedObserver delete_observer( |
| web_contents->GetPrimaryMainFrame()); |
| EXPECT_TRUE(NavigateToURL( |
| web_contents, embedded_test_server()->GetURL("/simple_page.html"))); |
| delete_observer.WaitUntilDeleted(); |
| |
| EXPECT_TRUE(received_send_message); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebUIImplBrowserTest, CoopCoepPolicies) { |
| auto* web_contents = shell()->web_contents(); |
| |
| TestUntrustedDataSourceHeaders headers; |
| headers.cross_origin_opener_policy = |
| network::mojom::CrossOriginOpenerPolicyValue::kSameOriginPlusCoep; |
| content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig( |
| std::make_unique<ui::TestUntrustedWebUIConfig>("isolated", headers)); |
| |
| const GURL isolated_url(GetChromeUntrustedUIURL("isolated/title2.html")); |
| ASSERT_TRUE(NavigateToURL(web_contents, isolated_url)); |
| |
| auto* main_frame = web_contents->GetPrimaryMainFrame(); |
| EXPECT_EQ(true, EvalJs(main_frame, "window.crossOriginIsolated;", |
| EXECUTE_SCRIPT_DEFAULT_OPTIONS, 1 /* world_id */)); |
| } |
| |
| // Regression test for: https://crbug.com/1308391 |
| // Check content/ supports its embedders closing WebContent during WebUI |
| // destruction, after the RenderFrameHost owning it has unloaded. |
| IN_PROC_BROWSER_TEST_F(WebUIImplBrowserTest, |
| SynchronousWebContentDeletionInUnload) { |
| static std::unique_ptr<WebContents> web_contents; |
| web_contents = WebContents::Create( |
| WebContents::CreateParams(shell()->web_contents()->GetBrowserContext())); |
| // Install a WebUI. When destroyed, it executes a callback releasing the |
| // WebContent. |
| class Config : public WebUIConfig { |
| public: |
| Config() : WebUIConfig(kChromeUIUntrustedScheme, "test-host") {} |
| std::unique_ptr<WebUIController> CreateWebUIController( |
| WebUI* web_ui, |
| const GURL& url) final { |
| class Controller : public WebUIController { |
| public: |
| explicit Controller(WebUI* web_ui) : WebUIController(web_ui) { |
| AddUntrustedDataSource(web_contents->GetBrowserContext(), |
| "test-host"); |
| } |
| ~Controller() override { web_contents.reset(); } |
| }; |
| return std::make_unique<Controller>(web_ui); |
| } |
| }; |
| |
| content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig( |
| std::make_unique<Config>()); |
| ASSERT_TRUE(NavigateToURL(web_contents.get(), |
| GetChromeUntrustedUIURL("test-host/title1.html"))); |
| RenderFrameHost* main_rfh = web_contents->GetPrimaryMainFrame(); |
| RenderFrameDeletedObserver rfh_deleted(web_contents->GetPrimaryMainFrame()); |
| RenderFrameDeletedObserver delete_observer(main_rfh); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL dummy_url = embedded_test_server()->GetURL("/simple_page.html"); |
| web_contents->GetController().LoadURL( |
| dummy_url, content::Referrer(), ui::PAGE_TRANSITION_TYPED, std::string()); |
| delete_observer.WaitUntilDeleted(); |
| ASSERT_FALSE(web_contents); |
| } |
| |
| class WebUIRequestSchemesTest : public ContentBrowserTest { |
| public: |
| WebUIRequestSchemesTest() = default; |
| |
| WebUIRequestSchemesTest(const WebUIRequestSchemesTest&) = delete; |
| |
| WebUIRequestSchemesTest& operator=(const WebUIRequestSchemesTest&) = delete; |
| |
| TestWebUIControllerFactory* factory() { return &factory_; } |
| |
| private: |
| TestWebUIControllerFactory factory_; |
| ScopedWebUIControllerFactoryRegistration factory_registration_{&factory_}; |
| }; |
| |
| // Verify that by default WebUI's child process security policy can request |
| // default schemes such as chrome. |
| // |
| // ChildProcessSecurityPolicy::CanRequestURL() always returns true for the |
| // following schemes, but in practice there are other checks that stop WebUIs |
| // from accessing these schemes. |
| IN_PROC_BROWSER_TEST_F(WebUIRequestSchemesTest, DefaultSchemesCanBeRequested) { |
| auto* web_contents = shell()->web_contents(); |
| |
| std::string host_and_path = "test-host/title2.html"; |
| const GURL chrome_url(GetWebUIURL(host_and_path)); |
| GURL url; |
| |
| std::vector<std::string> requestable_schemes = { |
| // WebSafe Schemes: |
| url::kHttpScheme, url::kHttpsScheme, url::kDataScheme, url::kWsScheme, |
| url::kWssScheme, |
| // Default added as requestable schemes: |
| url::kFileScheme, kChromeUIScheme}; |
| |
| std::vector<std::string> unrequestable_schemes = { |
| kChromeDevToolsScheme, url::kBlobScheme, kChromeUIUntrustedScheme, |
| base::StrCat({url::kFileSystemScheme, ":", kChromeUIUntrustedScheme})}; |
| |
| ASSERT_TRUE(NavigateToURL(web_contents, chrome_url)); |
| |
| for (const auto& requestable_scheme : requestable_schemes) { |
| url = GURL(base::StrCat( |
| {requestable_scheme, url::kStandardSchemeSeparator, host_and_path})); |
| EXPECT_TRUE(ChildProcessSecurityPolicy::GetInstance()->CanRequestURL( |
| web_contents->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID(), |
| url)); |
| } |
| |
| for (const auto& unrequestable_scheme : unrequestable_schemes) { |
| url = GURL(base::StrCat( |
| {unrequestable_scheme, url::kStandardSchemeSeparator, host_and_path})); |
| EXPECT_FALSE(ChildProcessSecurityPolicy::GetInstance()->CanRequestURL( |
| web_contents->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID(), |
| url)); |
| } |
| } |
| |
| // Verify that we can successfully allow non-default URL schemes to |
| // be requested by the WebUI's child process security policy. |
| IN_PROC_BROWSER_TEST_F(WebUIRequestSchemesTest, |
| AllowAdditionalSchemesToBeRequested) { |
| auto* web_contents = shell()->web_contents(); |
| |
| std::string host_and_path = "test-host/title2.html"; |
| GURL url; |
| |
| // All URLs with a web safe scheme, or with a scheme not |
| // handled by ContentBrowserClient are requestable. All other schemes are |
| // not requestable. |
| std::vector<std::string> requestable_schemes = { |
| // WebSafe schemes: |
| url::kHttpScheme, |
| url::kHttpsScheme, |
| url::kDataScheme, |
| url::kWsScheme, |
| url::kWssScheme, |
| // Default added as requestable schemes: |
| "file", |
| kChromeUIScheme, |
| // Schemes given requestable access: |
| kChromeUIUntrustedScheme, |
| base::StrCat({url::kFileSystemScheme, ":", kChromeUIUntrustedScheme}), |
| }; |
| std::vector<std::string> unrequestable_schemes = { |
| kChromeDevToolsScheme, url::kBlobScheme, |
| base::StrCat({url::kFileSystemScheme, ":", kChromeDevToolsScheme})}; |
| |
| const GURL chrome_ui_url = GetWebUIURL(base::StrCat( |
| {host_and_path, "?requestableschemes=", kChromeUIUntrustedScheme, ",", |
| url::kWsScheme})); |
| |
| ASSERT_TRUE(NavigateToURL(web_contents, chrome_ui_url)); |
| |
| for (const auto& requestable_scheme : requestable_schemes) { |
| url = GURL(base::StrCat( |
| {requestable_scheme, url::kStandardSchemeSeparator, host_and_path})); |
| EXPECT_TRUE(ChildProcessSecurityPolicy::GetInstance()->CanRequestURL( |
| web_contents->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID(), |
| url)); |
| } |
| |
| for (const auto& unrequestable_scheme : unrequestable_schemes) { |
| url = GURL(base::StrCat( |
| {unrequestable_scheme, url::kStandardSchemeSeparator, host_and_path})); |
| EXPECT_FALSE(ChildProcessSecurityPolicy::GetInstance()->CanRequestURL( |
| web_contents->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID(), |
| url)); |
| } |
| } |
| |
| class WebUIWorkerTest : public ContentBrowserTest { |
| public: |
| WebUIWorkerTest() = default; |
| |
| WebUIWorkerTest(const WebUIWorkerTest&) = delete; |
| |
| WebUIWorkerTest& operator=(const WebUIWorkerTest&) = delete; |
| |
| protected: |
| void SetUntrustedWorkerSrcToWebUIConfig(bool allow_embedded_frame) { |
| TestUntrustedDataSourceHeaders headers; |
| |
| if (allow_embedded_frame) { |
| // Allow the frame to be embedded in the chrome main page. |
| headers.frame_ancestors.emplace().push_back("chrome://trusted"); |
| } |
| |
| // These two lines are to avoid: |
| // "TypeError: Failed to construct 'SharedWorker': This document requires |
| // 'TrustedScriptURL' assignment." |
| headers.script_src = "worker-src chrome-untrusted://untrusted;"; |
| headers.no_trusted_types = true; |
| |
| content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig( |
| std::make_unique<ui::TestUntrustedWebUIConfig>("untrusted", headers)); |
| if (allow_embedded_frame) { |
| AddUntrustedDataSource(shell()->web_contents()->GetBrowserContext(), |
| "untrusted", headers); |
| } |
| } |
| |
| EvalJsResult RunWorkerTest(const GURL& page_url, |
| const GURL& worker_url, |
| const std::string& worker_script) { |
| auto* web_contents = shell()->web_contents(); |
| EXPECT_TRUE(NavigateToURL(web_contents, page_url)); |
| |
| return EvalJs(web_contents, |
| JsReplace(worker_script, worker_url.spec().c_str()), |
| EXECUTE_SCRIPT_DEFAULT_OPTIONS, 1 /* world_id */); |
| } |
| |
| private: |
| TestWebUIControllerFactory factory_; |
| content::ScopedWebUIControllerFactoryRegistration factory_registration_{ |
| &factory_}; |
| }; |
| |
| // TODO(crbug.com/40290702): Shared workers are not available on Android. |
| #if !BUILDFLAG(IS_ANDROID) |
| // Verify that we can create SharedWorker with scheme "chrome://" under |
| // WebUI page. |
| IN_PROC_BROWSER_TEST_F(WebUIWorkerTest, CanCreateWebUISharedWorkerForWebUI) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_EQ(true, RunWorkerTest( |
| GetWebUIURL("test-host/title2.html?notrustedtypes=true"), |
| GetWebUIURL("test-host/web_ui_shared_worker.js"), |
| kLoadSharedWorkerScript)); |
| } |
| |
| // Verify that pages with scheme other than "chrome://" cannot create |
| // SharedWorker with scheme "chrome://". |
| IN_PROC_BROWSER_TEST_F(WebUIWorkerTest, |
| CannotCreateWebUISharedWorkerForNonWebUI) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EvalJsResult result = RunWorkerTest( |
| embedded_test_server()->GetURL("/title1.html?notrustedtypes=true"), |
| GetWebUIURL("test-host/web_ui_shared_worker.js"), |
| kLoadSharedWorkerScript); |
| |
| std::string expected_failure = |
| "a JavaScript error: \"SecurityError: Failed to construct 'SharedWorker'"; |
| EXPECT_THAT(result, |
| EvalJsResult::ErrorIs(::testing::StartsWith(expected_failure))); |
| } |
| |
| // Test that we can start a Shared Worker from a chrome-untrusted:// iframe. |
| IN_PROC_BROWSER_TEST_F(WebUIWorkerTest, |
| CanCreateSharedWorkerFromUntrustedIframe) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| auto* web_contents = shell()->web_contents(); |
| |
| SetUntrustedWorkerSrcToWebUIConfig(/*allow_embedded_frame=*/true); |
| |
| // Set up the urls. |
| const GURL web_ui_url( |
| GetWebUIURL("trusted/" |
| "title2.html?notrustedtypes=true&requestableschemes=chrome-" |
| "untrusted&childsrc=")); |
| const GURL untrusted_iframe_url( |
| GetChromeUntrustedUIURL("untrusted/title1.html")); |
| const GURL untrusted_worker_url( |
| GetChromeUntrustedUIURL("untrusted/web_ui_shared_worker.js")); |
| |
| // Navigate to a chrome:// main page. |
| EXPECT_TRUE(NavigateToURL(web_contents, web_ui_url)); |
| auto* main_frame = web_contents->GetPrimaryMainFrame(); |
| // Add an iframe in chrome-untrusted://. |
| EXPECT_EQ(true, |
| EvalJs(main_frame, |
| JsReplace("var frame = document.createElement('iframe');\n" |
| "frame.src=$1;\n" |
| "!!document.body.appendChild(frame);\n", |
| untrusted_iframe_url.spec().c_str()), |
| EXECUTE_SCRIPT_DEFAULT_OPTIONS, 1 /* world_id */)); |
| EXPECT_TRUE(WaitForLoadStop(web_contents)); |
| |
| // Get the chrome-untrusted:// iframe. |
| RenderFrameHost* child = ChildFrameAt(main_frame, 0); |
| EXPECT_EQ(untrusted_iframe_url, child->GetLastCommittedURL()); |
| |
| // Start a shared worker from the chrome-untrusted iframe. |
| EXPECT_EQ(true, EvalJs(child, |
| JsReplace(kLoadSharedWorkerScript, |
| untrusted_worker_url.spec().c_str()), |
| EXECUTE_SCRIPT_DEFAULT_OPTIONS, 1 /* world_id */)); |
| } |
| |
| // Test that we can create a shared worker from a chrome-untrusted:// main |
| // frame. |
| IN_PROC_BROWSER_TEST_F(WebUIWorkerTest, |
| CanCreateUntrustedWebUISharedWorkerForUntrustedWebUI) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| SetUntrustedWorkerSrcToWebUIConfig(/*allow_embedded_frame=*/false); |
| |
| const GURL untrusted_page_url( |
| GetChromeUntrustedUIURL("untrusted/title2.html")); |
| |
| EXPECT_EQ(true, RunWorkerTest(untrusted_page_url, |
| GetChromeUntrustedUIURL( |
| "untrusted/web_ui_shared_worker.js"), |
| kLoadSharedWorkerScript)); |
| EXPECT_EQ(untrusted_page_url, shell()->web_contents()->GetLastCommittedURL()); |
| } |
| |
| // Verify that chrome:// pages cannot create a SharedWorker with scheme |
| // "chrome-untrusted://". |
| IN_PROC_BROWSER_TEST_F(WebUIWorkerTest, |
| CannotCreateUntrustedWebUISharedWorkerFromTrustedWebUI) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EvalJsResult result = RunWorkerTest( |
| GetWebUIURL("trusted/title2.html?notrustedtypes=true"), |
| GetChromeUntrustedUIURL("untrusted/web_ui_shared_worker.js"), |
| kLoadSharedWorkerScript); |
| |
| std::string expected_failure = |
| "a JavaScript error: \"SecurityError: Failed to construct " |
| "'SharedWorker': " |
| "Script at 'chrome-untrusted://untrusted/web_ui_shared_worker.js' cannot " |
| "be accessed from origin 'chrome://trusted'"; |
| EXPECT_THAT(result, |
| EvalJsResult::ErrorIs(::testing::StartsWith(expected_failure))); |
| } |
| |
| // Verify that pages with scheme other than "chrome-untrusted://" cannot create |
| // a SharedWorker with scheme "chrome-untrusted://". |
| IN_PROC_BROWSER_TEST_F(WebUIWorkerTest, |
| CannotCreateUntrustedWebUISharedWorkerForWebURL) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EvalJsResult result = RunWorkerTest( |
| embedded_test_server()->GetURL("localhost", |
| "/title1.html?notrustedtypes=true"), |
| GetChromeUntrustedUIURL("untrusted/web_ui_shared_worker.js"), |
| kLoadSharedWorkerScript); |
| |
| std::string expected_failure = |
| "a JavaScript error: \"SecurityError: Failed to construct " |
| "'SharedWorker': " |
| "Script at 'chrome-untrusted://untrusted/web_ui_shared_worker.js' cannot " |
| "be accessed from origin 'http://localhost"; |
| EXPECT_THAT(result, |
| EvalJsResult::ErrorIs(::testing::StartsWith(expected_failure))); |
| } |
| |
| // Verify that pages with scheme "chrome-untrusted://" cannot create a |
| // SharedWorker with scheme "chrome://". |
| IN_PROC_BROWSER_TEST_F(WebUIWorkerTest, |
| CannotCreateWebUISharedWorkerForUntrustedPage) { |
| SetUntrustedWorkerSrcToWebUIConfig(/*allow_embedded_frame=*/false); |
| |
| EvalJsResult result = RunWorkerTest( |
| GetChromeUntrustedUIURL("untrusted/title2.html?notrustedtypes=true"), |
| GetWebUIURL("trusted/web_ui_shared_worker.js"), kLoadSharedWorkerScript); |
| |
| std::string expected_failure = |
| "a JavaScript error: \"SecurityError: Failed to construct " |
| "'SharedWorker': Script " |
| "at 'chrome://trusted/web_ui_shared_worker.js' cannot be accessed from " |
| "origin 'chrome-untrusted://untrusted'."; |
| EXPECT_THAT(result, |
| EvalJsResult::ErrorIs(::testing::StartsWith(expected_failure))); |
| } |
| |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| // Verify that we can create a Worker with scheme "chrome://" under WebUI page. |
| IN_PROC_BROWSER_TEST_F(WebUIWorkerTest, |
| CanCreateWebUIDedicatedWorkerForWebUI) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_EQ(true, |
| RunWorkerTest( |
| GURL(GetWebUIURL("test-host/title2.html?notrustedtypes=true")), |
| GURL(GetWebUIURL("test-host/web_ui_dedicated_worker.js")), |
| kLoadDedicatedWorkerScript)); |
| } |
| |
| // Verify that pages with scheme other than "chrome://" cannot create a Worker |
| // with scheme "chrome://". |
| IN_PROC_BROWSER_TEST_F(WebUIWorkerTest, |
| CannotCreateWebUIDedicatedWorkerForNonWebUI) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EvalJsResult result = RunWorkerTest( |
| GURL(embedded_test_server()->GetURL("/title1.html?notrustedtypes=true")), |
| GURL(GetWebUIURL("test-host/web_ui_dedicated_worker.js")), |
| kLoadDedicatedWorkerScript); |
| |
| std::string expected_failure = |
| "a JavaScript error: \"SecurityError: Failed to construct 'Worker'"; |
| EXPECT_THAT(result, |
| EvalJsResult::ErrorIs(::testing::StartsWith(expected_failure))); |
| } |
| |
| // Test that we can start a Worker from a chrome-untrusted:// iframe. |
| IN_PROC_BROWSER_TEST_F(WebUIWorkerTest, |
| CanCreateDedicatedWorkerFromUntrustedIframe) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| auto* web_contents = shell()->web_contents(); |
| |
| SetUntrustedWorkerSrcToWebUIConfig(/*allow_embedded_frame=*/true); |
| |
| // Set up the urls. |
| const GURL web_ui_url( |
| GetWebUIURL("trusted/" |
| "title2.html?notrustedtypes=true&requestableschemes=chrome-" |
| "untrusted&childsrc=")); |
| const GURL untrusted_iframe_url( |
| GetChromeUntrustedUIURL("untrusted/title1.html")); |
| const GURL untrusted_worker_url( |
| GetChromeUntrustedUIURL("untrusted/web_ui_dedicated_worker.js")); |
| |
| // Navigate to a chrome:// main page. |
| EXPECT_TRUE(NavigateToURL(web_contents, web_ui_url)); |
| auto* main_frame = web_contents->GetPrimaryMainFrame(); |
| // Add an iframe in chrome-untrusted://. |
| EXPECT_EQ(true, |
| EvalJs(main_frame, |
| JsReplace("var frame = document.createElement('iframe');\n" |
| "frame.src=$1;\n" |
| "!!document.body.appendChild(frame);\n", |
| untrusted_iframe_url.spec().c_str()), |
| EXECUTE_SCRIPT_DEFAULT_OPTIONS, 1 /* world_id */)); |
| EXPECT_TRUE(WaitForLoadStop(web_contents)); |
| |
| // Get the chrome-untrusted:// iframe. |
| RenderFrameHost* child = ChildFrameAt(main_frame, 0); |
| EXPECT_EQ(untrusted_iframe_url, child->GetLastCommittedURL()); |
| |
| // Start a worker from the chrome-untrusted iframe. |
| EXPECT_EQ(true, EvalJs(child, |
| JsReplace(kLoadDedicatedWorkerScript, |
| untrusted_worker_url.spec().c_str()), |
| EXECUTE_SCRIPT_DEFAULT_OPTIONS, 1 /* world_id */)); |
| } |
| |
| // Test that we can create a Worker from a chrome-untrusted:// main frame. |
| IN_PROC_BROWSER_TEST_F( |
| WebUIWorkerTest, |
| CanCreateUntrustedWebUIDedicatedWorkerForUntrustedWebUI) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| SetUntrustedWorkerSrcToWebUIConfig(/*allow_embedded_frame=*/false); |
| |
| EXPECT_EQ(true, |
| RunWorkerTest( |
| GetChromeUntrustedUIURL("untrusted/title2.html"), |
| GetChromeUntrustedUIURL("untrusted/web_ui_dedicated_worker.js"), |
| kLoadDedicatedWorkerScript)); |
| } |
| |
| // Verify that chrome:// pages cannot create a Worker with scheme |
| // "chrome-untrusted://". |
| IN_PROC_BROWSER_TEST_F( |
| WebUIWorkerTest, |
| CannotCreateUntrustedWebUIDedicatedWorkerFromTrustedWebUI) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EvalJsResult result = RunWorkerTest( |
| GetWebUIURL("trusted/title2.html?notrustedtypes=true"), |
| GetChromeUntrustedUIURL("untrusted/web_ui_dedicated_worker.js"), |
| kLoadDedicatedWorkerScript); |
| |
| std::string expected_failure = |
| "a JavaScript error: \"SecurityError: Failed to construct 'Worker': " |
| "Script at 'chrome-untrusted://untrusted/web_ui_dedicated_worker.js' " |
| "cannot be accessed from origin 'chrome://trusted'"; |
| EXPECT_THAT(result, |
| EvalJsResult::ErrorIs(::testing::StartsWith(expected_failure))); |
| } |
| |
| // Verify that pages with scheme other than "chrome-untrusted://" cannot create |
| // a Worker with scheme "chrome-untrusted://". |
| IN_PROC_BROWSER_TEST_F(WebUIWorkerTest, |
| CannotCreateUntrustedWebUIDedicatedWorkerForWebURL) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EvalJsResult result = RunWorkerTest( |
| embedded_test_server()->GetURL("localhost", |
| "/title1.html?notrustedtypes=true"), |
| GetChromeUntrustedUIURL("untrusted/web_ui_dedicated_worker.js"), |
| kLoadDedicatedWorkerScript); |
| |
| std::string expected_failure = |
| "a JavaScript error: \"SecurityError: Failed to construct 'Worker': " |
| "Script at 'chrome-untrusted://untrusted/web_ui_dedicated_worker.js' " |
| "cannot be accessed from origin 'http://localhost"; |
| EXPECT_THAT(result, |
| EvalJsResult::ErrorIs(::testing::StartsWith(expected_failure))); |
| } |
| |
| // Verify that pages with scheme "chrome-untrusted://" cannot create a Worker |
| // with scheme "chrome://". |
| IN_PROC_BROWSER_TEST_F(WebUIWorkerTest, |
| CannotCreateWebUIDedicatedWorkerForUntrustedPage) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| SetUntrustedWorkerSrcToWebUIConfig(/*allow_embedded_frame=*/false); |
| |
| EvalJsResult result = RunWorkerTest( |
| GetChromeUntrustedUIURL("untrusted/title2.html?notrustedtypes=true"), |
| GetWebUIURL("trusted/web_ui_dedicated_worker.js"), |
| kLoadDedicatedWorkerScript); |
| |
| std::string expected_failure = |
| "a JavaScript error: \"SecurityError: Failed to construct 'Worker': " |
| "Script " |
| "at 'chrome://trusted/web_ui_dedicated_worker.js' cannot be accessed " |
| "from origin 'chrome-untrusted://untrusted'."; |
| EXPECT_THAT(result, |
| EvalJsResult::ErrorIs(::testing::StartsWith(expected_failure))); |
| } |
| |
| } // namespace content |