diff --git a/wp_api/src/api_error.rs b/wp_api/src/api_error.rs index 3702ef4d..c1af66b8 100644 --- a/wp_api/src/api_error.rs +++ b/wp_api/src/api_error.rs @@ -416,7 +416,7 @@ pub enum WpErrorCode { CustomError(String), } -#[derive(Debug, PartialEq, Eq, thiserror::Error, uniffi::Error)] +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, uniffi::Error)] pub enum RequestExecutionError { #[error( "Request execution failed!\nStatus Code: '{:?}'.\nResponse: '{}'", @@ -429,7 +429,7 @@ pub enum RequestExecutionError { }, } -#[derive(Debug, PartialEq, Eq, thiserror::Error, uniffi::Error)] +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, uniffi::Error)] pub enum MediaUploadRequestExecutionError { #[error( "Request execution failed!\nStatus Code: '{:?}'.\nResponse: '{}'", diff --git a/wp_api/src/login.rs b/wp_api/src/login.rs index 93a24731..87cf8e68 100644 --- a/wp_api/src/login.rs +++ b/wp_api/src/login.rs @@ -4,16 +4,13 @@ use std::str; use std::sync::Arc; use wp_serde_helper::deserialize_i64_or_string; -pub use login_client::WpLoginClient; -pub use url_discovery::{UrlDiscoveryError, UrlDiscoveryState, UrlDiscoverySuccess}; - use crate::ParsedUrl; use crate::WpUuid; const KEY_APPLICATION_PASSWORDS: &str = "application-passwords"; -mod login_client; -mod url_discovery; +pub mod login_client; +pub mod url_discovery; #[derive(Debug, uniffi::Record)] pub struct WpRestApiUrls { @@ -47,7 +44,7 @@ pub fn extract_login_details_from_url( }) } -#[derive(Debug, Serialize, Deserialize, uniffi::Object)] +#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Object)] pub struct WpApiDetails { pub name: String, pub description: String, @@ -70,12 +67,12 @@ impl WpApiDetails { } } -#[derive(Debug, Serialize, Deserialize, uniffi::Record)] +#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct WpRestApiAuthenticationScheme { pub endpoints: WpRestApiAuthenticationEndpoint, } -#[derive(Debug, Serialize, Deserialize, uniffi::Record)] +#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct WpRestApiAuthenticationEndpoint { pub authorization: String, } diff --git a/wp_api/src/login/login_client.rs b/wp_api/src/login/login_client.rs index abc4a804..30049708 100644 --- a/wp_api/src/login/login_client.rs +++ b/wp_api/src/login/login_client.rs @@ -1,16 +1,17 @@ use std::str; use std::sync::Arc; +use super::url_discovery::{ + self, AutoDiscoveryAttempt, AutoDiscoveryAttemptFailure, AutoDiscoveryAttemptResult, + AutoDiscoveryAttemptSuccess, AutoDiscoveryResult, AutoDiscoveryUniffiResult, + ParseApiRootUrlError, +}; +use super::WpApiDetails; use crate::request::endpoint::WpEndpointUrl; use crate::request::{ RequestExecutor, RequestMethod, WpNetworkHeaderMap, WpNetworkRequest, WpNetworkResponse, }; -use crate::ParsedUrl; - -use super::url_discovery::{ - self, FetchApiDetailsError, FetchApiRootUrlError, StateInitial, UrlDiscoveryAttemptError, - UrlDiscoveryAttemptSuccess, UrlDiscoveryError, UrlDiscoveryState, UrlDiscoverySuccess, -}; +use crate::{ParsedUrl, RequestExecutionError}; const API_ROOT_LINK_HEADER: &str = "https://api.w.org/"; @@ -28,11 +29,8 @@ impl UniffiWpLoginClient { } } - async fn api_discovery( - &self, - site_url: String, - ) -> Result { - self.inner.api_discovery(site_url).await + async fn api_discovery(&self, site_url: String) -> AutoDiscoveryUniffiResult { + self.inner.api_discovery(site_url).await.into() } } @@ -46,77 +44,101 @@ impl WpLoginClient { Self { request_executor } } - pub async fn api_discovery( - &self, - site_url: String, - ) -> Result { + pub async fn api_discovery(&self, site_url: String) -> AutoDiscoveryResult { let attempts = futures::future::join_all( url_discovery::construct_attempts(site_url) - .iter() - .map(|s| async { self.attempt_api_discovery(s).await }), + .into_iter() + .map(|attempt| async { self.attempt_api_discovery(attempt).await }), ) .await; - let successful_attempt = attempts.iter().find_map(|a| { - if let Ok(s) = a { - Some(( - Arc::clone(&s.site_url), - Arc::clone(&s.api_details), - Arc::clone(&s.api_root_url), - )) - } else { - None - } - }); - - let attempts = attempts - .into_iter() - .map(|a| match a { - Ok(s) => (s.site_url.url(), UrlDiscoveryState::Success(s)), - Err(e) => (e.site_url(), UrlDiscoveryState::Failure(e)), - }) - .collect(); - if let Some(s) = successful_attempt { - Ok(UrlDiscoverySuccess { - site_url: s.0, - api_details: s.1, - api_root_url: s.2, - attempts, - }) - } else { - Err(UrlDiscoveryError::UrlDiscoveryFailed { attempts }) + AutoDiscoveryResult { + attempts: attempts.into_iter().map(|r| (r.attempt_type, r)).collect(), } } async fn attempt_api_discovery( &self, - site_url: &str, - ) -> Result { - let initial_state = StateInitial::new(site_url); - let parsed_url_state = - initial_state - .parse() - .map_err(|e| UrlDiscoveryAttemptError::FailedToParseSiteUrl { - site_url: site_url.to_string(), - error: e, - })?; - let parsed_site_url = parsed_url_state.site_url.clone(); - let state_fetched_api_root_url = self - .fetch_api_root_url(&parsed_url_state.site_url) - .await - .and_then(|r| parsed_url_state.parse_api_root_response(r)) - .map_err(|e| UrlDiscoveryAttemptError::FetchApiRootUrlFailed { - site_url: Arc::new(parsed_site_url), - error: e, - })?; - match self - .fetch_wp_api_details(&state_fetched_api_root_url.api_root_url) - .await + attempt: AutoDiscoveryAttempt, + ) -> AutoDiscoveryAttemptResult { + let result = self + .inner_attempt_api_discovery(attempt.attempt_site_url.as_str()) + .await; + AutoDiscoveryAttemptResult { + attempt_type: attempt.attempt_type, + attempt_site_url: attempt.attempt_site_url, + result, + } + } + + async fn inner_attempt_api_discovery( + &self, + attempt_site_url: &str, + ) -> Result { + let parsed_site_url = ParsedUrl::parse(attempt_site_url) + .map_err(|error| AutoDiscoveryAttemptFailure::ParseSiteUrl { error })?; + let fetch_api_root_url_response = match self.fetch_api_root_url(&parsed_site_url).await { + Ok(r) => r, + Err(error) => { + return Err(AutoDiscoveryAttemptFailure::FetchApiRootUrl { + parsed_site_url, + error, + }) + } + }; + let api_root_url = + match self.parse_api_root_response(&parsed_site_url, fetch_api_root_url_response) { + Ok(api_root_url) => api_root_url, + Err(error) => { + return Err(AutoDiscoveryAttemptFailure::ParseApiRootUrl { + parsed_site_url, + error, + }) + } + }; + + let fetch_api_details_response = match self.fetch_wp_api_details(&api_root_url).await { + Ok(r) => r, + Err(error) => { + return Err(AutoDiscoveryAttemptFailure::FetchApiDetails { + parsed_site_url, + api_root_url, + error, + }) + } + }; + let api_details: WpApiDetails = + match serde_json::from_slice::(&fetch_api_details_response.body) { + Ok(api_details) => api_details, + Err(error) => { + return Err(AutoDiscoveryAttemptFailure::ParseApiDetails { + parsed_site_url, + api_root_url, + parsing_error_message: error.to_string(), + }) + } + }; + + Ok(AutoDiscoveryAttemptSuccess { + parsed_site_url, + api_root_url, + api_details, + }) + } + + fn parse_api_root_response( + &self, + site_url: &ParsedUrl, + response: WpNetworkResponse, + ) -> Result { + match response + .get_link_header(API_ROOT_LINK_HEADER) + .into_iter() + .nth(0) { - Ok(r) => state_fetched_api_root_url.parse_api_details_response(r), - Err(e) => Err(UrlDiscoveryAttemptError::FetchApiDetailsFailed { - site_url: Arc::new(state_fetched_api_root_url.site_url), - api_root_url: Arc::new(state_fetched_api_root_url.api_root_url), - error: e, + Some(url) => Ok(ParsedUrl::new(url)), + None => Err(ParseApiRootUrlError::ApiRootLinkHeaderNotFound { + header_map: response.header_map, + status_code: response.status_code, }), } } @@ -126,23 +148,20 @@ impl WpLoginClient { async fn fetch_api_root_url( &self, parsed_site_url: &ParsedUrl, - ) -> Result { + ) -> Result { let api_root_request = WpNetworkRequest { method: RequestMethod::HEAD, url: WpEndpointUrl(parsed_site_url.url()), header_map: WpNetworkHeaderMap::default().into(), body: None, }; - self.request_executor - .execute(api_root_request.into()) - .await - .map_err(FetchApiRootUrlError::from) + self.request_executor.execute(api_root_request.into()).await } async fn fetch_wp_api_details( &self, api_root_url: &ParsedUrl, - ) -> Result { + ) -> Result { self.request_executor .execute( WpNetworkRequest { @@ -154,6 +173,5 @@ impl WpLoginClient { .into(), ) .await - .map_err(FetchApiDetailsError::from) } } diff --git a/wp_api/src/login/url_discovery.rs b/wp_api/src/login/url_discovery.rs index a14d84d7..f0116851 100644 --- a/wp_api/src/login/url_discovery.rs +++ b/wp_api/src/login/url_discovery.rs @@ -1,194 +1,344 @@ -use std::{collections::HashMap, sync::Arc}; - -use crate::{ - request::{WpNetworkHeaderMap, WpNetworkResponse}, - ParseUrlError, ParsedUrl, RequestExecutionError, -}; - use super::WpApiDetails; +use crate::{request::WpNetworkHeaderMap, ParseUrlError, ParsedUrl, RequestExecutionError}; +use std::{collections::HashMap, sync::Arc}; const API_ROOT_LINK_HEADER: &str = "https://api.w.org/"; -pub fn construct_attempts(input_site_url: String) -> Vec { - let mut attempts = vec![input_site_url.clone()]; - if !input_site_url.starts_with("http") { - attempts.push(format!("https://{}", input_site_url)) - } - if input_site_url.ends_with("wp-admin") { - attempts.push(format!("{}.php", input_site_url)) - } else if input_site_url.ends_with("wp-admin/") { - let mut s = input_site_url.clone(); - s.pop() - .expect("Already verified that there is at least one char"); - attempts.push(format!("{}.php", s)); - } - attempts +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) struct AutoDiscoveryAttempt { + pub(crate) attempt_site_url: String, + pub(crate) attempt_type: AutoDiscoveryAttemptType, } -#[derive(Debug, uniffi::Enum)] -pub enum UrlDiscoveryState { - Success(UrlDiscoveryAttemptSuccess), - Failure(UrlDiscoveryAttemptError), +impl AutoDiscoveryAttempt { + fn new(attempt_site_url: impl Into, attempt_type: AutoDiscoveryAttemptType) -> Self { + Self { + attempt_site_url: attempt_site_url.into(), + attempt_type, + } + } } #[derive(Debug, uniffi::Record)] -pub struct UrlDiscoveryAttemptSuccess { - pub site_url: Arc, - pub api_details: Arc, - pub api_root_url: Arc, +pub struct AutoDiscoveryUniffiResult { + pub user_input_attempt: Arc, + pub successful_attempt: Option>, + pub auto_https_attempt: Option>, + pub auto_dot_php_extension_for_wp_admin_attempt: Option>, + pub is_successful: bool, } -#[derive(Debug, uniffi::Enum)] -pub enum UrlDiscoveryAttemptError { - FailedToParseSiteUrl { - site_url: String, - error: ParseUrlError, - }, - FetchApiRootUrlFailed { - site_url: Arc, - error: FetchApiRootUrlError, - }, - FetchApiDetailsFailed { - site_url: Arc, - api_root_url: Arc, - error: FetchApiDetailsError, - }, -} - -impl UrlDiscoveryAttemptError { - pub fn site_url(&self) -> String { - match self { - UrlDiscoveryAttemptError::FailedToParseSiteUrl { site_url, .. } => site_url.clone(), - UrlDiscoveryAttemptError::FetchApiRootUrlFailed { site_url, .. } => site_url.url(), - UrlDiscoveryAttemptError::FetchApiDetailsFailed { site_url, .. } => site_url.url(), +impl From for AutoDiscoveryUniffiResult { + fn from(value: AutoDiscoveryResult) -> Self { + let get_attempt_result = |attempt_type| { + value + .get_attempt(&attempt_type) + .map(|a| Arc::new(a.clone())) + }; + Self { + user_input_attempt: Arc::new(value.user_input_attempt().clone()), + successful_attempt: value.find_successful().map(|a| Arc::new(a.clone())), + auto_https_attempt: get_attempt_result(AutoDiscoveryAttemptType::AutoHttps), + auto_dot_php_extension_for_wp_admin_attempt: get_attempt_result( + AutoDiscoveryAttemptType::AutoDotPhpExtensionForWpAdmin, + ), + is_successful: value.is_successful(), } } } -#[derive(Debug, uniffi::Record)] -pub struct UrlDiscoverySuccess { - pub site_url: Arc, - pub api_details: Arc, - pub api_root_url: Arc, - pub attempts: HashMap, +#[derive(Debug)] +pub struct AutoDiscoveryResult { + pub attempts: HashMap, } -#[derive(Debug, thiserror::Error, uniffi::Error)] -pub enum UrlDiscoveryError { - #[error("Url discovery failed: {:?}", attempts)] - UrlDiscoveryFailed { - attempts: HashMap, - }, +impl AutoDiscoveryResult { + pub fn is_successful(&self) -> bool { + self.attempts + .iter() + .any(|(_, result)| result.is_successful()) + } + + pub fn find_successful(&self) -> Option<&AutoDiscoveryAttemptResult> { + // If the user attempt is successful, prefer it over other attempts + let user_input_attempt = self.user_input_attempt(); + if user_input_attempt.is_successful() { + return Some(user_input_attempt); + } + self.attempts.iter().find_map(|(_, result)| { + if result.is_successful() { + Some(result) + } else { + None + } + }) + } + + pub fn user_input_attempt(&self) -> &AutoDiscoveryAttemptResult { + self.get_attempt(&AutoDiscoveryAttemptType::UserInput) + .expect("User input url is always attempted") + } + + pub fn get_attempt( + &self, + attempt_type: &AutoDiscoveryAttemptType, + ) -> Option<&AutoDiscoveryAttemptResult> { + self.attempts.get(attempt_type) + } } -#[derive(Debug)] -pub(super) struct StateInitial { - pub site_url: String, +#[derive(Debug, Clone, uniffi::Object)] +pub struct AutoDiscoveryAttemptResult { + pub attempt_type: AutoDiscoveryAttemptType, + pub attempt_site_url: String, + pub result: Result, } -impl StateInitial { - pub fn new(site_url: &str) -> Self { - Self { - site_url: site_url.to_string(), +#[uniffi::export] +impl AutoDiscoveryAttemptResult { + fn attempt_site_url(&self) -> String { + self.attempt_site_url.clone() + } + + fn error_message(&self) -> Option { + match &self.result { + Ok(_) => None, + Err(error) => Some(error.error_message()), } } - pub fn parse(self) -> Result { - ParsedUrl::parse(self.site_url.as_str()).map(StateParsedUrl::new) + fn is_successful(&self) -> bool { + self.result.is_ok() } -} -#[derive(Debug)] -pub(super) struct StateParsedUrl { - pub site_url: ParsedUrl, -} + fn is_network_error(&self) -> bool { + match &self.result { + Ok(_) => false, + Err(error) => error.is_network_error(), + } + } -impl StateParsedUrl { - fn new(site_url: ParsedUrl) -> Self { - Self { site_url } + fn is_user_input_attempt(&self) -> bool { + matches!(self.attempt_type, AutoDiscoveryAttemptType::UserInput) } - pub fn parse_api_root_response( - self, - response: WpNetworkResponse, - ) -> Result { - match response - .get_link_header(API_ROOT_LINK_HEADER) - .into_iter() - .nth(0) - { - Some(url) => Ok(StateFetchedApiRootUrl { - site_url: self.site_url, - api_root_url: ParsedUrl::new(url), - }), - None => Err(FetchApiRootUrlError::ApiRootLinkHeaderNotFound { - header_map: response.header_map, - status_code: response.status_code, - }), + fn has_failed_to_parse_site_url(&self) -> bool { + match &self.result { + Ok(success) => false, + Err(error) => error.parsed_site_url().is_none(), + } + } + + fn has_failed_to_parse_api_root_url(&self) -> Option { + match &self.result { + Ok(success) => Some(false), + Err(error) => error.has_failed_to_parse_api_root_url(), + } + } + + fn has_failed_to_parse_api_details(&self) -> Option { + match &self.result { + Ok(success) => Some(false), + Err(error) => error.has_failed_to_parse_api_details(), + } + } + + fn parsed_site_url(&self) -> Option> { + match &self.result { + Ok(success) => Some(Arc::new(success.parsed_site_url.clone())), + Err(error) => error.parsed_site_url().map(|p| Arc::new(p.clone())), + } + } + + fn api_root_url(&self) -> Option> { + match &self.result { + Ok(success) => Some(Arc::new(success.api_root_url.clone())), + Err(error) => error.api_root_url().map(|p| Arc::new(p.clone())), + } + } + + fn api_details(&self) -> Option> { + match &self.result { + Ok(success) => Some(Arc::new(success.api_details.clone())), + Err(_) => None, } } } -#[derive(Debug)] -pub(super) struct StateFetchedApiRootUrl { - pub site_url: ParsedUrl, +#[derive(Debug, Clone)] +pub struct AutoDiscoveryAttemptSuccess { + pub parsed_site_url: ParsedUrl, pub api_root_url: ParsedUrl, + pub api_details: WpApiDetails, } -impl StateFetchedApiRootUrl { - pub fn parse_api_details_response( +#[derive(Debug, Clone)] +pub enum AutoDiscoveryAttemptFailure { + ParseSiteUrl { + error: ParseUrlError, + }, + FetchApiRootUrl { + parsed_site_url: ParsedUrl, + error: RequestExecutionError, + }, + ParseApiRootUrl { + parsed_site_url: ParsedUrl, + error: ParseApiRootUrlError, + }, + FetchApiDetails { + parsed_site_url: ParsedUrl, + api_root_url: ParsedUrl, + error: RequestExecutionError, + }, + ParseApiDetails { + parsed_site_url: ParsedUrl, + api_root_url: ParsedUrl, + parsing_error_message: String, + }, +} + +impl AutoDiscoveryAttemptFailure { + pub fn into_attempt_result( self, - response: WpNetworkResponse, - ) -> Result { - match serde_json::from_slice::(&response.body) { - Ok(api_details) => Ok(UrlDiscoveryAttemptSuccess { - site_url: Arc::new(self.site_url), - api_details: Arc::new(api_details), - api_root_url: Arc::new(self.api_root_url), - }), - Err(err) => { - let e = FetchApiDetailsError::ApiDetailsCouldntBeParsed { - reason: err.to_string(), - response: response.body_as_string(), - }; - Err(UrlDiscoveryAttemptError::FetchApiDetailsFailed { - site_url: Arc::new(self.site_url), - api_root_url: Arc::new(self.api_root_url), - error: e, - }) + attempt_type: AutoDiscoveryAttemptType, + attempt_site_url: String, + ) -> AutoDiscoveryAttemptResult { + AutoDiscoveryAttemptResult { + attempt_type, + attempt_site_url, + result: Err(self), + } + } + + pub fn error_message(&self) -> String { + match self { + AutoDiscoveryAttemptFailure::ParseSiteUrl { error } => error.to_string(), + AutoDiscoveryAttemptFailure::FetchApiRootUrl { error, .. } => error.to_string(), + AutoDiscoveryAttemptFailure::ParseApiRootUrl { error, .. } => error.to_string(), + AutoDiscoveryAttemptFailure::FetchApiDetails { error, .. } => error.to_string(), + AutoDiscoveryAttemptFailure::ParseApiDetails { + parsing_error_message, + .. + } => { + format!("Failed to parse api details: {:#?}", parsing_error_message) } } } -} -impl From for UrlDiscoveryAttemptSuccess { - fn from(state: StateFetchedApiDetails) -> Self { - UrlDiscoveryAttemptSuccess { - site_url: Arc::new(state.site_url), - api_details: Arc::new(state.api_details), - api_root_url: Arc::new(state.api_root_url), + pub fn is_network_error(&self) -> bool { + match self { + AutoDiscoveryAttemptFailure::FetchApiRootUrl { .. } => true, + AutoDiscoveryAttemptFailure::FetchApiDetails { .. } => true, + AutoDiscoveryAttemptFailure::ParseSiteUrl { .. } => false, + AutoDiscoveryAttemptFailure::ParseApiRootUrl { .. } => false, + AutoDiscoveryAttemptFailure::ParseApiDetails { .. } => false, + } + } + + pub fn parsed_site_url(&self) -> Option<&ParsedUrl> { + match self { + AutoDiscoveryAttemptFailure::ParseSiteUrl { .. } => None, + AutoDiscoveryAttemptFailure::FetchApiRootUrl { + parsed_site_url, .. + } => Some(parsed_site_url), + AutoDiscoveryAttemptFailure::ParseApiRootUrl { + parsed_site_url, .. + } => Some(parsed_site_url), + AutoDiscoveryAttemptFailure::FetchApiDetails { + parsed_site_url, .. + } => Some(parsed_site_url), + AutoDiscoveryAttemptFailure::ParseApiDetails { + parsed_site_url, .. + } => Some(parsed_site_url), + } + } + + // If it failed while parsing the site url or fetching the api root url, we never tried to + // parse it, so we return `None` + // + // If we fail to parse with `AutoDiscoveryAttemptFailure::ParseApiRootUrl`, we return + // `Some(true)`, because that's exactly when the failure happened. + // + // If an error occurs after parsing the api root url, we return `Some(false)`. + pub fn has_failed_to_parse_api_root_url(&self) -> Option { + match self { + AutoDiscoveryAttemptFailure::ParseSiteUrl { .. } => None, + AutoDiscoveryAttemptFailure::FetchApiRootUrl { .. } => None, + AutoDiscoveryAttemptFailure::ParseApiRootUrl { .. } => Some(true), + AutoDiscoveryAttemptFailure::FetchApiDetails { api_root_url, .. } => Some(false), + AutoDiscoveryAttemptFailure::ParseApiDetails { api_root_url, .. } => Some(false), + } + } + + pub fn api_root_url(&self) -> Option<&ParsedUrl> { + match self { + AutoDiscoveryAttemptFailure::ParseSiteUrl { .. } => None, + AutoDiscoveryAttemptFailure::FetchApiRootUrl { .. } => None, + AutoDiscoveryAttemptFailure::ParseApiRootUrl { .. } => None, + AutoDiscoveryAttemptFailure::FetchApiDetails { api_root_url, .. } => Some(api_root_url), + AutoDiscoveryAttemptFailure::ParseApiDetails { api_root_url, .. } => Some(api_root_url), + } + } + + // If it failed while parsing the site url, fetching the api root url, parsing the api root url + // or fetching the api details, we never tried to parse it, so we return `None`. + // + // If we fail to parse with `AutoDiscoveryAttemptFailure::ParseApiDetails`, we return + // `Some(true)`, because that's exactly when the failure happened. + pub fn has_failed_to_parse_api_details(&self) -> Option { + match self { + AutoDiscoveryAttemptFailure::ParseSiteUrl { .. } => None, + AutoDiscoveryAttemptFailure::FetchApiRootUrl { .. } => None, + AutoDiscoveryAttemptFailure::ParseApiRootUrl { .. } => None, + AutoDiscoveryAttemptFailure::FetchApiDetails { api_root_url, .. } => None, + AutoDiscoveryAttemptFailure::ParseApiDetails { api_root_url, .. } => Some(true), } } } -#[derive(Debug)] -pub(super) struct StateFetchedApiDetails { - pub site_url: ParsedUrl, - pub api_details: WpApiDetails, - pub api_root_url: ParsedUrl, +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, uniffi::Enum)] +pub enum AutoDiscoveryAttemptType { + UserInput, + AutoHttps, + AutoDotPhpExtensionForWpAdmin, } -#[derive(Debug, thiserror::Error, uniffi::Error)] -pub enum FetchApiRootUrlError { - #[error( - "Request execution failed!\nStatus Code: '{:?}'\nResponse: '{}'", - status_code, - reason - )] - RequestExecutionFailed { - status_code: Option, - reason: String, - }, +impl AutoDiscoveryAttemptType { + fn is_the_site_url_same_as_the_user_input(&self) -> bool { + matches!(self, AutoDiscoveryAttemptType::UserInput) + } +} + +pub(crate) fn construct_attempts(input_site_url: String) -> Vec { + let mut attempts = vec![AutoDiscoveryAttempt::new( + input_site_url.clone(), + AutoDiscoveryAttemptType::UserInput, + )]; + if !input_site_url.starts_with("http") { + attempts.push(AutoDiscoveryAttempt::new( + format!("https://{}", input_site_url), + AutoDiscoveryAttemptType::AutoHttps, + )) + } + if input_site_url.ends_with("wp-admin") { + attempts.push(AutoDiscoveryAttempt::new( + format!("{}.php", input_site_url), + AutoDiscoveryAttemptType::AutoDotPhpExtensionForWpAdmin, + )) + } else if input_site_url.ends_with("wp-admin/") { + let mut s = input_site_url.clone(); + s.pop() + .expect("Already verified that there is at least one char"); + attempts.push(AutoDiscoveryAttempt::new( + format!("{}.php", s), + AutoDiscoveryAttemptType::AutoDotPhpExtensionForWpAdmin, + )) + } + attempts +} + +#[derive(Debug, Clone, thiserror::Error, uniffi::Error)] +pub enum ParseApiRootUrlError { #[error( "Api root link header not found!\nStatus Code: '{:#?}'\nHeader Map: '{:#?}'", status_code, @@ -200,20 +350,6 @@ pub enum FetchApiRootUrlError { }, } -impl From for FetchApiRootUrlError { - fn from(value: RequestExecutionError) -> Self { - match value { - RequestExecutionError::RequestExecutionFailed { - status_code, - reason, - } => Self::RequestExecutionFailed { - status_code, - reason, - }, - } - } -} - #[derive(Debug, thiserror::Error, uniffi::Error)] pub enum FetchApiDetailsError { #[error( @@ -229,45 +365,31 @@ pub enum FetchApiDetailsError { ApiDetailsCouldntBeParsed { reason: String, response: String }, } -impl From for FetchApiDetailsError { - fn from(value: RequestExecutionError) -> Self { - match value { - RequestExecutionError::RequestExecutionFailed { - status_code, - reason, - } => Self::RequestExecutionFailed { - status_code, - reason, - }, - } - } -} - #[cfg(test)] mod tests { use super::*; use rstest::*; #[rstest] - #[case("localhost", vec!["localhost", "https://localhost"])] - #[case("http://localhost", vec!["http://localhost"])] - #[case("http://localhost/wp-json", vec!["http://localhost/wp-json"])] - #[case("http://localhost/wp-admin.php", vec!["http://localhost/wp-admin.php"])] - #[case("http://localhost/wp-admin", vec!["http://localhost/wp-admin", "http://localhost/wp-admin.php"])] - #[case("http://localhost/wp-admin/", vec!["http://localhost/wp-admin/", "http://localhost/wp-admin.php"])] - #[case("orchestremetropolitain.com/wp-json", vec!["orchestremetropolitain.com/wp-json", "https://orchestremetropolitain.com/wp-json"])] - #[case("https://orchestremetropolitain.com", vec!["https://orchestremetropolitain.com"])] + #[case("localhost", vec![AutoDiscoveryAttempt::new("localhost", AutoDiscoveryAttemptType::UserInput), AutoDiscoveryAttempt::new("https://localhost", AutoDiscoveryAttemptType::AutoHttps)])] + #[case("http://localhost", vec![AutoDiscoveryAttempt::new("http://localhost", AutoDiscoveryAttemptType::UserInput)])] + #[case("http://localhost/wp-json", vec![AutoDiscoveryAttempt::new("http://localhost/wp-json", AutoDiscoveryAttemptType::UserInput)])] + #[case("http://localhost/wp-admin.php", vec![AutoDiscoveryAttempt::new("http://localhost/wp-admin.php", AutoDiscoveryAttemptType::UserInput)])] + #[case("http://localhost/wp-admin", vec![AutoDiscoveryAttempt::new("http://localhost/wp-admin", AutoDiscoveryAttemptType::UserInput), AutoDiscoveryAttempt::new("http://localhost/wp-admin.php", AutoDiscoveryAttemptType::AutoDotPhpExtensionForWpAdmin)])] + #[case("http://localhost/wp-admin/", vec![AutoDiscoveryAttempt::new("http://localhost/wp-admin/", AutoDiscoveryAttemptType::UserInput), AutoDiscoveryAttempt::new("http://localhost/wp-admin.php", AutoDiscoveryAttemptType::AutoDotPhpExtensionForWpAdmin)])] + #[case("orchestremetropolitain.com/wp-json", vec![AutoDiscoveryAttempt::new("orchestremetropolitain.com/wp-json", AutoDiscoveryAttemptType::UserInput), AutoDiscoveryAttempt::new("https://orchestremetropolitain.com/wp-json", AutoDiscoveryAttemptType::AutoHttps)])] + #[case("https://orchestremetropolitain.com", vec![AutoDiscoveryAttempt::new("https://orchestremetropolitain.com", AutoDiscoveryAttemptType::UserInput)])] #[case( "https://orchestremetropolitain.com/fr/", - vec!["https://orchestremetropolitain.com/fr/"] + vec![AutoDiscoveryAttempt::new("https://orchestremetropolitain.com/fr/", AutoDiscoveryAttemptType::UserInput)] )] #[case( "https://orchestremetropolitain.com/wp-json", - vec!["https://orchestremetropolitain.com/wp-json"] + vec![AutoDiscoveryAttempt::new("https://orchestremetropolitain.com/wp-json", AutoDiscoveryAttemptType::UserInput)] )] fn test_construct_attempts( #[case] input_site_url: &str, - #[case] mut expected_attempts: Vec<&str>, + #[case] mut expected_attempts: Vec, ) { let mut found_attempts = construct_attempts(input_site_url.to_string()); found_attempts.sort(); diff --git a/wp_api/src/parsed_url.rs b/wp_api/src/parsed_url.rs index befb153e..d6f1b6ed 100644 --- a/wp_api/src/parsed_url.rs +++ b/wp_api/src/parsed_url.rs @@ -25,7 +25,7 @@ impl ParsedUrl { } } -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, thiserror::Error, uniffi::Error)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, thiserror::Error, uniffi::Error)] pub enum ParseUrlError { #[error("Error while parsing url: {}", reason)] Generic { reason: String }, diff --git a/wp_api_integration_tests/tests/test_login_err.rs b/wp_api_integration_tests/tests/test_login_err.rs index 5880e939..514c427d 100644 --- a/wp_api_integration_tests/tests/test_login_err.rs +++ b/wp_api_integration_tests/tests/test_login_err.rs @@ -1,18 +1,27 @@ use rstest::rstest; use serial_test::parallel; use std::sync::Arc; -use wp_api::login::{UrlDiscoveryError, WpLoginClient}; +use wp_api::login::login_client::WpLoginClient; +use wp_api::login::url_discovery::AutoDiscoveryAttemptType; use wp_api_integration_tests::AsyncWpNetworking; #[rstest] #[case("http://optional-https.wpmt.co")] // Fails because it's `http` #[tokio::test] #[parallel] -async fn test_login_flow_err_url_discovery_failed(#[case] site_url: &str) { +async fn test_login_flow_err_parse_api_details(#[case] site_url: &str) { let client = WpLoginClient::new(Arc::new(AsyncWpNetworking::default())); - let err = client - .api_discovery(site_url.to_string()) - .await + let mut result = client.api_discovery(site_url.to_string()).await; + let original_attempt_error = result + .attempts + .remove(&AutoDiscoveryAttemptType::UserInput) + .unwrap() + .result .unwrap_err(); - assert!(matches!(err, UrlDiscoveryError::UrlDiscoveryFailed { .. })); + assert_eq!( + original_attempt_error.has_failed_to_parse_api_details(), + Some(true), + "{:#?}", + original_attempt_error + ); } diff --git a/wp_api_integration_tests/tests/test_login_immut.rs b/wp_api_integration_tests/tests/test_login_immut.rs index dcdc248e..c73ce01f 100644 --- a/wp_api_integration_tests/tests/test_login_immut.rs +++ b/wp_api_integration_tests/tests/test_login_immut.rs @@ -1,8 +1,8 @@ use rstest::rstest; use serial_test::parallel; use std::sync::Arc; -use wp_api::login::WpLoginClient; -use wp_api_integration_tests::{AssertResponse, AsyncWpNetworking}; +use wp_api::login::login_client::WpLoginClient; +use wp_api_integration_tests::AsyncWpNetworking; const LOCALHOST_AUTH_URL: &str = "http://localhost/wp-admin/authorize-application.php"; const AUTOMATTIC_WIDGETS_AUTH_URL: &str = @@ -54,12 +54,19 @@ const VANILLA_WP_SITE_URL: &str = "https://vanilla.wpmt.co/wp-admin/authorize-ap #[parallel] async fn test_login_flow(#[case] site_url: &str, #[case] expected_auth_url: &str) { let client = WpLoginClient::new(Arc::new(AsyncWpNetworking::default())); - let url_discovery = client - .api_discovery(site_url.to_string()) - .await - .assert_response(); + let result = client.api_discovery(site_url.to_string()).await; + assert!( + result.is_successful(), + "Auto discovery failed: {:#?}", + result + ); assert_eq!( - url_discovery + result + .find_successful() + .expect("Already verified that auto discovery is successful") + .result + .clone() + .expect("Already verified that auto discovery is successful") .api_details .find_application_passwords_authentication_url(), Some(expected_auth_url.to_string())