From 3c105a7a9c1b5addaf6be5f150e58ebb4b0192f9 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 16 Apr 2025 10:23:37 +1200 Subject: [PATCH 1/5] Rename KnownApplicationPasswordBlockingPlugin --- wp_api/src/login.rs | 12 +++++------- wp_api/src/login/url_discovery.rs | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/wp_api/src/login.rs b/wp_api/src/login.rs index f4bcf7bea..d6599798b 100644 --- a/wp_api/src/login.rs +++ b/wp_api/src/login.rs @@ -109,16 +109,14 @@ impl WpApiDetails { /// Does the site use a plugin that disables application passwords? pub fn has_application_password_blocking_plugin(&self) -> bool { - KnownApplicationPasswordBlockingPlugin::all() + KnownAuthenticationBlockingPlugin::all() .iter() .any(|plugin| self.namespaces.contains(&plugin.namespace)) } /// Returns a list of plugins that might be responsible for disabling application passwords. - pub fn application_password_blocking_plugins( - &self, - ) -> Vec { - KnownApplicationPasswordBlockingPlugin::all() + pub fn application_password_blocking_plugins(&self) -> Vec { + KnownAuthenticationBlockingPlugin::all() .iter() .filter(|plugin| self.namespaces.contains(&plugin.namespace)) .cloned() @@ -138,7 +136,7 @@ impl WpApiDetails { } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, uniffi::Record)] -pub struct KnownApplicationPasswordBlockingPlugin { +pub struct KnownAuthenticationBlockingPlugin { /// The name of the plugin. pub name: String, /// The plugin's REST API namespace. @@ -147,7 +145,7 @@ pub struct KnownApplicationPasswordBlockingPlugin { pub support_url: String, } -impl KnownApplicationPasswordBlockingPlugin { +impl KnownAuthenticationBlockingPlugin { fn all() -> Vec { vec![ Self { diff --git a/wp_api/src/login/url_discovery.rs b/wp_api/src/login/url_discovery.rs index 7d70a7f56..a68a5facb 100644 --- a/wp_api/src/login/url_discovery.rs +++ b/wp_api/src/login/url_discovery.rs @@ -1,7 +1,7 @@ use super::WpApiDetails; use crate::{ ParseUrlError, ParsedUrl, RequestExecutionError, RequestExecutionErrorReason, WpErrorCode, - login::KnownApplicationPasswordBlockingPlugin, + login::KnownAuthenticationBlockingPlugin, request::{ResponseBodyType, WpRedirect}, }; use itertools::Itertools; @@ -513,7 +513,7 @@ impl FetchAndParseApiRootFailure { #[derive(Debug, Clone, PartialEq, uniffi::Enum)] pub enum ApplicationPasswordsNotSupportedReason { ApplicationPasswordBlockedByPlugin { - plugin: KnownApplicationPasswordBlockingPlugin, + plugin: KnownAuthenticationBlockingPlugin, }, ApplicationPasswordBlockedByMultiplePlugins, SiteIsLocalDevelopmentEnvironment, From f77391ac2ca7c57600965136a77668fd8b56f7dc Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 16 Apr 2025 10:30:56 +1200 Subject: [PATCH 2/5] Add functions to find plugins that block application password and xmlrpc --- wp_api/src/login.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/wp_api/src/login.rs b/wp_api/src/login.rs index d6599798b..3e2fce532 100644 --- a/wp_api/src/login.rs +++ b/wp_api/src/login.rs @@ -109,14 +109,23 @@ impl WpApiDetails { /// Does the site use a plugin that disables application passwords? pub fn has_application_password_blocking_plugin(&self) -> bool { - KnownAuthenticationBlockingPlugin::all() + KnownAuthenticationBlockingPlugin::application_passwords() .iter() .any(|plugin| self.namespaces.contains(&plugin.namespace)) } /// Returns a list of plugins that might be responsible for disabling application passwords. pub fn application_password_blocking_plugins(&self) -> Vec { - KnownAuthenticationBlockingPlugin::all() + KnownAuthenticationBlockingPlugin::application_passwords() + .iter() + .filter(|plugin| self.namespaces.contains(&plugin.namespace)) + .cloned() + .collect() + } + + /// Returns a list of plugins that might be responsible for disabling XML-RPC. + pub fn xmlrpc_blocking_plugins(&self) -> Vec { + KnownAuthenticationBlockingPlugin::xmlrpc() .iter() .filter(|plugin| self.namespaces.contains(&plugin.namespace)) .cloned() @@ -168,6 +177,14 @@ impl KnownAuthenticationBlockingPlugin { }, ] } + + fn application_passwords() -> Vec { + Self::all() + } + + fn xmlrpc() -> Vec { + Self::all() + } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, uniffi::Record)] From 4eea15d3e2f5ace35742ecbbbdff3a804ea458be Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 16 Apr 2025 10:40:02 +1200 Subject: [PATCH 3/5] Implement XML-RPC endpoint discovery --- Cargo.lock | 21 ++- Cargo.toml | 1 + wp_api/Cargo.toml | 1 + wp_api/src/login/login_client.rs | 125 ++++++++++++- wp_api/src/login/url_discovery.rs | 164 ++++++++++++++++++ .../tests/test_login_remote.rs | 41 ++++- wp_localization/localization/en-US/main.ftl | 4 + wp_localization/localization/tr-TR/main.ftl | 5 + 8 files changed, 351 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c2df2c126..dd8782d6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -748,7 +748,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1657,7 +1657,7 @@ checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" dependencies = [ "hermit-abi 0.4.0", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1753,7 +1753,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -2327,7 +2327,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2676,6 +2676,12 @@ dependencies = [ "uncased", ] +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + [[package]] name = "rstest" version = "0.24.0" @@ -2754,7 +2760,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3289,7 +3295,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4151,7 +4157,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -4472,6 +4478,7 @@ dependencies = [ "paste", "regex", "reqwest", + "roxmltree", "rstest", "rstest_reuse", "rustls", diff --git a/Cargo.toml b/Cargo.toml index 6d1327159..0bbef4f0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ quote = "1.0" regex = "1.11" reqwest = "0.12.15" rocket = "0.5" +roxmltree = "0.20" rstest = "0.24" rstest_reuse = "0.7.0" rustls = "0.23.26" diff --git a/wp_api/Cargo.toml b/wp_api/Cargo.toml index 04252ce85..a3306f31e 100644 --- a/wp_api/Cargo.toml +++ b/wp_api/Cargo.toml @@ -28,6 +28,7 @@ parse_link_header = { workspace = true } paste = { workspace = true } regex = { workspace = true } reqwest = { workspace = true, features = [ "multipart", "json", "stream", "gzip", "brotli", "zstd", "deflate", "rustls-tls", "hickory-dns" ], optional = true } +roxmltree = { workspace = true } rustls = { workspace = true, optional = true } scraper = { workspace = true } serde = { workspace = true, features = [ "derive", "rc" ] } diff --git a/wp_api/src/login/login_client.rs b/wp_api/src/login/login_client.rs index 3ceac8fcb..59e6db145 100644 --- a/wp_api/src/login/login_client.rs +++ b/wp_api/src/login/login_client.rs @@ -4,7 +4,8 @@ use super::{ self, API_ROOT_LINK_HEADER, ApiRootUrl, ApplicationPasswordsNotSupportedReason, AutoDiscoveryAttempt, AutoDiscoveryAttemptFailure, AutoDiscoveryAttemptResult, AutoDiscoveryAttemptSuccess, AutoDiscoveryResult, FetchAndParseApiRootFailure, - FindApiRootFailure, ParseHomepageResult, + FindApiRootFailure, ParseHomepageResult, XMLRPCDisabledReason, XMLRPCDiscoveryError, + extract_rsd_url, is_xmlrpc_response, parse_rsd_for_xmlrpc, }, }; use crate::{ @@ -12,10 +13,11 @@ use crate::{ middleware::{PerformsRequests, WpApiMiddlewarePipeline}, request::{ RequestExecutor, RequestMethod, ResponseBodyType, WpNetworkHeaderMap, WpNetworkRequest, - WpNetworkResponse, + WpNetworkRequestBody, WpNetworkResponse, endpoint::{WP_JSON_PATH_SEGMENTS, WpEndpointUrl}, }, }; +use itertools::Itertools; use std::sync::Arc; use uuid::Uuid; @@ -331,6 +333,125 @@ impl WpLoginClient { } }) } + + pub async fn xmlrpc_discovery( + &self, + details: AutoDiscoveryAttemptSuccess, + ) -> Result { + let mut candidates: Vec = vec![]; + // Prioritize discovered XML-RPC URL if it's available from the site. + if let Ok(url) = self.xmlrpc_from_rsd(&details.parsed_site_url).await { + candidates.push(url); + } + // Fallback to the default XML-RPC URL. + candidates.push( + details + .parsed_site_url + .by_extending_and_splitting_by_forward_slash(["xmlrpc.php"]) + .into(), + ); + candidates.dedup(); + + let mut failures: Vec = vec![]; + for candidate in candidates { + match self + .validate_xmlrpc_url(&candidate, &details.api_details) + .await + { + Ok(_) => return Ok(candidate), + Err(error) => { + failures.push(error); + } + } + } + + Err(failures + .into_iter() + .sorted_by(|a, b| b.importance().cmp(&a.importance())) + .next() + .expect("There is at least one failure")) + } + + async fn validate_xmlrpc_url( + &self, + url: &ParsedUrl, + api_details: &WpApiDetails, + ) -> Result<(), XMLRPCDiscoveryError> { + let response = self.perform( + WpNetworkRequest { + uuid: Uuid::new_v4().into(), + retry_count: 0, + method: RequestMethod::POST, + url: WpEndpointUrl(url.url()), + header_map: WpNetworkHeaderMap::default().into(), + body: Some(Arc::new(WpNetworkRequestBody::new(r#"system.listMethods"#.as_bytes().to_vec()))), + } + .into(), + ) + .await + // It's very likely xml-rpc is blocked by the hosting provider (the request has not reached to WordPress), + // if the site does not send any valid HTTP response. + .map_err(|_| XMLRPCDiscoveryError::Disabled { reason: XMLRPCDisabledReason::ByHost })?; + + // 200 status code and a valid XML-RPC response indicates that XML-RPC is enabled. + // All other responses indicate that XML-RPC is disabled. + if response.status_code == 200 && is_xmlrpc_response(&response.body_as_string()) { + return Ok(()); + } + + let mut plugins = api_details.xmlrpc_blocking_plugins(); + let reason = match plugins.len() { + 0 => XMLRPCDisabledReason::ByHost, + 1 => XMLRPCDisabledReason::ByPlugin { + plugin: plugins.pop().expect("Already verified there is one plugin"), + }, + _ => XMLRPCDisabledReason::ByMultiplePlugins, + }; + Err(XMLRPCDiscoveryError::Disabled { reason }) + } + + async fn xmlrpc_from_rsd( + &self, + parsed_site_url: &ParsedUrl, + ) -> Result { + let response = self + .perform( + WpNetworkRequest { + uuid: Uuid::new_v4().into(), + retry_count: 0, + method: RequestMethod::GET, + url: WpEndpointUrl(parsed_site_url.url()), + header_map: WpNetworkHeaderMap::default().into(), + body: None, + } + .into(), + ) + .await + .map_err(|error| XMLRPCDiscoveryError::FetchHomepage { error })?; + + let rsd_url = extract_rsd_url(&response.body_as_string()) + .ok_or(XMLRPCDiscoveryError::EndpointNotFound)?; + + let rsd_response = self + .perform( + WpNetworkRequest { + uuid: Uuid::new_v4().into(), + retry_count: 0, + method: RequestMethod::GET, + url: WpEndpointUrl(rsd_url), + header_map: WpNetworkHeaderMap::default().into(), + body: None, + } + .into(), + ) + .await + .map_err(|_| XMLRPCDiscoveryError::Disabled { + reason: XMLRPCDisabledReason::ByHost, + })?; + + parse_rsd_for_xmlrpc(&rsd_response.body_as_string()) + .ok_or(XMLRPCDiscoveryError::EndpointNotFound) + } } impl PerformsRequests for WpLoginClient { diff --git a/wp_api/src/login/url_discovery.rs b/wp_api/src/login/url_discovery.rs index a68a5facb..a9407d726 100644 --- a/wp_api/src/login/url_discovery.rs +++ b/wp_api/src/login/url_discovery.rs @@ -689,6 +689,80 @@ impl WpSupportsLocalization for FetchApiDetailsError { } } +#[derive(Debug, thiserror::Error, uniffi::Error, WpDeriveLocalizable)] +pub enum XMLRPCDiscoveryError { + FetchHomepage { error: RequestExecutionError }, + EndpointNotFound, + Disabled { reason: XMLRPCDisabledReason }, +} + +#[derive(Debug, uniffi::Enum)] +pub enum XMLRPCDisabledReason { + ByHost, + ByPlugin { + plugin: KnownAuthenticationBlockingPlugin, + }, + ByMultiplePlugins, +} + +impl WpSupportsLocalization for XMLRPCDiscoveryError { + fn message_bundle(&self) -> MessageBundle { + match self { + XMLRPCDiscoveryError::FetchHomepage { error } => error.message_bundle(), + XMLRPCDiscoveryError::EndpointNotFound => WpMessages::xmlrpc_endpoint_not_found(), + XMLRPCDiscoveryError::Disabled { reason } => match reason { + XMLRPCDisabledReason::ByHost => WpMessages::xmlrpc_disabled_by_host(), + XMLRPCDisabledReason::ByPlugin { plugin } => WpMessages::xmlrpc_disabled_by_plugin( + plugin.name.clone(), + plugin.namespace.clone(), + ), + XMLRPCDisabledReason::ByMultiplePlugins => { + WpMessages::xmlrpc_disabled_by_multiple_plugins() + } + }, + } + } +} + +impl XMLRPCDiscoveryError { + pub(crate) fn importance(&self) -> NonZeroUsize { + match self { + XMLRPCDiscoveryError::FetchHomepage { .. } => NonZero::new(1), + XMLRPCDiscoveryError::EndpointNotFound => NonZero::new(2), + XMLRPCDiscoveryError::Disabled { .. } => NonZero::new(3), + } + .expect("All values are valid") + } +} + +pub(crate) fn extract_rsd_url(html: &str) -> Option { + let selector = + Selector::parse("link[rel='EditURI'][type='application/rsd+xml'][title='RSD']").ok()?; + Html::parse_document(html) + .select(&selector) + .next()? + .value() + .attr("href") + .map(String::from) +} + +pub(crate) fn parse_rsd_for_xmlrpc(rsd_xml: &str) -> Option { + // The `rsd_xml` is typically from `/xmlrpc.php?rsd` + + roxmltree::Document::parse(rsd_xml) + .ok()? + .descendants() + .find(|n| n.has_tag_name("api") && n.attribute("name") == Some("WordPress"))? + .attribute("apiLink") + .and_then(|s| ParsedUrl::parse(s).ok()) +} + +pub(crate) fn is_xmlrpc_response(body: &str) -> bool { + roxmltree::Document::parse(body) + .map(|doc| doc.root_element().has_tag_name("methodResponse")) + .unwrap_or(false) +} + #[cfg(test)] mod tests { use super::AutoDiscoveryAttempt as A; @@ -813,6 +887,96 @@ mod tests { assert_eq!(first.compare_importance(&second), expected); } + #[test] + fn test_parse_rsd_content() { + let content = r#" + + + + WordPress + https://wordpress.org/ + https://example.com + + + + + + + + + + "#.trim(); + let parsed_url = parse_rsd_for_xmlrpc(content); + assert_eq!( + parsed_url, + Some(ParsedUrl::parse("https://example.com/xmlrpc.php").unwrap()) + ); + } + + #[test] + fn test_xmlrpc_response_success() { + let body = r#" + + + + + + + + system.multicall + system.listMethods + system.getCapabilities + + + + + + + "# + .trim(); + assert!(is_xmlrpc_response(body)); + } + + #[test] + fn test_xmlrpc_response_fault() { + let body = r#" + + + + + + + faultCode + -32601 + + + faultString + server error. requested method system.listMethos does not exist. + + + + + + "#.trim(); + assert!(is_xmlrpc_response(body)); + } + + #[test] + fn test_xmlrpc_response_not_xml() { + let body = r#" + + + Not XML + + +

This is not an XML-RPC response.

+ + + "# + .trim(); + assert!(!is_xmlrpc_response(body)); + } + // `adaf` refers to `AutoDiscoveryAttemptFailure` mod adaf_helpers { use crate::login::WpApiDetailsAuthenticationMap; diff --git a/wp_api_integration_tests/tests/test_login_remote.rs b/wp_api_integration_tests/tests/test_login_remote.rs index 581b69e8a..7b054539c 100644 --- a/wp_api_integration_tests/tests/test_login_remote.rs +++ b/wp_api_integration_tests/tests/test_login_remote.rs @@ -1,12 +1,13 @@ use serial_test::parallel; use std::{path::Path, sync::Arc}; use wp_api::{ - InvalidSslErrorReason, RequestExecutionError, RequestExecutionErrorReason, + InvalidSslErrorReason, ParsedUrl, RequestExecutionError, RequestExecutionErrorReason, login::{ login_client::WpLoginClient, url_discovery::{ ApplicationPasswordsNotSupportedReason, AutoDiscoveryAttemptFailure, - FetchAndParseApiRootFailure, FindApiRootFailure, + FetchAndParseApiRootFailure, FindApiRootFailure, XMLRPCDisabledReason, + XMLRPCDiscoveryError, }, }, middleware::{ @@ -352,6 +353,30 @@ async fn login_spec_17_invalid_https_with_exception_works() { ); } +#[tokio::test] +#[parallel] +async fn login_spec_18_xmlrpc_disabled() { + // The xmlrpc endpoint does not return a valid HTTP response: + // $ curl https://xmlrpc-disabled.wpmt.co/xmlrpc.php + // curl: (92) HTTP/2 stream 1 was not closed cleanly: PROTOCOL_ERROR (err 1) + let result = xmlrpc_url("https://xmlrpc-disabled.wpmt.co").await; + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + XMLRPCDiscoveryError::Disabled { + reason: XMLRPCDisabledReason::ByHost + } + )); +} + +#[tokio::test] +#[parallel] +async fn login_spec_18_xmlrpc_found() { + let result = xmlrpc_url("https://vanilla.wpmt.co").await; + assert!(result.is_ok()); + assert_eq!(result.unwrap().url(), "https://vanilla.wpmt.co/xmlrpc.php"); +} + async fn login_url(site_url: &str) -> String { login_url_with_executor(Arc::new(ReqwestRequestExecutor::default()), site_url).await } @@ -449,3 +474,15 @@ async fn discovery_helper( }) .map_err(|e| e.clone()) } + +async fn xmlrpc_url(site_url: &str) -> Result { + let client = WpLoginClient::new( + Arc::new(ReqwestRequestExecutor::default()), + Arc::new(WpApiMiddlewarePipeline { + middlewares: vec![], + }), + ); + let result = client.api_discovery(site_url.to_string()).await; + let success = result.combined_result().unwrap(); + client.xmlrpc_discovery(success.clone()).await +} diff --git a/wp_localization/localization/en-US/main.ftl b/wp_localization/localization/en-US/main.ftl index 35e1ce92b..712ba3c68 100644 --- a/wp_localization/localization/en-US/main.ftl +++ b/wp_localization/localization/en-US/main.ftl @@ -53,6 +53,10 @@ wordpress_org_api_client_error_request_encoding = Failed to encode request. Reas probably_not_wordpress_site = The site does not appear to be a WordPress site. rest_api_disabled = The site's REST API is disabled. Please update your site settings to enable REST API. +xmlrpc_disabled_by_host = The site's XML-RPC is disabled. Please contact your hosting provider to solve this problem. +xmlrpc_disabled_by_plugin = The site's XML-RPC is disabled – the {$plugin} plugin might have disabled XML-RPC. Please visit {$support_url} to learn more. +xmlrpc_disabled_by_multiple_plugins = The site's XML-RPC is disabled – there are multiple installed plugins that might have disabled XML-RPC. Please disable them and try again. +xmlrpc_endpoint_not_found = The site's XML-RPC endpoint could not be found. Please check your site settings and try again. application_password_blocked_by_plugin = Unable to login to {$url} – the {$plugin} plugin might have disabled Application Passwords. Please visit {$support_url} to learn more. application_password_blocked_by_multiple_plugins = Unable to login to {$url} – there are multiple installed plugins that might have disabled Application Passwords. Please disable them and try again. diff --git a/wp_localization/localization/tr-TR/main.ftl b/wp_localization/localization/tr-TR/main.ftl index 23a4584d8..750d12178 100644 --- a/wp_localization/localization/tr-TR/main.ftl +++ b/wp_localization/localization/tr-TR/main.ftl @@ -54,6 +54,11 @@ probably_not_wordpress_site = Site bir WordPress sitesi gibi görünmüyor. rest_api_disabled = Sitenin REST API'si devre dışı. Lütfen REST API'yi etkinleştirmek için site ayarlarınızı güncelleyin. +xmlrpc_disabled_by_host = Sitenin XML-RPC özelliği devre dışı bırakılmış. Lütfen bu sorunu çözmek için barındırma sağlayıcınızla iletişime geçin. +xmlrpc_disabled_by_plugin = Sitenin XML-RPC özelliği devre dışı – {$plugin} eklentisi XML-RPC'yi devre dışı bırakmış olabilir. Daha fazla bilgi için {$support_url} adresini ziyaret edin. +xmlrpc_disabled_by_multiple_plugins = Sitenin XML-RPC özelliği devre dışı – XML-RPC'yi devre dışı bırakmış olabilecek birden fazla eklenti yüklü. Lütfen bu eklentileri devre dışı bırakıp tekrar deneyin. +xmlrpc_endpoint_not_found = Sitenin XML-RPC uç noktası bulunamadı. Lütfen site ayarlarınızı kontrol edip tekrar deneyin. + application_password_blocked_by_plugin = {$url} adresine giriş yapılamıyor – {$plugin} eklentisi Uygulama Şifrelerini devre dışı bırakmış olabilir. Daha fazla bilgi için lütfen {$support_url} adresini ziyaret edin. application_password_blocked_by_multiple_plugins = {$url} adresine giriş yapılamıyor – Uygulama Şifrelerini devre dışı bırakmış olabilecek birden fazla eklenti yüklü. Lütfen bunları devre dışı bırakın ve tekrar deneyin. From 88b1dfd660dcb2cba6b5a7c267d8b524c8818de6 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 16 Apr 2025 13:01:29 +1200 Subject: [PATCH 4/5] Add another test site --- .../tests/test_login_remote.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/wp_api_integration_tests/tests/test_login_remote.rs b/wp_api_integration_tests/tests/test_login_remote.rs index 7b054539c..3a86656d9 100644 --- a/wp_api_integration_tests/tests/test_login_remote.rs +++ b/wp_api_integration_tests/tests/test_login_remote.rs @@ -355,7 +355,7 @@ async fn login_spec_17_invalid_https_with_exception_works() { #[tokio::test] #[parallel] -async fn login_spec_18_xmlrpc_disabled() { +async fn login_spec_18_xmlrpc_disabled_by_host() { // The xmlrpc endpoint does not return a valid HTTP response: // $ curl https://xmlrpc-disabled.wpmt.co/xmlrpc.php // curl: (92) HTTP/2 stream 1 was not closed cleanly: PROTOCOL_ERROR (err 1) @@ -369,6 +369,19 @@ async fn login_spec_18_xmlrpc_disabled() { )); } +#[tokio::test] +#[parallel] +async fn login_spec_18_xmlrpc_disabled_by_plugin() { + let result = xmlrpc_url("https://xmlrpc-disabled-by-plugin.wpmt.co").await; + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + XMLRPCDiscoveryError::Disabled { + reason: XMLRPCDisabledReason::ByPlugin { .. } + } + )); +} + #[tokio::test] #[parallel] async fn login_spec_18_xmlrpc_found() { From 4d930e5d9e56158349b93d631118520b1d18fdae Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 16 Apr 2025 13:10:10 +1200 Subject: [PATCH 5/5] Rename XMLRPC to Xmlrpc so that the Rust type name and the binding type name is the same. --- wp_api/src/login/login_client.rs | 30 +++++++++---------- wp_api/src/login/url_discovery.rs | 28 ++++++++--------- .../tests/test_login_remote.rs | 14 ++++----- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/wp_api/src/login/login_client.rs b/wp_api/src/login/login_client.rs index 59e6db145..6fdd5dfa8 100644 --- a/wp_api/src/login/login_client.rs +++ b/wp_api/src/login/login_client.rs @@ -4,7 +4,7 @@ use super::{ self, API_ROOT_LINK_HEADER, ApiRootUrl, ApplicationPasswordsNotSupportedReason, AutoDiscoveryAttempt, AutoDiscoveryAttemptFailure, AutoDiscoveryAttemptResult, AutoDiscoveryAttemptSuccess, AutoDiscoveryResult, FetchAndParseApiRootFailure, - FindApiRootFailure, ParseHomepageResult, XMLRPCDisabledReason, XMLRPCDiscoveryError, + FindApiRootFailure, ParseHomepageResult, XmlrpcDisabledReason, XmlrpcDiscoveryError, extract_rsd_url, is_xmlrpc_response, parse_rsd_for_xmlrpc, }, }; @@ -337,7 +337,7 @@ impl WpLoginClient { pub async fn xmlrpc_discovery( &self, details: AutoDiscoveryAttemptSuccess, - ) -> Result { + ) -> Result { let mut candidates: Vec = vec![]; // Prioritize discovered XML-RPC URL if it's available from the site. if let Ok(url) = self.xmlrpc_from_rsd(&details.parsed_site_url).await { @@ -352,7 +352,7 @@ impl WpLoginClient { ); candidates.dedup(); - let mut failures: Vec = vec![]; + let mut failures: Vec = vec![]; for candidate in candidates { match self .validate_xmlrpc_url(&candidate, &details.api_details) @@ -376,7 +376,7 @@ impl WpLoginClient { &self, url: &ParsedUrl, api_details: &WpApiDetails, - ) -> Result<(), XMLRPCDiscoveryError> { + ) -> Result<(), XmlrpcDiscoveryError> { let response = self.perform( WpNetworkRequest { uuid: Uuid::new_v4().into(), @@ -391,7 +391,7 @@ impl WpLoginClient { .await // It's very likely xml-rpc is blocked by the hosting provider (the request has not reached to WordPress), // if the site does not send any valid HTTP response. - .map_err(|_| XMLRPCDiscoveryError::Disabled { reason: XMLRPCDisabledReason::ByHost })?; + .map_err(|_| XmlrpcDiscoveryError::Disabled { reason: XmlrpcDisabledReason::ByHost })?; // 200 status code and a valid XML-RPC response indicates that XML-RPC is enabled. // All other responses indicate that XML-RPC is disabled. @@ -401,19 +401,19 @@ impl WpLoginClient { let mut plugins = api_details.xmlrpc_blocking_plugins(); let reason = match plugins.len() { - 0 => XMLRPCDisabledReason::ByHost, - 1 => XMLRPCDisabledReason::ByPlugin { + 0 => XmlrpcDisabledReason::ByHost, + 1 => XmlrpcDisabledReason::ByPlugin { plugin: plugins.pop().expect("Already verified there is one plugin"), }, - _ => XMLRPCDisabledReason::ByMultiplePlugins, + _ => XmlrpcDisabledReason::ByMultiplePlugins, }; - Err(XMLRPCDiscoveryError::Disabled { reason }) + Err(XmlrpcDiscoveryError::Disabled { reason }) } async fn xmlrpc_from_rsd( &self, parsed_site_url: &ParsedUrl, - ) -> Result { + ) -> Result { let response = self .perform( WpNetworkRequest { @@ -427,10 +427,10 @@ impl WpLoginClient { .into(), ) .await - .map_err(|error| XMLRPCDiscoveryError::FetchHomepage { error })?; + .map_err(|error| XmlrpcDiscoveryError::FetchHomepage { error })?; let rsd_url = extract_rsd_url(&response.body_as_string()) - .ok_or(XMLRPCDiscoveryError::EndpointNotFound)?; + .ok_or(XmlrpcDiscoveryError::EndpointNotFound)?; let rsd_response = self .perform( @@ -445,12 +445,12 @@ impl WpLoginClient { .into(), ) .await - .map_err(|_| XMLRPCDiscoveryError::Disabled { - reason: XMLRPCDisabledReason::ByHost, + .map_err(|_| XmlrpcDiscoveryError::Disabled { + reason: XmlrpcDisabledReason::ByHost, })?; parse_rsd_for_xmlrpc(&rsd_response.body_as_string()) - .ok_or(XMLRPCDiscoveryError::EndpointNotFound) + .ok_or(XmlrpcDiscoveryError::EndpointNotFound) } } diff --git a/wp_api/src/login/url_discovery.rs b/wp_api/src/login/url_discovery.rs index a9407d726..13fc451cb 100644 --- a/wp_api/src/login/url_discovery.rs +++ b/wp_api/src/login/url_discovery.rs @@ -690,14 +690,14 @@ impl WpSupportsLocalization for FetchApiDetailsError { } #[derive(Debug, thiserror::Error, uniffi::Error, WpDeriveLocalizable)] -pub enum XMLRPCDiscoveryError { +pub enum XmlrpcDiscoveryError { FetchHomepage { error: RequestExecutionError }, EndpointNotFound, - Disabled { reason: XMLRPCDisabledReason }, + Disabled { reason: XmlrpcDisabledReason }, } #[derive(Debug, uniffi::Enum)] -pub enum XMLRPCDisabledReason { +pub enum XmlrpcDisabledReason { ByHost, ByPlugin { plugin: KnownAuthenticationBlockingPlugin, @@ -705,18 +705,18 @@ pub enum XMLRPCDisabledReason { ByMultiplePlugins, } -impl WpSupportsLocalization for XMLRPCDiscoveryError { +impl WpSupportsLocalization for XmlrpcDiscoveryError { fn message_bundle(&self) -> MessageBundle { match self { - XMLRPCDiscoveryError::FetchHomepage { error } => error.message_bundle(), - XMLRPCDiscoveryError::EndpointNotFound => WpMessages::xmlrpc_endpoint_not_found(), - XMLRPCDiscoveryError::Disabled { reason } => match reason { - XMLRPCDisabledReason::ByHost => WpMessages::xmlrpc_disabled_by_host(), - XMLRPCDisabledReason::ByPlugin { plugin } => WpMessages::xmlrpc_disabled_by_plugin( + XmlrpcDiscoveryError::FetchHomepage { error } => error.message_bundle(), + XmlrpcDiscoveryError::EndpointNotFound => WpMessages::xmlrpc_endpoint_not_found(), + XmlrpcDiscoveryError::Disabled { reason } => match reason { + XmlrpcDisabledReason::ByHost => WpMessages::xmlrpc_disabled_by_host(), + XmlrpcDisabledReason::ByPlugin { plugin } => WpMessages::xmlrpc_disabled_by_plugin( plugin.name.clone(), plugin.namespace.clone(), ), - XMLRPCDisabledReason::ByMultiplePlugins => { + XmlrpcDisabledReason::ByMultiplePlugins => { WpMessages::xmlrpc_disabled_by_multiple_plugins() } }, @@ -724,12 +724,12 @@ impl WpSupportsLocalization for XMLRPCDiscoveryError { } } -impl XMLRPCDiscoveryError { +impl XmlrpcDiscoveryError { pub(crate) fn importance(&self) -> NonZeroUsize { match self { - XMLRPCDiscoveryError::FetchHomepage { .. } => NonZero::new(1), - XMLRPCDiscoveryError::EndpointNotFound => NonZero::new(2), - XMLRPCDiscoveryError::Disabled { .. } => NonZero::new(3), + XmlrpcDiscoveryError::FetchHomepage { .. } => NonZero::new(1), + XmlrpcDiscoveryError::EndpointNotFound => NonZero::new(2), + XmlrpcDiscoveryError::Disabled { .. } => NonZero::new(3), } .expect("All values are valid") } diff --git a/wp_api_integration_tests/tests/test_login_remote.rs b/wp_api_integration_tests/tests/test_login_remote.rs index 3a86656d9..b58ade0c6 100644 --- a/wp_api_integration_tests/tests/test_login_remote.rs +++ b/wp_api_integration_tests/tests/test_login_remote.rs @@ -6,8 +6,8 @@ use wp_api::{ login_client::WpLoginClient, url_discovery::{ ApplicationPasswordsNotSupportedReason, AutoDiscoveryAttemptFailure, - FetchAndParseApiRootFailure, FindApiRootFailure, XMLRPCDisabledReason, - XMLRPCDiscoveryError, + FetchAndParseApiRootFailure, FindApiRootFailure, XmlrpcDisabledReason, + XmlrpcDiscoveryError, }, }, middleware::{ @@ -363,8 +363,8 @@ async fn login_spec_18_xmlrpc_disabled_by_host() { assert!(result.is_err()); assert!(matches!( result.unwrap_err(), - XMLRPCDiscoveryError::Disabled { - reason: XMLRPCDisabledReason::ByHost + XmlrpcDiscoveryError::Disabled { + reason: XmlrpcDisabledReason::ByHost } )); } @@ -376,8 +376,8 @@ async fn login_spec_18_xmlrpc_disabled_by_plugin() { assert!(result.is_err()); assert!(matches!( result.unwrap_err(), - XMLRPCDiscoveryError::Disabled { - reason: XMLRPCDisabledReason::ByPlugin { .. } + XmlrpcDiscoveryError::Disabled { + reason: XmlrpcDisabledReason::ByPlugin { .. } } )); } @@ -488,7 +488,7 @@ async fn discovery_helper( .map_err(|e| e.clone()) } -async fn xmlrpc_url(site_url: &str) -> Result { +async fn xmlrpc_url(site_url: &str) -> Result { let client = WpLoginClient::new( Arc::new(ReqwestRequestExecutor::default()), Arc::new(WpApiMiddlewarePipeline {