diff --git a/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php b/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php index 792a5aaa80eef..6d975753d5e96 100644 --- a/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php +++ b/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php @@ -21,35 +21,41 @@ */ class WP_Font_Utils { /** - * Format font family names. + * Sanitizes and formats font family names. * - * Adds surrounding quotes to font family names containing spaces and not already quoted. + * - Applies `sanitize_text_field` + * - Adds surrounding quotes to names that contain spaces and are not already quoted * * @since 6.5.0 * @access private * + * @see sanitize_text_field() + * * @param string $font_family Font family name(s), comma-separated. - * @return string Formatted font family name(s). + * @return string Sanitized and formatted font family name(s). */ - public static function format_font_family( $font_family ) { - if ( $font_family ) { - $font_families = explode( ',', $font_family ); - $wrapped_font_families = array_map( - function ( $family ) { - $trimmed = trim( $family ); - if ( ! empty( $trimmed ) && strpos( $trimmed, ' ' ) !== false && strpos( $trimmed, "'" ) === false && strpos( $trimmed, '"' ) === false ) { - return '"' . $trimmed . '"'; - } - return $trimmed; - }, - $font_families - ); - - if ( count( $wrapped_font_families ) === 1 ) { - $font_family = $wrapped_font_families[0]; - } else { - $font_family = implode( ', ', $wrapped_font_families ); - } + public static function sanitize_font_family( $font_family ) { + if ( ! $font_family ) { + return ''; + } + + $font_family = sanitize_text_field( $font_family ); + $font_families = explode( ',', $font_family ); + $wrapped_font_families = array_map( + function ( $family ) { + $trimmed = trim( $family ); + if ( ! empty( $trimmed ) && false !== strpos( $trimmed, ' ' ) && false === strpos( $trimmed, "'" ) && false === strpos( $trimmed, '"' ) ) { + return '"' . $trimmed . '"'; + } + return $trimmed; + }, + $font_families + ); + + if ( count( $wrapped_font_families ) === 1 ) { + $font_family = $wrapped_font_families[0]; + } else { + $font_family = implode( ', ', $wrapped_font_families ); } return $font_family; @@ -128,7 +134,7 @@ function ( $elem ) { $slug_elements ); - return join( ';', $slug_elements ); + return sanitize_text_field( join( ';', $slug_elements ) ); } /** diff --git a/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php index efecd6c6821c3..22a843e7e69ed 100644 --- a/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php +++ b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php @@ -187,20 +187,32 @@ public function validate_create_font_face_settings( $value, $request ) { } } - $srcs = is_array( $settings['src'] ) ? $settings['src'] : array( $settings['src'] ); + $srcs = is_array( $settings['src'] ) ? $settings['src'] : array( $settings['src'] ); + $files = $request->get_file_params(); - // 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 ) - ); + foreach ( $srcs as $src ) { + // Check that each src is a non-empty string. + $src = ltrim( $src ); + if ( empty( $src ) ) { + return new WP_Error( + 'rest_invalid_param', + __( 'font_face_settings[src] values must be non-empty strings.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } + + // Check that srcs are valid URLs or file references. + if ( false === wp_http_validate_url( $src ) && ! isset( $files[ $src ] ) ) { + return new WP_Error( + 'rest_invalid_param', + /* translators: %s: src value in the font face settings. */ + sprintf( __( 'font_face_settings[src] value "%s" must be a valid URL or file reference.', 'gutenberg' ), $src ), + 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( @@ -227,9 +239,12 @@ public function validate_create_font_face_settings( $value, $request ) { 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 ); + $schema = $this->get_item_schema()['properties']['font_face_settings']['properties']; - if ( isset( $settings['fontFamily'] ) ) { - $settings['fontFamily'] = WP_Font_Utils::format_font_family( $settings['fontFamily'] ); + // Sanitize settings based on callbacks in the schema. + foreach ( $settings as $key => $value ) { + $sanitize_callback = $schema[ $key ]['arg_options']['sanitize_callback']; + $settings[ $key ] = call_user_func( $sanitize_callback, $value ); } return $settings; @@ -509,11 +524,17 @@ public function get_item_schema() { 'description' => __( 'CSS font-family value.', 'gutenberg' ), 'type' => 'string', 'default' => '', + 'arg_options' => array( + 'sanitize_callback' => array( 'WP_Font_Utils', 'sanitize_font_family' ), + ), ), 'fontStyle' => array( 'description' => __( 'CSS font-style value.', 'gutenberg' ), 'type' => 'string', 'default' => 'normal', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'fontWeight' => array( 'description' => __( 'List of available font weights, separated by a space.', 'gutenberg' ), @@ -521,6 +542,9 @@ public function get_item_schema() { // 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' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'fontDisplay' => array( 'description' => __( 'CSS font-display value.', 'gutenberg' ), @@ -533,10 +557,14 @@ public function get_item_schema() { 'swap', 'optional', ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'src' => array( 'description' => __( 'Paths or URLs to the font files.', 'gutenberg' ), - // Changed from `oneOf` to `anyOf` due to rest_sanitize_array converting a string into an array. + // Changed from `oneOf` to `anyOf` due to rest_sanitize_array converting a string into an array, + // and causing a "matches more than one of the expected formats" error. 'anyOf' => array( array( 'type' => 'string', @@ -549,46 +577,83 @@ public function get_item_schema() { ), ), 'default' => array(), + 'arg_options' => array( + 'sanitize_callback' => function ( $value ) { + return is_array( $value ) ? array_map( array( $this, 'sanitize_src' ), $value ) : $this->sanitize_src( $value ); + }, + ), ), 'fontStretch' => array( 'description' => __( 'CSS font-stretch value.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'ascentOverride' => array( 'description' => __( 'CSS ascent-override value.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'descentOverride' => array( 'description' => __( 'CSS descent-override value.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'fontVariant' => array( 'description' => __( 'CSS font-variant value.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'fontFeatureSettings' => array( 'description' => __( 'CSS font-feature-settings value.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'fontVariationSettings' => array( 'description' => __( 'CSS font-variation-settings value.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'lineGapOverride' => array( 'description' => __( 'CSS line-gap-override value.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'sizeAdjust' => array( 'description' => __( 'CSS size-adjust value.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'unicodeRange' => array( 'description' => __( 'CSS unicode-range value.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'preview' => array( 'description' => __( 'URL to a preview image of the font face.', 'gutenberg' ), 'type' => 'string', + 'format' => 'uri', + 'default' => '', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_url', + ), ), ), 'required' => array( 'fontFamily', 'src' ), @@ -602,6 +667,26 @@ public function get_item_schema() { return $this->add_additional_fields_schema( $this->schema ); } + /** + * Retrieves the item's schema for display / public consumption purposes. + * + * @since 6.5.0 + * + * @return array Public item schema data. + */ + public function get_public_item_schema() { + + $schema = parent::get_public_item_schema(); + + // Also remove `arg_options' from child font_family_settings properties, since the parent + // controller only handles the top level properties. + foreach ( $schema['properties']['font_face_settings']['properties'] as &$property ) { + unset( $property['arg_options'] ); + } + + return $schema; + } + /** * Retrieves the query params for the font face collection. * @@ -739,6 +824,20 @@ protected function prepare_item_for_database( $request ) { return $prepared_post; } + /** + * Sanitizes a single src value for a font face. + * + * @since 6.5.0 + * + * @param string $value Font face src that is a URL or the key for a $_FILES array item. + * + * @return string Sanitized value. + */ + protected function sanitize_src( $value ) { + $value = ltrim( $value ); + return false === wp_http_validate_url( $value ) ? (string) $value : sanitize_url( $value ); + } + /** * Handles the upload of a font file using wp_handle_upload(). * diff --git a/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-families-controller.php b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-families-controller.php index 7586fe0209329..e4a2b2f8e9781 100644 --- a/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-families-controller.php +++ b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-families-controller.php @@ -141,15 +141,14 @@ public function validate_font_family_settings( $value, $request ) { * @return array Decoded array font family settings. */ public function sanitize_font_family_settings( $value ) { + // Settings arrive as stringified JSON, since this is a multipart/form-data request. $settings = json_decode( $value, true ); + $schema = $this->get_item_schema()['properties']['font_family_settings']['properties']; - if ( isset( $settings['fontFamily'] ) ) { - $settings['fontFamily'] = WP_Font_Utils::format_font_family( $settings['fontFamily'] ); - } - - // Provide default for preview, if not provided. - if ( ! isset( $settings['preview'] ) ) { - $settings['preview'] = ''; + // Sanitize settings based on callbacks in the schema. + foreach ( $settings as $key => $value ) { + $sanitize_callback = $schema[ $key ]['arg_options']['sanitize_callback']; + $settings[ $key ] = call_user_func( $sanitize_callback, $value ); } return $settings; @@ -307,25 +306,39 @@ public function get_item_schema() { // Font family settings come directly from theme.json schema // See https://schemas.wp.org/trunk/theme.json 'font_family_settings' => array( - 'description' => __( 'font-face declaration in theme.json format.', 'gutenberg' ), + 'description' => __( 'font-face definition in theme.json format.', 'gutenberg' ), 'type' => 'object', 'context' => array( 'view', 'edit', 'embed' ), 'properties' => array( 'name' => array( - 'description' => 'Name of the font family preset, translatable.', + 'description' => __( 'Name of the font family preset, translatable.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ), 'slug' => array( - 'description' => 'Kebab-case unique identifier for the font family preset.', + 'description' => __( 'Kebab-case unique identifier for the font family preset.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), ), 'fontFamily' => array( - 'description' => 'CSS font-family value.', + 'description' => __( 'CSS font-family value.', 'gutenberg' ), 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => array( 'WP_Font_Utils', 'sanitize_font_family' ), + ), ), 'preview' => array( - 'description' => 'URL to a preview image of the font family.', + 'description' => __( 'URL to a preview image of the font family.', 'gutenberg' ), 'type' => 'string', + 'format' => 'uri', + 'default' => '', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_url', + ), ), ), 'required' => array( 'name', 'slug', 'fontFamily' ), @@ -339,6 +352,26 @@ public function get_item_schema() { return $this->add_additional_fields_schema( $this->schema ); } + /** + * Retrieves the item's schema for display / public consumption purposes. + * + * @since 6.5.0 + * + * @return array Public item schema data. + */ + public function get_public_item_schema() { + + $schema = parent::get_public_item_schema(); + + // Also remove `arg_options' from child font_family_settings properties, since the parent + // controller only handles the top level properties. + foreach ( $schema['properties']['font_family_settings']['properties'] as &$property ) { + unset( $property['arg_options'] ); + } + + return $schema; + } + /** * Retrieves the query params for the font family collection. * diff --git a/phpunit/tests/fonts/font-library/wpFontUtils/formatFontFamily.php b/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFontFamily.php similarity index 65% rename from phpunit/tests/fonts/font-library/wpFontUtils/formatFontFamily.php rename to phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFontFamily.php index f1acf5422a8b4..71511331c65dc 100644 --- a/phpunit/tests/fonts/font-library/wpFontUtils/formatFontFamily.php +++ b/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFontFamily.php @@ -1,6 +1,6 @@ assertSame( $expected, - WP_Font_Utils::format_font_family( + WP_Font_Utils::sanitize_font_family( $font_family ) ); @@ -32,7 +32,7 @@ public function test_should_format_font_family( $font_family, $expected ) { * * @return array */ - public function data_should_format_font_family() { + public function data_should_sanitize_font_family() { return array( 'data_families_with_spaces_and_numbers' => array( 'font_family' => 'Rock 3D , Open Sans,serif', @@ -54,6 +54,10 @@ public function data_should_format_font_family() { 'font_family' => ' ', 'expected' => '', ), + 'data_font_family_with_whitespace_tags_new_lines' => array( + 'font_family' => " Rock 3D\n ", + 'expected' => '"Rock 3D"', + ), ); } } diff --git a/phpunit/tests/fonts/font-library/wpRestFontFacesController.php b/phpunit/tests/fonts/font-library/wpRestFontFacesController.php index 3067e485822c8..273862e3047e8 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontFacesController.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFacesController.php @@ -711,12 +711,13 @@ public function test_create_item_invalid_file_src() { $files = $this->setup_font_file_upload( array( 'woff2' ) ); wp_set_current_user( self::$admin_id ); + $src = 'invalid'; $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' ) ) + array_merge( self::$default_settings, array( 'src' => $src ) ) ) ); $request->set_file_params( $files ); @@ -724,30 +725,57 @@ public function test_create_item_invalid_file_src() { $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400, 'The response should return an error for "rest_invalid_param" with 400 status.' ); - $expected_message = 'File ' . array_keys( $files )[0] . ' must be used in font_face_settings[src].'; + $expected_message = 'font_face_settings[src] value "' . $src . '" must be a valid URL or file reference.'; $message = $response->as_error()->get_all_error_data()[0]['params']['font_face_settings']; $this->assertSame( $expected_message, $message, 'The response error message should match.' ); } /** - * @dataProvider data_create_item_sanitize_font_family + * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_settings + */ + public function test_create_item_missing_file_src() { + $files = $this->setup_font_file_upload( array( 'woff2', 'woff' ) ); + + 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( array_keys( $files )[0] ) ) ) + ) + ); + $request->set_file_params( $files ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400, 'The response should return an error for "rest_invalid_param" with 400 status.' ); + $expected_message = 'File ' . array_keys( $files )[1] . ' 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, 'The response error message should match.' ); + } + + /** + * @dataProvider data_sanitize_font_face_settings * * @covers WP_REST_Font_Face_Controller::sanitize_font_face_settings * - * @param string $font_family_setting Setting to test. - * @param string $expected Expected result. + * @param string $settings Settings to test. + * @param string $expected Expected settings result. */ - public function test_create_item_sanitize_font_family( $font_family_setting, $expected ) { - $settings = array_merge( self::$default_settings, array( 'fontFamily' => $font_family_setting ) ); + public function test_create_item_sanitize_font_face_settings( $settings, $expected ) { + $settings = array_merge( self::$default_settings, $settings ); + $expected = array_merge( self::$default_settings, $expected ); 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(); + wp_delete_post( $data['id'], true ); $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); - $this->assertSame( $expected, $data['font_face_settings']['fontFamily'], 'The response fontFamily should match.' ); + $this->assertSame( $expected, $data['font_face_settings'], 'The response font_face_settings should match.' ); } /** @@ -755,19 +783,65 @@ public function test_create_item_sanitize_font_family( $font_family_setting, $ex * * @return array */ - public function data_create_item_sanitize_font_family() { + public function data_sanitize_font_face_settings() { return array( - 'multiword font with integer' => array( - 'font_family_setting' => 'Libre Barcode 128 Text', - 'expected' => '"Libre Barcode 128 Text"', + 'settings with tags, extra whitespace, new lines' => array( + 'settings' => array( + 'fontFamily' => " Open Sans\n ", + 'fontStyle' => " oblique 20deg 50deg\n ", + 'fontWeight' => " 200\n ", + 'src' => " https://example.com/ ", + 'fontStretch' => " expanded\n ", + 'ascentOverride' => " 70%\n ", + 'descentOverride' => " 30%\n ", + 'fontVariant' => " normal\n ", + 'fontFeatureSettings' => " \"swsh\" 2\n ", + 'fontVariationSettings' => " \"xhgt\" 0.7\n ", + 'lineGapOverride' => " 10%\n ", + 'sizeAdjust' => " 90%\n ", + 'unicodeRange' => " U+0025-00FF, U+4??\n ", + 'preview' => " https://example.com/ ", + ), + 'expected' => array( + 'fontFamily' => '"Open Sans"', + 'fontStyle' => 'oblique 20deg 50deg', + 'fontWeight' => '200', + 'src' => 'https://example.com//stylescriptalert(\'XSS\');/script%20%20%20%20%20%20', + 'fontStretch' => 'expanded', + 'ascentOverride' => '70%', + 'descentOverride' => '30%', + 'fontVariant' => 'normal', + 'fontFeatureSettings' => '"swsh" 2', + 'fontVariationSettings' => '"xhgt" 0.7', + 'lineGapOverride' => '10%', + 'sizeAdjust' => '90%', + 'unicodeRange' => 'U+0025-00FF, U+4??', + 'preview' => 'https://example.com//stylescriptalert(\'XSS\');/script%20%20%20%20%20%20', + ), + ), + 'multiword font family name with integer' => array( + 'settings' => array( + 'fontFamily' => 'Libre Barcode 128 Text', + ), + 'expected' => array( + 'fontFamily' => '"Libre Barcode 128 Text"', + ), ), - 'multiword font' => array( - 'font_family_setting' => 'B612 Mono', - 'expected' => '"B612 Mono"', + 'multiword font family name' => array( + 'settings' => array( + 'fontFamily' => 'B612 Mono', + ), + 'expected' => array( + 'fontFamily' => '"B612 Mono"', + ), ), - 'comma-separated fonts' => array( - 'font_family_setting' => 'Open Sans, Noto Sans, sans-serif', - 'expected' => '"Open Sans", "Noto Sans", sans-serif', + 'comma-separated font family names' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans, Noto Sans, sans-serif', + ), + 'expected' => array( + 'fontFamily' => '"Open Sans", "Noto Sans", sans-serif', + ), ), ); } @@ -905,6 +979,40 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'font_face_settings', $properties, 'The id property should exist in the schema::properties data.' ); } + /** + * @covers WP_REST_Font_Faces_Controller::get_item_schema + */ + public function test_get_item_schema_font_face_settings_should_all_have_sanitize_callbacks() { + $schema = ( new WP_REST_Font_Faces_Controller( 'wp_font_face' ) )->get_item_schema(); + $font_face_settings_schema = $schema['properties']['font_face_settings']; + + $this->assertArrayHasKey( 'properties', $font_face_settings_schema, 'font_face_settings schema is missing properties.' ); + $this->assertIsArray( $font_face_settings_schema['properties'], 'font_face_settings properties should be an array.' ); + + // arg_options should be removed for each setting property. + foreach ( $font_face_settings_schema['properties'] as $property ) { + $this->assertArrayHasKey( 'arg_options', $property, 'Setting schema should have arg_options.' ); + $this->assertArrayHasKey( 'sanitize_callback', $property['arg_options'], 'Setting schema should have a sanitize_callback.' ); + $this->assertIsCallable( $property['arg_options']['sanitize_callback'], 'The sanitize_callback value should be callable.' ); + } + } + + /** + * @covers WP_REST_Font_Faces_Controller::get_public_item_schema + */ + public function test_get_public_item_schema_should_not_have_arg_options() { + $schema = ( new WP_REST_Font_Faces_Controller( 'wp_font_face' ) )->get_public_item_schema(); + $font_face_settings_schema = $schema['properties']['font_face_settings']; + + $this->assertArrayHasKey( 'properties', $font_face_settings_schema, 'font_face_settings schema is missing properties.' ); + $this->assertIsArray( $font_face_settings_schema['properties'], 'font_face_settings properties should be an array.' ); + + // arg_options should be removed for each setting property. + foreach ( $font_face_settings_schema['properties'] as $property ) { + $this->assertArrayNotHasKey( 'arg_options', $property, 'arg_options should be removed from the schema for each setting.' ); + } + } + protected function check_font_face_data( $data, $post_id, $links ) { self::$post_ids_for_cleanup[] = $post_id; $post = get_post( $post_id ); diff --git a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php index 6468eff7e24a4..94ad5eccd7e57 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php @@ -124,7 +124,7 @@ public static function create_font_family_post( $settings = array() ) { } /** - * @covers WP_REST_Font_Faces_Controller::register_routes + * @covers WP_REST_Font_Families_Controller::register_routes */ public function test_register_routes() { $routes = rest_get_server()->get_routes(); @@ -209,7 +209,7 @@ public function data_get_context_param() { } /** - * @covers WP_REST_Font_Faces_Controller::get_items + * @covers WP_REST_Font_Families_Controller::get_items */ public function test_get_items() { wp_set_current_user( self::$admin_id ); @@ -226,7 +226,7 @@ public function test_get_items() { } /** - * @covers WP_REST_Font_Faces_Controller::get_items + * @covers WP_REST_Font_Families_Controller::get_items */ public function test_get_items_by_slug() { $font_family = get_post( self::$font_family_id2 ); @@ -244,7 +244,7 @@ public function test_get_items_by_slug() { } /** - * @covers WP_REST_Font_Faces_Controller::get_items + * @covers WP_REST_Font_Families_Controller::get_items */ public function test_get_items_no_permission() { wp_set_current_user( 0 ); @@ -259,7 +259,7 @@ public function test_get_items_no_permission() { } /** - * @covers WP_REST_Font_Faces_Controller::get_item + * @covers WP_REST_Font_Families_Controller::get_item */ public function test_get_item() { wp_set_current_user( self::$admin_id ); @@ -272,7 +272,7 @@ public function test_get_item() { } /** - * @covers WP_REST_Font_Faces_Controller::prepare_item_for_response + * @covers WP_REST_Font_Families_Controller::prepare_item_for_response */ public function test_get_item_embedded_font_faces() { wp_set_current_user( self::$admin_id ); @@ -344,7 +344,7 @@ public function test_get_item_malformed_post_content_returns_empty_settings() { } /** - * @covers WP_REST_Font_Faces_Controller::get_item + * @covers WP_REST_Font_Families_Controller::get_item */ public function test_get_item_invalid_font_family_id() { wp_set_current_user( self::$admin_id ); @@ -354,7 +354,7 @@ public function test_get_item_invalid_font_family_id() { } /** - * @covers WP_REST_Font_Faces_Controller::get_item + * @covers WP_REST_Font_Families_Controller::get_item */ public function test_get_item_no_permission() { wp_set_current_user( 0 ); @@ -369,7 +369,7 @@ public function test_get_item_no_permission() { } /** - * @covers WP_REST_Font_Faces_Controller::create_item + * @covers WP_REST_Font_Families_Controller::create_item */ public function test_create_item() { $settings = array_merge( self::$default_settings, array( 'slug' => 'open-sans-2' ) ); @@ -390,7 +390,7 @@ public function test_create_item() { } /** - * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_request + * @covers WP_REST_Font_Families_Controller::validate_create_font_face_request */ public function test_create_item_default_theme_json_version() { $settings = array_merge( self::$default_settings, array( 'slug' => 'open-sans-2' ) ); @@ -411,7 +411,7 @@ public function test_create_item_default_theme_json_version() { /** * @dataProvider data_create_item_invalid_theme_json_version * - * @covers WP_REST_Font_Faces_Controller::create_item + * @covers WP_REST_Font_Families_Controller::create_item * * @param int $theme_json_version Version to test. */ @@ -440,7 +440,7 @@ public function data_create_item_invalid_theme_json_version() { /** * @dataProvider data_create_item_with_default_preview * - * @covers WP_REST_Font_Faces_Controller::sanitize_font_family_settings + * @covers WP_REST_Font_Families_Controller::sanitize_font_family_settings * * @param array $settings Settings to test. */ @@ -481,10 +481,88 @@ public function data_create_item_with_default_preview() { ); } + /** + * @dataProvider data_sanitize_font_family_settings + * + * @covers WP_REST_Font_Families_Controller::sanitize_font_family_settings + * + * @param string $settings Font family settings to test. + * @param string $expected Expected settings result. + */ + public function test_create_item_santize_font_family_settings( $settings, $expected ) { + $settings = array_merge( self::$default_settings, $settings ); + $expected = array_merge( self::$default_settings, $expected ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + static::$post_ids_to_cleanup[] = $data['id']; + + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); + $this->assertSame( $expected, $data['font_family_settings'], 'The response font_family_settings should match.' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_sanitize_font_family_settings() { + return array( + 'settings with tags, extra whitespace, new lines' => array( + 'settings' => array( + 'name' => " Opening Sans\n ", + 'slug' => " OPENing SanS \n ", + 'fontFamily' => " Opening Sans\n ", + 'preview' => " https://example.com/ ", + ), + 'expected' => array( + 'name' => 'Opening Sans', + 'slug' => 'opening-sans-alertxss', + 'fontFamily' => '"Opening Sans"', + 'preview' => "https://example.com//stylescriptalert('XSS');/script%20%20%20%20%20%20", + ), + ), + 'multiword font family name with integer' => array( + 'settings' => array( + 'slug' => 'libre-barcode-128-text', + 'fontFamily' => 'Libre Barcode 128 Text', + ), + 'expected' => array( + 'slug' => 'libre-barcode-128-text', + 'fontFamily' => '"Libre Barcode 128 Text"', + ), + ), + 'multiword font family name' => array( + 'settings' => array( + 'slug' => 'b612-mono', + 'fontFamily' => 'B612 Mono', + ), + 'expected' => array( + 'slug' => 'b612-mono', + 'fontFamily' => '"B612 Mono"', + ), + ), + 'comma-separated font family names' => array( + 'settings' => array( + 'slug' => 'open-sans-noto-sans', + 'fontFamily' => 'Open Sans, Noto Sans, sans-serif', + ), + 'expected' => array( + 'slug' => 'open-sans-noto-sans', + 'fontFamily' => '"Open Sans", "Noto Sans", sans-serif', + ), + ), + ); + } + /** * @dataProvider data_create_item_invalid_settings * - * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_settings + * @covers WP_REST_Font_Families_Controller::validate_create_font_face_settings * * @param array $settings Settings to test. */ @@ -570,7 +648,7 @@ public function test_create_item_with_duplicate_slug() { } /** - * @covers WP_REST_Font_Faces_Controller::create_item + * @covers WP_REST_Font_Families_Controller::create_item */ public function test_create_item_no_permission() { $settings = array_merge( self::$default_settings, array( 'slug' => 'open-sans-2' ) ); @@ -669,52 +747,36 @@ public function data_update_item_individual_settings() { } /** - * @dataProvider data_update_item_santize_font_family + * @dataProvider data_sanitize_font_family_settings * - * @covers WP_REST_Font_Families_Controller::sanitize_font_face_settings + * @covers WP_REST_Font_Families_Controller::sanitize_font_family_settings * - * @param string $font_family_setting Font family setting to test. - * @param string $expected Expected result. + * @param string $settings Font family settings to test. + * @param string $expected Expected settings result. */ - public function test_update_item_santize_font_family( $font_family_setting, $expected ) { + public function test_update_item_santize_font_family_settings( $settings, $expected ) { + // Unset/modify slug from the data provider, since we're updating rather than creating. + unset( $settings['slug'] ); + $initial_settings = array( 'slug' => 'open-sans-update' ); + $expected = array_merge( self::$default_settings, $expected, $initial_settings ); + wp_set_current_user( self::$admin_id ); + $font_family_id = self::create_font_family_post( $initial_settings ); + static::$post_ids_to_cleanup[] = $font_family_id; - $font_family_id = self::create_font_family_post(); - $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . $font_family_id ); - $request->set_param( 'font_family_settings', wp_json_encode( array( 'fontFamily' => $font_family_setting ) ) ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . $font_family_id ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); - $this->assertSame( $expected, $data['font_family_settings']['fontFamily'], 'The font family should match.' ); - } - - /** - * Data provider. - * - * @return array - */ - public function data_update_item_santize_font_family() { - return array( - 'multiword font with integer' => array( - 'font_family_setting' => 'Libre Barcode 128 Text', - 'expected' => '"Libre Barcode 128 Text"', - ), - 'multiword font' => array( - 'font_family_setting' => 'B612 Mono', - 'expected' => '"B612 Mono"', - ), - 'comma-separated fonts' => array( - 'font_family_setting' => 'Open Sans, Noto Sans, sans-serif', - 'expected' => '"Open Sans", "Noto Sans", sans-serif', - ), - ); + $this->assertSame( $expected, $data['font_family_settings'], 'The response font_family_settings should match.' ); } /** * @dataProvider data_update_item_invalid_settings * - * @covers WP_REST_Font_Faces_Controller::update_item + * @covers WP_REST_Font_Families_Controller::update_item * * @param array $settings Settings to test. */ @@ -752,7 +814,7 @@ public function data_update_item_invalid_settings() { } /** - * @covers WP_REST_Font_Faces_Controller::update_item + * @covers WP_REST_Font_Families_Controller::update_item */ public function test_update_item_update_slug_not_allowed() { wp_set_current_user( self::$admin_id ); @@ -770,7 +832,7 @@ public function test_update_item_update_slug_not_allowed() { } /** - * @covers WP_REST_Font_Faces_Controller::update_item + * @covers WP_REST_Font_Families_Controller::update_item */ public function test_update_item_invalid_font_family_id() { $settings = array_diff_key( self::$default_settings, array( 'slug' => '' ) ); @@ -783,7 +845,7 @@ public function test_update_item_invalid_font_family_id() { } /** - * @covers WP_REST_Font_Faces_Controller::update_item + * @covers WP_REST_Font_Families_Controller::update_item */ public function test_update_item_no_permission() { $settings = array_diff_key( self::$default_settings, array( 'slug' => '' ) ); @@ -803,7 +865,7 @@ public function test_update_item_no_permission() { /** - * @covers WP_REST_Font_Faces_Controller::delete_item + * @covers WP_REST_Font_Families_Controller::delete_item */ public function test_delete_item() { wp_set_current_user( self::$admin_id ); @@ -817,7 +879,7 @@ public function test_delete_item() { } /** - * @covers WP_REST_Font_Faces_Controller::delete_item + * @covers WP_REST_Font_Families_Controller::delete_item */ public function test_delete_item_no_trash() { wp_set_current_user( self::$admin_id ); @@ -838,7 +900,7 @@ public function test_delete_item_no_trash() { } /** - * @covers WP_REST_Font_Faces_Controller::delete_item + * @covers WP_REST_Font_Families_Controller::delete_item */ public function test_delete_item_invalid_font_family_id() { wp_set_current_user( self::$admin_id ); @@ -848,7 +910,7 @@ public function test_delete_item_invalid_font_family_id() { } /** - * @covers WP_REST_Font_Faces_Controller::delete_item + * @covers WP_REST_Font_Families_Controller::delete_item */ public function test_delete_item_no_permissions() { $font_family_id = self::create_font_family_post(); @@ -865,7 +927,7 @@ public function test_delete_item_no_permissions() { } /** - * @covers WP_REST_Font_Faces_Controller::prepare_item_for_response + * @covers WP_REST_Font_Families_Controller::prepare_item_for_response */ public function test_prepare_item() { wp_set_current_user( self::$admin_id ); @@ -878,7 +940,7 @@ public function test_prepare_item() { } /** - * @covers WP_REST_Font_Faces_Controller::get_item_schema + * @covers WP_REST_Font_Families_Controller::get_item_schema */ public function test_get_item_schema() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/font-families' ); @@ -894,6 +956,40 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'font_family_settings', $properties, 'The font_family_settings property should exist in the schema::properties data.' ); } + /** + * @covers WP_REST_Font_Families_Controller::get_item_schema + */ + public function test_get_item_schema_font_family_settings_should_all_have_sanitize_callbacks() { + $schema = ( new WP_REST_Font_Families_Controller( 'wp_font_family' ) )->get_item_schema(); + $font_family_settings_schema = $schema['properties']['font_family_settings']; + + $this->assertArrayHasKey( 'properties', $font_family_settings_schema, 'font_family_settings schema is missing properties.' ); + $this->assertIsArray( $font_family_settings_schema['properties'], 'font_family_settings properties should be an array.' ); + + // arg_options should be removed for each setting property. + foreach ( $font_family_settings_schema['properties'] as $property ) { + $this->assertArrayHasKey( 'arg_options', $property, 'Setting schema should have arg_options.' ); + $this->assertArrayHasKey( 'sanitize_callback', $property['arg_options'], 'Setting schema should have a sanitize_callback.' ); + $this->assertIsCallable( $property['arg_options']['sanitize_callback'], 'That sanitize_callback value should be callable.' ); + } + } + + /** + * @covers WP_REST_Font_Families_Controller::get_public_item_schema + */ + public function test_get_public_item_schema_should_not_have_arg_options() { + $schema = ( new WP_REST_Font_Families_Controller( 'wp_font_family' ) )->get_public_item_schema(); + $font_family_settings_schema = $schema['properties']['font_family_settings']; + + $this->assertArrayHasKey( 'properties', $font_family_settings_schema, 'font_family_settings schema is missing properties.' ); + $this->assertIsArray( $font_family_settings_schema['properties'], 'font_family_settings properties should be an array.' ); + + // arg_options should be removed for each setting property. + foreach ( $font_family_settings_schema['properties'] as $property ) { + $this->assertArrayNotHasKey( 'arg_options', $property, 'arg_options should be removed from the schema for each setting.' ); + } + } + protected function check_font_family_data( $data, $post_id, $links ) { static::$post_ids_to_cleanup[] = $post_id; $post = get_post( $post_id );