| // 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 |
| ); |
| } |
| } |
| } |