Skip to content

Commit 4852ea7

Browse files
Abilities: normalize core/get-user output coercion
1 parent 00e334f commit 4852ea7

2 files changed

Lines changed: 173 additions & 28 deletions

File tree

src/wp-includes/abilities/class-wp-users-abilities.php

Lines changed: 71 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,12 @@ private static function get_user_output_schema(): array {
161161
'id',
162162
'username',
163163
),
164-
'properties' => array(
165-
'avatar_urls' => $avatar_urls_schema,
166-
'capabilities' => array(
167-
'type' => 'object',
168-
'description' => __( 'All capabilities assigned to the user. Only included if include_capabilities is true and the current user can view them.' ),
169-
),
164+
'properties' => array(
165+
'avatar_urls' => $avatar_urls_schema,
166+
'capabilities' => array(
167+
'type' => 'object',
168+
'description' => __( 'All capabilities assigned to the user. Only included if include_capabilities is true and the current user can view them.' ),
169+
),
170170
'description' => array(
171171
'type' => 'string',
172172
'description' => __( 'Description of the user.' ),
@@ -211,13 +211,13 @@ private static function get_user_output_schema(): array {
211211
'format' => 'date-time',
212212
'description' => __( 'Registration date for the user in ISO 8601 format.' ),
213213
),
214-
'roles' => array(
215-
'type' => 'array',
216-
'description' => __( 'Roles assigned to the user when the current user can view them.' ),
217-
'items' => array(
218-
'type' => 'string',
219-
),
214+
'roles' => array(
215+
'type' => 'array',
216+
'description' => __( 'Roles assigned to the user when the current user can view them.' ),
217+
'items' => array(
218+
'type' => 'string',
220219
),
220+
),
221221
'slug' => array(
222222
'type' => 'string',
223223
'description' => __( 'An alphanumeric identifier for the user.' ),
@@ -236,6 +236,44 @@ private static function get_user_output_schema(): array {
236236
);
237237
}
238238

239+
/**
240+
* Normalizes capabilities into an object with boolean values.
241+
*
242+
* @since 7.0.0
243+
*
244+
* @param mixed $capabilities Capabilities map.
245+
* @return object Normalized capabilities.
246+
*/
247+
private static function normalize_capabilities( $capabilities ): object {
248+
$normalized = array();
249+
250+
foreach ( (array) $capabilities as $capability => $granted ) {
251+
$normalized[ (string) $capability ] = rest_sanitize_boolean( $granted );
252+
}
253+
254+
return (object) $normalized;
255+
}
256+
257+
/**
258+
* Normalizes a list of values into an array of strings.
259+
*
260+
* @since 7.0.0
261+
*
262+
* @param mixed $values Values to normalize.
263+
* @return array<int, string> Normalized string list.
264+
*/
265+
private static function normalize_string_list( $values ): array {
266+
$normalized = array();
267+
268+
foreach ( (array) $values as $value ) {
269+
if ( null === $value || is_scalar( $value ) || ( is_object( $value ) && is_callable( array( $value, '__toString' ) ) ) ) {
270+
$normalized[] = (string) $value;
271+
}
272+
}
273+
274+
return $normalized;
275+
}
276+
239277
/**
240278
* Finds a user by id, username, or email from input parameters.
241279
*
@@ -300,42 +338,47 @@ public static function check_get_user_permission( array $input = array() ): bool
300338
*/
301339
public static function execute_get_user( array $input = array() ) {
302340
$input = is_array( $input ) ? $input : array();
303-
$include_capabilities = ! empty( $input['include_capabilities'] ) && ( true === $input['include_capabilities'] || 'true' === $input['include_capabilities'] );
341+
$include_capabilities = array_key_exists( 'include_capabilities', $input ) ? rest_sanitize_boolean( $input['include_capabilities'] ) : false;
304342

305343
$user = self::find_user( $input );
306344

307345
if ( ! $user || ! $user->exists() ) {
308346
return new WP_Error( 'user_not_found', __( 'User not found.' ), array( 'status' => 404 ) );
309347
}
310348

349+
$user_id = (int) $user->ID;
311350
$can_view_sensitive_user_fields = self::can_view_sensitive_user_fields( $user );
312351

313352
$result = array(
314-
'id' => $user->ID,
315-
'display_name' => $user->display_name,
316-
'description' => $user->description,
317-
'url' => $user->user_url,
318-
'link' => get_author_posts_url( $user->ID, $user->user_nicename ),
319-
'slug' => $user->user_nicename,
353+
'id' => $user_id,
354+
'display_name' => (string) $user->display_name,
355+
'description' => (string) $user->description,
356+
'url' => (string) $user->user_url,
357+
'link' => (string) get_author_posts_url( $user_id, $user->user_nicename ),
358+
'slug' => (string) $user->user_nicename,
320359
'avatar_urls' => rest_get_avatar_urls( $user ),
321360
);
322361

323362
if ( $can_view_sensitive_user_fields ) {
324-
$result['username'] = $user->user_login;
325-
$result['email'] = $user->user_email;
326-
$result['first_name'] = $user->first_name;
327-
$result['last_name'] = $user->last_name;
328-
$result['nickname'] = $user->nickname;
329-
$result['registered_date'] = gmdate( 'c', strtotime( $user->user_registered ) );
330-
$result['locale'] = get_user_locale( $user );
363+
$result['username'] = (string) $user->user_login;
364+
$result['email'] = (string) $user->user_email;
365+
$result['first_name'] = (string) $user->first_name;
366+
$result['last_name'] = (string) $user->last_name;
367+
$result['nickname'] = (string) $user->nickname;
368+
$result['locale'] = (string) get_user_locale( $user );
369+
370+
$registered_timestamp = strtotime( (string) $user->user_registered );
371+
if ( false !== $registered_timestamp ) {
372+
$result['registered_date'] = gmdate( 'c', $registered_timestamp );
373+
}
331374
}
332375

333376
if ( self::can_view_user_roles( $user ) ) {
334-
$result['roles'] = array_values( $user->roles );
377+
$result['roles'] = self::normalize_string_list( $user->roles );
335378
}
336379

337380
if ( $include_capabilities && $can_view_sensitive_user_fields ) {
338-
$result['capabilities'] = (object) $user->allcaps;
381+
$result['capabilities'] = self::normalize_capabilities( $user->allcaps );
339382
}
340383

341384
return $result;

tests/phpunit/tests/abilities-api/wpRegisterCoreAbilities.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,108 @@ public function test_core_get_current_user_info_returns_user_data(): void {
154154
$this->assertSame( get_userdata( $user_id )->display_name, $result['display_name'] );
155155
}
156156

157+
/**
158+
* Tests boolean-like include_capabilities values for the get-user ability.
159+
* @ticket 64146
160+
*/
161+
public function test_core_get_user_include_capabilities_accepts_boolean_like_values(): void {
162+
$user_id = self::factory()->user->create( array( 'role' => 'subscriber' ) );
163+
164+
wp_set_current_user( $user_id );
165+
166+
$ability = wp_get_ability( 'core/get-user' );
167+
168+
$result = $ability->execute(
169+
array(
170+
'id' => $user_id,
171+
'include_capabilities' => '1',
172+
)
173+
);
174+
$this->assertIsArray( $result );
175+
$this->assertArrayHasKey( 'capabilities', $result );
176+
177+
$result = $ability->execute(
178+
array(
179+
'id' => $user_id,
180+
'include_capabilities' => 1,
181+
)
182+
);
183+
$this->assertIsArray( $result );
184+
$this->assertArrayHasKey( 'capabilities', $result );
185+
186+
$result = $ability->execute(
187+
array(
188+
'id' => $user_id,
189+
'include_capabilities' => '0',
190+
)
191+
);
192+
$this->assertIsArray( $result );
193+
$this->assertArrayNotHasKey( 'capabilities', $result );
194+
}
195+
196+
/**
197+
* Tests get-user output is normalized to the declared schema types.
198+
* @ticket 64146
199+
*/
200+
public function test_core_get_user_output_is_normalized_to_schema_types(): void {
201+
$user_id = self::factory()->user->create( array( 'role' => 'subscriber' ) );
202+
203+
wp_set_current_user( $user_id );
204+
205+
$ability = wp_get_ability( 'core/get-user' );
206+
207+
$capability_filter = static function ( $allcaps, $caps, $args, $user ) use ( $user_id ) {
208+
if ( $user_id === (int) $user->ID ) {
209+
$allcaps['custom_capability_flag'] = '1';
210+
}
211+
212+
return $allcaps;
213+
};
214+
add_filter( 'user_has_cap', $capability_filter, 10, 4 );
215+
216+
try {
217+
$result = $ability->execute(
218+
array(
219+
'id' => $user_id,
220+
'include_capabilities' => 1,
221+
)
222+
);
223+
} finally {
224+
remove_filter( 'user_has_cap', $capability_filter, 10 );
225+
}
226+
227+
$this->assertIsArray( $result );
228+
$this->assertIsInt( $result['id'] );
229+
$this->assertIsString( $result['display_name'] );
230+
$this->assertIsString( $result['description'] );
231+
$this->assertIsString( $result['url'] );
232+
$this->assertIsString( $result['link'] );
233+
$this->assertIsString( $result['slug'] );
234+
$this->assertIsString( $result['username'] );
235+
$this->assertIsString( $result['email'] );
236+
$this->assertIsString( $result['first_name'] );
237+
$this->assertIsString( $result['last_name'] );
238+
$this->assertIsString( $result['nickname'] );
239+
$this->assertIsString( $result['locale'] );
240+
$this->assertIsString( $result['registered_date'] );
241+
$this->assertNotFalse( rest_parse_date( $result['registered_date'] ) );
242+
243+
$this->assertIsArray( $result['roles'] );
244+
foreach ( $result['roles'] as $role ) {
245+
$this->assertIsString( $role );
246+
}
247+
248+
$this->assertIsArray( $result['avatar_urls'] );
249+
foreach ( $result['avatar_urls'] as $avatar_url ) {
250+
$this->assertIsString( $avatar_url );
251+
}
252+
253+
$this->assertIsObject( $result['capabilities'] );
254+
$this->assertObjectHasProperty( 'custom_capability_flag', $result['capabilities'] );
255+
$this->assertIsBool( $result['capabilities']->custom_capability_flag );
256+
$this->assertTrue( $result['capabilities']->custom_capability_flag );
257+
}
258+
157259
/**
158260
* Tests executing the environment info ability.
159261
* @ticket 64146

0 commit comments

Comments
 (0)