Skip to content

Commit 3cf4957

Browse files
Abilities API: apply core/settings review feedback to core/content.
Mirrors the refinements from the core/settings review that also apply to core/content: - Memoize the exposed post types so the input schema and the permission/execute callbacks derive from a single walk of the registered post types. - Default the input schema to an empty object so the type:object default serializes as {}. - Harden input/value handling (type guards, a capability resolver, and a non-negative integer helper) against loosely-typed request data.
1 parent 4201a78 commit 3cf4957

1 file changed

Lines changed: 73 additions & 23 deletions

File tree

‎src/wp-includes/abilities/class-wp-content-abilities.php‎

Lines changed: 73 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,17 @@ class WP_Content_Abilities {
8181
*/
8282
const MAX_PER_PAGE = 100;
8383

84+
/**
85+
* Post types exposed through the Abilities API, computed once at registration.
86+
*
87+
* Cached so the input schema and the permission/execute callbacks derive from the exact
88+
* same set, and the post type list is only walked once per request.
89+
*
90+
* @since 7.1.0
91+
* @var array<string, WP_Post_Type>|null
92+
*/
93+
private static ?array $exposed_post_types = null;
94+
8495
/**
8596
* Registers all content abilities.
8697
*
@@ -105,7 +116,10 @@ public static function register(): void {
105116
* @since 7.1.0
106117
*/
107118
public static function register_get_content(): void {
108-
$post_types = array_keys( self::get_exposed_post_types() );
119+
// Compute once; check_permission()/execute_get_content() reuse this set.
120+
self::$exposed_post_types = self::get_exposed_post_types();
121+
122+
$post_types = array_keys( self::$exposed_post_types );
109123
$statuses = self::get_available_statuses();
110124

111125
wp_register_ability(
@@ -149,11 +163,11 @@ public static function register_get_content(): void {
149163
*/
150164
public static function check_permission( $input = array() ): bool {
151165
$input = is_array( $input ) ? $input : array();
152-
$exposed = self::get_exposed_post_types();
166+
$exposed = self::$exposed_post_types ?? self::get_exposed_post_types();
153167

154168
// Single-post mode (by ID).
155169
if ( ! empty( $input['id'] ) ) {
156-
$post = get_post( (int) $input['id'] );
170+
$post = get_post( self::input_int( $input['id'] ) );
157171

158172
/*
159173
* For a missing post, an unexposed post type, or a post type that does not
@@ -173,15 +187,15 @@ public static function check_permission( $input = array() ): bool {
173187
}
174188

175189
// Query / slug mode requires an exposed post type.
176-
$post_type = isset( $input['post_type'] ) ? (string) $input['post_type'] : '';
190+
$post_type = isset( $input['post_type'] ) && is_string( $input['post_type'] ) ? $input['post_type'] : '';
177191
if ( '' === $post_type || ! isset( $exposed[ $post_type ] ) ) {
178192
return false;
179193
}
180194

181195
$post_type_object = $exposed[ $post_type ];
182196

183197
// Base gate: must be able to read this post type at all.
184-
if ( ! current_user_can( $post_type_object->cap->read ?? 'read' ) ) {
198+
if ( ! current_user_can( self::capability( $post_type_object, 'read', 'read' ) ) ) {
185199
return false;
186200
}
187201

@@ -193,12 +207,12 @@ public static function check_permission( $input = array() ): bool {
193207
}
194208

195209
// Editors/authors of this post type may request any status set.
196-
if ( current_user_can( $post_type_object->cap->edit_posts ?? 'edit_posts' ) ) {
210+
if ( current_user_can( self::capability( $post_type_object, 'edit_posts', 'edit_posts' ) ) ) {
197211
return true;
198212
}
199213

200214
// Otherwise, private posts are allowed only with read_private_posts.
201-
if ( current_user_can( $post_type_object->cap->read_private_posts ?? 'read_private_posts' ) ) {
215+
if ( current_user_can( self::capability( $post_type_object, 'read_private_posts', 'read_private_posts' ) ) ) {
202216
foreach ( $statuses as $status ) {
203217
if ( 'private' !== $status && 'publish' !== $status ) {
204218
return false;
@@ -210,6 +224,34 @@ public static function check_permission( $input = array() ): bool {
210224
return false;
211225
}
212226

227+
/**
228+
* Resolves a capability name from a post type's capability object, with a fallback.
229+
*
230+
* @since 7.1.0
231+
*
232+
* @param WP_Post_Type $post_type_object The post type object.
233+
* @param string $name Capability key on the post type's `cap` object.
234+
* @param string $fallback Fallback capability name if unset or non-string.
235+
* @return string The resolved capability name.
236+
*/
237+
protected static function capability( WP_Post_Type $post_type_object, string $name, string $fallback ): string {
238+
$capability = $post_type_object->cap->$name ?? $fallback;
239+
240+
return is_string( $capability ) ? $capability : $fallback;
241+
}
242+
243+
/**
244+
* Casts a raw input value to a non-negative integer.
245+
*
246+
* @since 7.1.0
247+
*
248+
* @param mixed $value The raw input value.
249+
* @return int The value as a non-negative integer, or 0 when not scalar.
250+
*/
251+
protected static function input_int( $value ): int {
252+
return is_scalar( $value ) ? absint( $value ) : 0;
253+
}
254+
213255
/**
214256
* Executes the `core/content` ability.
215257
*
@@ -220,12 +262,12 @@ public static function check_permission( $input = array() ): bool {
220262
*/
221263
public static function execute_get_content( $input = array() ) {
222264
$input = is_array( $input ) ? $input : array();
223-
$exposed = self::get_exposed_post_types();
265+
$exposed = self::$exposed_post_types ?? self::get_exposed_post_types();
224266
$fields = self::normalize_fields( $input );
225267

226268
// Single-post mode (by ID).
227269
if ( ! empty( $input['id'] ) ) {
228-
$post = get_post( (int) $input['id'] );
270+
$post = get_post( self::input_int( $input['id'] ) );
229271

230272
if ( ! $post
231273
|| ! isset( $exposed[ $post->post_type ] )
@@ -243,13 +285,13 @@ public static function execute_get_content( $input = array() ) {
243285
}
244286

245287
// Query / slug mode.
246-
$post_type = isset( $input['post_type'] ) ? (string) $input['post_type'] : '';
288+
$post_type = isset( $input['post_type'] ) && is_string( $input['post_type'] ) ? $input['post_type'] : '';
247289
if ( '' === $post_type || ! isset( $exposed[ $post_type ] ) ) {
248290
return self::not_found_error();
249291
}
250292

251293
$per_page = self::normalize_per_page( $input );
252-
$page = isset( $input['page'] ) ? max( 1, (int) $input['page'] ) : 1;
294+
$page = isset( $input['page'] ) ? max( 1, self::input_int( $input['page'] ) ) : 1;
253295

254296
$query_args = array(
255297
'post_type' => $post_type,
@@ -259,22 +301,25 @@ public static function execute_get_content( $input = array() ) {
259301
'ignore_sticky_posts' => true,
260302
);
261303

262-
if ( ! empty( $input['slug'] ) ) {
263-
$query_args['name'] = sanitize_title( (string) $input['slug'] );
304+
if ( ! empty( $input['slug'] ) && is_string( $input['slug'] ) ) {
305+
$query_args['name'] = sanitize_title( $input['slug'] );
264306
}
265307

266308
if ( ! empty( $input['author'] ) ) {
267-
$query_args['author'] = (int) $input['author'];
309+
$query_args['author'] = self::input_int( $input['author'] );
268310
}
269311

270312
if ( isset( $input['parent'] ) ) {
271-
$query_args['post_parent'] = (int) $input['parent'];
313+
$query_args['post_parent'] = self::input_int( $input['parent'] );
272314
}
273315

274316
$query = new WP_Query( $query_args );
275317

276318
$posts = array();
277319
foreach ( $query->posts as $post ) {
320+
if ( ! $post instanceof WP_Post ) {
321+
continue;
322+
}
278323
// Authoritative, row-level visibility check (author/status scoped).
279324
if ( ! current_user_can( 'read_post', $post->ID ) ) {
280325
continue;
@@ -294,11 +339,11 @@ public static function execute_get_content( $input = array() ) {
294339
*
295340
* @since 7.1.0
296341
*
297-
* @param array<string, mixed> $input The ability input.
342+
* @param array<mixed> $input The ability input.
298343
* @return int The clamped per-page value.
299344
*/
300345
protected static function normalize_per_page( array $input ): int {
301-
$per_page = isset( $input['per_page'] ) ? (int) $input['per_page'] : self::DEFAULT_PER_PAGE;
346+
$per_page = isset( $input['per_page'] ) ? self::input_int( $input['per_page'] ) : self::DEFAULT_PER_PAGE;
302347

303348
return max( 1, min( self::MAX_PER_PAGE, $per_page ) );
304349
}
@@ -343,16 +388,18 @@ protected static function get_available_statuses(): array {
343388
*
344389
* @since 7.1.0
345390
*
346-
* @param array<string, mixed> $input The ability input.
391+
* @param array<mixed> $input The ability input.
347392
* @return string[] Normalized list of post status slugs.
348393
*/
349394
protected static function normalize_statuses( array $input ): array {
350395
$statuses = $input['status'] ?? array( 'publish' );
351-
if ( ! is_array( $statuses ) || array() === $statuses ) {
396+
if ( ! is_array( $statuses ) ) {
352397
return array( 'publish' );
353398
}
354399

355-
return array_map( 'sanitize_key', $statuses );
400+
$statuses = array_values( array_filter( $statuses, 'is_string' ) );
401+
402+
return array() === $statuses ? array( 'publish' ) : array_map( 'sanitize_key', $statuses );
356403
}
357404

358405
/**
@@ -362,15 +409,16 @@ protected static function normalize_statuses( array $input ): array {
362409
*
363410
* @since 7.1.0
364411
*
365-
* @param array<string, mixed> $input The ability input.
412+
* @param array<mixed> $input The ability input.
366413
* @return string[] List of requested field names.
367414
*/
368415
protected static function normalize_fields( array $input ): array {
369416
if ( empty( $input['fields'] ) || ! is_array( $input['fields'] ) ) {
370417
return self::FIELDS;
371418
}
372419

373-
$fields = array_intersect( self::FIELDS, array_map( 'strval', $input['fields'] ) );
420+
$requested = array_filter( $input['fields'], 'is_string' );
421+
$fields = array_intersect( self::FIELDS, $requested );
374422

375423
return array() === $fields ? self::FIELDS : array_values( $fields );
376424
}
@@ -387,7 +435,8 @@ protected static function normalize_fields( array $input ): array {
387435
protected static function get_content_input_schema( array $post_types, array $statuses ): array {
388436
return array(
389437
'type' => 'object',
390-
'default' => array(),
438+
// Object (not array()) so the serialized schema default is {}, consistent with type:object.
439+
'default' => (object) array(),
391440
// `post_type` is required unless a single post is requested by `id`.
392441
'anyOf' => array(
393442
array( 'required' => array( 'id' ) ),
@@ -670,6 +719,7 @@ public static function return_raw_title_format(): string {
670719
* @return string The ISO 8601 date, or an empty string if unavailable.
671720
*/
672721
protected static function format_gmt_date( WP_Post $post, string $field ): string {
722+
$field = 'modified' === $field ? 'modified' : 'date';
673723
$datetime = get_post_datetime( $post, $field, 'gmt' );
674724
if ( $datetime ) {
675725
return $datetime->format( 'c' );

0 commit comments

Comments
 (0)