From 201d1cf2cfaf069b85af9727c4dfb4cf9be5e504 Mon Sep 17 00:00:00 2001 From: Sandeep Dahiya Date: Sun, 15 Jun 2025 15:54:03 +0530 Subject: [PATCH 01/18] fix:build-font-face-css single inverted comma in name issue --- src/wp-includes/fonts/class-wp-font-face.php | 27 +++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-face.php b/src/wp-includes/fonts/class-wp-font-face.php index 07cd3d6de9002..cdeac73ef8537 100644 --- a/src/wp-includes/fonts/class-wp-font-face.php +++ b/src/wp-includes/fonts/class-wp-font-face.php @@ -346,6 +346,25 @@ private function order_src( array $font_face ) { return $font_face; } + /** + * Wraps font-family in quotes if needed. + * + * @since 6.x.x + * + * @param string $item Font-family name. + * @return string Quoted font-family if needed. + */ + + private function maybe_add_quotes( $item ) { + $regex = '/^(?!generic\([a-zA-Z\-]+\)$)(?!^[a-zA-Z\-]+$).+/'; + $item = trim( $item ); + if ( preg_match( $regex, $item ) ) { + $item = trim( $item, "\"'" ); + return '"' . $item . '"'; + } + return $item; + } + /** * Builds the font-family's CSS. @@ -362,13 +381,7 @@ private function build_font_face_css( array $font_face ) { * Wrap font-family in quotes if it contains spaces * and is not already wrapped in quotes. */ - if ( - str_contains( $font_face['font-family'], ' ' ) && - ! str_contains( $font_face['font-family'], '"' ) && - ! str_contains( $font_face['font-family'], "'" ) - ) { - $font_face['font-family'] = '"' . $font_face['font-family'] . '"'; - } + $font_face['font-family'] = self::maybe_add_quotes( $font_face['font-family'] ); foreach ( $font_face as $key => $value ) { // Compile the "src" parameter. From 08ad65bf20252589525599709fd975b7abe29a10 Mon Sep 17 00:00:00 2001 From: Sandeep Dahiya Date: Sun, 15 Jun 2025 17:30:47 +0530 Subject: [PATCH 02/18] removed whitespace from the end --- src/wp-includes/fonts/class-wp-font-face.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-face.php b/src/wp-includes/fonts/class-wp-font-face.php index cdeac73ef8537..68b8159164902 100644 --- a/src/wp-includes/fonts/class-wp-font-face.php +++ b/src/wp-includes/fonts/class-wp-font-face.php @@ -354,7 +354,6 @@ private function order_src( array $font_face ) { * @param string $item Font-family name. * @return string Quoted font-family if needed. */ - private function maybe_add_quotes( $item ) { $regex = '/^(?!generic\([a-zA-Z\-]+\)$)(?!^[a-zA-Z\-]+$).+/'; $item = trim( $item ); @@ -365,7 +364,6 @@ private function maybe_add_quotes( $item ) { return $item; } - /** * Builds the font-family's CSS. * From f09ea757565105b3734ae187bf2c43561f2de635 Mon Sep 17 00:00:00 2001 From: Sandeep Dahiya Date: Mon, 16 Jun 2025 12:14:25 +0530 Subject: [PATCH 03/18] using WP_Font_Utils::maybe_add_quotes() after exposing it public --- src/wp-includes/fonts/class-wp-font-face.php | 21 ++----------------- src/wp-includes/fonts/class-wp-font-utils.php | 2 +- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-face.php b/src/wp-includes/fonts/class-wp-font-face.php index 68b8159164902..e7aa8b9d1e538 100644 --- a/src/wp-includes/fonts/class-wp-font-face.php +++ b/src/wp-includes/fonts/class-wp-font-face.php @@ -346,24 +346,7 @@ private function order_src( array $font_face ) { return $font_face; } - /** - * Wraps font-family in quotes if needed. - * - * @since 6.x.x - * - * @param string $item Font-family name. - * @return string Quoted font-family if needed. - */ - private function maybe_add_quotes( $item ) { - $regex = '/^(?!generic\([a-zA-Z\-]+\)$)(?!^[a-zA-Z\-]+$).+/'; - $item = trim( $item ); - if ( preg_match( $regex, $item ) ) { - $item = trim( $item, "\"'" ); - return '"' . $item . '"'; - } - return $item; - } - + /** * Builds the font-family's CSS. * @@ -379,7 +362,7 @@ private function build_font_face_css( array $font_face ) { * Wrap font-family in quotes if it contains spaces * and is not already wrapped in quotes. */ - $font_face['font-family'] = self::maybe_add_quotes( $font_face['font-family'] ); + $font_face['font-family'] = WP_Font_Utils::maybe_add_quotes( $font_face['font-family'] ); foreach ( $font_face as $key => $value ) { // Compile the "src" parameter. diff --git a/src/wp-includes/fonts/class-wp-font-utils.php b/src/wp-includes/fonts/class-wp-font-utils.php index 0ec36abc3f64b..c783bebfe6bc9 100644 --- a/src/wp-includes/fonts/class-wp-font-utils.php +++ b/src/wp-includes/fonts/class-wp-font-utils.php @@ -29,7 +29,7 @@ class WP_Font_Utils { * @param string $item A font family name. * @return string The font family name with surrounding quotes, if necessary. */ - private static function maybe_add_quotes( $item ) { + public static function maybe_add_quotes( $item ) { // Matches strings that are not exclusively alphabetic characters or hyphens, and do not exactly follow the pattern generic(alphabetic characters or hyphens). $regex = '/^(?!generic\([a-zA-Z\-]+\)$)(?!^[a-zA-Z\-]+$).+/'; $item = trim( $item ); From 9f2953761ad7ed85284a46acc13c266258799a0d Mon Sep 17 00:00:00 2001 From: Sandeep Dahiya Date: Tue, 17 Jun 2025 09:18:09 +0530 Subject: [PATCH 04/18] renamed to normalize_quoted_font_family_name and added more edge cases --- src/wp-includes/fonts/class-wp-font-face.php | 2 +- src/wp-includes/fonts/class-wp-font-utils.php | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-face.php b/src/wp-includes/fonts/class-wp-font-face.php index e7aa8b9d1e538..e62a76e853d0e 100644 --- a/src/wp-includes/fonts/class-wp-font-face.php +++ b/src/wp-includes/fonts/class-wp-font-face.php @@ -362,7 +362,7 @@ private function build_font_face_css( array $font_face ) { * Wrap font-family in quotes if it contains spaces * and is not already wrapped in quotes. */ - $font_face['font-family'] = WP_Font_Utils::maybe_add_quotes( $font_face['font-family'] ); + $font_face['font-family'] = WP_Font_Utils::normalize_quoted_font_family_name( $font_face['font-family'] ); foreach ( $font_face as $key => $value ) { // Compile the "src" parameter. diff --git a/src/wp-includes/fonts/class-wp-font-utils.php b/src/wp-includes/fonts/class-wp-font-utils.php index c783bebfe6bc9..93c842d9d976b 100644 --- a/src/wp-includes/fonts/class-wp-font-utils.php +++ b/src/wp-includes/fonts/class-wp-font-utils.php @@ -29,12 +29,15 @@ class WP_Font_Utils { * @param string $item A font family name. * @return string The font family name with surrounding quotes, if necessary. */ - public static function maybe_add_quotes( $item ) { + public static function normalize_quoted_font_family_name( $item ) { // Matches strings that are not exclusively alphabetic characters or hyphens, and do not exactly follow the pattern generic(alphabetic characters or hyphens). $regex = '/^(?!generic\([a-zA-Z\-]+\)$)(?!^[a-zA-Z\-]+$).+/'; $item = trim( $item ); if ( preg_match( $regex, $item ) ) { $item = trim( $item, "\"'" ); + $item = str_replace( [ "\r\n", "\r", "\n" ], '\\A', $item ); + $item = str_replace( '\\', '\\\\', $item ); + $item = str_replace( '"', '\\"', $item ); return '"' . $item . '"'; } return $item; @@ -67,14 +70,14 @@ public static function sanitize_font_family( $font_family ) { if ( str_contains( $output, ',' ) ) { $items = explode( ',', $output ); foreach ( $items as $item ) { - $formatted_item = self::maybe_add_quotes( $item ); + $formatted_item = self::normalize_quoted_font_family_name( $item ); if ( ! empty( $formatted_item ) ) { $formatted_items[] = $formatted_item; } } return implode( ', ', $formatted_items ); } - return self::maybe_add_quotes( $output ); + return self::normalize_quoted_font_family_name( $output ); } /** From b88e32c6cfd83189e9d948da5e6469834873741d Mon Sep 17 00:00:00 2001 From: Sandeep Dahiya Date: Tue, 17 Jun 2025 14:24:27 +0530 Subject: [PATCH 05/18] "Reverting to previous minor fix" This reverts commit 9f2953761ad7ed85284a46acc13c266258799a0d. --- src/wp-includes/fonts/class-wp-font-face.php | 2 +- src/wp-includes/fonts/class-wp-font-utils.php | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-face.php b/src/wp-includes/fonts/class-wp-font-face.php index e62a76e853d0e..e7aa8b9d1e538 100644 --- a/src/wp-includes/fonts/class-wp-font-face.php +++ b/src/wp-includes/fonts/class-wp-font-face.php @@ -362,7 +362,7 @@ private function build_font_face_css( array $font_face ) { * Wrap font-family in quotes if it contains spaces * and is not already wrapped in quotes. */ - $font_face['font-family'] = WP_Font_Utils::normalize_quoted_font_family_name( $font_face['font-family'] ); + $font_face['font-family'] = WP_Font_Utils::maybe_add_quotes( $font_face['font-family'] ); foreach ( $font_face as $key => $value ) { // Compile the "src" parameter. diff --git a/src/wp-includes/fonts/class-wp-font-utils.php b/src/wp-includes/fonts/class-wp-font-utils.php index 93c842d9d976b..c783bebfe6bc9 100644 --- a/src/wp-includes/fonts/class-wp-font-utils.php +++ b/src/wp-includes/fonts/class-wp-font-utils.php @@ -29,15 +29,12 @@ class WP_Font_Utils { * @param string $item A font family name. * @return string The font family name with surrounding quotes, if necessary. */ - public static function normalize_quoted_font_family_name( $item ) { + public static function maybe_add_quotes( $item ) { // Matches strings that are not exclusively alphabetic characters or hyphens, and do not exactly follow the pattern generic(alphabetic characters or hyphens). $regex = '/^(?!generic\([a-zA-Z\-]+\)$)(?!^[a-zA-Z\-]+$).+/'; $item = trim( $item ); if ( preg_match( $regex, $item ) ) { $item = trim( $item, "\"'" ); - $item = str_replace( [ "\r\n", "\r", "\n" ], '\\A', $item ); - $item = str_replace( '\\', '\\\\', $item ); - $item = str_replace( '"', '\\"', $item ); return '"' . $item . '"'; } return $item; @@ -70,14 +67,14 @@ public static function sanitize_font_family( $font_family ) { if ( str_contains( $output, ',' ) ) { $items = explode( ',', $output ); foreach ( $items as $item ) { - $formatted_item = self::normalize_quoted_font_family_name( $item ); + $formatted_item = self::maybe_add_quotes( $item ); if ( ! empty( $formatted_item ) ) { $formatted_items[] = $formatted_item; } } return implode( ', ', $formatted_items ); } - return self::normalize_quoted_font_family_name( $output ); + return self::maybe_add_quotes( $output ); } /** From 044e2618df4f6d591b9f51d4b5c7798f4b5ad2a9 Mon Sep 17 00:00:00 2001 From: Sandeep Dahiya Date: Tue, 17 Jun 2025 14:47:28 +0530 Subject: [PATCH 06/18] removed whitespace and renamed to normalize_css_font_family_name --- src/wp-includes/fonts/class-wp-font-face.php | 4 ++-- src/wp-includes/fonts/class-wp-font-utils.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-face.php b/src/wp-includes/fonts/class-wp-font-face.php index e7aa8b9d1e538..3dc4ab405e0dd 100644 --- a/src/wp-includes/fonts/class-wp-font-face.php +++ b/src/wp-includes/fonts/class-wp-font-face.php @@ -346,7 +346,7 @@ private function order_src( array $font_face ) { return $font_face; } - + /** * Builds the font-family's CSS. * @@ -362,7 +362,7 @@ private function build_font_face_css( array $font_face ) { * Wrap font-family in quotes if it contains spaces * and is not already wrapped in quotes. */ - $font_face['font-family'] = WP_Font_Utils::maybe_add_quotes( $font_face['font-family'] ); + $font_face['font-family'] = WP_Font_Utils::normalize_css_font_family_name( $font_face['font-family'] ); foreach ( $font_face as $key => $value ) { // Compile the "src" parameter. diff --git a/src/wp-includes/fonts/class-wp-font-utils.php b/src/wp-includes/fonts/class-wp-font-utils.php index c783bebfe6bc9..df75c8983bb2d 100644 --- a/src/wp-includes/fonts/class-wp-font-utils.php +++ b/src/wp-includes/fonts/class-wp-font-utils.php @@ -29,7 +29,7 @@ class WP_Font_Utils { * @param string $item A font family name. * @return string The font family name with surrounding quotes, if necessary. */ - public static function maybe_add_quotes( $item ) { + public static function normalize_css_font_family_name( $item ) { // Matches strings that are not exclusively alphabetic characters or hyphens, and do not exactly follow the pattern generic(alphabetic characters or hyphens). $regex = '/^(?!generic\([a-zA-Z\-]+\)$)(?!^[a-zA-Z\-]+$).+/'; $item = trim( $item ); @@ -67,14 +67,14 @@ public static function sanitize_font_family( $font_family ) { if ( str_contains( $output, ',' ) ) { $items = explode( ',', $output ); foreach ( $items as $item ) { - $formatted_item = self::maybe_add_quotes( $item ); + $formatted_item = self::normalize_css_font_family_name( $item ); if ( ! empty( $formatted_item ) ) { $formatted_items[] = $formatted_item; } } return implode( ', ', $formatted_items ); } - return self::maybe_add_quotes( $output ); + return self::normalize_css_font_family_name( $output ); } /** From 58fffca7fecc55ee2cebeb15da667baf01a023d3 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 18 Sep 2025 17:09:02 +0200 Subject: [PATCH 07/18] Revert normalize font face changes --- src/wp-includes/fonts/class-wp-font-face.php | 8 +++++++- src/wp-includes/fonts/class-wp-font-utils.php | 6 +++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-face.php b/src/wp-includes/fonts/class-wp-font-face.php index 3dc4ab405e0dd..07cd3d6de9002 100644 --- a/src/wp-includes/fonts/class-wp-font-face.php +++ b/src/wp-includes/fonts/class-wp-font-face.php @@ -362,7 +362,13 @@ private function build_font_face_css( array $font_face ) { * Wrap font-family in quotes if it contains spaces * and is not already wrapped in quotes. */ - $font_face['font-family'] = WP_Font_Utils::normalize_css_font_family_name( $font_face['font-family'] ); + if ( + str_contains( $font_face['font-family'], ' ' ) && + ! str_contains( $font_face['font-family'], '"' ) && + ! str_contains( $font_face['font-family'], "'" ) + ) { + $font_face['font-family'] = '"' . $font_face['font-family'] . '"'; + } foreach ( $font_face as $key => $value ) { // Compile the "src" parameter. diff --git a/src/wp-includes/fonts/class-wp-font-utils.php b/src/wp-includes/fonts/class-wp-font-utils.php index df75c8983bb2d..0ec36abc3f64b 100644 --- a/src/wp-includes/fonts/class-wp-font-utils.php +++ b/src/wp-includes/fonts/class-wp-font-utils.php @@ -29,7 +29,7 @@ class WP_Font_Utils { * @param string $item A font family name. * @return string The font family name with surrounding quotes, if necessary. */ - public static function normalize_css_font_family_name( $item ) { + private static function maybe_add_quotes( $item ) { // Matches strings that are not exclusively alphabetic characters or hyphens, and do not exactly follow the pattern generic(alphabetic characters or hyphens). $regex = '/^(?!generic\([a-zA-Z\-]+\)$)(?!^[a-zA-Z\-]+$).+/'; $item = trim( $item ); @@ -67,14 +67,14 @@ public static function sanitize_font_family( $font_family ) { if ( str_contains( $output, ',' ) ) { $items = explode( ',', $output ); foreach ( $items as $item ) { - $formatted_item = self::normalize_css_font_family_name( $item ); + $formatted_item = self::maybe_add_quotes( $item ); if ( ! empty( $formatted_item ) ) { $formatted_items[] = $formatted_item; } } return implode( ', ', $formatted_items ); } - return self::normalize_css_font_family_name( $output ); + return self::maybe_add_quotes( $output ); } /** From 98fd99f8a66d2a44210bf9db5678ac816133d5ee Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 18 Sep 2025 17:17:00 +0200 Subject: [PATCH 08/18] Add normalize_css_font_face utility --- src/wp-includes/fonts/class-wp-font-face.php | 51 ++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/wp-includes/fonts/class-wp-font-face.php b/src/wp-includes/fonts/class-wp-font-face.php index 07cd3d6de9002..6bcdd2189484a 100644 --- a/src/wp-includes/fonts/class-wp-font-face.php +++ b/src/wp-includes/fonts/class-wp-font-face.php @@ -389,6 +389,57 @@ private function build_font_face_css( array $font_face ) { return $css; } + /** + * Normalizes a font-face name for use in CSS. + * + * Add quotes to the font-face name and escape problematic characters. + * + * @see https://www.w3.org/TR/css-fonts-4/#font-family-desc + * + * @since 6.9.0 + * + * @param string $font_face The font-face name to normalize. + * @return string The normalized font-face name. + */ + private function normalize_css_font_face( string $font_face ): string { + $font_face = trim( $font_face, " \t\r\f\n" ); + + if ( + strlen( $font_face ) > 1 && + ( '"' === $font_face[0] && '"' === $font_face[ strlen( $font_face ) - 1 ] ) || + ( "'" === $font_face[0] && "'" === $font_face[ strlen( $font_face ) - 1 ] ) + ) { + _doing_it_wrong( + __METHOD__, + __( 'Font font-family should not be wrapped in quotes; they will be added automatically.' ), + '6.9.0' + ); + $font_face = substr( $font_face, 1, -1 ); + } + + return '"' . strtr( + $font_face, + array( + /* + * Normalize preprocessed whitespace. + * https://www.w3.org/TR/css-syntax-3/#input-preprocessing + */ + "\r" => '\\A ', + "\f" => '\\A ', + "\r\n" => '\\A ', + + /* + * CSS unicode escaping for problematic characters. + * https://www.w3.org/TR/css-syntax-3/#escaping + */ + "\n" => '\\A ', + '\\' => '\\5C ', + ',' => '\\2C ', + '"' => '\\22 ', + ) + ) . '"'; + } + /** * Compiles the `src` into valid CSS. * From 93c7284d6f8435f724333171cd4c42d9ff48fdc2 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 18 Sep 2025 17:17:50 +0200 Subject: [PATCH 09/18] Use normalize_css_font_face --- src/wp-includes/fonts/class-wp-font-face.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-face.php b/src/wp-includes/fonts/class-wp-font-face.php index 6bcdd2189484a..23062cb85e5bf 100644 --- a/src/wp-includes/fonts/class-wp-font-face.php +++ b/src/wp-includes/fonts/class-wp-font-face.php @@ -358,17 +358,7 @@ private function order_src( array $font_face ) { private function build_font_face_css( array $font_face ) { $css = ''; - /* - * Wrap font-family in quotes if it contains spaces - * and is not already wrapped in quotes. - */ - if ( - str_contains( $font_face['font-family'], ' ' ) && - ! str_contains( $font_face['font-family'], '"' ) && - ! str_contains( $font_face['font-family'], "'" ) - ) { - $font_face['font-family'] = '"' . $font_face['font-family'] . '"'; - } + $font_face['font-family'] = $this->normalize_css_font_face( $font_face['font-family'] ); foreach ( $font_face as $key => $value ) { // Compile the "src" parameter. From f99a6444998aa0873544bd69a2199e51a46fcd38 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 18 Sep 2025 18:38:10 +0200 Subject: [PATCH 10/18] Update tests for quoted font family name --- .../fonts/font-face/wp-font-face-tests-dataset.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/phpunit/tests/fonts/font-face/wp-font-face-tests-dataset.php b/tests/phpunit/tests/fonts/font-face/wp-font-face-tests-dataset.php index d410acb7c4124..d207a18afcd34 100644 --- a/tests/phpunit/tests/fonts/font-face/wp-font-face-tests-dataset.php +++ b/tests/phpunit/tests/fonts/font-face/wp-font-face-tests-dataset.php @@ -34,7 +34,7 @@ public function data_should_print_given_fonts() { ), ), 'expected' => << << << Date: Thu, 18 Sep 2025 18:44:09 +0200 Subject: [PATCH 11/18] Add specific tests for ' character --- .../font-face/wp-font-face-tests-dataset.php | 87 ++++++++++++++++++- 1 file changed, 83 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/fonts/font-face/wp-font-face-tests-dataset.php b/tests/phpunit/tests/fonts/font-face/wp-font-face-tests-dataset.php index d207a18afcd34..2ab3765e48f51 100644 --- a/tests/phpunit/tests/fonts/font-face/wp-font-face-tests-dataset.php +++ b/tests/phpunit/tests/fonts/font-face/wp-font-face-tests-dataset.php @@ -17,7 +17,7 @@ trait WP_Font_Face_Tests_Datasets { */ public function data_should_print_given_fonts() { return array( - 'single truetype format font' => array( + 'single truetype format font' => array( 'fonts' => array( 'Inter' => array( @@ -38,7 +38,7 @@ public function data_should_print_given_fonts() { CSS , ), - 'multiple truetype format fonts' => array( + 'multiple truetype format fonts' => array( 'fonts' => array( 'Inter' => array( @@ -70,7 +70,7 @@ public function data_should_print_given_fonts() { CSS , ), - 'single woff2 format font' => array( + 'single woff2 format font' => array( 'fonts' => array( 'DM Sans' => array( @@ -91,7 +91,7 @@ public function data_should_print_given_fonts() { CSS , ), - 'multiple woff2 format fonts' => array( + 'multiple woff2 format fonts' => array( 'fonts' => array( 'DM Sans' => array( @@ -235,6 +235,85 @@ public function data_should_print_given_fonts() { @font-face{font-family:"Piazzolla";font-style:normal;font-weight:400;font-display:fallback;src:url('https://example.org/fonts/piazzolla500.ttf') format('truetype');font-stretch:normal;} @font-face{font-family:"Lobster";font-style:normal;font-weight:400;font-display:fallback;src:url('https://example.org/fonts/lobster400.ttf') format('truetype');font-stretch:normal;} @font-face{font-family:"Lobster";font-style:normal;font-weight:500;font-display:fallback;src:url('https://example.org/fonts/lobster500.ttf') format('truetype');font-stretch:normal;} +CSS + , + ), + ), + + "Fonts with `'` character (ticket #63568)" => array( + 'fonts' => array( + "O'Reilly Sans" => + array( + array( + 'src' => + array( + 'https://example.org/assets/fonts/oreilly-sans/oreilly-sans.woff2', + ), + 'font-family' => "O'Reilly Sans", + 'font-stretch' => 'normal', + 'font-style' => 'normal', + 'font-weight' => '400', + ), + ), + "Suisse BP Int'l" => + array( + array( + 'src' => + array( + 'https://example.org/assets/fonts/suisse-bp-intl/suisse-bp-intl.woff2', + ), + 'font-family' => "Suisse BP Int'l", + 'font-stretch' => 'normal', + 'font-style' => 'normal', + 'font-weight' => '400', + ), + ), + ), + 'expected' => << array( + 'fonts' => array( + array( + array( + 'font-family' => 'Piazzolla', + 'src' => array( 'https://example.org/fonts/piazzolla400.ttf' ), + 'font-style' => 'normal', + 'font-weight' => '400', + 'font-stretch' => 'normal', + ), + array( + 'font-family' => 'Piazzolla', + 'src' => array( 'https://example.org/fonts/piazzolla500.ttf' ), + 'font-style' => 'normal', + 'font-weight' => '400', + 'font-stretch' => 'normal', + ), + ), + array( + array( + 'font-family' => 'Lobster', + 'src' => array( 'https://example.org/fonts/lobster400.ttf' ), + 'font-style' => 'normal', + 'font-weight' => '400', + 'font-stretch' => 'normal', + ), + array( + 'font-family' => 'Lobster', + 'src' => array( 'https://example.org/fonts/lobster500.ttf' ), + 'font-style' => 'normal', + 'font-weight' => '500', + 'font-stretch' => 'normal', + ), + ), + ), + 'expected' => << Date: Fri, 19 Sep 2025 11:05:55 +0200 Subject: [PATCH 12/18] Use font_family name --- src/wp-includes/fonts/class-wp-font-face.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-face.php b/src/wp-includes/fonts/class-wp-font-face.php index 23062cb85e5bf..fd1b605d39863 100644 --- a/src/wp-includes/fonts/class-wp-font-face.php +++ b/src/wp-includes/fonts/class-wp-font-face.php @@ -358,7 +358,7 @@ private function order_src( array $font_face ) { private function build_font_face_css( array $font_face ) { $css = ''; - $font_face['font-family'] = $this->normalize_css_font_face( $font_face['font-family'] ); + $font_face['font-family'] = $this->normalize_css_font_family( $font_face['font-family'] ); foreach ( $font_face as $key => $value ) { // Compile the "src" parameter. @@ -388,27 +388,27 @@ private function build_font_face_css( array $font_face ) { * * @since 6.9.0 * - * @param string $font_face The font-face name to normalize. + * @param string $font_family The font-face name to normalize. * @return string The normalized font-face name. */ - private function normalize_css_font_face( string $font_face ): string { - $font_face = trim( $font_face, " \t\r\f\n" ); + protected function normalize_css_font_family( string $font_family ): string { + $font_family = trim( $font_family, " \t\r\f\n" ); if ( - strlen( $font_face ) > 1 && - ( '"' === $font_face[0] && '"' === $font_face[ strlen( $font_face ) - 1 ] ) || - ( "'" === $font_face[0] && "'" === $font_face[ strlen( $font_face ) - 1 ] ) + strlen( $font_family ) > 1 && + ( '"' === $font_family[0] && '"' === $font_family[ strlen( $font_family ) - 1 ] ) || + ( "'" === $font_family[0] && "'" === $font_family[ strlen( $font_family ) - 1 ] ) ) { _doing_it_wrong( __METHOD__, __( 'Font font-family should not be wrapped in quotes; they will be added automatically.' ), '6.9.0' ); - $font_face = substr( $font_face, 1, -1 ); + $font_family = substr( $font_family, 1, -1 ); } return '"' . strtr( - $font_face, + $font_family, array( /* * Normalize preprocessed whitespace. From b4fbd167831c7db2a9e10ef99d761f0259e563a1 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 19 Sep 2025 11:06:11 +0200 Subject: [PATCH 13/18] Add normalization-specific tests --- .../font-face/wpFontFace/generateAndPrint.php | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/phpunit/tests/fonts/font-face/wpFontFace/generateAndPrint.php b/tests/phpunit/tests/fonts/font-face/wpFontFace/generateAndPrint.php index d10ea500e8707..4e3d6d935bddd 100644 --- a/tests/phpunit/tests/fonts/font-face/wpFontFace/generateAndPrint.php +++ b/tests/phpunit/tests/fonts/font-face/wpFontFace/generateAndPrint.php @@ -37,4 +37,55 @@ public function test_should_generate_and_print_given_fonts( array $fonts, $expec $this->expectOutputString( $expected_output ); $font_face->generate_and_print( $fonts ); } + + + /** + * @ticket 63568 + * + * @dataProvider data_font_family_normalization + */ + public function test_font_family_css_normalization( string $font_name, string $expected ) { + $normalizer = new class() extends WP_Font_Face { + public function test_normalization( string $font_name ): string { + return $this->normalize_css_font_family( $font_name ); + } + }; + $this->assertSame( $expected, $normalizer->test_normalization( $font_name ) ); + } + + public static function data_font_family_normalization() { + return array( + 'Typical name' => array( 'A font name', '"A font name"' ), + 'Generic collision' => array( 'serif', '"serif"' ), + 'Trims whitespace' => array( ' A font name ', '"A font name"' ), + 'Name with \' character' => array( 'O\'Reilly Sans', '"O\'Reilly Sans"' ), + 'Unrealistically tricky' => array( "BS\\Quot\"Apos'Semi;Comma,Newline\nLTnormalize_css_font_family( $font_name ); + } + }; + $this->assertSame( $expected, $normalizer->test_normalization( $font_name ) ); + } + + public static function data_quoted_font_family_normalization() { + return array( + "Quoted with '" => array( "'A font name'", '"A font name"' ), + 'Quoted with "' => array( '"A font name"', '"A font name"' ), + 'Quoted still escaped' => array( '"O\'No (") Double quote in the middle"', '"O\'No (\22 ) Double quote in the middle"' ), + ); + } } From 31a0dee858a936585a9f1e1310dd673f23377379 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 19 Sep 2025 17:03:55 +0200 Subject: [PATCH 14/18] Escape `<` too for good measure --- src/wp-includes/fonts/class-wp-font-face.php | 1 + .../tests/fonts/font-face/wpFontFace/generateAndPrint.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/fonts/class-wp-font-face.php b/src/wp-includes/fonts/class-wp-font-face.php index fd1b605d39863..e74ba297cc07a 100644 --- a/src/wp-includes/fonts/class-wp-font-face.php +++ b/src/wp-includes/fonts/class-wp-font-face.php @@ -426,6 +426,7 @@ protected function normalize_css_font_family( string $font_family ): string { '\\' => '\\5C ', ',' => '\\2C ', '"' => '\\22 ', + '<' => '\\3C ', ) ) . '"'; } diff --git a/tests/phpunit/tests/fonts/font-face/wpFontFace/generateAndPrint.php b/tests/phpunit/tests/fonts/font-face/wpFontFace/generateAndPrint.php index 4e3d6d935bddd..b797de974dfb9 100644 --- a/tests/phpunit/tests/fonts/font-face/wpFontFace/generateAndPrint.php +++ b/tests/phpunit/tests/fonts/font-face/wpFontFace/generateAndPrint.php @@ -59,7 +59,7 @@ public static function data_font_family_normalization() { 'Generic collision' => array( 'serif', '"serif"' ), 'Trims whitespace' => array( ' A font name ', '"A font name"' ), 'Name with \' character' => array( 'O\'Reilly Sans', '"O\'Reilly Sans"' ), - 'Unrealistically tricky' => array( "BS\\Quot\"Apos'Semi;Comma,Newline\nLT array( "BS\\Quot\"Apos'Semi;Comma,Newline\nLT Date: Fri, 19 Sep 2025 19:04:48 +0200 Subject: [PATCH 15/18] Remove normalization of quoted strings --- src/wp-includes/fonts/class-wp-font-face.php | 2 +- .../tests/fonts/font-face/wpFontFace/generateAndPrint.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-face.php b/src/wp-includes/fonts/class-wp-font-face.php index e74ba297cc07a..63db2bf8ea089 100644 --- a/src/wp-includes/fonts/class-wp-font-face.php +++ b/src/wp-includes/fonts/class-wp-font-face.php @@ -404,7 +404,7 @@ protected function normalize_css_font_family( string $font_family ): string { __( 'Font font-family should not be wrapped in quotes; they will be added automatically.' ), '6.9.0' ); - $font_family = substr( $font_family, 1, -1 ); + return $font_family; } return '"' . strtr( diff --git a/tests/phpunit/tests/fonts/font-face/wpFontFace/generateAndPrint.php b/tests/phpunit/tests/fonts/font-face/wpFontFace/generateAndPrint.php index b797de974dfb9..59398b928f0a1 100644 --- a/tests/phpunit/tests/fonts/font-face/wpFontFace/generateAndPrint.php +++ b/tests/phpunit/tests/fonts/font-face/wpFontFace/generateAndPrint.php @@ -72,7 +72,7 @@ public static function data_font_family_normalization() { * * @dataProvider data_quoted_font_family_normalization */ - public function test_quoted_font_family_doing_it_wrong_normalization( string $font_name, string $expected ) { + public function test_quoted_font_family_doing_it_wrong_no_normalization( string $font_name, string $expected ) { $normalizer = new class() extends WP_Font_Face { public function test_normalization( string $font_name ): string { return $this->normalize_css_font_family( $font_name ); @@ -83,9 +83,9 @@ public function test_normalization( string $font_name ): string { public static function data_quoted_font_family_normalization() { return array( - "Quoted with '" => array( "'A font name'", '"A font name"' ), - 'Quoted with "' => array( '"A font name"', '"A font name"' ), - 'Quoted still escaped' => array( '"O\'No (") Double quote in the middle"', '"O\'No (\22 ) Double quote in the middle"' ), + "Quoted with '" => array( "'A font name'", "'A font name'" ), + 'Quoted with "' => array( '"A font name"', '"A font name"' ), + 'Quoted is not escaped' => array( '"""', '"""' ), ); } } From a61fa4e7c2e83129c2c8ca15a2f395951810bb51 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 19 Sep 2025 19:20:38 +0200 Subject: [PATCH 16/18] Fix typos, language, comment alignment --- src/wp-includes/fonts/class-wp-font-face.php | 12 ++++++------ .../fonts/font-face/wpFontFace/generateAndPrint.php | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/fonts/class-wp-font-face.php b/src/wp-includes/fonts/class-wp-font-face.php index 63db2bf8ea089..60d880941bc7e 100644 --- a/src/wp-includes/fonts/class-wp-font-face.php +++ b/src/wp-includes/fonts/class-wp-font-face.php @@ -411,17 +411,17 @@ protected function normalize_css_font_family( string $font_family ): string { $font_family, array( /* - * Normalize preprocessed whitespace. - * https://www.w3.org/TR/css-syntax-3/#input-preprocessing - */ + * Normalize preprocessed whitespace. + * https://www.w3.org/TR/css-syntax-3/#input-preprocessing + */ "\r" => '\\A ', "\f" => '\\A ', "\r\n" => '\\A ', /* - * CSS unicode escaping for problematic characters. - * https://www.w3.org/TR/css-syntax-3/#escaping - */ + * CSS Unicode escaping for problematic characters. + * https://www.w3.org/TR/css-syntax-3/#escaping + */ "\n" => '\\A ', '\\' => '\\5C ', ',' => '\\2C ', diff --git a/tests/phpunit/tests/fonts/font-face/wpFontFace/generateAndPrint.php b/tests/phpunit/tests/fonts/font-face/wpFontFace/generateAndPrint.php index 59398b928f0a1..515020ff77ddc 100644 --- a/tests/phpunit/tests/fonts/font-face/wpFontFace/generateAndPrint.php +++ b/tests/phpunit/tests/fonts/font-face/wpFontFace/generateAndPrint.php @@ -64,7 +64,7 @@ public static function data_font_family_normalization() { } /** - * Ensure unexpected quoted font family names are normlized with a doing it wrong notice. + * Ensure already-quoted font family names emit doing it wrong notice and skip normalization. * * @expectedIncorrectUsage WP_Font_Face::normalize_css_font_family * From 96a3293eec9bff094588c9265a6092341e076c06 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 19 Sep 2025 19:24:06 +0200 Subject: [PATCH 17/18] Improve doint it wrong message Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/wp-includes/fonts/class-wp-font-face.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/fonts/class-wp-font-face.php b/src/wp-includes/fonts/class-wp-font-face.php index 60d880941bc7e..aa514046bfc0f 100644 --- a/src/wp-includes/fonts/class-wp-font-face.php +++ b/src/wp-includes/fonts/class-wp-font-face.php @@ -401,7 +401,7 @@ protected function normalize_css_font_family( string $font_family ): string { ) { _doing_it_wrong( __METHOD__, - __( 'Font font-family should not be wrapped in quotes; they will be added automatically.' ), + __( 'Font family should not be wrapped in quotes; they will be added automatically.' ), '6.9.0' ); return $font_family; From 95a9d845cdbae78b1967f35a256ad53849344d56 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Tue, 4 Nov 2025 18:09:46 +0100 Subject: [PATCH 18/18] Document unusual escapes in font family name --- src/wp-includes/fonts/class-wp-font-face.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/wp-includes/fonts/class-wp-font-face.php b/src/wp-includes/fonts/class-wp-font-face.php index aa514046bfc0f..eeee3b235ef03 100644 --- a/src/wp-includes/fonts/class-wp-font-face.php +++ b/src/wp-includes/fonts/class-wp-font-face.php @@ -421,6 +421,17 @@ protected function normalize_css_font_family( string $font_family ): string { /* * CSS Unicode escaping for problematic characters. * https://www.w3.org/TR/css-syntax-3/#escaping + * + * These characters are not required by CSS but may be problematic in WordPress: + * + * - "<" is replaced to prevent issues with KSES and other sanitization when + * printing CSS later. + * - "," is replaced to prevent issues where multiple font family names may be + * split, sanitized, and joined on the `,` character (regardless of quoting + * or escaping). + * + * Note that the Unicode escape sequences are used rather than backslash-escaping. + * This also helps to prevent issues with problematic characters. */ "\n" => '\\A ', '\\' => '\\5C ',