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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Swift: Classify invalid-SSL failures from the failed handshake's `SecTrust` (`URLError.failureURLPeerTrust`), via `SecTrustCopyCertificateChain`, instead of reading the undocumented `NSErrorPeerCertificateChainKey` `userInfo` string that has no public constant. Behavior is unchanged on every platform: iOS/macOS/tvOS still surface the presented certificate as `certificateNotValidForName`, and watchOS — which exposes no peer trust — still degrades to `genericSslError`. ([#1510](https://github.com/Automattic/wordpress-rs/issues/1510))
- `isSiteUnreachable` now returns the same answer for a refused connection — the host resolves, but nothing is listening (server down, wrong port) — on every executor. Previously it was `NonExistentSiteError` on Swift (so `isSiteUnreachable` was `true`) but the generic `HttpError` on Kotlin and reqwest (so it was `false`); a refused connection is now a `ConnectionError` everywhere, which `isSiteUnreachable` covers. `NonExistentSiteError` is reserved for a DNS-resolution failure.
- Swift multipart form and media uploads no longer crash when a file becomes unreadable while its body is being serialized. `MultipartForm` fed a failed `InputStream.read` — a `-1` return, e.g. the file was deleted after the upload started or a mid-read I/O error on an external / file-provider / iCloud-evicted volume — straight into `Data(bytesNoCopy:count:deallocator:)`, whose negative count traps. The read failure now surfaces as a `RequestExecutionFailed` error carrying the underlying stream error instead of trapping the process.
- **BREAKING:** The comment create endpoint now returns `CommentWithViewContext`, fixing a deserialization failure for users without `moderate_comments` (core returns them a view-context body). Callers that need edit-only fields off the create result must retrieve the comment with edit context instead.
- Swift multipart serialization also throws `inaccessibleFile` when a field's `InputStream` fails to *open* — e.g. the backing file was deleted between the field's construction and serialization — not only when a `read` fails mid-body. A failed `open()` leaves the stream in `.error` with `hasBytesAvailable == false`, so the read loop never runs and the mid-read `-1` guard can't fire; without a post-loop `streamStatus == .error` check, serialization emitted the closing CRLF and handed back a well-formed-but-empty part. `multipart/form-data` carries no per-part length, so the server couldn't detect the truncation and a should-fail upload became a silently-wrong one. ([#1542](https://github.com/Automattic/wordpress-rs/issues/1542))

### Security
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class CommentsEndpointTest {
requestBuilder.comments()
.create(CommentCreateParams(post = 1, content = "foo", status = CommentStatus.Hold))
}.assertSuccessAndRetrieveData().data
assertEquals("foo", createdComment.content.raw)
assert(createdComment.content.rendered.contains("foo"))
assertEquals(CommentStatus.Hold, createdComment.status)
restoreTestServer()
}
Expand Down
6 changes: 5 additions & 1 deletion wp_api/src/request/endpoint/comments_endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ enum CommentsRequest {
List,
#[contextual_get(url = "/comments/<comment_id>", params = &crate::comments::CommentRetrieveParams, output = crate::comments::SparseComment, filter_by = crate::comments::SparseCommentField)]
Retrieve,
#[post(url = "/comments", params = &crate::comments::CommentCreateParams, output = crate::comments::CommentWithEditContext)]
// The output is a view-context type because core decides the create-response
// context by capability (`edit` only for users with `moderate_comments`,
// `view` otherwise) and ignores the request's `?context=`. Edit-context JSON
// is a superset of view-context JSON, so this parses for every role.
#[post(url = "/comments", params = &crate::comments::CommentCreateParams, output = crate::comments::CommentWithViewContext)]
Create,
#[delete(url = "/comments/<comment_id>", params = &crate::comments::CommentDeleteParams, output = crate::comments::CommentDeleteResponse)]
Delete,
Expand Down
24 changes: 20 additions & 4 deletions wp_api_integration_tests/tests/test_comments_mut.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use macro_helper::generate_update_test;
use wp_api::comments::{
CommentCreateParams, CommentCreateParamsBuilder, CommentDeleteParams, CommentStatus,
CommentUpdateParams, CommentWithEditContext,
CommentUpdateParams, CommentWithEditContext, CommentWithViewContext,
};
use wp_api_integration_tests::prelude::*;
use wp_cli::WpCliComment;
Expand All @@ -12,7 +12,7 @@ async fn create_comment_with_just_content() {
test_create_comment(
&CommentCreateParams::new(FIRST_POST_ID, "foo".to_string()),
|created_comment, comment_from_wp_cli| {
assert_eq!(created_comment.content.raw, "foo");
assert!(created_comment.content.rendered.contains("foo"));
assert_eq!(comment_from_wp_cli.content, "foo");
},
)
Expand All @@ -27,14 +27,30 @@ async fn create_comment_with_content_and_status() {
.status(Some(CommentStatus::Hold))
.build(),
|created_comment, comment_from_wp_cli| {
assert_eq!(created_comment.content.raw, "foo");
assert!(created_comment.content.rendered.contains("foo"));
assert_eq!(created_comment.status, CommentStatus::Hold);
assert_eq!(comment_from_wp_cli.content, "foo");
},
)
.await;
}

#[tokio::test]
#[serial]
async fn create_comment_as_subscriber() {
// Core returns a view-context response to users without `moderate_comments`,
// so the create response must parse for a subscriber as well.
let created_comment = api_client_as_subscriber()
.comments()
.create(&CommentCreateParams::new(FIRST_POST_ID, "foo".to_string()))
.await
.assert_response()
.data;
let created_comment_from_wp_cli = Backend::comment(&created_comment.id).await;
assert_eq!(created_comment_from_wp_cli.content, "foo");
RestoreServer::db().await;
}

#[tokio::test]
#[serial]
async fn delete_comment() {
Expand Down Expand Up @@ -211,7 +227,7 @@ generate_update_test!(

async fn test_create_comment<F>(params: &CommentCreateParams, assert: F)
where
F: Fn(CommentWithEditContext, WpCliComment),
F: Fn(CommentWithViewContext, WpCliComment),
{
let created_comment = api_client()
.comments()
Expand Down