// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "fuchsia_web/webengine/browser/navigation_controller_impl.h"
#include <fuchsia/mem/cpp/fidl.h>
#include <lib/fpromise/result.h>
#include <string_view>
#include "base/bits.h"
#include "base/fuchsia/fuchsia_logging.h"
#include "base/memory/page_size.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/trace_event/trace_event.h"
#include "base/trace_event/typed_macros.h"
#include "components/favicon/content/content_favicon_driver.h"
#include "content/public/browser/favicon_status.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "fuchsia_web/common/string_util.h"
#include "fuchsia_web/webengine/browser/trace_event.h"
#include "net/base/net_errors.h"
#include "net/http/http_util.h"
#include "third_party/blink/public/mojom/navigation/was_activated_option.mojom.h"
#include "third_party/perfetto/include/perfetto/tracing/track_event_args.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/page_transition_types.h"
#include "ui/gfx/image/image.h"
namespace {
// Converts a gfx::Image to a fuchsia::web::Favicon.
fuchsia::web::Favicon GfxImageToFidlFavicon(gfx::Image gfx_image) {
fuchsia::web::Favicon favicon;
if (gfx_image.IsEmpty())
return favicon;
int height = gfx_image.AsBitmap().pixmap().height();
int width = gfx_image.AsBitmap().pixmap().width();
size_t stride = width * SkColorTypeBytesPerPixel(kRGBA_8888_SkColorType);
// Create VMO.
fuchsia::mem::Buffer buffer;
buffer.size = stride * height;
zx_status_t status = zx::vmo::create(buffer.size, 0, &buffer.vmo);
ZX_CHECK(status == ZX_OK, status) << "zx_vmo_create";
// Map the VMO.
uintptr_t addr;
size_t mapped_size = base::bits::AlignUp(buffer.size, base::GetPageSize());
zx_vm_option_t options = ZX_VM_PERM_READ | ZX_VM_PERM_WRITE;
status = zx::vmar::root_self()->map(options, /*vmar_offset=*/0, buffer.vmo,
/*vmo_offset=*/0, mapped_size, &addr);
ZX_CHECK(status == ZX_OK, status) << "zx_vmar_map";
// Copy the data to the mapped VMO.
gfx_image.AsBitmap().readPixels(
SkImageInfo::Make(width, height, kRGBA_8888_SkColorType,
kPremul_SkAlphaType),
reinterpret_cast<void*>(addr), stride, 0, 0);
// Unmap the VMO.
status = zx::vmar::root_self()->unmap(addr, mapped_size);
ZX_DCHECK(status == ZX_OK, status) << "zx_vmar_unmap";
favicon.set_data(std::move(buffer));
favicon.set_height(height);
favicon.set_width(width);
return favicon;
}
} // namespace
namespace {
// For each field that differs between |old_entry| and |new_entry|, the field
// is set to its new value in |difference|. All other fields in |difference| are
// left unchanged, such that a series of DiffNavigationEntries() calls may be
// used to accumulate differences across a progression of NavigationStates.
void DiffNavigationEntries(const fuchsia::web::NavigationState& old_entry,
const fuchsia::web::NavigationState& new_entry,
fuchsia::web::NavigationState* difference) {
DCHECK(difference);
// NavigationStates will only be empty for "initial" navigation entries, so
// if |new_entry| is empty then |old_entry| must necessarily also be empty,
// and there is no difference to report.
if (new_entry.IsEmpty()) {
CHECK(old_entry.IsEmpty());
return;
}
DCHECK(new_entry.has_title());
if (!old_entry.has_title() || (new_entry.title() != old_entry.title())) {
difference->set_title(new_entry.title());
}
DCHECK(new_entry.has_url());
if (!old_entry.has_url() || (new_entry.url() != old_entry.url())) {
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())) {
difference->set_page_type(new_entry.page_type());
}
DCHECK(new_entry.has_can_go_back());
if (!old_entry.has_can_go_back() ||
old_entry.can_go_back() != new_entry.can_go_back()) {
difference->set_can_go_back(new_entry.can_go_back());
}
DCHECK(new_entry.has_can_go_forward());
if (!old_entry.has_can_go_forward() ||
old_entry.can_go_forward() != new_entry.can_go_forward()) {
difference->set_can_go_forward(new_entry.can_go_forward());
}
DCHECK(new_entry.has_is_main_document_loaded());
if (!old_entry.has_is_main_document_loaded() ||
old_entry.is_main_document_loaded() !=
new_entry.is_main_document_loaded()) {
difference->set_is_main_document_loaded(
new_entry.is_main_document_loaded());
}
}
} // namespace
NavigationControllerImpl::NavigationControllerImpl(
content::WebContents* web_contents,
void* parent_for_trace_flow)
: parent_for_trace_flow_(parent_for_trace_flow),
web_contents_(web_contents),
weak_factory_(this) {
DCHECK(parent_for_trace_flow_);
Observe(web_contents_);
}
NavigationControllerImpl::~NavigationControllerImpl() = default;
void NavigationControllerImpl::AddBinding(
fidl::InterfaceRequest<fuchsia::web::NavigationController> controller) {
controller_bindings_.AddBinding(this, std::move(controller));
}
void NavigationControllerImpl::SetEventListener(
fidl::InterfaceHandle<fuchsia::web::NavigationEventListener> listener,
fuchsia::web::NavigationEventListenerFlags flags) {
// Reset the event buffer state.
waiting_for_navigation_event_ack_ = false;
previous_navigation_state_ = {};
pending_navigation_event_ = {};
// Simply unbind if no new listener was set.
if (!listener) {
navigation_listener_.Unbind();
return;
}
send_favicon_ =
(flags & fuchsia::web::NavigationEventListenerFlags::FAVICON) ==
fuchsia::web::NavigationEventListenerFlags::FAVICON;
favicon::ContentFaviconDriver* favicon_driver =
favicon::ContentFaviconDriver::FromWebContents(web_contents_);
if (send_favicon_) {
if (!favicon_driver) {
favicon::ContentFaviconDriver::CreateForWebContents(
web_contents_,
/*favicon_service=*/nullptr);
favicon_driver =
favicon::ContentFaviconDriver::FromWebContents(web_contents_);
}
favicon_driver->AddObserver(this);
} else {
if (favicon_driver)
favicon_driver->RemoveObserver(this);
}
navigation_listener_.Bind(std::move(listener));
navigation_listener_.set_error_handler(
[this](zx_status_t status) { SetEventListener(nullptr, {}); });
// Send the current navigation state to the listener immediately.
waiting_for_navigation_event_ack_ = true;
previous_navigation_state_ = GetVisibleNavigationState();
fuchsia::web::NavigationState initial_state;
DiffNavigationEntries({}, previous_navigation_state_, &initial_state);
navigation_listener_->OnNavigationStateChanged(
std::move(initial_state), [this]() {
waiting_for_navigation_event_ack_ = false;
MaybeSendNavigationEvent();
});
}
fuchsia::web::NavigationState
NavigationControllerImpl::GetVisibleNavigationState() const {
content::NavigationEntry* const entry =
web_contents_->GetController().GetVisibleEntry();
if (entry->IsInitialEntry())
return fuchsia::web::NavigationState();
fuchsia::web::NavigationState state;
// Populate some fields directly from the NavigationEntry, if possible.
state.set_title(base::UTF16ToUTF8(entry->GetTitleForDisplay()));
state.set_url(entry->GetURL().spec());
if (web_contents_->IsCrashed()) {
// TODO(https:://crbug.com/1092506): Add an explicit crashed indicator to
// NavigationState, separate from PageType::ERROR.
state.set_page_type(fuchsia::web::PageType::ERROR);
} else if (uncommitted_load_error_) {
// If there was a loading error which prevented the navigation entry from
// being committed, then report PageType::ERROR.
state.set_page_type(fuchsia::web::PageType::ERROR);
} else {
switch (entry->GetPageType()) {
case content::PageType::PAGE_TYPE_NORMAL:
state.set_page_type(fuchsia::web::PageType::NORMAL);
break;
case content::PageType::PAGE_TYPE_ERROR:
state.set_page_type(fuchsia::web::PageType::ERROR);
break;
}
}
state.set_is_main_document_loaded(is_main_document_loaded_);
state.set_can_go_back(web_contents_->GetController().CanGoBack());
state.set_can_go_forward(web_contents_->GetController().CanGoForward());
return state;
}
void NavigationControllerImpl::OnNavigationEntryChanged() {
fuchsia::web::NavigationState new_state = GetVisibleNavigationState();
DiffNavigationEntries(previous_navigation_state_, new_state,
&pending_navigation_event_);
previous_navigation_state_ = std::move(new_state);
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&NavigationControllerImpl::MaybeSendNavigationEvent,
weak_factory_.GetWeakPtr()));
}
void NavigationControllerImpl::MaybeSendNavigationEvent() {
if (!navigation_listener_)
return;
if (pending_navigation_event_.IsEmpty() ||
waiting_for_navigation_event_ack_) {
return;
}
waiting_for_navigation_event_ack_ = true;
// Note that the events is logged to the parent Frame's flow.
TRACE_EVENT(kWebEngineFidlCategory,
"fuchsia.web/NavigationEventListener.OnNavigationStateChanged",
perfetto::Flow::FromPointer(parent_for_trace_flow_), "url",
previous_navigation_state_.url(), "title",
previous_navigation_state_.title().data(), "is_loaded",
is_main_document_loaded_);
// 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 NavigationControllerImpl::LoadUrl(std::string url,
fuchsia::web::LoadUrlParams params,
LoadUrlCallback callback) {
// Note that the event is logged to the parent Frame's flow.
TRACE_EVENT(kWebEngineFidlCategory,
"fuchsia.web/NavigationController.LoadUrl",
perfetto::Flow::FromPointer(parent_for_trace_flow_), "url", url);
GURL validated_url(url);
if (!validated_url.is_valid()) {
callback(
fpromise::error(fuchsia::web::NavigationControllerError::INVALID_URL));
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()) {
std::string_view header_name = BytesAsString(header.name);
std::string_view header_value = BytesAsString(header.value);
if (!net::HttpUtil::IsValidHeaderName(header_name) ||
!net::HttpUtil::IsValidHeaderValue(header_value)) {
callback(fpromise::error(
fuchsia::web::NavigationControllerError::INVALID_HEADER));
return;
}
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 = blink::mojom::WasActivatedOption::kYes;
} else {
params_converted.was_activated = blink::mojom::WasActivatedOption::kNo;
}
web_contents_->GetController().LoadURLWithParams(params_converted);
callback(fpromise::ok());
}
void NavigationControllerImpl::GoBack() {
TRACE_EVENT(kWebEngineFidlCategory, "fuchsia.web/NavigationController.GoBack",
perfetto::Flow::FromPointer(parent_for_trace_flow_));
if (web_contents_->GetController().CanGoBack())
web_contents_->GetController().GoBack();
}
void NavigationControllerImpl::GoForward() {
TRACE_EVENT(kWebEngineFidlCategory,
"fuchsia.web/NavigationController.GoForward",
perfetto::Flow::FromPointer(parent_for_trace_flow_));
if (web_contents_->GetController().CanGoForward())
web_contents_->GetController().GoForward();
}
void NavigationControllerImpl::Stop() {
TRACE_EVENT(kWebEngineFidlCategory, "fuchsia.web/NavigationController.Stop",
perfetto::Flow::FromPointer(parent_for_trace_flow_));
web_contents_->Stop();
}
void NavigationControllerImpl::Reload(fuchsia::web::ReloadType type) {
TRACE_EVENT(kWebEngineFidlCategory, "fuchsia.web/NavigationController.Reload",
perfetto::Flow::FromPointer(parent_for_trace_flow_));
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 NavigationControllerImpl::TitleWasSet(content::NavigationEntry* entry) {
// The title was changed after the document was loaded.
OnNavigationEntryChanged();
}
void NavigationControllerImpl::PrimaryMainDocumentElementAvailable() {
// The main document is loaded, but not necessarily all the subresources. Some
// fields like "title" will change here.
OnNavigationEntryChanged();
}
void NavigationControllerImpl::DidFinishLoad(
content::RenderFrameHost* render_frame_host,
const GURL& validated_url) {
// The current document and its statically-declared subresources are loaded.
// Don't process load completion on the current document if the WebContents
// is already in the process of navigating to a different page.
if (active_navigation_)
return;
// Only allow the primary main frame to transition this state.
if (!render_frame_host->IsInPrimaryMainFrame())
return;
is_main_document_loaded_ = true;
OnNavigationEntryChanged();
}
void NavigationControllerImpl::PrimaryMainFrameRenderProcessGone(
base::TerminationStatus status) {
// If the current RenderProcess terminates then trigger a NavigationState
// change to let the caller know that something is wrong.
LOG(WARNING) << "RenderProcess gone, TerminationStatus=" << status;
OnNavigationEntryChanged();
}
void NavigationControllerImpl::DidStartNavigation(
content::NavigationHandle* navigation_handle) {
if (!navigation_handle->IsInPrimaryMainFrame() ||
navigation_handle->IsSameDocument()) {
return;
}
// If favicons are enabled then reset favicon in the pending navigation.
if (send_favicon_)
pending_navigation_event_.set_favicon({});
uncommitted_load_error_ = false;
active_navigation_ = navigation_handle;
is_main_document_loaded_ = false;
OnNavigationEntryChanged();
}
void NavigationControllerImpl::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
if (navigation_handle != active_navigation_)
return;
active_navigation_ = nullptr;
uncommitted_load_error_ = !navigation_handle->HasCommitted() &&
navigation_handle->GetNetErrorCode() != net::OK;
OnNavigationEntryChanged();
}
void NavigationControllerImpl::OnFaviconUpdated(
favicon::FaviconDriver* favicon_driver,
NotificationIconType notification_icon_type,
const GURL& icon_url,
bool icon_url_changed,
const gfx::Image& image) {
// Currently FaviconDriverImpl loads only 16 DIP images, except on Android and
// iOS.
DCHECK_EQ(notification_icon_type, FaviconDriverObserver::NON_TOUCH_16_DIP);
pending_navigation_event_.set_favicon(GfxImageToFidlFavicon(image));
OnNavigationEntryChanged();
}
void DiffNavigationEntriesForTest( // IN-TEST
const fuchsia::web::NavigationState& old_entry,
const fuchsia::web::NavigationState& new_entry,
fuchsia::web::NavigationState* difference) {
DiffNavigationEntries(old_entry, new_entry, difference);
}