Skip to content
Draft
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
39 changes: 39 additions & 0 deletions src/wp-includes/rest-api/class-wp-rest-server.php
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,14 @@ public function serve_request( $path = null ) {
$embed = isset( $_GET['_embed'] ) ? rest_parse_embed_param( $_GET['_embed'] ) : false;
$result = $this->response_to_data( $result, $embed );

if (
$request->get_method() === 'OPTIONS' &&
isset( $result['schema'] ) &&
is_array( $result['schema'] )
) {
$result['schema'] = $this->sanitize_schema_properties_recursive( $result['schema'] );
}

/**
* Filters the REST API response.
*
Expand Down Expand Up @@ -1341,6 +1349,37 @@ protected function get_json_last_error() {
return json_last_error_msg();
}

/**
* Recursively sanitizes schema data ensuring empty properties arrays become objects.
*
* The JSON Schema specification requires 'properties' to always be an object.
* In PHP, empty associative arrays become empty arrays ([]) when JSON-encoded,
* not empty objects ({}), which would make the schema invalid.
*
* @since 7.0.0
*
* @param array|object $data The schema data to sanitize.
* @return array|object The sanitized schema data.
*/
protected function sanitize_schema_properties_recursive( $data ) {
$is_object = is_object( $data );
$data_array = $is_object ? (array) $data : $data;

// Convert empty properties array to empty object.
if ( isset( $data_array['properties'] ) && is_array( $data_array['properties'] ) && empty( $data_array['properties'] ) ) {
$data_array['properties'] = (object) array();
}

// Process nested elements recursively.
foreach ( $data_array as $key => $value ) {
if ( is_array( $value ) || is_object( $value ) ) {
$data_array[ $key ] = $this->sanitize_schema_properties_recursive( $value );
}
}

return $is_object ? (object) $data_array : $data_array;
}

/**
* Retrieves the site index.
*
Expand Down
4 changes: 4 additions & 0 deletions tests/phpunit/tests/functions/wpRemoteFopen.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public function test_wp_remote_fopen() {
$url = 'https://s.w.org/screenshots/3.9/dashboard.png';
$response = wp_remote_fopen( $url );

if ( false === $response ) {
$this->markTestSkipped( 'External HTTP request failed. The remote resource may be unavailable.' );
}

$this->assertIsString( $response );
$this->assertSame( 153204, strlen( $response ) );
}
Expand Down
184 changes: 184 additions & 0 deletions tests/phpunit/tests/rest-api/rest-server.php
Original file line number Diff line number Diff line change
Expand Up @@ -2684,4 +2684,188 @@ public function data_envelope_params() {
array( array( 'alternate' ) ),
);
}

/**
* Test that OPTIONS request returns empty properties as object instead of array.
*
* @ticket 63186
*/
public function test_options_request_empty_properties_returns_object() {
// Use the posts endpoint which has meta with empty properties by default.
$_SERVER['REQUEST_METHOD'] = 'OPTIONS';
rest_get_server()->serve_request( '/wp/v2/posts' );
$data = json_decode( rest_get_server()->sent_body );

$this->assertObjectHasProperty( 'schema', $data, 'Response should contain schema.' );
$this->assertObjectHasProperty( 'properties', $data->schema, 'Schema should contain properties.' );
$this->assertObjectHasProperty( 'meta', $data->schema->properties, 'Properties should contain meta.' );
$this->assertObjectHasProperty( 'properties', $data->schema->properties->meta, 'Meta should contain properties.' );
// Meta properties should be an object (empty or not) for JSON schema compliance.
$this->assertIsObject( $data->schema->properties->meta->properties, 'Meta properties should be an object, not an array.' );
}

/**
* Test that serve_request sanitizes nested empty properties in schema.
*
* @ticket 63186
*/
public function test_serve_request_sanitizes_nested_schema_properties() {
// Register a route with nested empty properties to test sanitization.
rest_get_server()->register_route(
'test-ns',
'/test-ns/test',
array(
array(
'methods' => 'GET',
'callback' => '__return_null',
'permission_callback' => '__return_true',
),
'schema' => function () {
return array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'test',
'type' => 'object',
'properties' => array(
'meta' => array(
'type' => 'object',
'properties' => array(),
),
),
);
},
)
);

$_SERVER['REQUEST_METHOD'] = 'OPTIONS';
rest_get_server()->serve_request( '/test-ns/test' );
$data = json_decode( rest_get_server()->sent_body );

$this->assertObjectHasProperty( 'schema', $data, 'Response should contain schema.' );
$this->assertIsObject( $data->schema->properties->meta->properties, 'Nested empty properties should be an object.' );
}


