blob: f45b0dc17a63499973ef4f1a0798c49717a87580 [file] [log] [blame]
// 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 "content/shell/browser/shell.h"
#include <stddef.h>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include "base/callback_helpers.h"
#include "base/command_line.h"
#include "base/location.h"
#include "base/macros.h"
#include "base/no_destructor.h"
#include "base/run_loop.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "build/build_config.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/picture_in_picture_window_controller.h"
#include "content/public/browser/presentation_receiver_flags.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/renderer_preferences_util.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "content/shell/app/resource.h"
#include "content/shell/browser/shell_content_browser_client.h"
#include "content/shell/browser/shell_devtools_frontend.h"
#include "content/shell/browser/shell_javascript_dialog_manager.h"
#include "content/shell/common/shell_switches.h"
#include "media/media_buildflags.h"
#include "third_party/blink/public/common/peerconnection/webrtc_ip_handling_policy.h"
#include "third_party/blink/public/common/renderer_preferences/renderer_preferences.h"
namespace content {
// Null until/unless the default main message loop is running.
base::NoDestructor<base::OnceClosure> g_quit_main_message_loop;
const int kDefaultTestWindowWidthDip = 800;
const int kDefaultTestWindowHeightDip = 600;
std::vector<Shell*> Shell::windows_;
base::OnceCallback<void(Shell*)> Shell::shell_created_callback_;
ShellPlatformDelegate* g_platform;
class Shell::DevToolsWebContentsObserver : public WebContentsObserver {
public:
DevToolsWebContentsObserver(Shell* shell, WebContents* web_contents)
: WebContentsObserver(web_contents),
shell_(shell) {
}
// WebContentsObserver
void WebContentsDestroyed() override {
shell_->OnDevToolsWebContentsDestroyed();
}
private:
Shell* shell_;
DISALLOW_COPY_AND_ASSIGN(DevToolsWebContentsObserver);
};
Shell::Shell(std::unique_ptr<WebContents> web_contents,
bool should_set_delegate)
: WebContentsObserver(web_contents.get()),
web_contents_(std::move(web_contents)) {
if (should_set_delegate)
web_contents_->SetDelegate(this);
if (!switches::IsRunWebTestsSwitchPresent()) {
UpdateFontRendererPreferencesFromSystemSettings(
web_contents_->GetMutableRendererPrefs());
}
windows_.push_back(this);
if (shell_created_callback_)
std::move(shell_created_callback_).Run(this);
}
Shell::~Shell() {
g_platform->CleanUp(this);
for (size_t i = 0; i < windows_.size(); ++i) {
if (windows_[i] == this) {
windows_.erase(windows_.begin() + i);
break;
}
}
// Always destroy WebContents before destroying ShellPlatformDelegate.
// WebContents destruction sequence may depend on the resources destroyed with
// ShellPlatformDelegate (e.g. the display::Screen singleton).
web_contents_->SetDelegate(nullptr);
web_contents_.reset();
if (windows_.empty()) {
delete g_platform;
g_platform = nullptr;
for (auto it = RenderProcessHost::AllHostsIterator(); !it.IsAtEnd();
it.Advance()) {
it.GetCurrentValue()->DisableKeepAliveRefCount();
}
if (*g_quit_main_message_loop)
std::move(*g_quit_main_message_loop).Run();
}
}
Shell* Shell::CreateShell(std::unique_ptr<WebContents> web_contents,
const gfx::Size& initial_size,
bool should_set_delegate) {
WebContents* raw_web_contents = web_contents.get();
Shell* shell = new Shell(std::move(web_contents), should_set_delegate);
g_platform->CreatePlatformWindow(shell, initial_size);
// Note: Do not make RenderFrameHost or RenderViewHost specific state changes
// here, because they will be forgotten after a cross-process navigation. Use
// RenderFrameCreated or RenderViewCreated instead.
if (switches::IsRunWebTestsSwitchPresent()) {
raw_web_contents->GetMutableRendererPrefs()->use_custom_colors = false;
raw_web_contents->SyncRendererPrefs();
}
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(switches::kForceWebRtcIPHandlingPolicy)) {
raw_web_contents->GetMutableRendererPrefs()->webrtc_ip_handling_policy =
command_line->GetSwitchValueASCII(
switches::kForceWebRtcIPHandlingPolicy);
}
g_platform->SetContents(shell);
g_platform->DidCreateOrAttachWebContents(shell, raw_web_contents);
// If the RenderFrame was created during WebContents construction (as happens
// for windows opened from the renderer) then the Shell won't hear about the
// main frame being created as a WebContentsObservers. This gives the delegate
// a chance to act on the main frame accordingly.
if (raw_web_contents->GetMainFrame()->IsRenderFrameCreated())
g_platform->MainFrameCreated(shell);
return shell;
}
void Shell::CloseAllWindows() {
DevToolsAgentHost::DetachAllClients();
std::vector<Shell*> open_windows(windows_);
for (Shell* open_window : open_windows)
open_window->Close();
DCHECK(windows_.empty());
// Pump the message loop to allow window teardown tasks to run.
base::RunLoop().RunUntilIdle();
}
void Shell::SetMainMessageLoopQuitClosure(base::OnceClosure quit_closure) {
*g_quit_main_message_loop = std::move(quit_closure);
}
void Shell::QuitMainMessageLoopForTesting() {
if (*g_quit_main_message_loop)
std::move(*g_quit_main_message_loop).Run();
}
void Shell::SetShellCreatedCallback(
base::OnceCallback<void(Shell*)> shell_created_callback) {
DCHECK(!shell_created_callback_);
shell_created_callback_ = std::move(shell_created_callback);
}
// static
bool Shell::ShouldHideToolbar() {
return base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kContentShellHideToolbar);
}
Shell* Shell::FromWebContents(WebContents* web_contents) {
for (Shell* window : windows_) {
if (window->web_contents() && window->web_contents() == web_contents) {
return window;
}
}
return nullptr;
}
void Shell::Initialize(std::unique_ptr<ShellPlatformDelegate> platform) {
g_platform = platform.release();
g_platform->Initialize(GetShellDefaultSize());
}
gfx::Size Shell::AdjustWindowSize(const gfx::Size& initial_size) {
if (!initial_size.IsEmpty())
return initial_size;
return GetShellDefaultSize();
}
Shell* Shell::CreateNewWindow(BrowserContext* browser_context,
const GURL& url,
const scoped_refptr<SiteInstance>& site_instance,
const gfx::Size& initial_size) {
WebContents::CreateParams create_params(browser_context, site_instance);
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kForcePresentationReceiverForTesting)) {
create_params.starting_sandbox_flags =
content::kPresentationReceiverSandboxFlags;
}
std::unique_ptr<WebContents> web_contents =
WebContents::Create(create_params);
Shell* shell =
CreateShell(std::move(web_contents), AdjustWindowSize(initial_size),
true /* should_set_delegate */);
if (!url.is_empty())
shell->LoadURL(url);
return shell;
}
void Shell::RenderFrameCreated(RenderFrameHost* frame_host) {
if (frame_host == web_contents_->GetMainFrame())
g_platform->MainFrameCreated(this);
}
void Shell::LoadURL(const GURL& url) {
LoadURLForFrame(
url, std::string(),
ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
ui::PAGE_TRANSITION_FROM_ADDRESS_BAR));
}
void Shell::LoadURLForFrame(const GURL& url,
const std::string& frame_name,
ui::PageTransition transition_type) {
NavigationController::LoadURLParams params(url);
params.frame_name = frame_name;
params.transition_type = transition_type;
web_contents_->GetController().LoadURLWithParams(params);
}
void Shell::LoadDataWithBaseURL(const GURL& url, const std::string& data,
const GURL& base_url) {
bool load_as_string = false;
LoadDataWithBaseURLInternal(url, data, base_url, load_as_string);
}
#if defined(OS_ANDROID)
void Shell::LoadDataAsStringWithBaseURL(const GURL& url,
const std::string& data,
const GURL& base_url) {
bool load_as_string = true;
LoadDataWithBaseURLInternal(url, data, base_url, load_as_string);
}
#endif
void Shell::LoadDataWithBaseURLInternal(const GURL& url,
const std::string& data,
const GURL& base_url,
bool load_as_string) {
#if !defined(OS_ANDROID)
DCHECK(!load_as_string); // Only supported on Android.
#endif
NavigationController::LoadURLParams params(GURL::EmptyGURL());
const std::string data_url_header = "data:text/html;charset=utf-8,";
if (load_as_string) {
params.url = GURL(data_url_header);
std::string data_url_as_string = data_url_header + data;
#if defined(OS_ANDROID)
params.data_url_as_string =
base::RefCountedString::TakeString(&data_url_as_string);
#endif
} else {
params.url = GURL(data_url_header + data);
}
params.load_type = NavigationController::LOAD_TYPE_DATA;
params.base_url_for_data_url = base_url;
params.virtual_url_for_data_url = url;
params.override_user_agent = NavigationController::UA_OVERRIDE_FALSE;
web_contents_->GetController().LoadURLWithParams(params);
}
void Shell::AddNewContents(WebContents* source,
std::unique_ptr<WebContents> new_contents,
const GURL& target_url,
WindowOpenDisposition disposition,
const gfx::Rect& initial_rect,
bool user_gesture,
bool* was_blocked) {
CreateShell(
std::move(new_contents), AdjustWindowSize(initial_rect.size()),
!delay_popup_contents_delegate_for_testing_ /* should_set_delegate */);
}
void Shell::GoBackOrForward(int offset) {
web_contents_->GetController().GoToOffset(offset);
}
void Shell::Reload() {
web_contents_->GetController().Reload(ReloadType::NORMAL, false);
}
void Shell::ReloadBypassingCache() {
web_contents_->GetController().Reload(ReloadType::BYPASSING_CACHE, false);
}
void Shell::Stop() {
web_contents_->Stop();
}
void Shell::UpdateNavigationControls(bool to_different_document) {
int current_index = web_contents_->GetController().GetCurrentEntryIndex();
int max_index = web_contents_->GetController().GetEntryCount() - 1;
g_platform->EnableUIControl(this, ShellPlatformDelegate::BACK_BUTTON,
current_index > 0);
g_platform->EnableUIControl(this, ShellPlatformDelegate::FORWARD_BUTTON,
current_index < max_index);
g_platform->EnableUIControl(
this, ShellPlatformDelegate::STOP_BUTTON,
to_different_document && web_contents_->IsLoading());
}
void Shell::ShowDevTools() {
if (!devtools_frontend_) {
devtools_frontend_ = ShellDevToolsFrontend::Show(web_contents());
devtools_observer_ = std::make_unique<DevToolsWebContentsObserver>(
this, devtools_frontend_->frontend_shell()->web_contents());
}
devtools_frontend_->Activate();
}
void Shell::CloseDevTools() {
if (!devtools_frontend_)
return;
devtools_observer_.reset();
devtools_frontend_->Close();
devtools_frontend_ = nullptr;
}
void Shell::ResizeWebContentForTests(const gfx::Size& content_size) {
g_platform->ResizeWebContent(this, content_size);
}
gfx::NativeView Shell::GetContentView() {
if (!web_contents_)
return nullptr;
return web_contents_->GetNativeView();
}
#if !defined(OS_ANDROID)
gfx::NativeWindow Shell::window() {
return g_platform->GetNativeWindow(this);
}
#endif
#if defined(OS_MAC)
void Shell::ActionPerformed(int control) {
switch (control) {
case IDC_NAV_BACK:
GoBackOrForward(-1);
break;
case IDC_NAV_FORWARD:
GoBackOrForward(1);
break;
case IDC_NAV_RELOAD:
Reload();
break;
case IDC_NAV_STOP:
Stop();
break;
}
}
void Shell::URLEntered(const std::string& url_string) {
if (!url_string.empty()) {
GURL url(url_string);
if (!url.has_scheme())
url = GURL("http://" + url_string);
LoadURL(url);
}
}
#endif
WebContents* Shell::OpenURLFromTab(WebContents* source,
const OpenURLParams& params) {
WebContents* target = nullptr;
switch (params.disposition) {
case WindowOpenDisposition::CURRENT_TAB:
target = source;
break;
// Normally, the difference between NEW_POPUP and NEW_WINDOW is that a popup
// should have no toolbar, no status bar, no menu bar, no scrollbars and be
// not resizable. For simplicity and to enable new testing scenarios in
// content shell and web tests, popups don't get special treatment below
// (i.e. they will have a toolbar and other things described here).
case WindowOpenDisposition::NEW_POPUP:
case WindowOpenDisposition::NEW_WINDOW:
// content_shell doesn't really support tabs, but some web tests use
// middle click (which translates into kNavigationPolicyNewBackgroundTab),
// so we treat the cases below just like a NEW_WINDOW disposition.
case WindowOpenDisposition::NEW_BACKGROUND_TAB:
case WindowOpenDisposition::NEW_FOREGROUND_TAB: {
Shell* new_window =
Shell::CreateNewWindow(source->GetBrowserContext(),
GURL(), // Don't load anything just yet.
params.source_site_instance,
gfx::Size()); // Use default size.
target = new_window->web_contents();
break;
}
// No tabs in content_shell:
case WindowOpenDisposition::SINGLETON_TAB:
// No incognito mode in content_shell:
case WindowOpenDisposition::OFF_THE_RECORD:
// TODO(lukasza): Investigate if some web tests might need support for
// SAVE_TO_DISK disposition. This would probably require that
// WebTestControlHost always sets up and cleans up a temporary directory
// as the default downloads destinations for the duration of a test.
case WindowOpenDisposition::SAVE_TO_DISK:
// Ignoring requests with disposition == IGNORE_ACTION...
case WindowOpenDisposition::IGNORE_ACTION:
default:
return nullptr;
}
target->GetController().LoadURLWithParams(
NavigationController::LoadURLParams(params));
return target;
}
void Shell::LoadingStateChanged(WebContents* source,
bool to_different_document) {
UpdateNavigationControls(to_different_document);
g_platform->SetIsLoading(this, source->IsLoading());
}
#if defined(OS_ANDROID)
void Shell::SetOverlayMode(bool use_overlay_mode) {
g_platform->SetOverlayMode(this, use_overlay_mode);
}
#endif
void Shell::EnterFullscreenModeForTab(
RenderFrameHost* requesting_frame,
const blink::mojom::FullscreenOptions& options) {
ToggleFullscreenModeForTab(WebContents::FromRenderFrameHost(requesting_frame),
true);
}
void Shell::ExitFullscreenModeForTab(WebContents* web_contents) {
ToggleFullscreenModeForTab(web_contents, false);
}
void Shell::ToggleFullscreenModeForTab(WebContents* web_contents,
bool enter_fullscreen) {
#if defined(OS_ANDROID)
g_platform->ToggleFullscreenModeForTab(this, web_contents, enter_fullscreen);
#endif
if (is_fullscreen_ != enter_fullscreen) {
is_fullscreen_ = enter_fullscreen;
web_contents->GetMainFrame()
->GetRenderViewHost()
->GetWidget()
->SynchronizeVisualProperties();
}
}
bool Shell::IsFullscreenForTabOrPending(const WebContents* web_contents) {
#if defined(OS_ANDROID)
return g_platform->IsFullscreenForTabOrPending(this, web_contents);
#else
return is_fullscreen_;
#endif
}
blink::mojom::DisplayMode Shell::GetDisplayMode(
const WebContents* web_contents) {
// TODO: should return blink::mojom::DisplayModeFullscreen wherever user puts
// a browser window into fullscreen (not only in case of renderer-initiated
// fullscreen mode): crbug.com/476874.
return IsFullscreenForTabOrPending(web_contents)
? blink::mojom::DisplayMode::kFullscreen
: blink::mojom::DisplayMode::kBrowser;
}
void Shell::RequestToLockMouse(WebContents* web_contents,
bool user_gesture,
bool last_unlocked_by_target) {
// Give the platform a chance to handle the lock request, if it doesn't
// indicate it handled it, allow the request.
if (!g_platform->HandleRequestToLockMouse(this, web_contents, user_gesture,
last_unlocked_by_target)) {
web_contents->GotResponseToLockMouseRequest(
blink::mojom::PointerLockResult::kSuccess);
}
}
void Shell::Close() {
// Shell is "self-owned" and destroys itself. The ShellPlatformDelegate
// has the chance to co-opt this and do its own destruction.
if (!g_platform->DestroyShell(this))
delete this;
}
void Shell::CloseContents(WebContents* source) {
Close();
}
bool Shell::CanOverscrollContent() {
#if defined(USE_AURA)
return true;
#else
return false;
#endif
}
void Shell::NavigationStateChanged(WebContents* source,
InvalidateTypes changed_flags) {
if (changed_flags & INVALIDATE_TYPE_URL)
g_platform->SetAddressBarURL(this, source->GetVisibleURL());
}
JavaScriptDialogManager* Shell::GetJavaScriptDialogManager(
WebContents* source) {
if (!dialog_manager_)
dialog_manager_ = g_platform->CreateJavaScriptDialogManager(this);
if (!dialog_manager_)
dialog_manager_ = std::make_unique<ShellJavaScriptDialogManager>();
return dialog_manager_.get();
}
#if defined(OS_MAC)
void Shell::DidNavigateMainFramePostCommit(WebContents* contents) {
g_platform->DidNavigateMainFramePostCommit(this, contents);
}
bool Shell::HandleKeyboardEvent(WebContents* source,
const NativeWebKeyboardEvent& event) {
return g_platform->HandleKeyboardEvent(this, source, event);
}
#endif
bool Shell::DidAddMessageToConsole(WebContents* source,
blink::mojom::ConsoleMessageLevel log_level,
const std::u16string& message,
int32_t line_no,
const std::u16string& source_id) {
return switches::IsRunWebTestsSwitchPresent();
}
void Shell::PortalWebContentsCreated(WebContents* portal_web_contents) {
g_platform->DidCreateOrAttachWebContents(this, portal_web_contents);
}
void Shell::RendererUnresponsive(
WebContents* source,
RenderWidgetHost* render_widget_host,
base::RepeatingClosure hang_monitor_restarter) {
LOG(WARNING) << "renderer unresponsive";
}
void Shell::ActivateContents(WebContents* contents) {
#if !defined(OS_MAC)
// TODO(danakj): Move this to ShellPlatformDelegate.
contents->Focus();
#else
// Mac headless mode is quite different than other platforms. Normally
// focusing the WebContents would cause the OS to focus the window. Because
// headless mac doesn't actually have system windows, we can't go down the
// normal path and have to fake it out in the browser process.
g_platform->ActivateContents(this, contents);
#endif
}
bool Shell::IsBackForwardCacheSupported() {
return true;
}
std::unique_ptr<WebContents> Shell::ActivatePortalWebContents(
WebContents* predecessor_contents,
std::unique_ptr<WebContents> portal_contents) {
DCHECK_EQ(predecessor_contents, web_contents_.get());
portal_contents->SetDelegate(this);
web_contents_->SetDelegate(nullptr);
std::swap(web_contents_, portal_contents);
g_platform->SetContents(this);
g_platform->SetAddressBarURL(this, web_contents_->GetVisibleURL());
LoadingStateChanged(web_contents_.get(), true);
return portal_contents;
}
namespace {
class PendingCallback : public base::RefCounted<PendingCallback> {
public:
explicit PendingCallback(base::OnceCallback<void()> cb)
: callback_(std::move(cb)) {}
private:
friend class base::RefCounted<PendingCallback>;
~PendingCallback() { std::move(callback_).Run(); }
base::OnceCallback<void()> callback_;
};
} // namespace
void Shell::UpdateInspectedWebContentsIfNecessary(
content::WebContents* old_contents,
content::WebContents* new_contents,
base::OnceCallback<void()> callback) {
scoped_refptr<PendingCallback> pending_callback =
base::MakeRefCounted<PendingCallback>(std::move(callback));
for (auto* shell_devtools_bindings :
ShellDevToolsBindings::GetInstancesForWebContents(old_contents)) {
shell_devtools_bindings->UpdateInspectedWebContents(
new_contents,
base::BindOnce(base::DoNothing::Once<scoped_refptr<PendingCallback>>(),
pending_callback));
}
}
bool Shell::ShouldAllowRunningInsecureContent(WebContents* web_contents,
bool allowed_per_prefs,
const url::Origin& origin,
const GURL& resource_url) {
if (allowed_per_prefs)
return true;
return g_platform->ShouldAllowRunningInsecureContent(this);
}
PictureInPictureResult Shell::EnterPictureInPicture(
WebContents* web_contents,
const viz::SurfaceId& surface_id,
const gfx::Size& natural_size) {
// During tests, returning success to pretend the window was created and allow
// tests to run accordingly.
if (!switches::IsRunWebTestsSwitchPresent())
return PictureInPictureResult::kNotSupported;
return PictureInPictureResult::kSuccess;
}
bool Shell::ShouldResumeRequestsForCreatedWindow() {
return !delay_popup_contents_delegate_for_testing_;
}
void Shell::SetContentsBounds(WebContents* source, const gfx::Rect& bounds) {
DCHECK(source == web_contents()); // There's only one WebContents per Shell.
if (switches::IsRunWebTestsSwitchPresent()) {
// Note that chrome drops these requests on normal windows.
// TODO(danakj): The position is dropped here but we use the size. Web tests
// can't move the window in headless mode anyways, but maybe we should be
// letting them pretend?
g_platform->ResizeWebContent(this, bounds.size());
}
}
gfx::Size Shell::GetShellDefaultSize() {
static gfx::Size default_shell_size; // Only go through this method once.
if (!default_shell_size.IsEmpty())
return default_shell_size;
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(switches::kContentShellHostWindowSize)) {
const std::string size_str = command_line->GetSwitchValueASCII(
switches::kContentShellHostWindowSize);
int width, height;
if (sscanf(size_str.c_str(), "%dx%d", &width, &height) == 2) {
default_shell_size = gfx::Size(width, height);
} else {
LOG(ERROR) << "Invalid size \"" << size_str << "\" given to --"
<< switches::kContentShellHostWindowSize;
}
}
if (default_shell_size.IsEmpty()) {
default_shell_size = gfx::Size(
kDefaultTestWindowWidthDip, kDefaultTestWindowHeightDip);
}
return default_shell_size;
}
#if defined(OS_ANDROID)
void Shell::LoadProgressChanged(double progress) {
g_platform->LoadProgressChanged(this, progress);
}
#endif
void Shell::TitleWasSet(NavigationEntry* entry) {
if (entry)
g_platform->SetTitle(this, entry->GetTitle());
}
void Shell::OnDevToolsWebContentsDestroyed() {
devtools_observer_.reset();
devtools_frontend_ = nullptr;
}
} // namespace content