Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class WpLoginClient @JvmOverloads constructor(
siteUrl: String
): ApiDiscoveryResult = withContext(dispatcher) {
try {
val success = internalClient.apiDiscovery(siteUrl)
val success = internalClient.apiDiscovery(siteUrl, null)
ApiDiscoveryResult.Success(success)
} catch (exception: AutoDiscoveryAttemptFailure) {
when (exception) {
Expand Down
25 changes: 22 additions & 3 deletions native/swift/Sources/wordpress-api/WordPressLoginClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,26 @@ public final class WordPressLoginClient: @unchecked Sendable {
public func details(
ofSite proposedSiteUrl: String
) async throws -> AutoDiscoveryAttemptSuccess {
try await client.apiDiscovery(siteUrl: proposedSiteUrl)
let context = RequestContext()
return try await withTaskCancellationHandler {
let result = try await client.apiDiscovery(siteUrl: proposedSiteUrl, context: context)

// The API discovery process looks something like this:
// 1. Send a few requests to find the potential API root, which is typically the `/wp-json` URL.
// 2. Getting site details:
// a) Send requests to the API root found in step 1 to get details.
// b) If step 1 fails, send requests to a hard-coded `/wp-json` path to get details.
//
// When cancellation happens too early at step 1, the process continues and will most likely
// find a successful result using the hard-coded `wp-json` URL.
//
// Here we manually check cancellation to make sure an error is returned when cancelled.
try Task.checkCancellation()

return result
} onCancel: {
requestExecutor.cancel(context: context)
}
}

/// Uses the proposed site URL to scan the website it points to and find the Application Passwords login URL
Expand All @@ -45,7 +64,7 @@ public final class WordPressLoginClient: @unchecked Sendable {
forSite proposedSiteUrl: String
) async throws -> ParsedUrl {
// All sites should have some form of authentication we can use
try await client.apiDiscovery(siteUrl: proposedSiteUrl).applicationPasswordsAuthenticationUrl
try await details(ofSite: proposedSiteUrl).applicationPasswordsAuthenticationUrl
}

/// Uses the proposed site URL to scan the website it points to and find the Application Passwords login URL,
Expand All @@ -58,7 +77,7 @@ public final class WordPressLoginClient: @unchecked Sendable {
forSite proposedSiteUrl: String,
application: Application
) async throws -> URL {
try await client.apiDiscovery(siteUrl: proposedSiteUrl).loginURL(for: application)
try await details(ofSite: proposedSiteUrl).loginURL(for: application)
}

/// Convert the callback URL into a set of authentication credentials
Expand Down
20 changes: 20 additions & 0 deletions native/swift/Tests/wordpress-api/LoginTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,26 @@ class LoginTests {
_ = try await self.client.findLoginUrl(forSite: "https://vanilla1.wpmt.co")
}

@Test("Cancel API discovery process")
func testCancellation() async throws {
let task = Task { [client] in
let success = try await client.details(ofSite: "https://vanilla.wpmt.co")
Issue.record("The function should throw. \(success)")
}

await #expect(
performing: {
try await Task.sleep(for: .milliseconds(800))
task.cancel()

try await task.value
},
throws: { error in
error is AutoDiscoveryAttemptFailure || error is CancellationError
},
)
}

private func getApplicationPasswordsNotSupportedReason(
from error: any Error
) throws -> ApplicationPasswordsNotSupportedReason? {
Expand Down
2 changes: 1 addition & 1 deletion wp_api/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ impl CookiesNonceAuthenticationProvider {

let client =
WpLoginClient::new_with_default_middleware_pipeline(self.request_executor.clone());
let result = client.api_discovery(self.site_url.clone()).await;
let result = client.api_discovery(self.site_url.clone(), None).await;
let details = match result.combined_result() {
Ok(details) => details.clone(),
Err(err) => {
Expand Down
61 changes: 40 additions & 21 deletions wp_api/src/login/login_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use crate::{
middleware::{PerformsRequests, WpApiMiddlewarePipeline},
parsed_url::ParsedUrl,
request::{
RequestExecutor, RequestMethod, ResponseBodyType, WpNetworkHeaderMap, WpNetworkRequest,
WpNetworkRequestBody, WpNetworkResponse,
RequestContext, RequestExecutor, RequestMethod, ResponseBodyType, WpNetworkHeaderMap,
WpNetworkRequest, WpNetworkRequestBody, WpNetworkResponse,
endpoint::{WP_JSON_PATH_SEGMENTS, WpEndpointUrl},
},
};
Expand Down Expand Up @@ -42,9 +42,10 @@ impl UniffiWpLoginClient {
async fn api_discovery(
&self,
site_url: String,
context: Option<Arc<RequestContext>>,
) -> Result<AutoDiscoveryAttemptSuccess, AutoDiscoveryAttemptFailure> {
self.inner
.api_discovery(site_url)
.api_discovery(site_url, context)
.await
.combined_result()
.cloned()
Expand Down Expand Up @@ -77,13 +78,16 @@ impl WpLoginClient {
)
}

pub async fn api_discovery(&self, site_url: String) -> AutoDiscoveryResult {
let attempts = futures::future::join_all(
url_discovery::construct_attempts(site_url)
.into_iter()
.map(|attempt| async { self.attempt_api_discovery(attempt).await }),
)
.await;
pub async fn api_discovery(
&self,
site_url: String,
context: Option<Arc<RequestContext>>,
) -> AutoDiscoveryResult {
let attempts =
futures::future::join_all(url_discovery::construct_attempts(site_url).into_iter().map(
|attempt| async { self.attempt_api_discovery(attempt, context.clone()).await },
))
.await;
AutoDiscoveryResult {
attempts: attempts.into_iter().map(|r| (r.attempt_type, r)).collect(),
}
Expand All @@ -92,6 +96,7 @@ impl WpLoginClient {
async fn attempt_api_discovery(
&self,
attempt: AutoDiscoveryAttempt,
context: Option<Arc<RequestContext>>,
) -> AutoDiscoveryAttemptResult {
let parsed_site_url: Arc<ParsedUrl> = match ParsedUrl::parse(&attempt.attempt_site_url) {
Ok(u) => u,
Expand All @@ -101,12 +106,15 @@ impl WpLoginClient {
}
.into();

match self.find_api_root_url(Arc::clone(&parsed_site_url)).await {
match self
.find_api_root_url(Arc::clone(&parsed_site_url), context.clone())
.await
{
Ok(api_root_url) => AutoDiscoveryAttemptResult {
attempt_type: attempt.attempt_type,
attempt_site_url: attempt.attempt_site_url,
api_discovery_result: self
.fetch_and_parse_api_root(Arc::clone(&parsed_site_url), &api_root_url)
.fetch_and_parse_api_root(Arc::clone(&parsed_site_url), &api_root_url, context)
.await
.map_err(|fetch_and_parse_api_root_failure| {
AutoDiscoveryAttemptFailure::from_fetch_and_parse_api_root_failure(
Expand Down Expand Up @@ -140,6 +148,7 @@ impl WpLoginClient {
.fetch_and_parse_api_root(
Arc::clone(&parsed_site_url),
&ApiRootUrl(Arc::clone(&root_wp_json_url)),
context,
)
.await
{
Expand Down Expand Up @@ -193,8 +202,9 @@ impl WpLoginClient {
&self,
parsed_site_url: Arc<ParsedUrl>,
api_root_url: &ApiRootUrl,
context: Option<Arc<RequestContext>>,
) -> Result<AutoDiscoveryAttemptSuccess, FetchAndParseApiRootFailure> {
let fetch_api_details_response = match self.fetch_api_root(api_root_url).await {
let fetch_api_details_response = match self.fetch_api_root(api_root_url, context).await {
Ok(r) => r,
Err(error) => return Err(FetchAndParseApiRootFailure::FetchApiRoot { error }),
};
Expand Down Expand Up @@ -251,9 +261,10 @@ impl WpLoginClient {
async fn find_api_root_url(
&self,
parsed_site_url: Arc<ParsedUrl>,
context: Option<Arc<RequestContext>>,
) -> Result<ApiRootUrl, FindApiRootFailure> {
let response = self
.fetch_homepage(Arc::clone(&parsed_site_url))
.fetch_homepage(Arc::clone(&parsed_site_url), context)
.await
.map_err(|error| FindApiRootFailure::FetchHomepage { error })?;
// First check if we can find and parse the api root from the link header
Expand Down Expand Up @@ -288,6 +299,7 @@ impl WpLoginClient {
async fn fetch_api_root(
&self,
api_root_url: &ApiRootUrl,
context: Option<Arc<RequestContext>>,
) -> Result<WpNetworkResponse, RequestExecutionError> {
self.perform(
WpNetworkRequest {
Expand All @@ -299,7 +311,7 @@ impl WpLoginClient {
body: None,
}
.into(),
None,
context,
)
.await
}
Expand All @@ -316,6 +328,7 @@ impl WpLoginClient {
async fn fetch_homepage(
&self,
parsed_site_url: Arc<ParsedUrl>,
context: Option<Arc<RequestContext>>,
) -> Result<WpNetworkResponse, RequestExecutionError> {
self.perform(
WpNetworkRequest {
Expand All @@ -327,7 +340,7 @@ impl WpLoginClient {
body: None,
}
.into(),
None,
context,
)
.await
}
Expand Down Expand Up @@ -363,10 +376,14 @@ impl WpLoginClient {
pub async fn xmlrpc_discovery(
&self,
details: AutoDiscoveryAttemptSuccess,
context: Option<Arc<RequestContext>>,
) -> Result<ParsedUrl, XmlrpcDiscoveryError> {
let mut candidates: Vec<ParsedUrl> = 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 {
if let Ok(url) = self
.xmlrpc_from_rsd(&details.parsed_site_url, context.clone())
.await
{
candidates.push(url);
}
// Fallback to the default XML-RPC URL.
Expand All @@ -381,7 +398,7 @@ impl WpLoginClient {
let mut failures: Vec<XmlrpcDiscoveryError> = vec![];
for candidate in candidates {
match self
.validate_xmlrpc_url(&candidate, &details.api_details)
.validate_xmlrpc_url(&candidate, &details.api_details, context.clone())
.await
{
Ok(_) => return Ok(candidate),
Expand All @@ -402,6 +419,7 @@ impl WpLoginClient {
&self,
url: &ParsedUrl,
api_details: &WpApiDetails,
context: Option<Arc<RequestContext>>,
) -> Result<(), XmlrpcDiscoveryError> {
let response = self.perform(
WpNetworkRequest {
Expand All @@ -413,7 +431,7 @@ impl WpLoginClient {
body: Some(Arc::new(WpNetworkRequestBody::new(r#"<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName></methodCall>"#.as_bytes().to_vec()))),
}
.into(),
None,
context,
)
.await
// It's very likely xml-rpc is blocked by the hosting provider (the request has not reached to WordPress),
Expand All @@ -440,6 +458,7 @@ impl WpLoginClient {
async fn xmlrpc_from_rsd(
&self,
parsed_site_url: &ParsedUrl,
context: Option<Arc<RequestContext>>,
) -> Result<ParsedUrl, XmlrpcDiscoveryError> {
let response = self
.perform(
Expand All @@ -452,7 +471,7 @@ impl WpLoginClient {
body: None,
}
.into(),
None,
context.clone(),
)
.await
.map_err(|error| XmlrpcDiscoveryError::FetchHomepage { error })?;
Expand All @@ -471,7 +490,7 @@ impl WpLoginClient {
body: None,
}
.into(),
None,
context,
)
.await
.map_err(|_| XmlrpcDiscoveryError::Disabled {
Expand Down
2 changes: 1 addition & 1 deletion wp_api_integration_tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ pub async fn api_client_with_account_credentials(
Arc::new(WpApiMiddlewarePipeline::default()),
);
let discovery_result = login_client
.api_discovery(TestCredentials::instance().site_url.to_string())
.api_discovery(TestCredentials::instance().site_url.to_string(), None)
.await;
let details = discovery_result
.combined_result()
Expand Down
6 changes: 3 additions & 3 deletions wp_api_integration_tests/tests/test_login_remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ async fn discovery_helper(
Arc::new(WpApiMiddlewarePipeline { middlewares }),
);
client
.api_discovery(site_url.to_string())
.api_discovery(site_url.to_string(), None)
.await
.combined_result()
.map(|success| {
Expand All @@ -476,7 +476,7 @@ async fn xmlrpc_url(site_url: &str) -> Result<ParsedUrl, XmlrpcDiscoveryError> {
middlewares: vec![],
}),
);
let result = client.api_discovery(site_url.to_string()).await;
let result = client.api_discovery(site_url.to_string(), None).await;
let success = result.combined_result().unwrap();
client.xmlrpc_discovery(success.clone()).await
client.xmlrpc_discovery(success.clone(), None).await
}
4 changes: 2 additions & 2 deletions wp_rs_cli/src/bin/wp_rs_cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ async fn perform_api_discovery(
) -> SimplifiedDiscoveryResult {
println!("Testing {url}");
match login_client
.api_discovery(url.clone())
.api_discovery(url.clone(), None)
.await
.combined_result()
{
Expand Down Expand Up @@ -333,7 +333,7 @@ impl SiteApiType {
let login_client =
WpLoginClient::new_with_default_middleware_pipeline(request_executor.clone());
match login_client
.api_discovery(url.clone())
.api_discovery(url.clone(), None)
.await
.combined_result()
.cloned()
Expand Down
2 changes: 1 addition & 1 deletion wp_rs_web/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ async fn test(form: Form<TestForm<'_>>) -> Template {
println!("Testing {}", form.value);

match login_client
.api_discovery(form.value.to_string())
.api_discovery(form.value.to_string(), None)
.await
.combined_result()
{
Expand Down