/**
* Test that sanitize_schema_properties_recursive handles empty properties array.
*
* @ticket 63186
*/
public function test_sanitize_schema_properties_recursive_converts_empty_properties_to_object() {
$server = new WP_REST_Server();

$method = new ReflectionMethod( $server, 'sanitize_schema_properties_recursive' );
$method->setAccessible( true );

$data = array(
'type' => 'object',
'properties' => array(),
);

$result = $method->invoke( $server, $data );

$this->assertIsObject( $result['properties'], 'Empty properties should be converted to an object.' );
}

/**
* Test that sanitize_schema_properties_recursive preserves non-empty properties.
*
* @ticket 63186
*/
public function test_sanitize_schema_properties_recursive_preserves_non_empty_properties() {
$server = new WP_REST_Server();

$method = new ReflectionMethod( $server, 'sanitize_schema_properties_recursive' );
$method->setAccessible( true );

$data = array(
'type' => 'object',
'properties' => array(
'id' => array(
'type' => 'integer',
),
),
);

$result = $method->invoke( $server, $data );

$this->assertIsArray( $result['properties'], 'Non-empty properties should remain an array.' );
$this->assertArrayHasKey( 'id', $result['properties'] );
}

/**
* Test that sanitize_schema_properties_recursive handles nested structures.
*
* @ticket 63186
*/
public function test_sanitize_schema_properties_recursive_handles_nested_structures() {
$server = new WP_REST_Server();

$method = new ReflectionMethod( $server, 'sanitize_schema_properties_recursive' );
$method->setAccessible( true );

$data = array(
'type' => 'object',
'properties' => array(
'meta' => array(
'type' => 'object',
'properties' => array(),
),
),
);

$result = $method->invoke( $server, $data );

$this->assertIsArray( $result['properties'], 'Top-level non-empty properties should remain an array.' );
$this->assertIsObject( $result['properties']['meta']['properties'], 'Nested empty properties should be converted to an object.' );
}

/**
* Test that sanitize_schema_properties_recursive handles object input.
*
* @ticket 63186
*/
public function test_sanitize_schema_properties_recursive_handles_object_input() {
$server = new WP_REST_Server();

$method = new ReflectionMethod( $server, 'sanitize_schema_properties_recursive' );
$method->setAccessible( true );

$data = (object) array(
'type' => 'object',
'properties' => array(),
);

$result = $method->invoke( $server, $data );

$this->assertIsObject( $result, 'Result should be an object when input is an object.' );
$this->assertIsObject( $result->properties, 'Empty properties should be converted to an object.' );
}

/**
* Test that sanitize_schema_properties_recursive preserves other data.
*
* @ticket 63186
*/
public function test_sanitize_schema_properties_recursive_preserves_other_data() {
$server = new WP_REST_Server();

$method = new ReflectionMethod( $server, 'sanitize_schema_properties_recursive' );
$method->setAccessible( true );

$data = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'test',
'type' => 'object',
'description' => 'A test schema',
'properties' => array(),
);

$result = $method->invoke( $server, $data );

$this->assertSame( 'http://json-schema.org/draft-04/schema#', $result['$schema'] );
$this->assertSame( 'test', $result['title'] );
$this->assertSame( 'object', $result['type'] );
$this->assertSame( 'A test schema', $result['description'] );
$this->assertIsObject( $result['properties'] );
}
}
Loading