diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php index c6b51dc17060b..4a5d191409548 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php @@ -120,7 +120,7 @@ public function register_routes() { } /** - * Checks if a given request has access to read posts. + * Checks if a given request has access to font faces. * * @since 6.5.0 * @@ -140,41 +140,69 @@ public function get_font_faces_permissions_check() { return true; } + /** + * Validates settings when creating a font face. + * + * @since 6.5.0 + * + * @param string $value Encoded JSON string of font face settings. + * @param WP_REST_Request $request Request object. + * @return false|WP_Error True if the settings are valid, otherwise a WP_Error object. + */ public function validate_create_font_face_settings( $value, $request ) { $settings = json_decode( $value, true ); - $schema = $this->get_item_schema()['properties']['font_face_settings']; + + // Check settings string is valid JSON. + if ( null === $settings ) { + return new WP_Error( + 'rest_invalid_param', + __( 'font_face_settings parameter must be a valid JSON string.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } // Check that the font face settings match the theme.json schema. - $valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_face_settings' ); + $schema = $this->get_item_schema()['properties']['font_face_settings']; + $has_valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_face_settings' ); - // Some properties trigger a multiple "oneOf" types error that we ignore, because they are still valid. - // e.g. a fontWeight of "400" validates as both a string and an integer due to is_numeric type checking. - if ( is_wp_error( $valid_settings ) && $valid_settings->get_error_code() !== 'rest_one_of_multiple_matches' ) { - $valid_settings->add_data( array( 'status' => 400 ) ); - return $valid_settings; + if ( is_wp_error( $has_valid_settings ) ) { + $has_valid_settings->add_data( array( 'status' => 400 ) ); + return $has_valid_settings; } - $srcs = is_array( $settings['src'] ) ? $settings['src'] : array( $settings['src'] ); - $files = $request->get_file_params(); - - // Check that each file in the request references a src in the settings. - foreach ( array_keys( $files ) as $file ) { - if ( ! in_array( $file, $srcs, true ) ) { + // Check that none of the required settings are empty values. + $required = $schema['required']; + foreach ( $required as $key ) { + if ( isset( $settings[ $key ] ) && ! $settings[ $key ] ) { return new WP_Error( 'rest_invalid_param', - /* translators: %s: A URL. */ - __( 'Every file uploaded must be used as a font face src.', 'gutenberg' ), + /* translators: %s: Font family setting key. */ + sprintf( __( 'font_face_setting[%s] cannot be empty.', 'gutenberg' ), $key ), array( 'status' => 400 ) ); } } - // Check that src strings are non-empty. - foreach ( $srcs as $src ) { - if ( ! $src ) { + $srcs = is_array( $settings['src'] ) ? $settings['src'] : array( $settings['src'] ); + + // Check that srcs are non-empty strings. + $filtered_src = array_filter( array_filter( $srcs, 'is_string' ) ); + if ( empty( $filtered_src ) ) { + return new WP_Error( + 'rest_invalid_param', + __( 'font_face_settings[src] values must be non-empty strings.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } + + // Check that each file in the request references a src in the settings. + $files = $request->get_file_params(); + foreach ( array_keys( $files ) as $file ) { + if ( ! in_array( $file, $srcs, true ) ) { return new WP_Error( 'rest_invalid_param', - __( 'Font face src values must be non-empty strings.', 'gutenberg' ), + // translators: %s: File key (e.g. `file-0`) in the request data. + sprintf( __( 'File %1$s must be used in font_face_settings[src].', 'gutenberg' ), $file ), array( 'status' => 400 ) ); } @@ -183,6 +211,26 @@ public function validate_create_font_face_settings( $value, $request ) { return true; } + /** + * Sanitizes the font face settings when creating a font face. + * + * @since 6.5.0 + * + * @param string $value Encoded JSON string of font face settings. + * @param WP_REST_Request $request Request object. + * @return array Decoded array of font face settings. + */ + public function sanitize_font_face_settings( $value ) { + // Settings arrive as stringified JSON, since this is a multipart/form-data request. + $settings = json_decode( $value, true ); + + if ( isset( $settings['fontFamily'] ) ) { + $settings['fontFamily'] = WP_Font_Family_Utils::format_font_family( $settings['fontFamily'] ); + } + + return $settings; + } + /** * Retrieves a collection of font faces within the parent font family. * @@ -201,7 +249,7 @@ public function get_items( $request ) { } /** - * Retrieves a single font face for within parent font family. + * Retrieves a single font face within the parent font family. * * @since 6.5.0 * @@ -214,6 +262,7 @@ public function get_item( $request ) { return $post; } + // Check that the font face has a valid parent font family. $font_family = $this->get_font_family_post( $request['font_family_id'] ); if ( is_wp_error( $font_family ) ) { return $font_family; @@ -242,8 +291,8 @@ public function get_item( $request ) { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function create_item( $request ) { - // Settings arrive as stringified JSON, since this is a multipart/form-data request. - $settings = json_decode( $request->get_param( 'font_face_settings' ), true ); + // Settings have already been decoded by ::sanitize_font_face_settings(). + $settings = $request->get_param( 'font_face_settings' ); $file_params = $request->get_file_params(); // Move the uploaded font asset from the temp folder to the fonts directory. @@ -264,12 +313,8 @@ public function create_item( $request ) { $file = $file_params[ $src ]; $font_file = $this->handle_font_file_upload( $file ); - if ( isset( $font_file['error'] ) ) { - return new WP_Error( - 'rest_font_upload_unknown_error', - $font_file['error'], - array( 'status' => 500 ) - ); + if ( is_wp_error( $font_file ) ) { + return $font_file; } $processed_srcs[] = $font_file['url']; @@ -336,7 +381,20 @@ public function prepare_item_for_response( $item, $request ) { // phpcs:ignore V $data['id'] = $item->ID; $data['theme_json_version'] = 2; $data['parent'] = $item->post_parent; - $data['font_face_settings'] = json_decode( $item->post_content, true ); + + $settings = json_decode( $item->post_content, true ); + $properties = $this->get_item_schema()['properties']['font_face_settings']['properties']; + + // Provide required, empty settings if the post_content is not valid JSON. + if ( null === $settings ) { + $settings = array( + 'fontFamily' => '', + 'src' => array(), + ); + } + + // Only return the properties defined in the schema. + $data['font_face_settings'] = array_intersect_key( $settings, $properties ); $response = rest_ensure_response( $data ); $links = $this->prepare_links( $item ); @@ -369,7 +427,7 @@ public function get_item_schema() { 'readonly' => true, ), 'theme_json_version' => array( - 'description' => __( 'Version of the theme.json schema used for the font face typography settings.', 'gutenberg' ), + 'description' => __( 'Version of the theme.json schema used for the typography settings.', 'gutenberg' ), 'type' => 'integer', 'default' => 2, 'minimum' => 2, @@ -398,14 +456,9 @@ public function get_item_schema() { 'fontWeight' => array( 'description' => 'List of available font weights, separated by a space.', 'default' => '400', - 'oneOf' => array( - array( - 'type' => 'string', - ), - array( - 'type' => 'integer', - ), - ), + // Changed from `oneOf` to avoid errors from loose type checking. + // e.g. a fontWeight of "400" validates as both a string and an integer due to is_numeric check. + 'type' => array( 'string', 'integer' ), ), 'fontDisplay' => array( 'description' => 'CSS font-display value.', @@ -421,7 +474,8 @@ public function get_item_schema() { ), 'src' => array( 'description' => 'Paths or URLs to the font files.', - 'oneOf' => array( + // Changed from `oneOf` to `anyOf` due to rest_sanitize_array converting a string into an array. + 'anyOf' => array( array( 'type' => 'string', ), @@ -494,30 +548,12 @@ public function get_item_schema() { * @return array Collection parameters. */ public function get_collection_params() { + $params = parent::get_collection_params(); + return array( - 'page' => array( - 'description' => __( 'Current page of the collection.', 'default' ), - 'type' => 'integer', - 'default' => 1, - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - 'minimum' => 1, - ), - 'per_page' => array( - 'description' => __( 'Maximum number of items to be returned in result set.', 'default' ), - 'type' => 'integer', - 'default' => 10, - 'minimum' => 1, - 'maximum' => 100, - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - ), - 'search' => array( - 'description' => __( 'Limit results to those matching a string.', 'default' ), - 'type' => 'string', - 'sanitize_callback' => 'sanitize_text_field', - 'validate_callback' => 'rest_validate_request_arg', - ), + 'page' => $params['page'], + 'per_page' => $params['per_page'], + 'search' => $params['search'], ); } @@ -539,6 +575,7 @@ public function get_create_params() { 'type' => 'string', 'required' => true, 'validate_callback' => array( $this, 'validate_create_font_face_settings' ), + 'sanitize_callback' => array( $this, 'sanitize_font_face_settings' ), ), ); } @@ -610,10 +647,18 @@ protected function prepare_links( $post ) { return $links; } + /** + * Prepares a single font face post for creation. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Request object. + * @return stdClass|WP_Error Post object or WP_Error. + */ protected function prepare_item_for_database( $request ) { $prepared_post = new stdClass(); - // Settings have already been decoded and processed by create_item(). + // Settings have already been decoded by ::sanitize_font_face_settings(). $settings = $request->get_param( 'font_face_settings' ); $prepared_post->post_type = $this->post_type; @@ -626,19 +671,30 @@ protected function prepare_item_for_database( $request ) { return $prepared_post; } + /** + * Handles the upload of a font file using wp_handle_upload(). + * + * @since 6.5.0 + * + * @param array $file Single file item from $_FILES. + * @return array Array containing uploaded file attributes on success, or error on failure. + */ protected function handle_font_file_upload( $file ) { add_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); add_filter( 'upload_dir', 'wp_get_font_dir' ); $overrides = array( + 'upload_error_handler' => array( $this, 'handle_font_file_upload_error' ), // Arbitrary string to avoid the is_uploaded_file() check applied // when using 'wp_handle_upload'. - 'action' => 'wp_handle_font_upload', + 'action' => 'wp_handle_font_upload', // Not testing a form submission. - 'test_form' => false, + 'test_form' => false, // Seems mime type for files that are not images cannot be tested. // See wp_check_filetype_and_ext(). - 'test_type' => true, + 'test_type' => true, + // Only allow uploading font files for this request. + 'mimes' => WP_Font_Library::get_expected_font_mime_types_per_php_version(), ); $uploaded_file = wp_handle_upload( $file, $overrides ); @@ -649,6 +705,27 @@ protected function handle_font_file_upload( $file ) { return $uploaded_file; } + /** + * Handles file upload error. + * + * @since 6.5.0 + * + * @param array $file File upload data. + * @param string $message Error message from wp_handle_upload(). + * @return WP_Error WP_Error object. + */ + public function handle_font_file_upload_error( $file, $message ) { + $status = 500; + $code = 'rest_font_upload_unknown_error'; + + if ( 'Sorry, you are not allowed to upload this file type.' === $message ) { + $status = 400; + $code = 'rest_font_upload_invalid_file_type'; + } + + return new WP_Error( $code, $message, array( 'status' => $status ) ); + } + /** * Returns relative path to an uploaded font file. * diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php index 8abc9894206f3..0c4b3d8c6c0c7 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php @@ -117,7 +117,7 @@ public function get_font_families_permissions_check() { } /** - * Validates font family settings when creating or updating a font family. + * Validates settings when creating or updating a font family. * * @since 6.5.0 * @@ -127,6 +127,16 @@ public function get_font_families_permissions_check() { */ public function validate_font_family_settings( $value, $request ) { $settings = json_decode( $value, true ); + + // Check settings string is valid JSON. + if ( null === $settings ) { + return new WP_Error( + 'rest_invalid_param', + __( 'font_family_settings parameter must be a valid JSON string.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } + $schema = $this->get_item_schema()['properties']['font_family_settings']; $required = $schema['required']; @@ -136,11 +146,11 @@ public function validate_font_family_settings( $value, $request ) { } // Check that the font face settings match the theme.json schema. - $valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_family_settings' ); + $has_valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_family_settings' ); - if ( is_wp_error( $valid_settings ) ) { - $valid_settings->add_data( array( 'status' => 400 ) ); - return $valid_settings; + if ( is_wp_error( $has_valid_settings ) ) { + $has_valid_settings->add_data( array( 'status' => 400 ) ); + return $has_valid_settings; } // Check that none of the required settings are empty values. @@ -149,7 +159,7 @@ public function validate_font_family_settings( $value, $request ) { return new WP_Error( 'rest_invalid_param', /* translators: %s: Font family setting key. */ - sprintf( __( 'Font family setting "%s" cannot be empty.', 'gutenberg' ), $key ), + sprintf( __( 'font_family_settings[%s] cannot be empty.', 'gutenberg' ), $key ), array( 'status' => 400 ) ); } @@ -183,7 +193,7 @@ public function sanitize_font_family_settings( $value ) { } /** - * Deletes a single item. + * Deletes a single font family. * * @since 6.5.0 * @@ -216,7 +226,7 @@ public function delete_item( $request ) { } /** - * Prepares a single item output for response. + * Prepares a single font family output for response. * * @since 6.5.0 * @@ -234,6 +244,16 @@ public function prepare_item_for_response( $item, $request ) { // phpcs:ignore V $settings = json_decode( $item->post_content, true ); $properties = $this->get_item_schema()['properties']['font_family_settings']['properties']; + // Provide empty settings if the post_content is not valid JSON. + if ( null === $settings ) { + $settings = array( + 'name' => '', + 'slug' => '', + 'fontFamily' => '', + 'preview' => '', + ); + } + // Only return the properties defined in the schema. $data['font_family_settings'] = array_intersect_key( $settings, $properties ); @@ -333,7 +353,7 @@ public function get_collection_params() { } /** - * Checks if a given request has access to read items. + * Checks if a given request has access to read font families. * * @since 6.5.0 * diff --git a/phpunit/tests/fonts/font-library/wpRestFontFacesController.php b/phpunit/tests/fonts/font-library/wpRestFontFacesController.php index 3846acba5a4ba..9b5b596e41ac6 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontFacesController.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFacesController.php @@ -21,19 +21,19 @@ class WP_REST_Font_Faces_Controller_Test extends WP_Test_REST_Controller_Testcas protected static $font_face_id2; protected static $default_settings = array( - 'fontFamily' => 'Open Sans', + 'fontFamily' => '"Open Sans"', 'fontWeight' => '400', 'fontStyle' => 'normal', 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf', ); public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { - self::$font_family_id = $factory->post->create( array( 'post_type' => 'wp_font_family' ) ); - self::$other_font_family_id = $factory->post->create( array( 'post_type' => 'wp_font_family' ) ); + self::$font_family_id = WP_REST_Font_Families_Controller_Test::create_font_family_post(); + self::$other_font_family_id = WP_REST_Font_Families_Controller_Test::create_font_family_post(); self::$font_face_id1 = self::create_font_face_post( self::$font_family_id, array( - 'fontFamily' => 'Open Sans', + 'fontFamily' => '"Open Sans"', 'fontWeight' => '400', 'fontStyle' => 'normal', 'src' => home_url( '/wp-content/fonts/open-sans-medium.ttf' ), @@ -43,7 +43,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { self::$font_face_id2 = self::create_font_face_post( self::$font_family_id, array( - 'fontFamily' => 'Open Sans', + 'fontFamily' => '"Open Sans"', 'fontWeight' => '900', 'fontStyle' => 'normal', 'src' => home_url( '/wp-content/fonts/open-sans-bold.ttf' ), @@ -165,12 +165,56 @@ public function test_get_item() { wp_set_current_user( self::$admin_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . self::$font_face_id1 ); $response = rest_get_server()->dispatch( $request ); - $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); - $data = $response->get_data(); + $this->assertSame( 200, $response->get_status() ); $this->check_font_face_data( $data, self::$font_face_id1, $response->get_links() ); } + /** + * @covers WP_REST_Font_Faces_Controller::prepare_item_for_response + */ + public function test_get_item_removes_extra_settings() { + $font_face_id = self::create_font_face_post( self::$font_family_id, array( 'extra' => array() ) ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertArrayNotHasKey( 'extra', $data['font_face_settings'] ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::prepare_item_for_response + */ + public function test_get_item_malformed_post_content_returns_empty_settings() { + $font_face_id = wp_insert_post( + array( + 'post_type' => 'wp_font_face', + 'post_parent' => self::$font_family_id, + 'post_status' => 'publish', + 'post_content' => 'invalid', + ) + ); + + $empty_settings = array( + 'fontFamily' => '', + 'src' => array(), + ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( $empty_settings, $data['font_face_settings'] ); + + wp_delete_post( $font_face_id, true ); + } + /** * @covers WP_REST_Font_Faces_Controller::get_item */ @@ -216,6 +260,7 @@ public function test_get_item_valid_parent_id() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( 200, $response->get_status() ); $this->assertSame( self::$font_family_id, $data['parent'], 'The returned parent id should match the font family id.' ); } @@ -245,7 +290,7 @@ public function test_create_item() { 'font_face_settings', wp_json_encode( array( - 'fontFamily' => 'Open Sans', + 'fontFamily' => '"Open Sans"', 'fontWeight' => '400', 'fontStyle' => 'normal', 'src' => array_keys( $files )[0], @@ -257,6 +302,7 @@ public function test_create_item() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( 201, $response->get_status() ); $this->check_font_face_data( $data, $data['id'], $response->get_links() ); $this->check_file_meta( $data['id'], array( $data['font_face_settings']['src'] ) ); @@ -265,7 +311,7 @@ public function test_create_item() { $this->assertSame( $settings, array( - 'fontFamily' => 'Open Sans', + 'fontFamily' => '"Open Sans"', 'fontWeight' => '400', 'fontStyle' => 'normal', ) @@ -289,7 +335,7 @@ public function test_create_item_with_multiple_font_files() { 'font_face_settings', wp_json_encode( array( - 'fontFamily' => 'Open Sans', + 'fontFamily' => '"Open Sans"', 'fontWeight' => '400', 'fontStyle' => 'normal', 'src' => array_keys( $files ), @@ -301,6 +347,7 @@ public function test_create_item_with_multiple_font_files() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( 201, $response->get_status() ); $this->check_font_face_data( $data, $data['id'], $response->get_links() ); $this->check_file_meta( $data['id'], $data['font_face_settings']['src'] ); @@ -310,6 +357,41 @@ public function test_create_item_with_multiple_font_files() { wp_delete_post( $data['id'], true ); } + /** + * @covers WP_REST_Font_Faces_Controller::create_item + */ + public function test_create_item_invalid_file_type() { + $image_file = DIR_TESTDATA . '/images/canola.jpg'; + $image_path = wp_tempnam( 'canola.jpg' ); + copy( $image_file, $image_path ); + + $files = array( + 'file-0' => array( + 'name' => 'canola.jpg', + 'full_path' => 'canola.jpg', + 'type' => 'font/woff2', + 'tmp_name' => $image_path, + 'error' => 0, + 'size' => filesize( $image_path ), + ), + ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( 'theme_json_version', 2 ); + $request->set_param( + 'font_face_settings', + wp_json_encode( + array_merge( self::$default_settings, array( 'src' => array_keys( $files )[0] ) ) + ) + ); + $request->set_file_params( $files ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_font_upload_invalid_file_type', $response, 400 ); + } + /** * @covers WP_REST_Font_Faces_Controller::create_item */ @@ -321,7 +403,7 @@ public function test_create_item_with_url_src() { 'font_face_settings', wp_json_encode( array( - 'fontFamily' => 'Open Sans', + 'fontFamily' => '"Open Sans"', 'fontWeight' => '400', 'fontStyle' => 'normal', 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf', @@ -332,6 +414,7 @@ public function test_create_item_with_url_src() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( 201, $response->get_status() ); $this->check_font_face_data( $data, $data['id'], $response->get_links() ); wp_delete_post( $data['id'], true ); @@ -344,7 +427,7 @@ public function test_create_item_with_all_properties() { wp_set_current_user( self::$admin_id ); $properties = array( - 'fontFamily' => 'Open Sans', + 'fontFamily' => '"Open Sans"', 'fontWeight' => '300 500', 'fontStyle' => 'oblique 30deg 50deg', 'fontDisplay' => 'swap', @@ -368,6 +451,7 @@ public function test_create_item_with_all_properties() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( 201, $response->get_status() ); $this->assertArrayHasKey( 'font_face_settings', $data ); $this->assertSame( $properties, $data['font_face_settings'] ); @@ -384,7 +468,7 @@ public function test_create_item_default_theme_json_version() { 'font_face_settings', wp_json_encode( array( - 'fontFamily' => 'Open Sans', + 'fontFamily' => '"Open Sans"', 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf', ) ) @@ -393,6 +477,8 @@ public function test_create_item_default_theme_json_version() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( 201, $response->get_status() ); + $this->assertArrayHasKey( 'theme_json_version', $data ); $this->assertSame( 2, $data['theme_json_version'], 'The default theme.json version should be 2.' ); wp_delete_post( $data['id'], true ); @@ -432,41 +518,118 @@ public function test_create_item_invalid_settings( $settings ) { $request->set_param( 'font_face_settings', wp_json_encode( $settings ) ); $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } public function data_create_item_invalid_settings() { return array( - 'Missing src' => array( - 'settings' => array( - 'fontFamily' => 'Open Sans', - 'fontWeight' => '400', - 'fontStyle' => 'normal', - ), + 'Missing fontFamily' => array( + 'settings' => array_diff_key( self::$default_settings, array( 'fontFamily' => '' ) ), ), - 'Invalid src' => array( - 'settings' => array( - 'fontFamily' => 'Open Sans', - 'src' => '', - ), + 'Empty fontFamily' => array( + 'settings' => array_merge( self::$default_settings, array( 'fontFamily' => '' ) ), ), - 'Missing fontFamily' => array( - 'settings' => array( - 'fontWeight' => '400', - 'fontStyle' => 'normal', - 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf', - ), + 'Wrong fontFamily type' => array( + 'settings' => array_merge( self::$default_settings, array( 'fontFamily' => 1234 ) ), + ), + 'Invalid fontDisplay' => array( + 'settings' => array_merge( self::$default_settings, array( 'fontDisplay' => 'invalid' ) ), + ), + 'Missing src' => array( + 'settings' => array_diff_key( self::$default_settings, array( 'src' => '' ) ), + ), + 'Empty src string' => array( + 'settings' => array_merge( self::$default_settings, array( 'src' => '' ) ), + ), + 'Empty src array' => array( + 'settings' => array_merge( self::$default_settings, array( 'src' => array() ) ), + ), + 'Empty src array values' => array( + 'settings' => array_merge( self::$default_settings, array( '', '' ) ), ), - 'Invalid fontDisplay' => array( - 'settings' => array( - 'fontFamily' => 'Open Sans', - 'fontDisplay' => 'invalid', - 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf', - ), + 'Wrong src type' => array( + 'settings' => array_merge( self::$default_settings, array( 'src' => 1234 ) ), + ), + 'Wrong src array types' => array( + 'settings' => array_merge( self::$default_settings, array( 'src' => array( 1234, 5678 ) ) ), ), ); } + /** + * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_settings + */ + public function test_create_item_invalid_settings_json() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( 'theme_json_version', 2 ); + $request->set_param( 'font_face_settings', 'invalid' ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $expected_message = 'font_face_settings parameter must be a valid JSON string.'; + $message = $response->as_error()->get_all_error_data()[0]['params']['font_face_settings']; + $this->assertSame( $expected_message, $message ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_settings + */ + public function test_create_item_invalid_file_src() { + $files = $this->setup_font_file_upload( array( 'woff2' ) ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( 'theme_json_version', 2 ); + $request->set_param( + 'font_face_settings', + wp_json_encode( + array_merge( self::$default_settings, array( 'src' => 'invalid' ) ) + ) + ); + $request->set_file_params( $files ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $expected_message = 'File ' . array_keys( $files )[0] . ' must be used in font_face_settings[src].'; + $message = $response->as_error()->get_all_error_data()[0]['params']['font_face_settings']; + $this->assertSame( $expected_message, $message ); + } + + /** + * @dataProvider data_create_item_santize_font_family + * + * @covers WP_REST_Font_Families_Controller::update_item + */ + public function test_create_item_santize_font_family( $font_family_setting, $expected ) { + $settings = array_merge( self::$default_settings, array( 'fontFamily' => $font_family_setting ) ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( 'font_face_settings', wp_json_encode( $settings ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 201, $response->get_status() ); + $this->assertSame( $expected, $data['font_face_settings']['fontFamily'] ); + } + + public function data_create_item_santize_font_family() { + return array( + array( 'Libre Barcode 128 Text', "'Libre Barcode 128 Text'" ), + array( 'B612 Mono', "'B612 Mono'" ), + array( 'Open Sans, Noto Sans, sans-serif', "'Open Sans', 'Noto Sans', sans-serif" ), + ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::create_item + */ + // public function test_create_item_no_permission() {} + public function test_update_item() { $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . self::$font_face_id1 ); $response = rest_get_server()->dispatch( $request ); @@ -478,12 +641,7 @@ public function test_update_item() { */ public function test_delete_item() { wp_set_current_user( self::$admin_id ); - $font_face_id = self::factory()->post->create( - array( - 'post_type' => 'wp_font_face', - 'post_parent' => self::$font_family_id, - ) - ); + $font_face_id = self::create_font_face_post( self::$font_family_id ); $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id ); $request['force'] = true; $response = rest_get_server()->dispatch( $request ); @@ -497,12 +655,7 @@ public function test_delete_item() { */ public function test_delete_item_no_trash() { wp_set_current_user( self::$admin_id ); - $font_face_id = self::factory()->post->create( - array( - 'post_type' => 'wp_font_face', - 'post_parent' => self::$font_family_id, - ) - ); + $font_face_id = self::create_font_face_post( self::$font_family_id ); // Attempt trashing. $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id ); @@ -521,17 +674,27 @@ public function test_delete_item_no_trash() { /** * @covers WP_REST_Font_Faces_Controller::delete_item */ - public function test_delete_item_invalid_delete_permissions() { - wp_set_current_user( self::$editor_id ); - $font_face_id = self::factory()->post->create( - array( - 'post_type' => 'wp_font_face', - 'post_parent' => self::$font_family_id, - ) - ); - $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id ); - $response = rest_get_server()->dispatch( $request ); + public function test_delete_item_invalid_font_face_id() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::delete_item + */ + public function test_delete_item_no_permissions() { + $font_face_id = $this->create_font_face_post( self::$font_family_id ); + + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_delete', $response, 401 ); + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id ); + $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_delete', $response, 403 ); } @@ -542,9 +705,9 @@ public function test_prepare_item() { wp_set_current_user( self::$admin_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . self::$font_face_id2 ); $response = rest_get_server()->dispatch( $request ); - $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); - $data = $response->get_data(); + $this->assertSame( 200, $response->get_status() ); $this->check_font_face_data( $data, self::$font_face_id2, $response->get_links() ); } @@ -556,6 +719,7 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( 200, $response->get_status() ); $properties = $data['schema']['properties']; $this->assertCount( 4, $properties ); $this->assertArrayHasKey( 'id', $properties ); @@ -597,6 +761,7 @@ protected function check_file_meta( $font_face_id, $srcs ) { protected function setup_font_file_upload( $formats ) { $files = array(); foreach ( $formats as $format ) { + // @core-merge Use `DIR_TESTDATA` instead of `GUTENBERG_DIR_TESTDATA`. $font_file = GUTENBERG_DIR_TESTDATA . 'fonts/OpenSans-Regular.' . $format; $font_path = wp_tempnam( 'OpenSans-Regular.' . $format ); copy( $font_file, $font_path ); diff --git a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php index 9b896cc9c9732..d00d653c0f727 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php @@ -174,7 +174,7 @@ public function test_get_item() { } /** - * @covers WP_REST_Font_Faces_Controller::get_item + * @covers WP_REST_Font_Families_Controller::get_item */ public function test_get_item_removes_extra_settings() { $font_family_id = self::create_font_family_post( array( 'fontFace' => array() ) ); @@ -186,6 +186,38 @@ public function test_get_item_removes_extra_settings() { $this->assertSame( 200, $response->get_status() ); $this->assertArrayNotHasKey( 'fontFace', $data['font_family_settings'] ); + + wp_delete_post( $font_family_id, true ); + } + + /** + * @covers WP_REST_Font_Families_Controller::prepare_item_for_response + */ + public function test_get_item_malformed_post_content_returns_empty_settings() { + $font_family_id = wp_insert_post( + array( + 'post_type' => 'wp_font_family', + 'post_status' => 'publish', + 'post_content' => 'invalid', + ) + ); + + $empty_settings = array( + 'name' => '', + 'slug' => '', + 'fontFamily' => '', + 'preview' => '', + ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . $font_family_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( $empty_settings, $data['font_family_settings'] ); + + wp_delete_post( $font_family_id, true ); } /** @@ -278,7 +310,7 @@ public function data_create_item_invalid_theme_json_version() { /** * @dataProvider data_create_item_with_default_preview * - * @covers WP_REST_Font_Faces_Controller::create_item + * @covers WP_REST_Font_Faces_Controller::sanitize_font_family_settings */ public function test_create_item_with_default_preview( $settings ) { wp_set_current_user( self::$admin_id ); @@ -321,41 +353,60 @@ public function test_create_item_invalid_settings( $settings ) { wp_set_current_user( self::$admin_id ); $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); $request->set_param( 'theme_json_version', 2 ); - $request->set_param( 'font_face_settings', wp_json_encode( $settings ) ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_missing_callback_param', $response, 400 ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } public function data_create_item_invalid_settings() { - $default_settings = array( - 'name' => 'Open Sans', - 'slug' => 'open-sans', - 'fontFamily' => '"Open Sans", sans-serif', - 'preview' => 'https://s.w.org/images/fonts/16.7/previews/open-sans/open-sans-400-normal.svg', - ); return array( - 'Missing name' => array( - 'settings' => array_diff_key( $default_settings, array( 'name' => '' ) ), + 'Missing name' => array( + 'settings' => array_diff_key( self::$default_settings, array( 'name' => '' ) ), ), - 'Empty name' => array( - 'settings' => array_merge( $default_settings, array( 'name' => '' ) ), + 'Empty name' => array( + 'settings' => array_merge( self::$default_settings, array( 'name' => '' ) ), ), - 'Missing slug' => array( - 'settings' => array_diff_key( $default_settings, array( 'slug' => '' ) ), + 'Wrong name type' => array( + 'settings' => array_merge( self::$default_settings, array( 'name' => 1234 ) ), ), - 'Empty slug' => array( - 'settings' => array_merge( $default_settings, array( 'slug' => '' ) ), + 'Missing slug' => array( + 'settings' => array_diff_key( self::$default_settings, array( 'slug' => '' ) ), ), - 'Missing fontFamily' => array( - 'settings' => array_diff_key( $default_settings, array( 'fontFamily' => '' ) ), + 'Empty slug' => array( + 'settings' => array_merge( self::$default_settings, array( 'slug' => '' ) ), ), - 'Empty fontFamily' => array( - 'settings' => array_merge( $default_settings, array( 'fontFamily' => '' ) ), + 'Wrong slug type' => array( + 'settings' => array_merge( self::$default_settings, array( 'slug' => 1234 ) ), + ), + 'Missing fontFamily' => array( + 'settings' => array_diff_key( self::$default_settings, array( 'fontFamily' => '' ) ), + ), + 'Empty fontFamily' => array( + 'settings' => array_merge( self::$default_settings, array( 'fontFamily' => '' ) ), + ), + 'Wrong fontFamily type' => array( + 'settings' => array_merge( self::$default_settings, array( 'fontFamily' => 1234 ) ), ), ); } + /** + * @covers WP_REST_Font_Family_Controller::validate_font_family_settings + */ + public function test_create_item_invalid_settings_json() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $request->set_param( 'theme_json_version', 2 ); + $request->set_param( 'font_family_settings', 'invalid' ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $expected_message = 'font_family_settings parameter must be a valid JSON string.'; + $actual_message = $response->as_error()->get_all_error_data()[0]['params']['font_family_settings']; + $this->assertSame( $expected_message, $actual_message ); + } /** * @covers WP_REST_Font_Faces_Controller::create_item @@ -448,6 +499,7 @@ public function data_update_item_individual_settings() { /** * @dataProvider data_update_item_santize_font_family + * * @covers WP_REST_Font_Families_Controller::update_item */ public function test_update_item_santize_font_family( $font_family_setting, $expected ) { @@ -490,15 +542,24 @@ public function test_update_item_empty_settings( $settings ) { public function data_update_item_invalid_settings() { return array( - 'Empty name' => array( + 'Empty name' => array( array( 'name' => '' ), ), - 'Empty slug' => array( + 'Wrong name type' => array( + array( 'name' => 1234 ), + ), + 'Empty slug' => array( array( 'slug' => '' ), ), - 'Empty fontFamily' => array( + 'Wrong slug type' => array( + array( 'slug' => 1234 ), + ), + 'Empty fontFamily' => array( array( 'fontFamily' => '' ), ), + 'Wrong fontFamily type' => array( + array( 'fontFamily' => 1234 ), + ), ); } diff --git a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/base.php b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/base.php deleted file mode 100644 index e2d190cd76af1..0000000000000 --- a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/base.php +++ /dev/null @@ -1,43 +0,0 @@ -factory->user->create( - array( - 'role' => 'administrator', - ) - ); - wp_set_current_user( $admin_id ); - } - - /** - * Tear down each test method. - */ - public function tear_down() { - parent::tear_down(); - - // Clean up the /fonts directory. - foreach ( $this->files_in_dir( static::$fonts_dir ) as $file ) { - @unlink( $file ); - } - } -}