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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **BREAKING:** `PostCreateParams::meta`, `PostUpdateParams::meta`, and `SparseAnyPost::meta` are now `Option<Arc<PostMeta>>` instead of `Option<PostMeta>`
- **BREAKING:** [Replace `WpService.selfHosted` and `WpService.wordpressCom` with a single `WpService.new(siteInfo:)` constructor](https://github.com/Automattic/wordpress-rs/pull/1239). `SiteInfo.SelfHosted` now carries `ParsedUrl` values for `site_url` and `api_root` instead of `String`, and the `wordpress_com_site_api_root` helper has been removed — construct `SiteInfo.WordPressCom` directly.
- **BREAKING:** [Swift `WordPressAPI` initializers now accept a `SiteInfo`](https://github.com/Automattic/wordpress-rs/pull/1239) instead of `apiRootUrl`/`apiUrlResolver`, and the `siteUrl` parameter is now a `ParsedUrl` rather than a `String`.
- **BREAKING:** `SparseUser::extra_capabilities` is now `Option<UserCapabilitiesMap>` instead of `Option<HashMap<String, bool>>`, mirroring the wrapper used by `SparseUser::capabilities`. In Swift and Kotlin bindings, `UserWithEditContext.extraCapabilities` is now `UserCapabilitiesMap` rather than `[String: Bool]` / `Map<String, Boolean>`
- Reformat `CHANGELOG.md` to follow [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and enforce changelog updates on PRs via a Buildkite check that also surfaces the failure as a GitHub PR comment (using the shared `comment_on_pr` helper from `a8c-ci-toolkit`)
- Release documentation

Expand All @@ -39,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [Support both Integer and String for `WPApiDetails.gmt_offset`](https://github.com/Automattic/wordpress-rs/pull/209)
- Pin `wp-cli/restful` to v0.4.1 to fix Docker build (v0.4.2+ requires unreleased `wp-cli ^2.13`)
- **Internal:** `WpApiCache` lookups for a self-hosted site by URL now tolerate trailing-slash and other URL-normalization differences.
- [Deserialize `extra_capabilities` on legacy WordPress sites and on sites where plugins write non-bool capability values to `wp_capabilities` usermeta](https://github.com/Automattic/wordpress-rs/issues/1313)

## [0.1]

Expand Down
2 changes: 2 additions & 0 deletions integration_test_credentials/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub struct TestCredentials {
pub subscriber_password_uuid: &'static str,
pub author_username: &'static str,
pub author_password: &'static str,
pub legacy_admin_user_id: i64,
pub wpbakery_admin_user_id: i64,
pub password_protected_post_id: i64,
pub password_protected_post_password: &'static str,
pub password_protected_post_title: &'static str,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ const val SECOND_USER_ID: UserId = 2
const val FIRST_USER_EMAIL = "test@example.com"
const val SECOND_USER_EMAIL = "themeshaperwp+demos@gmail.com"
const val SECOND_USER_SLUG = "themedemos"
const val NUMBER_OF_USERS = 4
const val NUMBER_OF_USERS = 6
const val NUMBER_OF_USERS_WITH_PUBLISHED_POSTS = 3
const val NUMBER_OF_PLUGINS = 6
const val HELLO_DOLLY_PLUGIN_SLUG = "hello-dolly/hello"
const val WP_ORG_PLUGIN_SLUG_CLASSIC_WIDGETS = "classic-widgets"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ class UsersEndpointTest {
val userList =
client.request { requestBuilder -> requestBuilder.users().listWithEditContext(params) }
.assertSuccessAndRetrieveData().data
// One of the test users don't have any posts or pages
assertEquals(NUMBER_OF_USERS - 1, userList.count())
assertEquals(NUMBER_OF_USERS_WITH_PUBLISHED_POSTS, userList.count())
}

@Test
Expand Down
15 changes: 15 additions & 0 deletions scripts/setup-test-site.sh
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,19 @@ create_test_credentials () {
AUTHOR_USERNAME="test_author"
AUTHOR_PASSWORD="$(wp user application-password create test_author test --porcelain)"

# Fixture admin users with `wp_capabilities` shapes that exercise non-bool capability values.
# These reproduce the deserialization failures fixed by PR #1263 (`capabilities`) and
# issue #1313 (`extra_capabilities`).
LEGACY_ADMIN_USERNAME="legacy_admin"
LEGACY_ADMIN_USER_ID="$(wp user create "$LEGACY_ADMIN_USERNAME" legacy_admin@example.com --role=administrator --porcelain)"
# Pre-2012 sites stored role assignments as the string "1" rather than boolean true.
wp eval "update_user_meta( $LEGACY_ADMIN_USER_ID, 'wp_capabilities', array( 'administrator' => '1' ) );"

WPBAKERY_ADMIN_USERNAME="wpbakery_admin"
WPBAKERY_ADMIN_USER_ID="$(wp user create "$WPBAKERY_ADMIN_USERNAME" wpbakery_admin@example.com --role=administrator --porcelain)"
# WPBakery and similar plugins call `WP_User::add_cap($cap, $grant)` with non-bool grants.
wp eval "update_user_meta( $WPBAKERY_ADMIN_USER_ID, 'wp_capabilities', array( 'administrator' => true, 'vc_access_rules_post_types' => 'custom' ) );"

PASSWORD_PROTECTED_POST_ID="$(wp post create --post_type=post --post_password=INTEGRATION_TEST --post_title=Password_Protected --porcelain)"
TRASHED_POST_ID="$(wp post create --post_type=post --post_title=Trashed_Post --porcelain)"

Expand Down Expand Up @@ -321,6 +334,8 @@ create_test_credentials () {
subscriber_password_uuid="$SUBSCRIBER_PASSWORD_UUID" \
author_username="$AUTHOR_USERNAME" \
author_password="$AUTHOR_PASSWORD" \
legacy_admin_user_id="$LEGACY_ADMIN_USER_ID" \
wpbakery_admin_user_id="$WPBAKERY_ADMIN_USER_ID" \
password_protected_post_id="$PASSWORD_PROTECTED_POST_ID" \
password_protected_post_password="INTEGRATION_TEST" \
password_protected_post_title="Password_Protected" \
Expand Down
2 changes: 1 addition & 1 deletion wp_api/src/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ pub struct SparseUser {
#[WpContext(edit)]
pub capabilities: Option<UserCapabilitiesMap>,
#[WpContext(edit)]
pub extra_capabilities: Option<HashMap<String, bool>>,
pub extra_capabilities: Option<UserCapabilitiesMap>,
#[WpContext(edit, embed, view)]
// According to our tests, `avatar_urls` is not available for all site types. It's marked with
// `#[WpContextual]` which will make it an `Option` in the generated contextual types.
Expand Down
57 changes: 55 additions & 2 deletions wp_api_integration_tests/tests/test_users_immut.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use reusable_test_cases::list_users_cases;
use wp_api::users::{
SparseUserFieldWithEditContext, SparseUserFieldWithEmbedContext,
SparseUserFieldWithViewContext, UserId, UserListParams, WpApiParamUsersHasPublishedPosts,
WpApiParamUsersOrderBy, WpApiParamUsersWho,
SparseUserFieldWithViewContext, UserCapability, UserId, UserListParams,
WpApiParamUsersHasPublishedPosts, WpApiParamUsersOrderBy, WpApiParamUsersWho,
};
use wp_api_integration_tests::prelude::*;

Expand Down Expand Up @@ -136,6 +136,59 @@ async fn retrieve_user_with_view_context(#[values(FIRST_USER_ID, SECOND_USER_ID)
assert_eq!(user_id, user.id);
}

// Regression test for issue #1313: legacy WordPress sites can store the role assignment
// in `wp_capabilities` as the string `"1"` instead of boolean `true`. The fixture user
// `legacy_admin` has its `wp_capabilities` poisoned to `{"administrator": "1"}` so the
// REST response carries the legacy string shape on `extra_capabilities` (raw user meta)
// and on `capabilities` (allcaps merged with the user-level overlay).
#[tokio::test]
#[parallel]
async fn retrieve_legacy_admin_with_edit_context_parses() {
let user_id = UserId(TestCredentials::instance().legacy_admin_user_id);
let user = api_client()
.users()
.retrieve_with_edit_context(&user_id)
.await
.assert_response()
.data;
assert_eq!(user_id, user.id);
let administrator = UserCapability::Custom("administrator".to_string());
assert!(user.extra_capabilities.map.contains_key(&administrator));
assert!(user.capabilities.map.contains_key(&administrator));
// The string `"1"` is not boolean `true`, so `has_cap` reports false even though
// the legacy site treats this user as an administrator. Other admin caps come from
// the role's defaults (booleans), so they still answer true.
assert!(!user.extra_capabilities.has_cap(administrator));
assert!(user.capabilities.has_cap(UserCapability::EditPosts));
}

// Regression test for PR #1263: plugins like WPBakery call `WP_User::add_cap($cap, $grant)`
// with non-bool grants, leaving entries such as `"vc_access_rules_post_types": "custom"` in
// `wp_capabilities`. The fixture user `wpbakery_admin` carries both a boolean role entry
// and a plugin-style string entry, exercising the deserializer on both response fields.
#[tokio::test]
#[parallel]
async fn retrieve_wpbakery_admin_with_edit_context_parses() {
let user_id = UserId(TestCredentials::instance().wpbakery_admin_user_id);
let user = api_client()
.users()
.retrieve_with_edit_context(&user_id)
.await
.assert_response()
.data;
assert_eq!(user_id, user.id);
let plugin_key = UserCapability::Custom("vc_access_rules_post_types".to_string());
assert!(user.extra_capabilities.map.contains_key(&plugin_key));
assert!(user.capabilities.map.contains_key(&plugin_key));
// String `"custom"` is not boolean `true`, so `has_cap` reports false on the plugin
// entry, while the boolean administrator role still answers true.
assert!(!user.capabilities.has_cap(plugin_key));
assert!(
user.extra_capabilities
.has_cap(UserCapability::Custom("administrator".to_string()))
);
}

#[tokio::test]
#[parallel]
async fn retrieve_me_with_edit_context() {
Expand Down