Skip to content

Commit 69f94ee

Browse files
Introduce WpAppNotifier (#693)
* Introduce `WpAppNotifier` * Generate an app notification for 401 responses * Adds `test_notification_unauthorized_request` * Use an invalid password for test_notification_unauthorized_request * Remove `WpApiNotification` in favor of individual functions * Check current application password in `is_unauthorized_request` * Rename WpAppNotifier::unauthorized_request to authentication_becomes_invalid * Simplify is_unauthorized_request implementation * Add `has_valid_authentication` method to request executors * Adds `IsValidAuthenticationResult` * Rename `authentication_becomes_invalid` as `requested_with_invalid_authentication` * Add `Err` prefix to IsValidAuthenticationResult error variants * Add WpAppNotifier to Kotlin WpApiClient * Fix Swift compiling issues * Don't request `/application-passwords/introspect` if response is already unauthorized * Rename is_valid_authentication as fetch_authentication_state and return a Result --------- Co-authored-by: Tony Li <[email protected]>
1 parent ab3cdc0 commit 69f94ee

File tree

16 files changed

+315
-37
lines changed

16 files changed

+315
-37
lines changed

jetpack/src/client.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1+
use super::endpoint::connection_endpoint::{ConnectionRequestBuilder, ConnectionRequestExecutor};
12
use std::sync::Arc;
23
use wp_api::{
34
ParsedUrl, WpApiClientDelegate, api_client_generate_api_client,
45
api_client_generate_endpoint_impl, auth::WpAuthenticationProvider,
56
};
67

7-
use super::endpoint::connection_endpoint::{ConnectionRequestBuilder, ConnectionRequestExecutor};
8-
98
#[derive(uniffi::Object)]
109
struct UniffiJetpackApiRequestBuilder {
1110
inner: JetpackApiRequestBuilder,

jetpack/src/connection.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,11 @@ impl JetpackConnectionClient {
180180
.map_err(JetpackConnectionClientError::Unhandled)?
181181
.data;
182182

183-
let wp_com_client = WpComApiClient::new(wp_api::WpApiClientDelegate {
183+
let wp_com_client = WpComApiClient::new(WpApiClientDelegate {
184184
auth_provider: WpAuthenticationProvider::static_with_auth(wp_com_authentication).into(),
185185
request_executor: self.delegate.request_executor.clone(),
186186
middleware_pipeline: self.delegate.middleware_pipeline.clone(),
187+
app_notifier: self.delegate.app_notifier.clone(),
187188
});
188189
let params = JetpackRemoteConnectionParams {
189190
secret: provision_info.secret,

native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpApiClient.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import uniffi.wp_api.UniffiWpApiClient
99
import uniffi.wp_api.WpApiClientDelegate
1010
import uniffi.wp_api.WpApiException
1111
import uniffi.wp_api.WpApiMiddlewarePipeline
12+
import uniffi.wp_api.WpAppNotifier
1213
import uniffi.wp_api.WpAuthenticationProvider
1314

1415
class WpApiClient
@@ -17,6 +18,7 @@ constructor(
1718
apiRootUrl: ParsedUrl,
1819
authProvider: WpAuthenticationProvider,
1920
private val requestExecutor: RequestExecutor = WpRequestExecutor(),
21+
private val appNotifier: WpAppNotifier = EmptyAppNotifier(),
2022
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
2123
) {
2224
// Don't expose `WpRequestBuilder` directly so we can control how it's used
@@ -26,7 +28,8 @@ constructor(
2628
WpApiClientDelegate(
2729
authProvider,
2830
requestExecutor = requestExecutor,
29-
middlewarePipeline = WpApiMiddlewarePipeline(emptyList())
31+
middlewarePipeline = WpApiMiddlewarePipeline(emptyList()),
32+
appNotifier
3033
)
3134
)
3235
}
@@ -76,3 +79,9 @@ constructor(
7679
}
7780
}
7881
}
82+
83+
class EmptyAppNotifier : WpAppNotifier {
84+
override suspend fun requestedWithInvalidAuthentication() {
85+
// no-op
86+
}
87+
}

native/swift/Sources/wordpress-api/JetpackConnectionClient+Extensions.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ extension JetpackConnectionClient {
1717
delegate: .init(
1818
authProvider: .staticWithAuth(auth: authentication),
1919
requestExecutor: WpRequestExecutor(urlSession: urlSession),
20-
middlewarePipeline: middlewarePipeline
20+
middlewarePipeline: middlewarePipeline,
21+
appNotifier: NoopAppNotifier()
2122
)
2223
)
2324
}

native/swift/Sources/wordpress-api/WordPressAPI.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ public actor WordPressAPI {
3535
delegate: .init(
3636
authProvider: .staticWithAuth(auth: authenticationStategy),
3737
requestExecutor: executor,
38-
middlewarePipeline: middlewarePipeline
38+
middlewarePipeline: middlewarePipeline,
39+
appNotifier: NoopAppNotifier()
3940
)
4041
)
4142
}
@@ -169,3 +170,9 @@ public extension ParsedUrl {
169170
try parse(input: url.absoluteString)
170171
}
171172
}
173+
174+
class NoopAppNotifier: @unchecked Sendable, WpAppNotifier {
175+
func requestedWithInvalidAuthentication() async {
176+
// Do nothing.
177+
}
178+
}

wp_api/src/api_client.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
use crate::auth::WpAuthenticationProvider;
21
use crate::{
3-
ParsedUrl, api_client_generate_api_client, api_client_generate_endpoint_impl,
2+
ParsedUrl, WpAppNotifier, api_client_generate_api_client, api_client_generate_endpoint_impl,
43
api_client_generate_request_builder,
5-
};
6-
use crate::{
4+
auth::WpAuthenticationProvider,
75
middleware::WpApiMiddlewarePipeline,
86
request::{
97
RequestExecutor,
@@ -152,6 +150,7 @@ pub struct WpApiClientDelegate {
152150
pub auth_provider: Arc<WpAuthenticationProvider>,
153151
pub request_executor: Arc<dyn RequestExecutor>,
154152
pub middleware_pipeline: Arc<WpApiMiddlewarePipeline>,
153+
pub app_notifier: Arc<dyn WpAppNotifier>,
155154
}
156155

157156
pub trait IsWpApiClientDelegate {

wp_api/src/api_error.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ where
1212
fn as_parse_error(reason: String, response: String) -> Self;
1313
}
1414

15+
pub trait MaybeWpError {
16+
fn wp_error_code(&self) -> Option<&WpErrorCode>;
17+
18+
fn is_unauthorized_error(&self) -> Option<bool>;
19+
}
20+
1521
#[derive(Debug, PartialEq, Eq, thiserror::Error, uniffi::Error, WpDeriveLocalizable)]
1622
pub enum WpApiError {
1723
InvalidHttpStatusCode {
@@ -44,6 +50,36 @@ pub enum WpApiError {
4450
},
4551
}
4652

53+
impl MaybeWpError for WpApiError {
54+
fn wp_error_code(&self) -> Option<&WpErrorCode> {
55+
match self {
56+
WpApiError::WpError { error_code, .. } => Some(error_code),
57+
_ => None,
58+
}
59+
}
60+
61+
fn is_unauthorized_error(&self) -> Option<bool> {
62+
self.wp_error_code().map(|e| e.is_unauthorized())
63+
}
64+
}
65+
66+
impl<T, E> MaybeWpError for Result<T, E>
67+
where
68+
E: MaybeWpError,
69+
{
70+
fn wp_error_code(&self) -> Option<&WpErrorCode> {
71+
if let Err(e) = self {
72+
e.wp_error_code()
73+
} else {
74+
None
75+
}
76+
}
77+
78+
fn is_unauthorized_error(&self) -> Option<bool> {
79+
self.wp_error_code().map(|e| e.is_unauthorized())
80+
}
81+
}
82+
4783
impl WpSupportsLocalization for WpApiError {
4884
fn message_bundle(&self) -> MessageBundle {
4985
match self {
@@ -431,6 +467,12 @@ pub enum WpErrorCode {
431467
CustomError(String),
432468
}
433469

470+
impl WpErrorCode {
471+
fn is_unauthorized(&self) -> bool {
472+
self == &Self::Unauthorized
473+
}
474+
}
475+
434476
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, uniffi::Error, WpDeriveLocalizable)]
435477
pub enum RequestExecutionError {
436478
RequestExecutionFailed {

wp_api/src/lib.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ pub use api_client::{
22
IsWpApiClientDelegate, WpApiClient, WpApiClientDelegate, WpApiRequestBuilder,
33
};
44
pub use api_error::{
5-
InvalidSslErrorReason, MediaUploadRequestExecutionError, ParsedRequestError,
5+
InvalidSslErrorReason, MaybeWpError, MediaUploadRequestExecutionError, ParsedRequestError,
66
RequestExecutionError, RequestExecutionErrorReason, WpApiError, WpError, WpErrorCode,
77
};
88
pub use parsed_url::{ParseUrlError, ParsedUrl};
@@ -191,6 +191,12 @@ pub enum IntegerOrString {
191191
String(String),
192192
}
193193

194+
#[uniffi::export(with_foreign)]
195+
#[async_trait::async_trait]
196+
pub trait WpAppNotifier: Send + Sync + std::fmt::Debug {
197+
async fn requested_with_invalid_authentication(&self);
198+
}
199+
194200
#[macro_export]
195201
macro_rules! generate {
196202
($type_name:ident) => {

wp_api/src/request.rs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
use self::endpoint::WpEndpointUrl;
2-
use crate::RequestExecutionErrorReason;
3-
use crate::auth::WpAuthenticationProvider;
42
use crate::{
5-
WpApiError,
3+
ParsedUrl, RequestExecutionErrorReason, WpApiError, WpErrorCode,
64
api_error::{MediaUploadRequestExecutionError, ParsedRequestError, RequestExecutionError},
5+
auth::WpAuthenticationProvider,
76
url_query::{FromUrlQueryPairs, UrlQueryPairsMap},
87
};
98
use base64::Engine;
109
use chrono::{DateTime, Utc};
10+
use endpoint::application_passwords_endpoint::{
11+
ApplicationPasswordsRequestBuilder,
12+
ApplicationPasswordsRequestRetrieveCurrentWithEditContextResponse,
13+
};
1114
use endpoint::{ApiEndpointUrl, media_endpoint::MediaUploadRequest};
1215
use http::{HeaderMap, HeaderName, HeaderValue};
1316
use regex::Regex;
@@ -753,6 +756,48 @@ pub fn is_valid_json(value: impl AsRef<str>) -> bool {
753756
serde_json::from_str::<serde_json::Value>(value.as_ref()).is_ok()
754757
}
755758

759+
#[uniffi::export]
760+
pub async fn fetch_authentication_state(
761+
request_executor: Arc<dyn RequestExecutor>,
762+
api_root_url: Arc<ParsedUrl>,
763+
authentication_provider: Arc<WpAuthenticationProvider>,
764+
) -> Result<AuthenticationState, WpApiError> {
765+
let request = ApplicationPasswordsRequestBuilder::new(api_root_url, authentication_provider)
766+
.retrieve_current_with_edit_context()
767+
.into();
768+
let response = request_executor.execute(request).await?;
769+
let parsed_res: Result<
770+
ApplicationPasswordsRequestRetrieveCurrentWithEditContextResponse,
771+
WpApiError,
772+
> = response.parse();
773+
match parsed_res {
774+
Ok(_) => Ok(AuthenticationState::Authenticated),
775+
Err(wp_api_error) => {
776+
if let WpApiError::WpError {
777+
error_code: WpErrorCode::Unauthorized,
778+
..
779+
} = wp_api_error
780+
{
781+
Ok(AuthenticationState::Unauthorized)
782+
} else {
783+
Err(wp_api_error)
784+
}
785+
}
786+
}
787+
}
788+
789+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, uniffi::Enum)]
790+
pub enum AuthenticationState {
791+
Authenticated,
792+
Unauthorized,
793+
}
794+
795+
impl AuthenticationState {
796+
pub fn is_unauthorized(&self) -> bool {
797+
self == &Self::Unauthorized
798+
}
799+
}
800+
756801
pub mod user_agent {
757802
#[uniffi::export]
758803
pub fn default_user_agent(client_specific_postfix: &str) -> String {

wp_api_integration_tests/src/lib.rs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
use async_trait::async_trait;
12
use std::sync::Arc;
23
use url::Url;
34
use wp_api::{
4-
ParsedUrl, WpApiClient, WpApiClientDelegate, WpApiError, WpErrorCode,
5+
ParsedUrl, WpApiClient, WpApiClientDelegate, WpApiError, WpAppNotifier, WpErrorCode,
56
auth::WpAuthenticationProvider, categories::CategoryId, comments::CommentId,
67
date::WpGmtDateTime, media::MediaId, middleware::WpApiMiddlewarePipeline, posts::PostId,
78
reqwest_request_executor::ReqwestRequestExecutor, tags::TagId, users::UserId,
@@ -82,6 +83,7 @@ pub fn api_client() -> WpApiClient {
8283
)),
8384
request_executor: Arc::new(ReqwestRequestExecutor::default()),
8485
middleware_pipeline: Arc::new(WpApiMiddlewarePipeline::default()),
86+
app_notifier: Arc::new(EmptyAppNotifier),
8587
},
8688
)
8789
}
@@ -96,6 +98,7 @@ pub fn api_client_as_author() -> WpApiClient {
9698
)),
9799
request_executor: Arc::new(ReqwestRequestExecutor::default()),
98100
middleware_pipeline: Arc::new(WpApiMiddlewarePipeline::default()),
101+
app_notifier: Arc::new(EmptyAppNotifier),
99102
},
100103
)
101104
}
@@ -110,17 +113,7 @@ pub fn api_client_as_subscriber() -> WpApiClient {
110113
)),
111114
request_executor: Arc::new(ReqwestRequestExecutor::default()),
112115
middleware_pipeline: Arc::new(WpApiMiddlewarePipeline::default()),
113-
},
114-
)
115-
}
116-
117-
pub fn api_client_as_unauthenticated() -> WpApiClient {
118-
WpApiClient::new(
119-
test_site_url(),
120-
WpApiClientDelegate {
121-
auth_provider: Arc::new(WpAuthenticationProvider::none()),
122-
request_executor: Arc::new(ReqwestRequestExecutor::default()),
123-
middleware_pipeline: Arc::new(WpApiMiddlewarePipeline::default()),
116+
app_notifier: Arc::new(EmptyAppNotifier),
124117
},
125118
)
126119
}
@@ -132,6 +125,7 @@ pub fn api_client_with_auth_provider(auth_provider: Arc<WpAuthenticationProvider
132125
auth_provider,
133126
request_executor: Arc::new(ReqwestRequestExecutor::default()),
134127
middleware_pipeline: Arc::new(WpApiMiddlewarePipeline::default()),
128+
app_notifier: Arc::new(EmptyAppNotifier),
135129
},
136130
)
137131
}
@@ -187,3 +181,13 @@ impl<T: std::fmt::Debug, E: std::error::Error> AssertResponse for Result<T, E> {
187181
pub fn unwrapped_wp_gmt_date_time(s: &str) -> WpGmtDateTime {
188182
s.parse::<WpGmtDateTime>().expect("Expected a valid date")
189183
}
184+
185+
#[derive(Debug)]
186+
pub struct EmptyAppNotifier;
187+
188+
#[async_trait]
189+
impl WpAppNotifier for EmptyAppNotifier {
190+
async fn requested_with_invalid_authentication(&self) {
191+
// no-op
192+
}
193+
}

0 commit comments

Comments
 (0)