Skip to content

Commit 26069e1

Browse files
Abilities API: support schema arrays for show_in_abilities meta
1 parent cd25859 commit 26069e1

4 files changed

Lines changed: 230 additions & 16 deletions

File tree

src/wp-includes/abilities/class-wp-post-type-abilities.php

Lines changed: 122 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -821,13 +821,19 @@ private static function build_post_schema( WP_Post_Type $post_type_object ): arr
821821
);
822822

823823
if ( post_type_supports( $slug, 'custom-fields' ) ) {
824+
$meta_properties = self::get_meta_value_schema_properties( $slug );
825+
824826
$properties['meta'] = array(
825827
'type' => 'object',
826828
'description' => __( 'Public post meta key-value pairs. Only present when include.meta is true.' ),
827829
'additionalProperties' => array(
828830
'type' => array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' ),
829831
),
830832
);
833+
834+
if ( ! empty( $meta_properties ) ) {
835+
$properties['meta']['properties'] = $meta_properties;
836+
}
831837
}
832838

833839
return array(
@@ -1218,25 +1224,21 @@ static function ( $term ): array {
12181224
}
12191225

12201226
if ( ! empty( $include['meta'] ) && post_type_supports( $slug, 'custom-fields' ) ) {
1221-
$meta = get_post_meta( $post->ID );
1222-
$public_meta = array();
1223-
$allowed_meta_keys = self::get_allowed_meta_keys( $slug );
1224-
$registered_meta = array_merge(
1225-
get_registered_meta_keys( 'post' ),
1226-
get_registered_meta_keys( 'post', $slug )
1227-
);
1227+
$meta = get_post_meta( $post->ID );
1228+
$public_meta = array();
1229+
$allowed_meta = self::get_allowed_meta( $slug );
12281230

12291231
foreach ( $meta as $key => $values ) {
12301232
// Skip protected meta keys.
12311233
if ( is_protected_meta( $key, 'post' ) ) {
12321234
continue;
12331235
}
12341236
// Only include meta keys that are explicitly allowed.
1235-
if ( ! in_array( $key, $allowed_meta_keys, true ) ) {
1237+
if ( ! isset( $allowed_meta[ $key ] ) ) {
12361238
continue;
12371239
}
12381240
// Respect the registered 'single' property for consistent behavior with get_post_meta().
1239-
$is_single = ! empty( $registered_meta[ $key ]['single'] );
1241+
$is_single = ! empty( $allowed_meta[ $key ]['single'] );
12401242
$public_meta[ $key ] = $is_single ? ( $values[0] ?? null ) : $values;
12411243
}
12421244

@@ -1498,19 +1500,124 @@ private static function process_date_top_level( array $input, array &$result ):
14981500
* @return string[] List of allowed meta keys.
14991501
*/
15001502
private static function get_allowed_meta_keys( string $post_type_slug ): array {
1501-
$registered_meta = array_merge(
1503+
return array_keys( self::get_allowed_meta( $post_type_slug ) );
1504+
}
1505+
1506+
/**
1507+
* Returns all registered post meta entries that are exposed through abilities for a post type.
1508+
*
1509+
* @since 7.0.0
1510+
*
1511+
* @param string $post_type_slug The post type slug.
1512+
* @return array<string, array<string, mixed>> Allowed meta registration args keyed by meta key.
1513+
*/
1514+
private static function get_allowed_meta( string $post_type_slug ): array {
1515+
$registered_meta = self::get_registered_meta_for_post_type( $post_type_slug );
1516+
$allowed = array();
1517+
1518+
foreach ( $registered_meta as $key => $args ) {
1519+
if ( self::is_meta_enabled_in_abilities( $args ) ) {
1520+
$allowed[ $key ] = $args;
1521+
}
1522+
}
1523+
1524+
return $allowed;
1525+
}
1526+
1527+
/**
1528+
* Returns all registered post meta entries for a post type, with subtype values overriding global ones.
1529+
*
1530+
* @since 7.0.0
1531+
*
1532+
* @param string $post_type_slug The post type slug.
1533+
* @return array<string, array<string, mixed>> Registered meta args keyed by meta key.
1534+
*/
1535+
private static function get_registered_meta_for_post_type( string $post_type_slug ): array {
1536+
return array_merge(
15021537
get_registered_meta_keys( 'post' ),
15031538
get_registered_meta_keys( 'post', $post_type_slug )
15041539
);
1540+
}
15051541

1506-
$allowed = array();
1507-
foreach ( $registered_meta as $key => $args ) {
1508-
if ( ! empty( $args['show_in_abilities'] ) ) {
1509-
$allowed[] = $key;
1542+
/**
1543+
* Determines whether a meta key is enabled for abilities.
1544+
*
1545+
* `show_in_abilities` can be either a boolean or an options array.
1546+
*
1547+
* @since 7.0.0
1548+
*
1549+
* @param array<string, mixed> $args Meta registration args.
1550+
* @return bool True if enabled for abilities, false otherwise.
1551+
*/
1552+
private static function is_meta_enabled_in_abilities( array $args ): bool {
1553+
return ! empty( $args['show_in_abilities'] );
1554+
}
1555+
1556+
/**
1557+
* Builds keyed value schema properties for meta keys that provide `show_in_abilities.schema`.
1558+
*
1559+
* Keys without a schema continue using the generic additionalProperties fallback.
1560+
*
1561+
* @since 7.0.0
1562+
*
1563+
* @param string $post_type_slug The post type slug.
1564+
* @return array<string, array<string, mixed>> Value schema properties keyed by meta key.
1565+
*/
1566+
private static function get_meta_value_schema_properties( string $post_type_slug ): array {
1567+
$properties = array();
1568+
1569+
foreach ( self::get_allowed_meta( $post_type_slug ) as $key => $args ) {
1570+
if (
1571+
! is_array( $args['show_in_abilities'] )
1572+
|| ! isset( $args['show_in_abilities']['schema'] )
1573+
|| ! is_array( $args['show_in_abilities']['schema'] )
1574+
) {
1575+
continue;
15101576
}
1577+
1578+
$properties[ $key ] = self::build_meta_value_schema( $args );
1579+
}
1580+
1581+
ksort( $properties );
1582+
1583+
return $properties;
1584+
}
1585+
1586+
/**
1587+
* Builds the value schema for a single meta key.
1588+
*
1589+
* @since 7.0.0
1590+
*
1591+
* @param array<string, mixed> $args Meta registration args.
1592+
* @return array<string, mixed> JSON Schema for the meta value.
1593+
*/
1594+
private static function build_meta_value_schema( array $args ): array {
1595+
$schema = array(
1596+
'type' => ! empty( $args['type'] ) ? $args['type'] : 'string',
1597+
);
1598+
1599+
if ( ! empty( $args['label'] ) ) {
1600+
$schema['title'] = $args['label'];
1601+
}
1602+
1603+
if ( ! empty( $args['description'] ) ) {
1604+
$schema['description'] = $args['description'];
1605+
}
1606+
1607+
if ( array_key_exists( 'default', $args ) ) {
1608+
$schema['default'] = $args['default'];
1609+
}
1610+
1611+
$schema = array_merge( $schema, $args['show_in_abilities']['schema'] );
1612+
1613+
if ( empty( $args['single'] ) ) {
1614+
$schema = array(
1615+
'type' => 'array',
1616+
'items' => $schema,
1617+
);
15111618
}
15121619

1513-
return array_unique( $allowed );
1620+
return $schema;
15141621
}
15151622

15161623
/**

src/wp-includes/meta.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1425,7 +1425,9 @@ function sanitize_meta( $meta_key, $meta_value, $object_type, $object_subtype =
14251425
* support for custom fields for registered meta to be accessible via REST.
14261426
* When registering complex meta values this argument may optionally be an
14271427
* array with 'schema' or 'prepare_callback' keys instead of a boolean.
1428-
* @type bool $show_in_abilities Whether this meta key should be exposed through the Abilities API.
1428+
* @type bool|array $show_in_abilities Whether this meta key should be exposed through the Abilities API.
1429+
* When registering complex meta values this argument may optionally be an
1430+
* array with a 'schema' key instead of a boolean.
14291431
* Default false.
14301432
* @type bool $revisions_enabled Whether to enable revisions support for this meta_key. Can only be used when the
14311433
* object type is 'post'.

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

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ public static function wpTearDownAfterClass(): void {
172172
unregister_meta_key( 'post', 'c' );
173173
unregister_meta_key( 'post', 'My.Key' );
174174
unregister_meta_key( 'post', 'structured_object' );
175+
unregister_meta_key( 'post', 'schema_constrained' );
175176
}
176177

177178
/**
@@ -237,6 +238,20 @@ public function set_up(): void {
237238
'show_in_abilities' => true,
238239
)
239240
);
241+
register_meta(
242+
'post',
243+
'schema_constrained',
244+
array(
245+
'type' => 'string',
246+
'single' => true,
247+
'show_in_abilities' => array(
248+
'schema' => array(
249+
'type' => 'string',
250+
'enum' => array( 'allowed' ),
251+
),
252+
),
253+
)
254+
);
240255

241256
$this->reregister_post_type_abilities();
242257
}
@@ -722,6 +737,69 @@ public function test_meta_only_includes_show_in_abilities_registered_keys(): voi
722737
$this->assertArrayNotHasKey( 'x', (array) $data['meta'] );
723738
}
724739

740+
/**
741+
* Tests that a schema-based `show_in_abilities` registration is accepted for meta queries.
742+
*
743+
* @ticket 64606
744+
*/
745+
public function test_meta_query_with_schema_based_registration_succeeds(): void {
746+
update_post_meta( self::$post_ids[1], 'schema_constrained', 'allowed' );
747+
748+
$response = $this->dispatch_get_ability(
749+
array(
750+
'query' => array(
751+
'meta' => array(
752+
'queries' => array(
753+
array(
754+
'key' => 'schema_constrained',
755+
'compare' => '=',
756+
'value' => 'allowed',
757+
),
758+
),
759+
),
760+
),
761+
'per_page' => 100,
762+
)
763+
);
764+
765+
$this->assertSame( 200, $response->get_status() );
766+
767+
$data = $response->get_data();
768+
$post_ids = $this->get_response_post_ids( $data );
769+
770+
$this->assertContains( self::$post_ids[1], $post_ids );
771+
}
772+
773+
/**
774+
* Tests that `show_in_abilities.schema` is enforced when meta is included in output.
775+
*
776+
* @ticket 64606
777+
*/
778+
public function test_meta_include_with_schema_invalid_value_fails_output_validation(): void {
779+
$post_id = self::factory()->post->create(
780+
array(
781+
'post_title' => 'Post with invalid schema-constrained meta',
782+
'post_status' => 'publish',
783+
)
784+
);
785+
update_post_meta( $post_id, 'schema_constrained', 'blocked' );
786+
787+
$response = $this->dispatch_get_ability(
788+
array(
789+
'id' => $post_id,
790+
'include' => array( 'meta' => true ),
791+
)
792+
);
793+
794+
wp_delete_post( $post_id, true );
795+
796+
$this->assertSame( 500, $response->get_status() );
797+
$data = $response->get_data();
798+
$this->assertSame( 'ability_invalid_output', $data['code'] );
799+
$this->assertStringContainsString( 'schema_constrained', $data['message'] );
800+
$this->assertStringContainsString( 'output', $data['message'] );
801+
}
802+
725803
/**
726804
* Tests that object-like meta values are allowed by the output schema.
727805
*

tests/phpunit/tests/meta/registerMeta.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,33 @@ public function test_register_meta_with_post_object_type_returns_true() {
8080
$this->assertTrue( $result );
8181
}
8282

83+
public function test_register_meta_with_show_in_abilities_schema() {
84+
register_meta(
85+
'post',
86+
'flight_number',
87+
array(
88+
'show_in_abilities' => array(
89+
'schema' => array(
90+
'type' => 'string',
91+
'enum' => array( 'Oceanic 815' ),
92+
),
93+
),
94+
)
95+
);
96+
$meta_keys = get_registered_meta_keys( 'post' );
97+
unregister_meta_key( 'post', 'flight_number' );
98+
99+
$this->assertSame(
100+
array(
101+
'schema' => array(
102+
'type' => 'string',
103+
'enum' => array( 'Oceanic 815' ),
104+
),
105+
),
106+
$meta_keys['flight_number']['show_in_abilities']
107+
);
108+
}
109+
83110
public function test_register_meta_with_post_object_type_populates_wp_meta_keys() {
84111
global $wp_meta_keys;
85112

0 commit comments

Comments
 (0)