From d9531a1a3adc3c239a166aa4a3a705de76e9051f Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 26 Sep 2025 17:00:48 +1000 Subject: [PATCH 01/12] Enhancement: Add support for filtering attachments by multiple media types. This update introduces a new `media_types` parameter to the WP REST Attachments Controller, allowing users to filter attachment queries by an array of media types. Additionally, the existing `media_type` parameter remains functional, ensuring backward compatibility. `post_mime_type` already supports multiple mime types. --- .../class-wp-rest-attachments-controller.php | 27 +++- .../rest-api/rest-attachments-controller.php | 131 ++++++++++++++++++ 2 files changed, 156 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 9ec818298aff9..5cc759acc9e62 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -88,6 +88,18 @@ protected function prepare_items_query( $prepared_args = array(), $request = nul $query_args['post_mime_type'] = $media_types[ $request['media_type'] ]; } + if ( empty( $query_args['post_mime_type'] ) && ! empty( $request['media_types'] ) && is_array( $request['media_types'] ) ) { + $mime_types_query = array(); + + foreach ( $request['media_types'] as $media_type ) { + if ( isset( $media_types[ $media_type ] ) ) { + $mime_types_query[] = $media_type; + } + } + + $query_args['post_mime_type'] = $mime_types_query; + } + if ( ! empty( $request['mime_type'] ) ) { $parts = explode( '/', $request['mime_type'] ); if ( isset( $media_types[ $parts[0] ] ) && in_array( $request['mime_type'], $media_types[ $parts[0] ], true ) ) { @@ -1342,6 +1354,7 @@ public static function get_filename_from_disposition( $disposition_header ) { * Retrieves the query params for collections of attachments. * * @since 4.7.0 + * @since 6.6.0 Adds the `media_types` parameter to filter by multiple media types. * * @return array Query parameters for the attachment collection as an array. */ @@ -1349,13 +1362,23 @@ public function get_collection_params() { $params = parent::get_collection_params(); $params['status']['default'] = 'inherit'; $params['status']['items']['enum'] = array( 'inherit', 'private', 'trash' ); - $media_types = $this->get_media_types(); + $media_types = array_keys( $this->get_media_types() ); $params['media_type'] = array( 'default' => null, 'description' => __( 'Limit result set to attachments of a particular media type.' ), 'type' => 'string', - 'enum' => array_keys( $media_types ), + 'enum' => $media_types, + ); + + $params['media_types'] = array( + 'default' => null, + 'description' => __( 'Limit result set to an array of media types.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + 'enum' => $media_types, + ), ); $params['mime_type'] = array( diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 31acd0b56355a..b700fcaf6c570 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -37,6 +37,17 @@ class WP_Test_REST_Attachments_Controller extends WP_Test_REST_Post_Type_Control */ private static $test_svg_file; + /** + * @var string The path to the test video. + */ + private static $test_video_file; + + /** + * @var string The path to the test audio. + */ + private static $test_audio_file; + + /** * @var array The recorded posts query clauses. */ @@ -85,6 +96,12 @@ public static function wpTearDownAfterClass() { if ( file_exists( self::$test_avif_file ) ) { unlink( self::$test_avif_file ); } + if ( file_exists( self::$test_video_file ) ) { + unlink( self::$test_video_file ); + } + if ( file_exists( self::$test_audio_file ) ) { + unlink( self::$test_audio_file ); + } self::delete_user( self::$editor_id ); self::delete_user( self::$author_id ); @@ -126,6 +143,18 @@ public function set_up() { copy( $test_svg_file, self::$test_svg_file ); } + $test_video_file = DIR_TESTDATA . '/uploads/small-video.mp4'; + self::$test_video_file = get_temp_dir() . 'small-video.mp4'; + if ( ! file_exists( self::$test_video_file ) ) { + copy( $test_video_file, self::$test_video_file ); + } + + $test_audio_file = DIR_TESTDATA . '/uploads/small-audio.mp3'; + self::$test_audio_file = get_temp_dir() . 'small-audio.mp3'; + if ( ! file_exists( self::$test_audio_file ) ) { + copy( $test_audio_file, self::$test_audio_file ); + } + add_filter( 'rest_pre_dispatch', array( $this, 'wpSetUpBeforeRequest' ), 10, 3 ); add_filter( 'posts_clauses', array( $this, 'save_posts_clauses' ), 10, 2 ); } @@ -242,6 +271,7 @@ public function test_registered_query_params() { 'exclude', 'include', 'media_type', + 'media_types', 'mime_type', 'modified_after', 'modified_before', @@ -2827,4 +2857,105 @@ public function test_edit_image_vertical_flip_only() { // The controller converts the integer values to booleans: 0 !== (int) 1 = true. $this->assertSame( array( true, false ), WP_Image_Editor_Mock::$spy['flip'][0], 'Vertical flip of the image is not identical.' ); } + + /** + * @ticket ?????? + */ + public function test_get_items_with_media_types() { + $video_id = self::factory()->attachment->create_object( + self::$test_video_file, + 0, + array( + 'post_mime_type' => 'video/mp4', + 'post_excerpt' => 'A sample caption', + ) + ); + + $audio_id = self::factory()->attachment->create_object( + self::$test_audio_file, + 0, + array( + 'post_mime_type' => 'audio/mpeg', + 'post_excerpt' => 'A sample caption', + ) + ); + + $image_id = self::factory()->attachment->create_object( + self::$test_file, + 0, + array( + 'post_mime_type' => 'image/jpeg', + 'post_excerpt' => 'A sample caption', + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + $request->set_param( 'media_types', array( 'audio', 'video' ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( 2, $response->get_data() ); + $this->assertContains( $video_id, wp_list_pluck( $response->get_data(), 'id' ), 'Video ID not found in response for [audio, video]' ); + $this->assertContains( $audio_id, wp_list_pluck( $response->get_data(), 'id' ), 'Audio ID not found in response for [audio, video]' ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + $request->set_param( 'media_types', array( 'image' ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( 1, $response->get_data() ); + $this->assertContains( $image_id, wp_list_pluck( $response->get_data(), 'id' ), 'Image ID not found in response for [image]' ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + $request->set_param( 'media_types', array( 'image', 'audio' ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( 2, $response->get_data() ); + $this->assertContains( $image_id, wp_list_pluck( $response->get_data(), 'id' ), 'Image ID not found in response for [image, audio]' ); + $this->assertContains( $audio_id, wp_list_pluck( $response->get_data(), 'id' ), 'Audio ID not found in response for [image, audio]' ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + $request->set_param( 'media_types', array( 'image', 'video', 'audio' ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( 3, $response->get_data() ); + $this->assertContains( $image_id, wp_list_pluck( $response->get_data(), 'id' ), 'Image ID not found in response for [image, video, audio]' ); + $this->assertContains( $video_id, wp_list_pluck( $response->get_data(), 'id' ), 'Video ID not found in response for [image, video, audio]' ); + $this->assertContains( $audio_id, wp_list_pluck( $response->get_data(), 'id' ), 'Audio ID not found in response for [image, video, audio]' ); + } + + /** + * Test that the `media_type` parameter overrides the `media_types` parameter. + * + * @ticket ?????? + */ + public function test_get_items_with_media_type_and_media_types() { + self::factory()->attachment->create_object( + self::$test_video_file, + 0, + array( + 'post_mime_type' => 'video/mp4', + 'post_excerpt' => 'A sample caption', + ) + ); + + self::factory()->attachment->create_object( + self::$test_audio_file, + 0, + array( + 'post_mime_type' => 'audio/mpeg', + 'post_excerpt' => 'A sample caption', + ) + ); + + $image_id = self::factory()->attachment->create_object( + self::$test_file, + 0, + array( + 'post_mime_type' => 'image/jpeg', + 'post_excerpt' => 'A sample caption', + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + $request->set_param( 'media_types', array( 'audio', 'video' ) ); + $request->set_param( 'media_type', 'image' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( 1, $response->get_data() ); + $this->assertContains( $image_id, wp_list_pluck( $response->get_data(), 'id' ), 'Image ID not found in response' ); + } } From 1d812e0fb1b3d03cd10bdb3b00d734606cda3846 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 26 Sep 2025 17:05:10 +1000 Subject: [PATCH 02/12] Added since 6.9.0 annotation to prepare_items_query --- .../rest-api/endpoints/class-wp-rest-attachments-controller.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 5cc759acc9e62..6c55e9beec64f 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -70,6 +70,7 @@ public function register_routes() { * prepares for WP_Query. * * @since 4.7.0 + * @since 6.9.0 Added the `media_types` parameter to filter by multiple media types. * * @param array $prepared_args Optional. Array of prepared arguments. Default empty array. * @param WP_REST_Request $request Optional. Request to prepare items for. From 154ba922863408012179b628f72a4d397fd1e6f1 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 29 Sep 2025 10:14:27 +1000 Subject: [PATCH 03/12] Update API generated fixture --- tests/qunit/fixtures/wp-api-generated.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 19405f0567924..c582d9c5fc30c 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -2974,6 +2974,22 @@ mockedApiResponse.Schema = { ], "required": false }, + "media_types": { + "default": null, + "description": "Limit result set to an array of media types.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "image", + "video", + "text", + "application", + "audio" + ] + }, + "required": false + }, "mime_type": { "default": null, "description": "Limit result set to attachments of a particular MIME type.", From 1c0c5e372d897f720c051df9d073471cdf002d5b Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 29 Sep 2025 10:29:52 +1000 Subject: [PATCH 04/12] Use right version 6.9.0 --- .../rest-api/endpoints/class-wp-rest-attachments-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 6c55e9beec64f..ea9a883ef527c 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -1355,7 +1355,7 @@ public static function get_filename_from_disposition( $disposition_header ) { * Retrieves the query params for collections of attachments. * * @since 4.7.0 - * @since 6.6.0 Adds the `media_types` parameter to filter by multiple media types. + * @since 6.9.0 Adds the `media_types` parameter to filter by multiple media types. * * @return array Query parameters for the attachment collection as an array. */ From 8ac8bf3ffa15c60684d198f200aea85f3688df38 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 29 Sep 2025 10:49:12 +1000 Subject: [PATCH 05/12] Tests: Update docblocks for media type filtering tests to include ticket reference 64046. --- .../phpunit/tests/rest-api/rest-attachments-controller.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index b700fcaf6c570..83cb5531d14f0 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -2715,6 +2715,7 @@ public function test_upload_svg_image() { /** * Tests that the attachment fields caption, description, and title, post and alt_text are updated correctly. + * * @ticket 64035 * @requires function imagejpeg */ @@ -2859,7 +2860,9 @@ public function test_edit_image_vertical_flip_only() { } /** - * @ticket ?????? + * Test that the `media_types` parameter filters the response by multiple media types. + * + * @ticket 64046 */ public function test_get_items_with_media_types() { $video_id = self::factory()->attachment->create_object( @@ -2921,7 +2924,7 @@ public function test_get_items_with_media_types() { /** * Test that the `media_type` parameter overrides the `media_types` parameter. * - * @ticket ?????? + * @ticket 64046 */ public function test_get_items_with_media_type_and_media_types() { self::factory()->attachment->create_object( From 9cda1c8a9da652bae2ddf569a75154f43cf3282f Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 29 Sep 2025 19:58:14 +1000 Subject: [PATCH 06/12] Extend `media_type` parameter to support filtering by multiple media types in the WP REST Attachments Controller. Updated related tests to verify functionality and ensure backward compatibility. --- .../class-wp-rest-attachments-controller.php | 51 +++++---- .../rest-api/rest-attachments-controller.php | 108 +++++++++++++++++- 2 files changed, 132 insertions(+), 27 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index ea9a883ef527c..f0a9b3b1fd185 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -70,7 +70,7 @@ public function register_routes() { * prepares for WP_Query. * * @since 4.7.0 - * @since 6.9.0 Added the `media_types` parameter to filter by multiple media types. + * @since 6.9.0 Extends the `media_type` parameter to support filtering by multiple media types. * * @param array $prepared_args Optional. Array of prepared arguments. Default empty array. * @param WP_REST_Request $request Optional. Request to prepare items for. @@ -85,20 +85,22 @@ protected function prepare_items_query( $prepared_args = array(), $request = nul $media_types = $this->get_media_types(); - if ( ! empty( $request['media_type'] ) && isset( $media_types[ $request['media_type'] ] ) ) { - $query_args['post_mime_type'] = $media_types[ $request['media_type'] ]; - } + if ( ! empty( $request['media_type'] ) ) { + if ( is_array( $request['media_type'] ) ) { + $mime_types_query = array(); - if ( empty( $query_args['post_mime_type'] ) && ! empty( $request['media_types'] ) && is_array( $request['media_types'] ) ) { - $mime_types_query = array(); + foreach ( $request['media_type'] as $request_media_type ) { + if ( isset( $media_types[ $request_media_type ] ) ) { + $mime_types_query = array_merge( $mime_types_query, $media_types[ $request_media_type ] ); + } + } - foreach ( $request['media_types'] as $media_type ) { - if ( isset( $media_types[ $media_type ] ) ) { - $mime_types_query[] = $media_type; + $query_args['post_mime_type'] = $mime_types_query; + } else { + if ( isset( $media_types[ $request['media_type'] ] ) ) { + $query_args['post_mime_type'] = $media_types[ $request['media_type'] ]; } } - - $query_args['post_mime_type'] = $mime_types_query; } if ( ! empty( $request['mime_type'] ) ) { @@ -1355,7 +1357,7 @@ public static function get_filename_from_disposition( $disposition_header ) { * Retrieves the query params for collections of attachments. * * @since 4.7.0 - * @since 6.9.0 Adds the `media_types` parameter to filter by multiple media types. + * @since 6.9.0 Extends the `media_type` parameter to support filtering by multiple media types. * * @return array Query parameters for the attachment collection as an array. */ @@ -1367,18 +1369,19 @@ public function get_collection_params() { $params['media_type'] = array( 'default' => null, - 'description' => __( 'Limit result set to attachments of a particular media type.' ), - 'type' => 'string', - 'enum' => $media_types, - ); - - $params['media_types'] = array( - 'default' => null, - 'description' => __( 'Limit result set to an array of media types.' ), - 'type' => 'array', - 'items' => array( - 'type' => 'string', - 'enum' => $media_types, + 'description' => __( 'Limit result set to attachments of a particular media type or media types.' ), + 'oneOf' => array( + array( + 'type' => 'string', + 'enum' => $media_types, + ), + array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + 'enum' => $media_types, + ), + ), ), ); diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 83cb5531d14f0..ccf8c983010e0 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -47,7 +47,6 @@ class WP_Test_REST_Attachments_Controller extends WP_Test_REST_Post_Type_Control */ private static $test_audio_file; - /** * @var array The recorded posts query clauses. */ @@ -271,7 +270,6 @@ public function test_registered_query_params() { 'exclude', 'include', 'media_type', - 'media_types', 'mime_type', 'modified_after', 'modified_before', @@ -297,7 +295,8 @@ public function test_registered_query_params() { 'audio', 'text', ); - $this->assertSameSets( $media_types, $data['endpoints'][0]['args']['media_type']['enum'] ); + $this->assertSameSets( $media_types, $data['endpoints'][0]['args']['media_type']['oneOf'][0]['enum'] ); + $this->assertSameSets( $media_types, $data['endpoints'][0]['args']['media_type']['oneOf'][1]['items']['enum'] ); } public function test_registered_get_item_params() { @@ -448,6 +447,109 @@ public function test_get_items_media_type() { $this->assertSame( $id1, $data[0]['id'] ); } + /** + * Test that the `media_type` array parameter filters the response by multiple media types. + * + * @ticket 64046 + */ + public function test_get_items_with_media_types() { + $video_id = self::factory()->attachment->create_object( + self::$test_video_file, + 0, + array( + 'post_mime_type' => 'video/mp4', + 'post_excerpt' => 'A sample caption', + ) + ); + + $audio_id = self::factory()->attachment->create_object( + self::$test_audio_file, + 0, + array( + 'post_mime_type' => 'audio/mpeg', + 'post_excerpt' => 'A sample caption', + ) + ); + + $image_id = self::factory()->attachment->create_object( + self::$test_file, + 0, + array( + 'post_mime_type' => 'image/jpeg', + 'post_excerpt' => 'A sample caption', + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + $request->set_param( 'media_type', array( 'audio', 'video' ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( 2, $response->get_data() ); + $this->assertContains( $video_id, wp_list_pluck( $response->get_data(), 'id' ), 'Video ID not found in response for [audio, video]' ); + $this->assertContains( $audio_id, wp_list_pluck( $response->get_data(), 'id' ), 'Audio ID not found in response for [audio, video]' ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + $request->set_param( 'media_type', array( 'image' ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( 1, $response->get_data() ); + $this->assertContains( $image_id, wp_list_pluck( $response->get_data(), 'id' ), 'Image ID not found in response for [image]' ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + $request->set_param( 'media_type', array( 'image', 'audio' ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( 2, $response->get_data() ); + $this->assertContains( $image_id, wp_list_pluck( $response->get_data(), 'id' ), 'Image ID not found in response for [image, audio]' ); + $this->assertContains( $audio_id, wp_list_pluck( $response->get_data(), 'id' ), 'Audio ID not found in response for [image, audio]' ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + $request->set_param( 'media_type', array( 'image', 'video', 'audio' ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( 3, $response->get_data() ); + $this->assertContains( $image_id, wp_list_pluck( $response->get_data(), 'id' ), 'Image ID not found in response for [image, video, audio]' ); + $this->assertContains( $video_id, wp_list_pluck( $response->get_data(), 'id' ), 'Video ID not found in response for [image, video, audio]' ); + $this->assertContains( $audio_id, wp_list_pluck( $response->get_data(), 'id' ), 'Audio ID not found in response for [image, video, audio]' ); + } + + /** + * Test that the `media_type` string parameter filters the response by a single media type even + * where there are multiple media types. + * + * @ticket 64046 + */ + public function test_get_items_with_media_type_and_media_types() { + self::factory()->attachment->create_object( + self::$test_video_file, + 0, + array( + 'post_mime_type' => 'video/mp4', + 'post_excerpt' => 'A sample caption', + ) + ); + + self::factory()->attachment->create_object( + self::$test_audio_file, + 0, + array( + 'post_mime_type' => 'audio/mpeg', + 'post_excerpt' => 'A sample caption', + ) + ); + + $image_id = self::factory()->attachment->create_object( + self::$test_file, + 0, + array( + 'post_mime_type' => 'image/jpeg', + 'post_excerpt' => 'A sample caption', + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + $request->set_param( 'media_type', 'image' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( 1, $response->get_data() ); + $this->assertContains( $image_id, wp_list_pluck( $response->get_data(), 'id' ), 'Image ID not found in response' ); + } + public function test_get_items_mime_type() { $id1 = self::factory()->attachment->create_object( self::$test_file, From 39d7680f8909075c4572bf9cb5f960f2784fa47f Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 29 Sep 2025 19:59:07 +1000 Subject: [PATCH 07/12] Regenerate fixtures --- tests/qunit/fixtures/wp-api-generated.js | 49 ++++++++++++------------ 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index c582d9c5fc30c..f2a596364db76 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -2963,33 +2963,34 @@ mockedApiResponse.Schema = { }, "media_type": { "default": null, - "description": "Limit result set to attachments of a particular media type.", - "type": "string", - "enum": [ - "image", - "video", - "text", - "application", - "audio" + "description": "Limit result set to attachments of a particular media type or media types.", + "oneOf": [ + { + "type": "string", + "enum": [ + "image", + "video", + "text", + "application", + "audio" + ] + }, + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "image", + "video", + "text", + "application", + "audio" + ] + } + } ], "required": false }, - "media_types": { - "default": null, - "description": "Limit result set to an array of media types.", - "type": "array", - "items": { - "type": "string", - "enum": [ - "image", - "video", - "text", - "application", - "audio" - ] - }, - "required": false - }, "mime_type": { "default": null, "description": "Limit result set to attachments of a particular MIME type.", From 545f84152b0d14f2c28d66a2524edaf59b0e9463 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 3 Oct 2025 13:31:38 +1000 Subject: [PATCH 08/12] Extend `media_type` and `mime_type` parameters in the WP REST Attachments Controller to support array values for filtering. Updated tests to validate functionality and ensure compatibility with existing features. Pulling over work from https://github.com/WordPress/wordpress-develop/pull/9211 Props to @himanshupathak95 --- .../class-wp-rest-attachments-controller.php | 64 ++--- .../rest-api/rest-attachments-controller.php | 239 ++++++++++++++---- 2 files changed, 228 insertions(+), 75 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index f0a9b3b1fd185..f56683e148b60 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -70,7 +70,7 @@ public function register_routes() { * prepares for WP_Query. * * @since 4.7.0 - * @since 6.9.0 Extends the `media_type` parameter to support filtering by multiple media types. + * @since 6.9.0 Extends the `media_type` and `mime_type` request arguments to support array values. * * @param array $prepared_args Optional. Array of prepared arguments. Default empty array. * @param WP_REST_Request $request Optional. Request to prepare items for. @@ -83,33 +83,38 @@ protected function prepare_items_query( $prepared_args = array(), $request = nul $query_args['post_status'] = 'inherit'; } - $media_types = $this->get_media_types(); + $all_mime_types = array(); + $media_types = $this->get_media_types(); if ( ! empty( $request['media_type'] ) ) { - if ( is_array( $request['media_type'] ) ) { - $mime_types_query = array(); + $media_type_input = is_array( $request['media_type'] ) + ? $request['media_type'] + : explode( ',', $request['media_type'] ); - foreach ( $request['media_type'] as $request_media_type ) { - if ( isset( $media_types[ $request_media_type ] ) ) { - $mime_types_query = array_merge( $mime_types_query, $media_types[ $request_media_type ] ); - } - } - - $query_args['post_mime_type'] = $mime_types_query; - } else { - if ( isset( $media_types[ $request['media_type'] ] ) ) { - $query_args['post_mime_type'] = $media_types[ $request['media_type'] ]; + foreach ( array_map( 'trim', $media_type_input ) as $type ) { + if ( isset( $media_types[ $type ] ) ) { + $all_mime_types = array_merge( $all_mime_types, $media_types[ $type ] ); } } } if ( ! empty( $request['mime_type'] ) ) { - $parts = explode( '/', $request['mime_type'] ); - if ( isset( $media_types[ $parts[0] ] ) && in_array( $request['mime_type'], $media_types[ $parts[0] ], true ) ) { - $query_args['post_mime_type'] = $request['mime_type']; + $mime_type_input = is_array( $request['mime_type'] ) + ? $request['mime_type'] + : explode( ',', $request['mime_type'] ); + + foreach ( array_map( 'trim', $mime_type_input ) as $mime_type ) { + $parts = explode( '/', $mime_type ); + if ( isset( $media_types[ $parts[0] ] ) && in_array( $mime_type, $media_types[ $parts[0] ], true ) ) { + $all_mime_types[] = $mime_type; + } } } + if ( ! empty( $all_mime_types ) ) { + $query_args['post_mime_type'] = array_values( array_unique( $all_mime_types ) ); + } + // Filter query clauses to include filenames. if ( isset( $query_args['s'] ) ) { add_filter( 'wp_allow_query_attachment_by_filename', '__return_true' ); @@ -1357,7 +1362,7 @@ public static function get_filename_from_disposition( $disposition_header ) { * Retrieves the query params for collections of attachments. * * @since 4.7.0 - * @since 6.9.0 Extends the `media_type` parameter to support filtering by multiple media types. + * @since 6.9.0 Extends the `media_type` and `mime_type` request arguments to support array values. * * @return array Query parameters for the attachment collection as an array. */ @@ -1370,25 +1375,20 @@ public function get_collection_params() { $params['media_type'] = array( 'default' => null, 'description' => __( 'Limit result set to attachments of a particular media type or media types.' ), - 'oneOf' => array( - array( - 'type' => 'string', - 'enum' => $media_types, - ), - array( - 'type' => 'array', - 'items' => array( - 'type' => 'string', - 'enum' => $media_types, - ), - ), + 'type' => array( 'string', 'array' ), + 'items' => array( + 'type' => 'string', + 'enum' => $media_types, ), ); $params['mime_type'] = array( 'default' => null, - 'description' => __( 'Limit result set to attachments of a particular MIME type.' ), - 'type' => 'string', + 'description' => __( 'Limit result set to attachments of a particular MIME type or MIME types.' ), + 'type' => array( 'string', 'array' ), + 'items' => array( + 'type' => 'string', + ), ); return $params; diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index ccf8c983010e0..101d500eb436d 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -47,6 +47,11 @@ class WP_Test_REST_Attachments_Controller extends WP_Test_REST_Post_Type_Control */ private static $test_audio_file; + /** + * @var string The path to the test RTF file. + */ + private static $test_rtf_file; + /** * @var array The recorded posts query clauses. */ @@ -101,6 +106,9 @@ public static function wpTearDownAfterClass() { if ( file_exists( self::$test_audio_file ) ) { unlink( self::$test_audio_file ); } + if ( file_exists( self::$test_rtf_file ) ) { + unlink( self::$test_rtf_file ); + } self::delete_user( self::$editor_id ); self::delete_user( self::$author_id ); @@ -154,6 +162,12 @@ public function set_up() { copy( $test_audio_file, self::$test_audio_file ); } + $test_rtf_file = DIR_TESTDATA . '/uploads/test.rtf'; + self::$test_rtf_file = get_temp_dir() . 'test.rtf'; + if ( ! file_exists( self::$test_rtf_file ) ) { + copy( $test_rtf_file, self::$test_rtf_file ); + } + add_filter( 'rest_pre_dispatch', array( $this, 'wpSetUpBeforeRequest' ), 10, 3 ); add_filter( 'posts_clauses', array( $this, 'save_posts_clauses' ), 10, 2 ); } @@ -295,8 +309,7 @@ public function test_registered_query_params() { 'audio', 'text', ); - $this->assertSameSets( $media_types, $data['endpoints'][0]['args']['media_type']['oneOf'][0]['enum'] ); - $this->assertSameSets( $media_types, $data['endpoints'][0]['args']['media_type']['oneOf'][1]['items']['enum'] ); + $this->assertSameSets( $media_types, $data['endpoints'][0]['args']['media_type']['items']['enum'] ); } public function test_registered_get_item_params() { @@ -448,17 +461,24 @@ public function test_get_items_media_type() { } /** - * Test that the `media_type` array parameter filters the response by multiple media types. + * Test multiple media types support with various input formats. * - * @ticket 64046 + * @ticket 63668 */ - public function test_get_items_with_media_types() { + public function test_get_items_multiple_media_types() { + $image_id = self::factory()->attachment->create_object( + self::$test_file, + 0, + array( + 'post_mime_type' => 'image/jpeg', + ) + ); + $video_id = self::factory()->attachment->create_object( self::$test_video_file, 0, array( 'post_mime_type' => 'video/mp4', - 'post_excerpt' => 'A sample caption', ) ); @@ -467,87 +487,220 @@ public function test_get_items_with_media_types() { 0, array( 'post_mime_type' => 'audio/mpeg', - 'post_excerpt' => 'A sample caption', ) ); - $image_id = self::factory()->attachment->create_object( + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + + // Test single media type. + $request->set_param( 'media_type', 'image' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertCount( 1, $data, 'Response count for single media type is not 1' ); + $this->assertSame( $image_id, $data[0]['id'], 'Image ID not found in response for single media type' ); + + // Test multiple media types with comma-separated string. + $request->set_param( 'media_type', 'image,video' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertCount( 2, $data, 'Response count for multiple media types with comma-separated string is not 2' ); + $ids = wp_list_pluck( $data, 'id' ); + $this->assertContains( $image_id, $ids, 'Image ID not found in response for multiple media types with comma-separated string' ); + $this->assertContains( $video_id, $ids, 'Video ID not found in response for multiple media types with comma-separated string' ); + $this->assertNotContains( $audio_id, $ids, 'Audio ID found in response for multiple media types with comma-separated string' ); + + // Test multiple media types with array format. + $request->set_param( 'media_type', array( 'image', 'video', 'audio' ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertCount( 3, $data, 'Response count for multiple media types with array format is not 3' ); + $ids = wp_list_pluck( $data, 'id' ); + $this->assertContains( $image_id, $ids, 'Image ID not found in response for multiple media types with array format' ); + $this->assertContains( $video_id, $ids, 'Video ID not found in response for multiple media types with array format' ); + $this->assertContains( $audio_id, $ids, 'Audio ID not found in response for multiple media types with array format' ); + + // Test invalid media type mixed with valid ones. + $request->set_param( 'media_type', 'image,invalid,video' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertCount( 2, $data, 'Response count for multiple media types with comma-separated string is not 2' ); + $ids = wp_list_pluck( $data, 'id' ); + $this->assertContains( $image_id, $ids, 'Image ID not found in response for multiple media types with comma-separated string and invalid media type' ); + $this->assertContains( $video_id, $ids, 'Video ID not found in response for multiple media types with comma-separated string and invalid media type' ); + } + + /** + * Test multiple MIME types support and combination with media types. + * + * @ticket 63668 + */ + public function test_get_items_multiple_mime_types_and_combination() { + $jpeg_id = self::factory()->attachment->create_object( self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', - 'post_excerpt' => 'A sample caption', + ) + ); + + $png_id = self::factory()->attachment->create_object( + self::$test_file2, + 0, + array( + 'post_mime_type' => 'image/png', + ) + ); + + $mp4_id = self::factory()->attachment->create_object( + self::$test_video_file, + 0, + array( + 'post_mime_type' => 'video/mp4', + ) + ); + + $rtf_id = self::factory()->attachment->create_object( + self::$test_rtf_file, + 0, + array( + 'post_mime_type' => 'application/rtf', ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); - $request->set_param( 'media_type', array( 'audio', 'video' ) ); + + // Test single MIME type + $request->set_param( 'mime_type', 'image/jpeg' ); $response = rest_get_server()->dispatch( $request ); - $this->assertCount( 2, $response->get_data() ); - $this->assertContains( $video_id, wp_list_pluck( $response->get_data(), 'id' ), 'Video ID not found in response for [audio, video]' ); - $this->assertContains( $audio_id, wp_list_pluck( $response->get_data(), 'id' ), 'Audio ID not found in response for [audio, video]' ); + $data = $response->get_data(); + $this->assertCount( 1, $data, 'Response count for single MIME type is not 1' ); + $this->assertSame( $jpeg_id, $data[0]['id'], 'JPEG ID not found in response for single MIME type' ); - $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); - $request->set_param( 'media_type', array( 'image' ) ); + // Test multiple MIME types with comma-separated string. + $request->set_param( 'mime_type', 'image/jpeg,image/png' ); $response = rest_get_server()->dispatch( $request ); - $this->assertCount( 1, $response->get_data() ); - $this->assertContains( $image_id, wp_list_pluck( $response->get_data(), 'id' ), 'Image ID not found in response for [image]' ); + $data = $response->get_data(); + $this->assertCount( 2, $data, 'Response count for multiple MIME types with comma-separated string is not 2' ); + $ids = wp_list_pluck( $data, 'id' ); + $this->assertContains( $jpeg_id, $ids, 'JPEG ID not found in response for multiple MIME types with comma-separated string' ); + $this->assertContains( $png_id, $ids, 'PNG ID not found in response for multiple MIME types with comma-separated string' ); - $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); - $request->set_param( 'media_type', array( 'image', 'audio' ) ); + // Test multiple MIME types with array format. + $request->set_param( 'mime_type', array( 'image/jpeg', 'video/mp4' ) ); $response = rest_get_server()->dispatch( $request ); - $this->assertCount( 2, $response->get_data() ); - $this->assertContains( $image_id, wp_list_pluck( $response->get_data(), 'id' ), 'Image ID not found in response for [image, audio]' ); - $this->assertContains( $audio_id, wp_list_pluck( $response->get_data(), 'id' ), 'Audio ID not found in response for [image, audio]' ); + $data = $response->get_data(); + $this->assertCount( 2, $data, 'Response count for multiple MIME types with array format is not 2' ); + $ids = wp_list_pluck( $data, 'id' ); - $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); - $request->set_param( 'media_type', array( 'image', 'video', 'audio' ) ); + $this->assertContains( $jpeg_id, $ids, 'JPEG ID not found in response for multiple MIME types with array format' ); + $this->assertContains( $mp4_id, $ids, 'MP4 ID not found in response for multiple MIME types with array format' ); + + // Test multiple media types with multiple MIME types. + $request->set_param( 'media_type', 'image,video' ); + $request->set_param( 'mime_type', 'application/rtf' ); $response = rest_get_server()->dispatch( $request ); - $this->assertCount( 3, $response->get_data() ); - $this->assertContains( $image_id, wp_list_pluck( $response->get_data(), 'id' ), 'Image ID not found in response for [image, video, audio]' ); - $this->assertContains( $video_id, wp_list_pluck( $response->get_data(), 'id' ), 'Video ID not found in response for [image, video, audio]' ); - $this->assertContains( $audio_id, wp_list_pluck( $response->get_data(), 'id' ), 'Audio ID not found in response for [image, video, audio]' ); + $data = $response->get_data(); + + $this->assertCount( 4, $data, 'Response count for multiple media types with multiple MIME types is not 4' ); + $ids = wp_list_pluck( $data, 'id' ); + $this->assertContains( $jpeg_id, $ids, 'JPEG ID not found in response for multiple media types with multiple MIME types' ); + $this->assertContains( $png_id, $ids, 'PNG ID not found in response for multiple media types with multiple MIME types' ); + $this->assertContains( $mp4_id, $ids, 'MP4 ID not found in response for multiple media types with multiple MIME types' ); + $this->assertContains( $rtf_id, $ids, 'RTF ID not found in response for multiple media types with multiple MIME types' ); } /** - * Test that the `media_type` string parameter filters the response by a single media type even - * where there are multiple media types. + * Test combination of media type and mime type parameters. * - * @ticket 64046 + * @ticket 63668 */ public function test_get_items_with_media_type_and_media_types() { - self::factory()->attachment->create_object( - self::$test_video_file, + $audio_id = self::factory()->attachment->create_object( + self::$test_audio_file, 0, array( - 'post_mime_type' => 'video/mp4', + 'post_mime_type' => 'audio/mpeg', 'post_excerpt' => 'A sample caption', ) ); - self::factory()->attachment->create_object( - self::$test_audio_file, + $jpeg_id = self::factory()->attachment->create_object( + self::$test_file, 0, array( - 'post_mime_type' => 'audio/mpeg', + 'post_mime_type' => 'image/jpeg', 'post_excerpt' => 'A sample caption', ) ); - $image_id = self::factory()->attachment->create_object( - self::$test_file, + $png_id = self::factory()->attachment->create_object( + self::$test_file2, 0, array( - 'post_mime_type' => 'image/jpeg', - 'post_excerpt' => 'A sample caption', + 'post_mime_type' => 'image/png', ) ); + $video_id = self::factory()->attachment->create_object( + self::$test_video_file, + 0, + array( + 'post_mime_type' => 'video/mp4', + ) + ); + + // Test combination of single media type and single mime type parameters. $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); $request->set_param( 'media_type', 'image' ); + $request->set_param( 'mime_type', 'audio/mpeg' ); $response = rest_get_server()->dispatch( $request ); - $this->assertCount( 1, $response->get_data() ); - $this->assertContains( $image_id, wp_list_pluck( $response->get_data(), 'id' ), 'Image ID not found in response' ); + $data = $response->get_data(); + $ids = wp_list_pluck( $data, 'id' ); + + $this->assertCount( 3, $data, 'Response count for combination of single media type and single mime type parameters is not 3' ); + $this->assertContains( $jpeg_id, $ids, 'JPEG ID not found in response' ); + $this->assertContains( $png_id, $ids, 'PNG ID not found in response' ); + $this->assertContains( $audio_id, $ids, 'Audio ID found in response' ); + + // Test combination of single media type and multiple mime type parameters. + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + $request->set_param( 'media_type', 'audio' ); + $request->set_param( 'mime_type', array( 'image/jpeg', 'image/png' ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $ids = wp_list_pluck( $data, 'id' ); + + $this->assertCount( 3, $data, 'Response count for combination of single media type and multiple mime type parameters is not 3' ); + $this->assertContains( $audio_id, $ids, 'Audio ID not found in response' ); + $this->assertContains( $jpeg_id, $ids, 'JPEG ID not found in response' ); + $this->assertContains( $png_id, $ids, 'PNG ID not found in response' ); + + // Test combination of multiple media types and single mime type parameters. + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + $request->set_param( 'media_type', 'audio,video' ); + $request->set_param( 'mime_type', array( 'image/jpeg' ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $ids = wp_list_pluck( $data, 'id' ); + + $this->assertCount( 3, $data, 'Response count for combination of multiple media type and multiple mime type parameters is not 3' ); + $this->assertContains( $audio_id, $ids, 'Audio ID not found in response' ); + $this->assertContains( $jpeg_id, $ids, 'JPEG ID not found in response' ); + $this->assertContains( $video_id, $ids, 'Video ID not found in response' ); + + // Test combination of multiple media types and multiple mime type parameters. + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + $request->set_param( 'media_type', 'audio,video' ); + $request->set_param( 'mime_type', array( 'image/jpeg', 'image/png' ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $ids = wp_list_pluck( $data, 'id' ); + + $this->assertCount( 4, $data, 'Response count for combination of multiple media type and multiple mime type parameters is not 3' ); + $this->assertContains( $audio_id, $ids, 'Audio ID not found in response' ); + $this->assertContains( $jpeg_id, $ids, 'JPEG ID not found in response' ); + $this->assertContains( $video_id, $ids, 'Video ID not found in response' ); + $this->assertContains( $png_id, $ids, 'PNG ID not found in response' ); } public function test_get_items_mime_type() { From 686891f84620416677b78573e3b0ab2606bc189f Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 3 Oct 2025 13:32:32 +1000 Subject: [PATCH 09/12] regenerate fixture --- tests/qunit/fixtures/wp-api-generated.js | 47 +++++++++++------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index f2a596364db76..5be2b4e801502 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -2964,37 +2964,32 @@ mockedApiResponse.Schema = { "media_type": { "default": null, "description": "Limit result set to attachments of a particular media type or media types.", - "oneOf": [ - { - "type": "string", - "enum": [ - "image", - "video", - "text", - "application", - "audio" - ] - }, - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "image", - "video", - "text", - "application", - "audio" - ] - } - } + "type": [ + "string", + "array" ], + "items": { + "type": "string", + "enum": [ + "image", + "video", + "text", + "application", + "audio" + ] + }, "required": false }, "mime_type": { "default": null, - "description": "Limit result set to attachments of a particular MIME type.", - "type": "string", + "description": "Limit result set to attachments of a particular MIME type or MIME types.", + "type": [ + "string", + "array" + ], + "items": { + "type": "string" + }, "required": false } } From 7ad084cc67e4d314a1b848585dfa3843e81ba93d Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 3 Oct 2025 15:14:16 +1000 Subject: [PATCH 10/12] Refactor media type and mime type handling in WP REST Attachments Controller to enforce array input. Updated tests to validate new behavior and ensure proper error responses for invalid parameters. --- .../class-wp-rest-attachments-controller.php | 21 ++++------ .../rest-api/rest-attachments-controller.php | 41 +++++++------------ tests/qunit/fixtures/wp-api-generated.js | 10 +---- 3 files changed, 24 insertions(+), 48 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index f56683e148b60..cff3e74e58f6f 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -86,24 +86,16 @@ protected function prepare_items_query( $prepared_args = array(), $request = nul $all_mime_types = array(); $media_types = $this->get_media_types(); - if ( ! empty( $request['media_type'] ) ) { - $media_type_input = is_array( $request['media_type'] ) - ? $request['media_type'] - : explode( ',', $request['media_type'] ); - - foreach ( array_map( 'trim', $media_type_input ) as $type ) { + if ( ! empty( $request['media_type'] ) && is_array( $request['media_type'] ) ) { + foreach ( $request['media_type'] as $type ) { if ( isset( $media_types[ $type ] ) ) { $all_mime_types = array_merge( $all_mime_types, $media_types[ $type ] ); } } } - if ( ! empty( $request['mime_type'] ) ) { - $mime_type_input = is_array( $request['mime_type'] ) - ? $request['mime_type'] - : explode( ',', $request['mime_type'] ); - - foreach ( array_map( 'trim', $mime_type_input ) as $mime_type ) { + if ( ! empty( $request['mime_type'] ) && is_array( $request['mime_type'] ) ) { + foreach ( $request['mime_type'] as $mime_type ) { $parts = explode( '/', $mime_type ); if ( isset( $media_types[ $parts[0] ] ) && in_array( $mime_type, $media_types[ $parts[0] ], true ) ) { $all_mime_types[] = $mime_type; @@ -1375,7 +1367,7 @@ public function get_collection_params() { $params['media_type'] = array( 'default' => null, 'description' => __( 'Limit result set to attachments of a particular media type or media types.' ), - 'type' => array( 'string', 'array' ), + 'type' => 'array', 'items' => array( 'type' => 'string', 'enum' => $media_types, @@ -1385,9 +1377,10 @@ public function get_collection_params() { $params['mime_type'] = array( 'default' => null, 'description' => __( 'Limit result set to attachments of a particular MIME type or MIME types.' ), - 'type' => array( 'string', 'array' ), + 'type' => 'array', 'items' => array( 'type' => 'string', + 'enum' => get_allowed_mime_types(), ), ); diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 101d500eb436d..4929399fb104e 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -522,11 +522,7 @@ public function test_get_items_multiple_media_types() { // Test invalid media type mixed with valid ones. $request->set_param( 'media_type', 'image,invalid,video' ); $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $this->assertCount( 2, $data, 'Response count for multiple media types with comma-separated string is not 2' ); - $ids = wp_list_pluck( $data, 'id' ); - $this->assertContains( $image_id, $ids, 'Image ID not found in response for multiple media types with comma-separated string and invalid media type' ); - $this->assertContains( $video_id, $ids, 'Video ID not found in response for multiple media types with comma-separated string and invalid media type' ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } /** @@ -559,14 +555,6 @@ public function test_get_items_multiple_mime_types_and_combination() { ) ); - $rtf_id = self::factory()->attachment->create_object( - self::$test_rtf_file, - 0, - array( - 'post_mime_type' => 'application/rtf', - ) - ); - $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); // Test single MIME type @@ -595,18 +583,10 @@ public function test_get_items_multiple_mime_types_and_combination() { $this->assertContains( $jpeg_id, $ids, 'JPEG ID not found in response for multiple MIME types with array format' ); $this->assertContains( $mp4_id, $ids, 'MP4 ID not found in response for multiple MIME types with array format' ); - // Test multiple media types with multiple MIME types. - $request->set_param( 'media_type', 'image,video' ); - $request->set_param( 'mime_type', 'application/rtf' ); + // Test invalid mime type mixed with valid ones. + $request->set_param( 'mime_type', array( 'video/mp4', 'cat/gif' ) ); $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - - $this->assertCount( 4, $data, 'Response count for multiple media types with multiple MIME types is not 4' ); - $ids = wp_list_pluck( $data, 'id' ); - $this->assertContains( $jpeg_id, $ids, 'JPEG ID not found in response for multiple media types with multiple MIME types' ); - $this->assertContains( $png_id, $ids, 'PNG ID not found in response for multiple media types with multiple MIME types' ); - $this->assertContains( $mp4_id, $ids, 'MP4 ID not found in response for multiple media types with multiple MIME types' ); - $this->assertContains( $rtf_id, $ids, 'RTF ID not found in response for multiple media types with multiple MIME types' ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } /** @@ -649,6 +629,14 @@ public function test_get_items_with_media_type_and_media_types() { ) ); + $rtf_id = self::factory()->attachment->create_object( + self::$test_rtf_file, + 0, + array( + 'post_mime_type' => 'application/rtf', + ) + ); + // Test combination of single media type and single mime type parameters. $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); $request->set_param( 'media_type', 'image' ); @@ -691,16 +679,17 @@ public function test_get_items_with_media_type_and_media_types() { // Test combination of multiple media types and multiple mime type parameters. $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); $request->set_param( 'media_type', 'audio,video' ); - $request->set_param( 'mime_type', array( 'image/jpeg', 'image/png' ) ); + $request->set_param( 'mime_type', array( 'image/jpeg', 'image/png', 'application/rtf' ) ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $ids = wp_list_pluck( $data, 'id' ); - $this->assertCount( 4, $data, 'Response count for combination of multiple media type and multiple mime type parameters is not 3' ); + $this->assertCount( 5, $data, 'Response count for combination of multiple media type and multiple mime type parameters is not 3' ); $this->assertContains( $audio_id, $ids, 'Audio ID not found in response' ); $this->assertContains( $jpeg_id, $ids, 'JPEG ID not found in response' ); $this->assertContains( $video_id, $ids, 'Video ID not found in response' ); $this->assertContains( $png_id, $ids, 'PNG ID not found in response' ); + $this->assertContains( $rtf_id, $ids, 'RTF ID not found in response' ); } public function test_get_items_mime_type() { diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 5be2b4e801502..bc9ea0a2dc424 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -2964,10 +2964,7 @@ mockedApiResponse.Schema = { "media_type": { "default": null, "description": "Limit result set to attachments of a particular media type or media types.", - "type": [ - "string", - "array" - ], + "type": "array", "items": { "type": "string", "enum": [ @@ -2983,10 +2980,7 @@ mockedApiResponse.Schema = { "mime_type": { "default": null, "description": "Limit result set to attachments of a particular MIME type or MIME types.", - "type": [ - "string", - "array" - ], + "type": "array", "items": { "type": "string" }, From 3f81a01306767419551a297d7f28866eb7cf2799 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 3 Oct 2025 15:23:33 +1000 Subject: [PATCH 11/12] regen fixtures --- tests/qunit/fixtures/wp-api-generated.js | 98 +++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index bc9ea0a2dc424..a2becfa264496 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -2982,7 +2982,103 @@ mockedApiResponse.Schema = { "description": "Limit result set to attachments of a particular MIME type or MIME types.", "type": "array", "items": { - "type": "string" + "type": "string", + "enum": { + "jpg|jpeg|jpe": "image/jpeg", + "gif": "image/gif", + "png": "image/png", + "bmp": "image/bmp", + "tiff|tif": "image/tiff", + "webp": "image/webp", + "avif": "image/avif", + "ico": "image/x-icon", + "heic": "image/heic", + "heif": "image/heif", + "heics": "image/heic-sequence", + "heifs": "image/heif-sequence", + "asf|asx": "video/x-ms-asf", + "wmv": "video/x-ms-wmv", + "wmx": "video/x-ms-wmx", + "wm": "video/x-ms-wm", + "avi": "video/avi", + "divx": "video/divx", + "flv": "video/x-flv", + "mov|qt": "video/quicktime", + "mpeg|mpg|mpe": "video/mpeg", + "mp4|m4v": "video/mp4", + "ogv": "video/ogg", + "webm": "video/webm", + "mkv": "video/x-matroska", + "3gp|3gpp": "video/3gpp", + "3g2|3gp2": "video/3gpp2", + "txt|asc|c|cc|h|srt": "text/plain", + "csv": "text/csv", + "tsv": "text/tab-separated-values", + "ics": "text/calendar", + "rtx": "text/richtext", + "css": "text/css", + "vtt": "text/vtt", + "dfxp": "application/ttaf+xml", + "mp3|m4a|m4b": "audio/mpeg", + "aac": "audio/aac", + "ra|ram": "audio/x-realaudio", + "wav|x-wav": "audio/wav", + "ogg|oga": "audio/ogg", + "flac": "audio/flac", + "mid|midi": "audio/midi", + "wma": "audio/x-ms-wma", + "wax": "audio/x-ms-wax", + "mka": "audio/x-matroska", + "rtf": "application/rtf", + "pdf": "application/pdf", + "class": "application/java", + "tar": "application/x-tar", + "zip": "application/zip", + "gz|gzip": "application/x-gzip", + "rar": "application/rar", + "7z": "application/x-7z-compressed", + "psd": "application/octet-stream", + "xcf": "application/octet-stream", + "doc": "application/msword", + "pot|pps|ppt": "application/vnd.ms-powerpoint", + "wri": "application/vnd.ms-write", + "xla|xls|xlt|xlw": "application/vnd.ms-excel", + "mdb": "application/vnd.ms-access", + "mpp": "application/vnd.ms-project", + "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "docm": "application/vnd.ms-word.document.macroEnabled.12", + "dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template", + "dotm": "application/vnd.ms-word.template.macroEnabled.12", + "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12", + "xlsb": "application/vnd.ms-excel.sheet.binary.macroEnabled.12", + "xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template", + "xltm": "application/vnd.ms-excel.template.macroEnabled.12", + "xlam": "application/vnd.ms-excel.addin.macroEnabled.12", + "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12", + "ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow", + "ppsm": "application/vnd.ms-powerpoint.slideshow.macroEnabled.12", + "potx": "application/vnd.openxmlformats-officedocument.presentationml.template", + "potm": "application/vnd.ms-powerpoint.template.macroEnabled.12", + "ppam": "application/vnd.ms-powerpoint.addin.macroEnabled.12", + "sldx": "application/vnd.openxmlformats-officedocument.presentationml.slide", + "sldm": "application/vnd.ms-powerpoint.slide.macroEnabled.12", + "onetoc|onetoc2|onetmp|onepkg": "application/onenote", + "oxps": "application/oxps", + "xps": "application/vnd.ms-xpsdocument", + "odt": "application/vnd.oasis.opendocument.text", + "odp": "application/vnd.oasis.opendocument.presentation", + "ods": "application/vnd.oasis.opendocument.spreadsheet", + "odg": "application/vnd.oasis.opendocument.graphics", + "odc": "application/vnd.oasis.opendocument.chart", + "odb": "application/vnd.oasis.opendocument.database", + "odf": "application/vnd.oasis.opendocument.formula", + "wp|wpd": "application/wordperfect", + "key": "application/vnd.apple.keynote", + "numbers": "application/vnd.apple.numbers", + "pages": "application/vnd.apple.pages" + } }, "required": false } From c43d2dc6f92118afcbd73ac32c0fef54dd280cb0 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 3 Oct 2025 17:02:36 +1000 Subject: [PATCH 12/12] Refactor WP REST Attachments Controller to remove MIME type enumeration from schema definition. Updated related tests and fixtures to reflect this change. --- .../class-wp-rest-attachments-controller.php | 1 - .../rest-api/rest-attachments-controller.php | 108 ------------------ tests/qunit/fixtures/wp-api-generated.js | 98 +--------------- 3 files changed, 1 insertion(+), 206 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index cff3e74e58f6f..faeb01c93904a 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -1380,7 +1380,6 @@ public function get_collection_params() { 'type' => 'array', 'items' => array( 'type' => 'string', - 'enum' => get_allowed_mime_types(), ), ); diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 4929399fb104e..bc1ca41b578ee 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -582,11 +582,6 @@ public function test_get_items_multiple_mime_types_and_combination() { $this->assertContains( $jpeg_id, $ids, 'JPEG ID not found in response for multiple MIME types with array format' ); $this->assertContains( $mp4_id, $ids, 'MP4 ID not found in response for multiple MIME types with array format' ); - - // Test invalid mime type mixed with valid ones. - $request->set_param( 'mime_type', array( 'video/mp4', 'cat/gif' ) ); - $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } /** @@ -3102,107 +3097,4 @@ public function test_edit_image_vertical_flip_only() { // The controller converts the integer values to booleans: 0 !== (int) 1 = true. $this->assertSame( array( true, false ), WP_Image_Editor_Mock::$spy['flip'][0], 'Vertical flip of the image is not identical.' ); } - - /** - * Test that the `media_types` parameter filters the response by multiple media types. - * - * @ticket 64046 - */ - public function test_get_items_with_media_types() { - $video_id = self::factory()->attachment->create_object( - self::$test_video_file, - 0, - array( - 'post_mime_type' => 'video/mp4', - 'post_excerpt' => 'A sample caption', - ) - ); - - $audio_id = self::factory()->attachment->create_object( - self::$test_audio_file, - 0, - array( - 'post_mime_type' => 'audio/mpeg', - 'post_excerpt' => 'A sample caption', - ) - ); - - $image_id = self::factory()->attachment->create_object( - self::$test_file, - 0, - array( - 'post_mime_type' => 'image/jpeg', - 'post_excerpt' => 'A sample caption', - ) - ); - - $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); - $request->set_param( 'media_types', array( 'audio', 'video' ) ); - $response = rest_get_server()->dispatch( $request ); - $this->assertCount( 2, $response->get_data() ); - $this->assertContains( $video_id, wp_list_pluck( $response->get_data(), 'id' ), 'Video ID not found in response for [audio, video]' ); - $this->assertContains( $audio_id, wp_list_pluck( $response->get_data(), 'id' ), 'Audio ID not found in response for [audio, video]' ); - - $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); - $request->set_param( 'media_types', array( 'image' ) ); - $response = rest_get_server()->dispatch( $request ); - $this->assertCount( 1, $response->get_data() ); - $this->assertContains( $image_id, wp_list_pluck( $response->get_data(), 'id' ), 'Image ID not found in response for [image]' ); - - $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); - $request->set_param( 'media_types', array( 'image', 'audio' ) ); - $response = rest_get_server()->dispatch( $request ); - $this->assertCount( 2, $response->get_data() ); - $this->assertContains( $image_id, wp_list_pluck( $response->get_data(), 'id' ), 'Image ID not found in response for [image, audio]' ); - $this->assertContains( $audio_id, wp_list_pluck( $response->get_data(), 'id' ), 'Audio ID not found in response for [image, audio]' ); - - $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); - $request->set_param( 'media_types', array( 'image', 'video', 'audio' ) ); - $response = rest_get_server()->dispatch( $request ); - $this->assertCount( 3, $response->get_data() ); - $this->assertContains( $image_id, wp_list_pluck( $response->get_data(), 'id' ), 'Image ID not found in response for [image, video, audio]' ); - $this->assertContains( $video_id, wp_list_pluck( $response->get_data(), 'id' ), 'Video ID not found in response for [image, video, audio]' ); - $this->assertContains( $audio_id, wp_list_pluck( $response->get_data(), 'id' ), 'Audio ID not found in response for [image, video, audio]' ); - } - - /** - * Test that the `media_type` parameter overrides the `media_types` parameter. - * - * @ticket 64046 - */ - public function test_get_items_with_media_type_and_media_types() { - self::factory()->attachment->create_object( - self::$test_video_file, - 0, - array( - 'post_mime_type' => 'video/mp4', - 'post_excerpt' => 'A sample caption', - ) - ); - - self::factory()->attachment->create_object( - self::$test_audio_file, - 0, - array( - 'post_mime_type' => 'audio/mpeg', - 'post_excerpt' => 'A sample caption', - ) - ); - - $image_id = self::factory()->attachment->create_object( - self::$test_file, - 0, - array( - 'post_mime_type' => 'image/jpeg', - 'post_excerpt' => 'A sample caption', - ) - ); - - $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); - $request->set_param( 'media_types', array( 'audio', 'video' ) ); - $request->set_param( 'media_type', 'image' ); - $response = rest_get_server()->dispatch( $request ); - $this->assertCount( 1, $response->get_data() ); - $this->assertContains( $image_id, wp_list_pluck( $response->get_data(), 'id' ), 'Image ID not found in response' ); - } } diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index a2becfa264496..bc9ea0a2dc424 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -2982,103 +2982,7 @@ mockedApiResponse.Schema = { "description": "Limit result set to attachments of a particular MIME type or MIME types.", "type": "array", "items": { - "type": "string", - "enum": { - "jpg|jpeg|jpe": "image/jpeg", - "gif": "image/gif", - "png": "image/png", - "bmp": "image/bmp", - "tiff|tif": "image/tiff", - "webp": "image/webp", - "avif": "image/avif", - "ico": "image/x-icon", - "heic": "image/heic", - "heif": "image/heif", - "heics": "image/heic-sequence", - "heifs": "image/heif-sequence", - "asf|asx": "video/x-ms-asf", - "wmv": "video/x-ms-wmv", - "wmx": "video/x-ms-wmx", - "wm": "video/x-ms-wm", - "avi": "video/avi", - "divx": "video/divx", - "flv": "video/x-flv", - "mov|qt": "video/quicktime", - "mpeg|mpg|mpe": "video/mpeg", - "mp4|m4v": "video/mp4", - "ogv": "video/ogg", - "webm": "video/webm", - "mkv": "video/x-matroska", - "3gp|3gpp": "video/3gpp", - "3g2|3gp2": "video/3gpp2", - "txt|asc|c|cc|h|srt": "text/plain", - "csv": "text/csv", - "tsv": "text/tab-separated-values", - "ics": "text/calendar", - "rtx": "text/richtext", - "css": "text/css", - "vtt": "text/vtt", - "dfxp": "application/ttaf+xml", - "mp3|m4a|m4b": "audio/mpeg", - "aac": "audio/aac", - "ra|ram": "audio/x-realaudio", - "wav|x-wav": "audio/wav", - "ogg|oga": "audio/ogg", - "flac": "audio/flac", - "mid|midi": "audio/midi", - "wma": "audio/x-ms-wma", - "wax": "audio/x-ms-wax", - "mka": "audio/x-matroska", - "rtf": "application/rtf", - "pdf": "application/pdf", - "class": "application/java", - "tar": "application/x-tar", - "zip": "application/zip", - "gz|gzip": "application/x-gzip", - "rar": "application/rar", - "7z": "application/x-7z-compressed", - "psd": "application/octet-stream", - "xcf": "application/octet-stream", - "doc": "application/msword", - "pot|pps|ppt": "application/vnd.ms-powerpoint", - "wri": "application/vnd.ms-write", - "xla|xls|xlt|xlw": "application/vnd.ms-excel", - "mdb": "application/vnd.ms-access", - "mpp": "application/vnd.ms-project", - "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "docm": "application/vnd.ms-word.document.macroEnabled.12", - "dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template", - "dotm": "application/vnd.ms-word.template.macroEnabled.12", - "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12", - "xlsb": "application/vnd.ms-excel.sheet.binary.macroEnabled.12", - "xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template", - "xltm": "application/vnd.ms-excel.template.macroEnabled.12", - "xlam": "application/vnd.ms-excel.addin.macroEnabled.12", - "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", - "pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12", - "ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow", - "ppsm": "application/vnd.ms-powerpoint.slideshow.macroEnabled.12", - "potx": "application/vnd.openxmlformats-officedocument.presentationml.template", - "potm": "application/vnd.ms-powerpoint.template.macroEnabled.12", - "ppam": "application/vnd.ms-powerpoint.addin.macroEnabled.12", - "sldx": "application/vnd.openxmlformats-officedocument.presentationml.slide", - "sldm": "application/vnd.ms-powerpoint.slide.macroEnabled.12", - "onetoc|onetoc2|onetmp|onepkg": "application/onenote", - "oxps": "application/oxps", - "xps": "application/vnd.ms-xpsdocument", - "odt": "application/vnd.oasis.opendocument.text", - "odp": "application/vnd.oasis.opendocument.presentation", - "ods": "application/vnd.oasis.opendocument.spreadsheet", - "odg": "application/vnd.oasis.opendocument.graphics", - "odc": "application/vnd.oasis.opendocument.chart", - "odb": "application/vnd.oasis.opendocument.database", - "odf": "application/vnd.oasis.opendocument.formula", - "wp|wpd": "application/wordperfect", - "key": "application/vnd.apple.keynote", - "numbers": "application/vnd.apple.numbers", - "pages": "application/vnd.apple.pages" - } + "type": "string" }, "required": false }