blob: 29ad5c99042755855a16fe6e6b84475e4eb926a7 [file] [log] [blame]
// Copyright 2018 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 "fuchsia/engine/browser/frame_impl.h"
#include <limits>
#include "base/bind_helpers.h"
#include "base/fuchsia/fuchsia_logging.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/message_port_provider.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/renderer_preferences_util.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/was_activated_option.h"
#include "fuchsia/base/mem_buffer_util.h"
#include "fuchsia/engine/browser/context_impl.h"
#include "fuchsia/engine/browser/message_port_impl.h"
#include "mojo/public/cpp/system/platform_handle.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "ui/aura/layout_manager.h"
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host_platform.h"
#include "ui/base/ime/input_method_base.h"
#include "ui/platform_window/platform_window_init_properties.h"
#include "ui/wm/core/base_focus_rules.h"
#include "url/gurl.h"
namespace {
// logging::LogSeverity does not define a value to disable logging so set a
// value much lower than logging::LOG_VERBOSE here.
const logging::LogSeverity kLogSeverityNone =
std::numeric_limits<logging::LogSeverity>::min();
// Layout manager that allows only one child window and stretches it to fill the
// parent.
class LayoutManagerImpl : public aura::LayoutManager {
public:
LayoutManagerImpl() = default;
~LayoutManagerImpl() override = default;
// aura::LayoutManager.
void OnWindowResized() override {
// Resize the child to match the size of the parent
if (child_) {
SetChildBoundsDirect(child_,
gfx::Rect(child_->parent()->bounds().size()));
}
}
void OnWindowAddedToLayout(aura::Window* child) override {
DCHECK(!child_);
child_ = child;
SetChildBoundsDirect(child_, gfx::Rect(child_->parent()->bounds().size()));
}
void OnWillRemoveWindowFromLayout(aura::Window* child) override {
DCHECK_EQ(child, child_);
child_ = nullptr;
}
void OnWindowRemovedFromLayout(aura::Window* child) override {}
void OnChildWindowVisibilityChanged(aura::Window* child,
bool visible) override {}
void SetChildBounds(aura::Window* child,
const gfx::Rect& requested_bounds) override {}
private:
aura::Window* child_ = nullptr;
DISALLOW_COPY_AND_ASSIGN(LayoutManagerImpl);
};
fuchsia::web::NavigationState ConvertContentNavigationEntry(
content::NavigationEntry* entry) {
DCHECK(entry);
fuchsia::web::NavigationState converted;
converted.set_title(base::UTF16ToUTF8(entry->GetTitleForDisplay()));
converted.set_url(entry->GetURL().spec());
switch (entry->GetPageType()) {
case content::PageType::PAGE_TYPE_NORMAL:
case content::PageType::PAGE_TYPE_INTERSTITIAL:
converted.set_page_type(fuchsia::web::PageType::NORMAL);
break;
case content::PageType::PAGE_TYPE_ERROR:
converted.set_page_type(fuchsia::web::PageType::ERROR);
break;
}
return converted;
}
// Computes the observable differences between |old_entry| and |new_entry|.
// Returns true if they are different, |false| if their observable fields are
// identical.
bool DiffNavigationEntries(const fuchsia::web::NavigationState& old_entry,
const fuchsia::web::NavigationState& new_entry,
fuchsia::web::NavigationState* difference) {
DCHECK(difference);
bool is_changed = false;
DCHECK(new_entry.has_title());
if (!old_entry.has_title() || (new_entry.title() != old_entry.title())) {
is_changed = true;
difference->set_title(new_entry.title());
}
DCHECK(new_entry.has_url());
if (!old_entry.has_url() || (new_entry.url() != old_entry.url())) {
is_changed = true;
difference->set_url(new_entry.url());
}
DCHECK(new_entry.has_page_type());
if (!old_entry.has_page_type() ||
(new_entry.page_type() != old_entry.page_type())) {
is_changed = true;
difference->set_page_type(new_entry.page_type());
}
return is_changed;
}
class FrameFocusRules : public wm::BaseFocusRules {
public:
FrameFocusRules() = default;
~FrameFocusRules() override = default;
// wm::BaseFocusRules implementation.
bool SupportsChildActivation(const aura::Window*) const override;
private:
DISALLOW_COPY_AND_ASSIGN(FrameFocusRules);
};
bool FrameFocusRules::SupportsChildActivation(const aura::Window*) const {
// TODO(crbug.com/878439): Return a result based on window properties such as
// visibility.
return true;
}
bool IsOriginWhitelisted(const GURL& url,
const std::vector<std::string>& allowed_origins) {
constexpr const char kWildcard[] = "*";
for (const std::string& origin : allowed_origins) {
if (origin == kWildcard)
return true;
GURL origin_url(origin);
if (!origin_url.is_valid()) {
DLOG(WARNING) << "Ignored invalid origin spec for whitelisting: "
<< origin;
continue;
}
if (origin_url != url.GetOrigin())
continue;
// TODO(crbug.com/893236): Add handling for nonstandard origins
// (e.g. data: URIs).
return true;
}
return false;
}
class ScenicWindowTreeHost : public aura::WindowTreeHostPlatform {
public:
explicit ScenicWindowTreeHost(ui::PlatformWindowInitProperties properties)
: aura::WindowTreeHostPlatform(std::move(properties)) {}
~ScenicWindowTreeHost() override = default;
// Route focus & blur events to the window's focus observer and its
// InputMethod.
void OnActivationChanged(bool active) override {
if (active) {
aura::client::GetFocusClient(window())->FocusWindow(window());
GetInputMethod()->OnFocus();
} else {
aura::client::GetFocusClient(window())->FocusWindow(nullptr);
GetInputMethod()->OnBlur();
}
}
private:
DISALLOW_COPY_AND_ASSIGN(ScenicWindowTreeHost);
};
logging::LogSeverity ConsoleLogLevelToLoggingSeverity(
fuchsia::web::ConsoleLogLevel level) {
switch (level) {
case fuchsia::web::ConsoleLogLevel::NONE:
return kLogSeverityNone;
case fuchsia::web::ConsoleLogLevel::DEBUG:
return logging::LOG_VERBOSE;
case fuchsia::web::ConsoleLogLevel::INFO:
return logging::LOG_INFO;
case fuchsia::web::ConsoleLogLevel::WARN:
return logging::LOG_WARNING;
case fuchsia::web::ConsoleLogLevel::ERROR:
return logging::LOG_ERROR;
}
NOTREACHED()
<< "Unknown log level: "
<< static_cast<std::underlying_type<fuchsia::web::ConsoleLogLevel>::type>(
level);
}
} // namespace
FrameImpl::FrameImpl(std::unique_ptr<content::WebContents> web_contents,
ContextImpl* context,
fidl::InterfaceRequest<fuchsia::web::Frame> frame_request)
: web_contents_(std::move(web_contents)),
log_level_(kLogSeverityNone),
context_(context),
binding_(this, std::move(frame_request)),
weak_factory_(this) {
web_contents_->SetDelegate(this);
Observe(web_contents_.get());
binding_.set_error_handler([this](zx_status_t status) {
ZX_LOG_IF(ERROR, status != ZX_ERR_PEER_CLOSED, status)
<< " Frame disconnected.";
context_->DestroyFrame(this);
});
content::UpdateFontRendererPreferencesFromSystemSettings(
web_contents_->GetMutableRendererPrefs());
}
FrameImpl::~FrameImpl() {
TearDownView();
}
zx::unowned_channel FrameImpl::GetBindingChannelForTest() const {
return zx::unowned_channel(binding_.channel());
}
void FrameImpl::CreateView(fuchsia::ui::views::ViewToken view_token) {
// If a View to this Frame is already active then disconnect it.
TearDownView();
ui::PlatformWindowInitProperties properties;
properties.view_token = std::move(view_token);
window_tree_host_ =
std::make_unique<ScenicWindowTreeHost>(std::move(properties));
window_tree_host_->InitHost();
window_tree_host_->window()->GetHost()->AddEventRewriter(
&discarding_event_filter_);
focus_controller_ =
std::make_unique<wm::FocusController>(new FrameFocusRules);
aura::client::SetFocusClient(root_window(), focus_controller_.get());
wm::SetActivationClient(root_window(), focus_controller_.get());
// Add hooks which automatically set the focus state when input events are
// received.
root_window()->AddPreTargetHandler(focus_controller_.get());
// Track child windows for enforcement of window management policies and
// propagate window manager events to them (e.g. window resizing).
root_window()->SetLayoutManager(new LayoutManagerImpl());
root_window()->AddChild(web_contents_->GetNativeView());
web_contents_->GetNativeView()->Show();
window_tree_host_->Show();
}
void FrameImpl::GetNavigationController(
fidl::InterfaceRequest<fuchsia::web::NavigationController> controller) {
controller_bindings_.AddBinding(this, std::move(controller));
}
void FrameImpl::ExecuteJavaScriptNoResult(
std::vector<std::string> origins,
fuchsia::mem::Buffer script,
ExecuteJavaScriptNoResultCallback callback) {
fuchsia::web::Frame_ExecuteJavaScriptNoResult_Result result;
if (!context_->IsJavaScriptInjectionAllowed()) {
result.set_err(fuchsia::web::FrameError::INTERNAL_ERROR);
callback(std::move(result));
return;
}
if (!IsOriginWhitelisted(web_contents_->GetLastCommittedURL(), origins)) {
result.set_err(fuchsia::web::FrameError::INVALID_ORIGIN);
callback(std::move(result));
return;
}
base::string16 script_utf16;
if (!cr_fuchsia::ReadUTF8FromVMOAsUTF16(script, &script_utf16)) {
result.set_err(fuchsia::web::FrameError::BUFFER_NOT_UTF8);
callback(std::move(result));
return;
}
web_contents_->GetMainFrame()->ExecuteJavaScript(script_utf16,
base::NullCallback());
result.set_response(fuchsia::web::Frame_ExecuteJavaScriptNoResult_Response());
callback(std::move(result));
}
void FrameImpl::AddBeforeLoadJavaScript(
uint64_t id,
std::vector<std::string> origins,
fuchsia::mem::Buffer script,
AddBeforeLoadJavaScriptCallback callback) {
fuchsia::web::Frame_AddBeforeLoadJavaScript_Result result;
if (!context_->IsJavaScriptInjectionAllowed()) {
result.set_err(fuchsia::web::FrameError::INTERNAL_ERROR);
callback(std::move(result));
return;
}
// Convert the script to UTF8 and store it as a shared memory buffer, so that
// it can be efficiently shared with multiple renderer processes.
base::string16 script_utf16;
if (!cr_fuchsia::ReadUTF8FromVMOAsUTF16(script, &script_utf16)) {
result.set_err(fuchsia::web::FrameError::BUFFER_NOT_UTF8);
callback(std::move(result));
return;
}
// Create a read-only VMO from |script|.
fuchsia::mem::Buffer script_buffer =
cr_fuchsia::MemBufferFromString16(script_utf16);
// Wrap the VMO into a read-only shared-memory container that Mojo can work
// with.
base::subtle::PlatformSharedMemoryRegion script_region =
base::subtle::PlatformSharedMemoryRegion::Take(
std::move(script_buffer.vmo),
base::subtle::PlatformSharedMemoryRegion::Mode::kWritable,
script_buffer.size, base::UnguessableToken::Create());
script_region.ConvertToReadOnly();
auto script_region_mojo =
base::ReadOnlySharedMemoryRegion::Deserialize(std::move(script_region));
// If there is no script with the identifier |id|, then create a place for it
// at the end of the injection sequence.
if (before_load_scripts_.find(id) == before_load_scripts_.end())
before_load_scripts_order_.push_back(id);
before_load_scripts_[id] =
OriginScopedScript(origins, std::move(script_region_mojo));
result.set_response(fuchsia::web::Frame_AddBeforeLoadJavaScript_Response());
callback(std::move(result));
}
void FrameImpl::RemoveBeforeLoadJavaScript(uint64_t id) {
before_load_scripts_.erase(id);
for (auto script_id_iter = before_load_scripts_order_.begin();
script_id_iter != before_load_scripts_order_.end(); ++script_id_iter) {
if (*script_id_iter == id) {
before_load_scripts_order_.erase(script_id_iter);
return;
}
}
}
void FrameImpl::PostMessage(std::string origin,
fuchsia::web::WebMessage message,
PostMessageCallback callback) {
constexpr char kWildcardOrigin[] = "*";
fuchsia::web::Frame_PostMessage_Result result;
if (origin.empty()) {
result.set_err(fuchsia::web::FrameError::INVALID_ORIGIN);
callback(std::move(result));
return;
}
if (!message.has_data()) {
result.set_err(fuchsia::web::FrameError::NO_DATA_IN_MESSAGE);
callback(std::move(result));
return;
}
base::Optional<base::string16> origin_utf16;
if (origin != kWildcardOrigin)
origin_utf16 = base::UTF8ToUTF16(origin);
base::string16 data_utf16;
if (!cr_fuchsia::ReadUTF8FromVMOAsUTF16(message.data(), &data_utf16)) {
result.set_err(fuchsia::web::FrameError::BUFFER_NOT_UTF8);
callback(std::move(result));
return;
}
// Include outgoing MessagePorts in the message.
std::vector<mojo::ScopedMessagePipeHandle> message_ports;
if (message.has_outgoing_transfer()) {
// Verify that all the Transferables are valid before we start allocating
// resources to them.
for (const fuchsia::web::OutgoingTransferable& outgoing :
message.outgoing_transfer()) {
if (!outgoing.is_message_port()) {
result.set_err(fuchsia::web::FrameError::INTERNAL_ERROR);
callback(std::move(result));
return;
}
}
for (fuchsia::web::OutgoingTransferable& outgoing :
*message.mutable_outgoing_transfer()) {
mojo::ScopedMessagePipeHandle port =
MessagePortImpl::FromFidl(std::move(outgoing.message_port()));
if (!port) {
result.set_err(fuchsia::web::FrameError::INTERNAL_ERROR);
callback(std::move(result));
return;
}
message_ports.push_back(std::move(port));
}
}
content::MessagePortProvider::PostMessageToFrame(
web_contents_.get(), base::string16(), origin_utf16,
std::move(data_utf16), std::move(message_ports));
result.set_response(fuchsia::web::Frame_PostMessage_Response());
callback(std::move(result));
}
void FrameImpl::SetNavigationEventListener(
fidl::InterfaceHandle<fuchsia::web::NavigationEventListener> listener) {
// Reset the event buffer state.
waiting_for_navigation_event_ack_ = false;
cached_navigation_state_ = {};
pending_navigation_event_ = {};
pending_navigation_event_is_dirty_ = false;
if (listener) {
navigation_listener_.Bind(std::move(listener));
navigation_listener_.set_error_handler(
[this](zx_status_t status) { SetNavigationEventListener(nullptr); });
} else {
navigation_listener_.Unbind();
}
}
void FrameImpl::SetJavaScriptLogLevel(fuchsia::web::ConsoleLogLevel level) {
log_level_ = ConsoleLogLevelToLoggingSeverity(level);
}
void FrameImpl::SetEnableInput(bool enable_input) {
discarding_event_filter_.set_discard_events(!enable_input);
}
FrameImpl::OriginScopedScript::OriginScopedScript() = default;
FrameImpl::OriginScopedScript::OriginScopedScript(
std::vector<std::string> origins,
base::ReadOnlySharedMemoryRegion script)
: origins_(std::move(origins)), script_(std::move(script)) {}
FrameImpl::OriginScopedScript& FrameImpl::OriginScopedScript::operator=(
FrameImpl::OriginScopedScript&& other) {
origins_ = std::move(other.origins_);
script_ = std::move(other.script_);
return *this;
}
FrameImpl::OriginScopedScript::~OriginScopedScript() = default;
void FrameImpl::TearDownView() {
if (window_tree_host_) {
aura::client::SetFocusClient(root_window(), nullptr);
wm::SetActivationClient(root_window(), nullptr);
root_window()->RemovePreTargetHandler(focus_controller_.get());
web_contents_->GetNativeView()->Hide();
window_tree_host_->Hide();
window_tree_host_->compositor()->SetVisible(false);
window_tree_host_ = nullptr;
// Allows posted focus events to process before the FocusController is torn
// down.
content::BrowserThread::DeleteSoon(content::BrowserThread::UI, FROM_HERE,
std::move(focus_controller_));
}
}
void FrameImpl::OnNavigationEntryChanged(content::NavigationEntry* entry) {
fuchsia::web::NavigationState entry_converted =
ConvertContentNavigationEntry(entry);
pending_navigation_event_is_dirty_ |= DiffNavigationEntries(
cached_navigation_state_, entry_converted, &pending_navigation_event_);
cached_navigation_state_ = std::move(entry_converted);
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&FrameImpl::MaybeSendNavigationEvent,
weak_factory_.GetWeakPtr()));
}
void FrameImpl::MaybeSendNavigationEvent() {
if (!navigation_listener_)
return;
if (!pending_navigation_event_is_dirty_ ||
waiting_for_navigation_event_ack_) {
return;
}
pending_navigation_event_is_dirty_ = false;
waiting_for_navigation_event_ack_ = true;
// Send the event to the observer and, upon acknowledgement, revisit this
// function to send another update.
navigation_listener_->OnNavigationStateChanged(
std::move(pending_navigation_event_), [this]() {
waiting_for_navigation_event_ack_ = false;
MaybeSendNavigationEvent();
});
pending_navigation_event_ = {};
}
void FrameImpl::LoadUrl(std::string url,
fuchsia::web::LoadUrlParams params,
LoadUrlCallback callback) {
fuchsia::web::NavigationController_LoadUrl_Result result;
GURL validated_url(url);
if (!validated_url.is_valid()) {
result.set_err(fuchsia::web::NavigationControllerError::INVALID_URL);
callback(std::move(result));
return;
}
content::NavigationController::LoadURLParams params_converted(validated_url);
if (params.has_headers()) {
std::vector<std::string> extra_headers;
extra_headers.reserve(params.headers().size());
for (const auto& header : params.headers()) {
// TODO(crbug.com/964732): Check there is no colon in |header_name|.
base::StringPiece header_name(
reinterpret_cast<const char*>(header.name.data()),
header.name.size());
base::StringPiece header_value(
reinterpret_cast<const char*>(header.value.data()),
header.value.size());
extra_headers.emplace_back(
base::StrCat({header_name, ": ", header_value}));
}
params_converted.extra_headers = base::JoinString(extra_headers, "\n");
}
if (validated_url.scheme() == url::kDataScheme)
params_converted.load_type = content::NavigationController::LOAD_TYPE_DATA;
params_converted.transition_type = ui::PageTransitionFromInt(
ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
if (params.has_was_user_activated() && params.was_user_activated()) {
params_converted.was_activated = content::WasActivatedOption::kYes;
} else {
params_converted.was_activated = content::WasActivatedOption::kNo;
}
web_contents_->GetController().LoadURLWithParams(params_converted);
result.set_response(fuchsia::web::NavigationController_LoadUrl_Response());
callback(std::move(result));
}
void FrameImpl::GoBack() {
if (web_contents_->GetController().CanGoBack())
web_contents_->GetController().GoBack();
}
void FrameImpl::GoForward() {
if (web_contents_->GetController().CanGoForward())
web_contents_->GetController().GoForward();
}
void FrameImpl::Stop() {
web_contents_->Stop();
}
void FrameImpl::Reload(fuchsia::web::ReloadType type) {
content::ReloadType internal_reload_type;
switch (type) {
case fuchsia::web::ReloadType::PARTIAL_CACHE:
internal_reload_type = content::ReloadType::NORMAL;
break;
case fuchsia::web::ReloadType::NO_CACHE:
internal_reload_type = content::ReloadType::BYPASSING_CACHE;
break;
}
web_contents_->GetController().Reload(internal_reload_type, false);
}
void FrameImpl::GetVisibleEntry(
fuchsia::web::NavigationController::GetVisibleEntryCallback callback) {
content::NavigationEntry* entry =
web_contents_->GetController().GetVisibleEntry();
if (!entry) {
callback({});
return;
}
callback(ConvertContentNavigationEntry(entry));
}
bool FrameImpl::ShouldCreateWebContents(
content::WebContents* web_contents,
content::RenderFrameHost* opener,
content::SiteInstance* source_site_instance,
int32_t route_id,
int32_t main_frame_route_id,
int32_t main_frame_widget_route_id,
content::mojom::WindowContainerType window_container_type,
const GURL& opener_url,
const std::string& frame_name,
const GURL& target_url,
const std::string& partition_id,
content::SessionStorageNamespace* session_storage_namespace) {
DCHECK_EQ(web_contents, web_contents_.get());
// Prevent any child WebContents (popup windows, tabs, etc.) from spawning.
// TODO(crbug.com/888131): Implement support for popup windows.
NOTIMPLEMENTED() << "Ignored popup window request for URL: "
<< target_url.spec();
return false;
}
bool FrameImpl::DidAddMessageToConsole(content::WebContents* source,
int32_t level,
const base::string16& message,
int32_t line_no,
const base::string16& source_id) {
if (log_level_ > level) {
return false;
}
std::string formatted_message =
base::StringPrintf("%s:%d : %s", base::UTF16ToUTF8(source_id).data(),
line_no, base::UTF16ToUTF8(message).data());
switch (level) {
case logging::LOG_VERBOSE:
LOG(INFO) << "debug:" << formatted_message;
break;
case logging::LOG_INFO:
LOG(INFO) << "info:" << formatted_message;
break;
case logging::LOG_WARNING:
LOG(WARNING) << "warn:" << formatted_message;
break;
case logging::LOG_ERROR:
LOG(ERROR) << "error:" << formatted_message;
break;
default:
DLOG(WARNING) << "Unknown log level: " << level;
return false;
}
if (console_log_message_hook_)
console_log_message_hook_.Run(formatted_message);
return true;
}
void FrameImpl::DidFinishLoad(content::RenderFrameHost* render_frame_host,
const GURL& validated_url) {
if (web_contents_->GetMainFrame() != render_frame_host) {
return;
}
OnNavigationEntryChanged(web_contents_->GetController().GetVisibleEntry());
}
void FrameImpl::ReadyToCommitNavigation(
content::NavigationHandle* navigation_handle) {
if (before_load_scripts_.empty())
return;
if (!navigation_handle->IsInMainFrame() ||
navigation_handle->IsSameDocument() || navigation_handle->IsErrorPage())
return;
mojom::OnLoadScriptInjectorAssociatedPtr before_load_script_injector;
navigation_handle->GetRenderFrameHost()
->GetRemoteAssociatedInterfaces()
->GetInterface(&before_load_script_injector);
// Provision the renderer's ScriptInjector with the scripts scoped to this
// page's origin.
before_load_script_injector->ClearOnLoadScripts();
for (uint64_t script_id : before_load_scripts_order_) {
const OriginScopedScript& script = before_load_scripts_[script_id];
if (IsOriginWhitelisted(navigation_handle->GetURL(), script.origins())) {
before_load_script_injector->AddOnLoadScript(
mojo::WrapReadOnlySharedMemoryRegion(script.script().Duplicate()));
}
}
}
void FrameImpl::TitleWasSet(content::NavigationEntry* entry) {
OnNavigationEntryChanged(entry);
}