blob: 7e7d468e2b65873cb1b14f134ea17f362aa1fadb [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! This module provides methods for controlling and responding to events from
//! a web frame.
use crate::error::{ResultExt, TokenProviderError};
use async_trait::async_trait;
use fidl::endpoints::{create_proxy, create_request_stream};
use fidl_fuchsia_identity_external::Error as ApiError;
use fidl_fuchsia_ui_views::ViewToken;
use fidl_fuchsia_web::{
ContextProxy, FrameProxy, LoadUrlParams, NavigationControllerMarker,
NavigationEventListenerMarker, NavigationEventListenerRequest,
NavigationEventListenerRequestStream, PageType,
};
use futures::prelude::*;
use log::warn;
use url::Url;
type TokenProviderResult<T> = Result<T, TokenProviderError>;
/// A trait for representations of a web frame that is the only frame in a web context.
#[async_trait]
pub trait StandaloneWebFrame {
/// Creates a new scenic view using the given |view_token| and loads the
/// given |url| as a webpage in the view. The view must be attached to
/// the global Scenic graph using the ViewHolderToken paired with
/// |view_token|. This method should be called prior to attaching to the
/// Scenic graph to ensure that loading is successful prior to displaying
/// the page to the user.
async fn display_url(&mut self, view_token: ViewToken, url: Url) -> TokenProviderResult<()>;
/// Waits until the frame redirects to a URL matching the scheme,
/// domain, and path of |redirect_target|. Returns the matching URL,
/// including any query parameters.
async fn wait_for_redirect(&mut self, redirect_target: Url) -> TokenProviderResult<Url>;
}
/// Trait for structs capable of creating new Web frames.
pub trait WebFrameSupplier {
/// The concrete `StandaloneWebFrame` type the supplier produces.
type Frame: StandaloneWebFrame;
/// Creates a new `StandaloneWebFrame`. This method guarantees that the
/// new frame is in its own web context.
/// Although implementation of this method does not require state, `self`
/// is added here to allow injection of mocks with canned responses.
fn new_standalone_frame(&self) -> Result<Self::Frame, anyhow::Error>;
}
/// A `StandaloneWebFrame` implementation that uses the default fuchsia.web
/// implementation to display a web frame.
pub struct DefaultStandaloneWebFrame {
/// Connection to the web context. Needs to be kept in scope to
/// keep context alive.
_context: ContextProxy,
/// Connection to the web frame within the context.
frame: FrameProxy,
}
#[async_trait]
impl StandaloneWebFrame for DefaultStandaloneWebFrame {
async fn display_url(
&mut self,
mut view_token: ViewToken,
url: Url,
) -> TokenProviderResult<()> {
let (navigation_controller_proxy, navigation_controller_server_end) =
create_proxy::<NavigationControllerMarker>()
.token_provider_error(ApiError::Resource)?;
self.frame
.get_navigation_controller(navigation_controller_server_end)
.token_provider_error(ApiError::Resource)?;
navigation_controller_proxy
.load_url(
url.as_str(),
LoadUrlParams {
type_: None,
referrer_url: None,
was_user_activated: None,
headers: None,
..LoadUrlParams::EMPTY
},
)
.await
.token_provider_error(ApiError::Resource)??;
self.frame.create_view(&mut view_token).token_provider_error(ApiError::Resource)?;
let navigation_event_stream = self.get_navigation_event_stream()?;
Self::poll_until_loaded(navigation_event_stream).await
}
async fn wait_for_redirect(&mut self, redirect_target: Url) -> TokenProviderResult<Url> {
let navigation_event_stream = self.get_navigation_event_stream()?;
// pull redirect URL out from events.
Self::poll_for_url_navigation_event(navigation_event_stream, |url| {
(url.scheme(), url.domain(), url.path())
== (redirect_target.scheme(), redirect_target.domain(), redirect_target.path())
})
.await
}
}
impl DefaultStandaloneWebFrame {
/// Create a new `StandaloneWebFrame`. The context and frame passed
/// in should not be reused.
pub fn new(context: ContextProxy, frame: FrameProxy) -> Self {
DefaultStandaloneWebFrame { _context: context, frame }
}
/// Registers a navigation listener with the web frame and returns the created event
/// stream.
fn get_navigation_event_stream(
&self,
) -> TokenProviderResult<NavigationEventListenerRequestStream> {
let (navigation_event_client, navigation_event_stream) =
create_request_stream::<NavigationEventListenerMarker>()
.token_provider_error(ApiError::Resource)?;
self.frame
.set_navigation_event_listener(Some(navigation_event_client))
.token_provider_error(ApiError::Resource)?;
Ok(navigation_event_stream)
}
/// Polls for events on the given request stream until a event with a
/// matching url is found.
async fn poll_for_url_navigation_event<F>(
mut request_stream: NavigationEventListenerRequestStream,
url_match_fn: F,
) -> TokenProviderResult<Url>
where
F: Fn(&Url) -> bool,
{
// Any errors encountered with the stream here may be a result of the
// overlay being canceled externally.
while let Some(request) =
request_stream.try_next().await.token_provider_error(ApiError::Resource)?
{
let NavigationEventListenerRequest::OnNavigationStateChanged { change, responder } =
request;
responder.send().token_provider_error(ApiError::Resource)?;
match change.url.map(|raw_url| Url::parse(raw_url.as_str())) {
Some(Ok(url)) => {
if url_match_fn(&url) {
return Ok(url);
}
}
Some(Err(err)) => {
warn!("Browser redirected to malformed URL: {:?}", &err);
return Err(TokenProviderError::new(ApiError::Unknown).with_cause(err));
}
None => (),
}
}
Err(TokenProviderError::new(ApiError::Unknown))
}
/// Completes when the frame has finished loading or some error has occurred.
async fn poll_until_loaded(
mut request_stream: NavigationEventListenerRequestStream,
) -> TokenProviderResult<()> {
// Verify that the page has loaded and is not an error page. Since
// this information may be delivered through two different events,
// we need to keep track of the known state and search through events
// until both points are found.
let mut known_page_type: Option<PageType> = None;
let mut main_document_loaded = false;
while let Some(request) =
request_stream.try_next().await.token_provider_error(ApiError::Resource)?
{
// update known state.
let NavigationEventListenerRequest::OnNavigationStateChanged { change, responder } =
request;
responder.send().token_provider_error(ApiError::Resource)?;
if let Some(is_main_document_loaded) = change.is_main_document_loaded {
main_document_loaded = is_main_document_loaded;
}
if let Some(page_type) = change.page_type {
known_page_type.replace(page_type);
}
// check if state is terminal
match (known_page_type, main_document_loaded) {
(Some(PageType::Normal), true) => return Ok(()),
(Some(PageType::Normal), false) => (),
(Some(PageType::Error), _) => {
return Err(TokenProviderError::new(ApiError::Network))
}
(None, _) => (),
}
}
Err(TokenProviderError::new(ApiError::Unknown))
}
}
#[cfg(test)]
mod test {
use super::*;
use anyhow::Error;
use fidl::endpoints::ClientEnd;
use fidl_fuchsia_web::{
ContextMarker, FrameMarker, FrameRequest, FrameRequestStream, NavigationState,
};
use fuchsia_async as fasync;
use log::error;
fn create_frame_with_events(
events: Vec<NavigationState>,
) -> Result<DefaultStandaloneWebFrame, Error> {
let (context, _) = create_proxy::<ContextMarker>()?;
let (frame, frame_server_end) = create_request_stream::<FrameMarker>()?;
fasync::Task::spawn(async move {
handle_frame_stream(frame_server_end, events)
.await
.unwrap_or_else(|e| error!("Error running frame stream: {:?}", e));
})
.detach();
Ok(DefaultStandaloneWebFrame::new(context, frame.into_proxy()?))
}
async fn handle_frame_stream(
mut stream: FrameRequestStream,
events: Vec<NavigationState>,
) -> Result<(), Error> {
if let Some(request) = stream.try_next().await? {
match request {
FrameRequest::SetNavigationEventListener { listener, .. } => {
fasync::Task::spawn(async move {
feed_event_requests(listener.unwrap(), events)
.await
.unwrap_or_else(|e| error!("Error in event sender: {:?}", e));
})
.detach();
}
req => panic!("Unimplemented method {:?} called in test stub", req),
}
}
Ok(())
}
async fn feed_event_requests(
client_end: ClientEnd<NavigationEventListenerMarker>,
events: Vec<NavigationState>,
) -> Result<(), Error> {
let client_end = client_end.into_proxy()?;
for event in events.into_iter() {
client_end.on_navigation_state_changed(event).await?;
}
Ok(())
}
fn create_navigate_to_url_event(url: Option<&str>) -> NavigationState {
let parsed_url = url.map(|url| String::from(url));
NavigationState {
url: parsed_url,
title: None,
page_type: None,
can_go_forward: None,
can_go_back: None,
is_main_document_loaded: None,
..NavigationState::EMPTY
}
}
fn create_navigate_to_page_event(
page_type: Option<PageType>,
is_main_document_loaded: Option<bool>,
) -> NavigationState {
NavigationState {
url: None,
title: None,
page_type,
can_go_forward: None,
can_go_back: None,
is_main_document_loaded,
..NavigationState::EMPTY
}
}
#[fasync::run_until_stalled(test)]
async fn test_wait_for_redirect() -> Result<(), Error> {
let events = vec![
create_navigate_to_url_event(None),
create_navigate_to_url_event(None),
create_navigate_to_url_event(Some("http://test/path/")),
create_navigate_to_url_event(Some("http://test/?key=val")),
create_navigate_to_url_event(None),
];
let mut web_frame = create_frame_with_events(events)?;
let target_url = Url::parse("http://test/")?;
let matched_url = web_frame.wait_for_redirect(target_url).await?;
assert_eq!(matched_url, Url::parse("http://test/?key=val").unwrap());
Ok(())
}
#[fasync::run_until_stalled(test)]
async fn test_no_matching_redirect_found() -> Result<(), Error> {
let events = vec![
create_navigate_to_url_event(None),
create_navigate_to_url_event(Some("http://domain/")),
];
let mut web_frame = create_frame_with_events(events)?;
let target_url = Url::parse("http://test/")?;
let result = web_frame.wait_for_redirect(target_url).await;
assert!(result.is_err());
Ok(())
}
#[fasync::run_until_stalled(test)]
async fn test_poll_until_loaded() -> Result<(), Error> {
let events = vec![
create_navigate_to_page_event(None, None),
create_navigate_to_page_event(Some(PageType::Normal), Some(true)),
];
let web_frame = create_frame_with_events(events)?;
let stream = web_frame.get_navigation_event_stream()?;
assert!(DefaultStandaloneWebFrame::poll_until_loaded(stream).await.is_ok());
// Verify functionality when pagetype and document_loaded events sent
// separately
let events = vec![
create_navigate_to_page_event(None, None),
create_navigate_to_page_event(Some(PageType::Normal), None),
create_navigate_to_page_event(None, Some(true)),
];
let web_frame = create_frame_with_events(events)?;
let stream = web_frame.get_navigation_event_stream()?;
assert!(DefaultStandaloneWebFrame::poll_until_loaded(stream).await.is_ok());
Ok(())
}
#[fasync::run_until_stalled(test)]
async fn test_poll_until_loaded_network_error() -> Result<(), Error> {
let events = vec![
create_navigate_to_page_event(None, None),
create_navigate_to_page_event(None, Some(true)),
create_navigate_to_page_event(Some(PageType::Error), None),
];
let web_frame = create_frame_with_events(events)?;
let stream = web_frame.get_navigation_event_stream()?;
assert_eq!(
DefaultStandaloneWebFrame::poll_until_loaded(stream).await.unwrap_err().api_error,
ApiError::Network
);
Ok(())
}
#[fasync::run_until_stalled(test)]
async fn test_poll_until_loaded_stream_closed() -> Result<(), Error> {
let events = vec![
create_navigate_to_page_event(None, None),
create_navigate_to_page_event(Some(PageType::Normal), None),
];
let web_frame = create_frame_with_events(events)?;
let stream = web_frame.get_navigation_event_stream()?;
assert_eq!(
DefaultStandaloneWebFrame::poll_until_loaded(stream).await.unwrap_err().api_error,
ApiError::Unknown
);
Ok(())
}
}
#[cfg(test)]
pub mod mock {
use super::*;
/// A mock implementation of StandaloneWebFrame that always returns the responses
/// specified during its creation.
pub struct TestWebFrame {
display_url_response: TokenProviderResult<()>,
wait_for_redirect_response: TokenProviderResult<Url>,
}
impl TestWebFrame {
pub fn new(
display_url_response: TokenProviderResult<()>,
wait_for_redirect_response: TokenProviderResult<Url>,
) -> Self {
TestWebFrame { display_url_response, wait_for_redirect_response }
}
}
#[async_trait]
impl StandaloneWebFrame for TestWebFrame {
async fn display_url(
&mut self,
_view_token: ViewToken,
_url: Url,
) -> TokenProviderResult<()> {
// manual clone since TokenProviderError.cause is !Clone
match &self.display_url_response {
Ok(()) => Ok(()),
Err(err) => Err(TokenProviderError::new(err.api_error)),
}
}
async fn wait_for_redirect(&mut self, _redirect_target: Url) -> TokenProviderResult<Url> {
// manual clone since TokenProviderError.cause is !Clone
match &self.wait_for_redirect_response {
Ok(url) => Ok(url.clone()),
Err(err) => Err(TokenProviderError::new(err.api_error)),
}
}
}
/// Clones a TokenProviderResult. This is provided instead of a Clone
/// implementation due to orphan rules.
fn clone_result<T: Clone>(result: &TokenProviderResult<T>) -> TokenProviderResult<T> {
match result {
Ok(res) => Ok(res.clone()),
// error cause cannot be cloned so don't replicate it.
Err(err) => Err(TokenProviderError::new(err.api_error)),
}
}
/// A mock implementation of `WebFrameSupplier` that supplies `TestWebFrames`.
/// The supplied `TestWebFrames` will return the responses provided during
/// creation of the `TestWebFrameSupplier`.
pub struct TestWebFrameSupplier {
display_url_response: TokenProviderResult<()>,
wait_for_redirect_response: TokenProviderResult<Url>,
}
impl TestWebFrameSupplier {
pub fn new(
display_url_response: TokenProviderResult<()>,
wait_for_redirect_response: TokenProviderResult<Url>,
) -> Self {
TestWebFrameSupplier { display_url_response, wait_for_redirect_response }
}
}
impl WebFrameSupplier for TestWebFrameSupplier {
type Frame = TestWebFrame;
fn new_standalone_frame(&self) -> Result<TestWebFrame, anyhow::Error> {
Ok(TestWebFrame::new(
clone_result(&self.display_url_response),
clone_result(&self.wait_for_redirect_response),
))
}
}
mod test {
use super::*;
use fuchsia_async as fasync;
use fuchsia_scenic::ViewTokenPair;
use lazy_static::lazy_static;
lazy_static! {
static ref TEST_URL: Url = Url::parse("http://example.com").unwrap();
}
#[fasync::run_until_stalled(test)]
async fn test_web_frame_test() {
let mut test_web_frame =
TestWebFrame::new(Ok(()), Err(TokenProviderError::new(ApiError::Internal)));
let ViewTokenPair { view_token, view_holder_token: _ } = ViewTokenPair::new().unwrap();
assert!(test_web_frame.display_url(view_token, TEST_URL.clone()).await.is_ok());
assert_eq!(
test_web_frame.wait_for_redirect(TEST_URL.clone()).await.unwrap_err().api_error,
ApiError::Internal
);
}
#[fasync::run_until_stalled(test)]
async fn test_web_frame_supplier_test() {
// frames given by the test supplier should just replicate the behavior the supplier
// is configured with.
let success_frame_supplier = TestWebFrameSupplier::new(Ok(()), Ok(TEST_URL.clone()));
let ViewTokenPair { view_token, view_holder_token: _ } = ViewTokenPair::new().unwrap();
let mut success_frame = success_frame_supplier.new_standalone_frame().unwrap();
assert!(success_frame.display_url(view_token, TEST_URL.clone()).await.is_ok(),);
assert_eq!(
success_frame.wait_for_redirect(TEST_URL.clone()).await.unwrap(),
TEST_URL.clone()
);
let error_frame_supplier = TestWebFrameSupplier::new(
Err(TokenProviderError::new(ApiError::Internal)),
Err(TokenProviderError::new(ApiError::Unknown)),
);
let mut error_frame = error_frame_supplier.new_standalone_frame().unwrap();
let ViewTokenPair { view_token, view_holder_token: _ } = ViewTokenPair::new().unwrap();
assert_eq!(
error_frame.display_url(view_token, TEST_URL.clone()).await.unwrap_err().api_error,
ApiError::Internal
);
assert_eq!(
error_frame.wait_for_redirect(TEST_URL.clone()).await.unwrap_err().api_error,
ApiError::Unknown
);
}
}
}