From ca0851410b93e28228e890e3156793cb28334d0b Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Thu, 16 May 2024 15:04:45 -0300 Subject: [PATCH] Fix template texts localizing/escaping (#641) Co-authored-by: Jason Crist --- admin/class-create-theme.php | 1 + admin/create-theme/theme-locale.php | 158 ++++++++++++++ admin/create-theme/theme-templates.php | 105 +--------- tests/CbtThemeLocale/base.php | 51 +++++ tests/CbtThemeLocale/escapeString.php | 48 +++++ .../escapeTextContentOfBlocks.php | 195 ++++++++++++++++++ tests/data/themes/test-theme-locale/style.css | 15 ++ .../data/themes/test-theme-locale/theme.json | 6 + tests/test-theme-templates.php | 3 +- 9 files changed, 485 insertions(+), 97 deletions(-) create mode 100644 admin/create-theme/theme-locale.php create mode 100644 tests/CbtThemeLocale/base.php create mode 100644 tests/CbtThemeLocale/escapeString.php create mode 100644 tests/CbtThemeLocale/escapeTextContentOfBlocks.php create mode 100644 tests/data/themes/test-theme-locale/style.css create mode 100644 tests/data/themes/test-theme-locale/theme.json diff --git a/admin/class-create-theme.php b/admin/class-create-theme.php index 153d3f59..50fde92d 100644 --- a/admin/class-create-theme.php +++ b/admin/class-create-theme.php @@ -1,6 +1,7 @@ get( 'TextDomain' ) . "');?>"; + } + + /** + * Get a replacement pattern for escaping the text from the html content of a block. + * + * @param string $block_name The block name. + * @return array|null The regex patterns to match the content that needs to be escaped. + * Returns null if the block is not supported. + * Returns an array of regex patterns if the block has html elements that need to be escaped. + */ + private static function get_text_replacement_patterns_for_html( $block_name ) { + switch ( $block_name ) { + case 'core/paragraph': + return array( '/(]*>)(.*?)(<\/p>)/' ); + case 'core/heading': + return array( '/(]*>)(.*?)(<\/h[^>]*>)/' ); + case 'core/list-item': + return array( '/(]*>)(.*?)(<\/li>)/' ); + case 'core/verse': + return array( '/(]*>)(.*?)(<\/pre>)/' ); + case 'core/button': + return array( '/(]*>)(.*?)(<\/a>)/' ); + case 'core/image': + case 'core/cover': + case 'core/media-text': + return array( '/alt="(.*?)"/' ); + case 'core/quote': + case 'core/pullquote': + return array( + '/(]*>)(.*?)(<\/p>)/', + '/(]*>)(.*?)(<\/cite>)/', + ); + case 'core/table': + return array( + '/(]*>)(.*?)(<\/td>)/', + '/(]*>)(.*?)(<\/th>)/', + '/(]*>)(.*?)(<\/figcaption>)/', + ); + default: + return null; + } + } + + /* + * Localize text in text blocks. + * + * @param array $blocks The blocks to localize. + * @return array The localized blocks. + */ + public static function escape_text_content_of_blocks( $blocks ) { + foreach ( $blocks as &$block ) { + + // Recursively escape the inner blocks. + if ( ! empty( $block['innerBlocks'] ) ) { + $block['innerBlocks'] = self::escape_text_content_of_blocks( $block['innerBlocks'] ); + } + + /* + * Set the pattern based on the block type. + * The pattern is used to match the content that needs to be escaped. + * Patterns are defined in the get_text_replacement_patterns_for_html method. + */ + $patterns = self::get_text_replacement_patterns_for_html( $block['blockName'] ); + + // If the block does not have any patterns leave the block as is and continue to the next block. + if ( ! $patterns ) { + continue; + } + + // Builds the replacement callback function based on the block type. + switch ( $block['blockName'] ) { + case 'core/paragraph': + case 'core/heading': + case 'core/list-item': + case 'core/verse': + case 'core/button': + case 'core/quote': + case 'core/pullquote': + case 'core/table': + $replace_content_callback = function ( $content, $pattern ) { + if ( empty( $content ) ) { + return; + } + return preg_replace_callback( + $pattern, + function( $matches ) { + return $matches[1] . self::escape_string( $matches[2] ) . $matches[3]; + }, + $content + ); + }; + break; + case 'core/image': + case 'core/cover': + case 'core/media-text': + $replace_content_callback = function ( $content, $pattern ) { + if ( empty( $content ) ) { + return; + } + return preg_replace_callback( + $pattern, + function( $matches ) { + return 'alt="' . self::escape_string( $matches[1] ) . '"'; + }, + $content + ); + }; + break; + default: + $replace_content_callback = null; + break; + } + + // Apply the replacement patterns to the block content. + foreach ( $patterns as $pattern ) { + if ( + ! empty( $block['innerContent'] ) && + is_callable( $replace_content_callback ) + ) { + $block['innerContent'] = is_array( $block['innerContent'] ) + ? array_map( + function( $content ) use ( $replace_content_callback, $pattern ) { + return $replace_content_callback( $content, $pattern ); + }, + $block['innerContent'] + ) + : $replace_content_callback( $block['innerContent'], $pattern ); + } + } + } + return $blocks; + } +} diff --git a/admin/create-theme/theme-templates.php b/admin/create-theme/theme-templates.php index 254af526..3298e7ee 100644 --- a/admin/create-theme/theme-templates.php +++ b/admin/create-theme/theme-templates.php @@ -268,105 +268,20 @@ public static function add_templates_to_local( $export_type, $path = null, $slug } } + /** + * Escape text in template content. + * + * @param object $template The template to escape text content in. + * @return object The template with the content escaped. + */ public static function escape_text_in_template( $template ) { - - $template_blocks = parse_blocks( $template->content ); - $text_to_localize = array(); - - // Gather up all the strings that need to be localized - foreach ( $template_blocks as &$block ) { - $text_to_localize = array_merge( $text_to_localize, self::get_text_to_localize_from_block( $block ) ); - } - $text_to_localize = array_unique( $text_to_localize ); - - // Localize the strings - foreach ( $text_to_localize as $text ) { - $template->content = str_replace( $text, self::escape_text( $text ), $template->content ); - } - + $template_blocks = parse_blocks( $template->content ); + $localized_blocks = CBT_Theme_Locale::escape_text_content_of_blocks( $template_blocks ); + $updated_template_content = serialize_blocks( $localized_blocks ); + $template->content = $updated_template_content; return $template; } - private static function get_text_to_localize_from_block( $block ) { - - $text_to_localize = array(); - - // Text Blocks (paragraphs and headings) - if ( in_array( $block['blockName'], array( 'core/paragraph', 'core/heading', 'core/list-item', 'core/verse' ), true ) ) { - $markup = $block['innerContent'][0]; - // remove the tags from the beginning and end of the markup - $markup = substr( $markup, strpos( $markup, '>' ) + 1 ); - $markup = substr( $markup, 0, strrpos( $markup, '<' ) ); - $text_to_localize[] = $markup; - } - - // Quote Blocks - if ( in_array( $block['blockName'], array( 'core/quote', 'core/pullquote' ), true ) ) { - $markup = serialize_blocks( array( $block ) ); - // Grab paragraph tag content - if ( preg_match( '/]*>(.*?)<\/p>/', $markup, $matches ) ) { - $text_to_localize[] = $matches[1]; - } - // Grab cite tag content - if ( preg_match( '/]*>(.*?)<\/cite>/', $markup, $matches ) ) { - $text_to_localize[] = $matches[1]; - } - } - - // Button Blocks - if ( in_array( $block['blockName'], array( 'core/button' ), true ) ) { - $markup = $block['innerContent'][0]; - if ( preg_match( '/]*>(.*?)<\/a>/', $markup, $matches ) ) { - $text_to_localize[] = $matches[1]; - } - } - - // Alt text in Image and Cover Blocks - if ( in_array( $block['blockName'], array( 'core/image', 'core/cover', 'core/media-text' ), true ) ) { - $markup = $block['innerContent'][0]; - if ( preg_match( '/alt="(.*?)"/', $markup, $matches ) ) { - $text_to_localize[] = $matches[1]; - } - if ( array_key_exists( 'alt', $block['attrs'] ) ) { - $text_to_localize[] = $block['attrs']['alt']; - } - } - - // Table Blocks - if ( in_array( $block['blockName'], array( 'core/table' ), true ) ) { - $markup = serialize_blocks( array( $block ) ); - // Grab table cell content - if ( preg_match_all( '/]*>(.*?)<\/td>/', $markup, $matches ) ) { - $text_to_localize = array_merge( $text_to_localize, $matches[1] ); - } - // Grab table header content - if ( preg_match_all( '/]*>(.*?)<\/th>/', $markup, $matches ) ) { - $text_to_localize = array_merge( $text_to_localize, $matches[1] ); - } - // Grab the caption - if ( preg_match_all( '/]*>(.*?)<\/figcaption>/', $markup, $matches ) ) { - $text_to_localize = array_merge( $text_to_localize, $matches[1] ); - } - } - - // process inner blocks - if ( ! empty( $block['innerBlocks'] ) ) { - foreach ( $block['innerBlocks'] as $inner_block ) { - $text_to_localize = array_merge( $text_to_localize, self::get_text_to_localize_from_block( $inner_block ) ); - } - } - - return $text_to_localize; - } - - public static function escape_text( $text ) { - if ( ! $text ) { - return $text; - } - $text = addcslashes( $text, "'" ); - return "get( 'TextDomain' ) . "');?>"; - } - private static function eliminate_environment_specific_content_from_block( $block, $options = null ) { // remove theme attribute from template parts diff --git a/tests/CbtThemeLocale/base.php b/tests/CbtThemeLocale/base.php new file mode 100644 index 00000000..bf86d292 --- /dev/null +++ b/tests/CbtThemeLocale/base.php @@ -0,0 +1,51 @@ +orig_active_theme_slug = get_option( 'stylesheet' ); + + // Create a test theme directory. + $this->test_theme_dir = DIR_TESTDATA . '/themes/'; + + // Register test theme directory. + register_theme_directory( $this->test_theme_dir ); + + // Switch to the test theme. + switch_theme( 'test-theme-locale' ); + } + + /** + * Tears down tests. + */ + public function tear_down() { + parent::tear_down(); + + // Restore the original active theme. + switch_theme( $this->orig_active_theme_slug ); + } +} diff --git a/tests/CbtThemeLocale/escapeString.php b/tests/CbtThemeLocale/escapeString.php new file mode 100644 index 00000000..4ffcaf9a --- /dev/null +++ b/tests/CbtThemeLocale/escapeString.php @@ -0,0 +1,48 @@ +assertEquals( "", $escaped_string ); + } + + public function test_escape_string_with_single_quote() { + $string = "This is a test text with a single quote '"; + $escaped_string = CBT_Theme_Locale::escape_string( $string ); + $this->assertEquals( "", $escaped_string ); + } + + public function test_escape_string_with_double_quote() { + $string = 'This is a test text with a double quote "'; + $escaped_string = CBT_Theme_Locale::escape_string( $string ); + $this->assertEquals( "", $escaped_string ); + } + + public function test_escape_string_with_html() { + $string = '

This is a test text with HTML.

'; + $escaped_string = CBT_Theme_Locale::escape_string( $string ); + $this->assertEquals( "This is a test text with HTML.

', 'test-locale-theme');?>", $escaped_string ); + } + + public function test_escape_string_with_already_escaped_string() { + $string = ""; + $escaped_string = CBT_Theme_Locale::escape_string( $string ); + $this->assertEquals( $string, $escaped_string ); + } + + public function test_escape_string_with_non_string() { + $string = null; + $escaped_string = CBT_Theme_Locale::escape_string( $string ); + $this->assertEquals( $string, $escaped_string ); + } +} diff --git a/tests/CbtThemeLocale/escapeTextContentOfBlocks.php b/tests/CbtThemeLocale/escapeTextContentOfBlocks.php new file mode 100644 index 00000000..c138a24a --- /dev/null +++ b/tests/CbtThemeLocale/escapeTextContentOfBlocks.php @@ -0,0 +1,195 @@ +assertEquals( $expected_markup, $escaped_markup, 'The markup result is not as the expected one.' ); + } + + public function data_test_escape_text_content_of_blocks() { + return array( + + 'paragraph' => array( + 'block_markup' => '

This is a test text.

', + 'expected_markup' => '

', + ), + + 'paragraph on nested groups' => array( + 'block_markup' => + ' +
+
+

This is a test text.

+
+
+ ', + 'expected_markup' => + ' +
+
+

+
+
+ ', + ), + + 'heading 1' => array( + 'block_markup' => + ' +

A passion for creating spaces

+ ', + 'expected_markup' => + ' +

+ ', + ), + + 'heading 2' => array( + 'block_markup' => + ' +

A passion for creating spaces

+ ', + 'expected_markup' => + ' +

+ ', + ), + + 'list item' => array( + 'block_markup' => + ' +
    +
  • Collaborate with fellow architects.
  • + + +
  • Showcase your projects.
  • + + +
  • Experience the world of architecture.
  • +
+ ', + 'expected_markup' => + ' +
    +
  • + + +
  • + + +
  • +
+ ', + ), + + 'verse' => array( + 'block_markup' => + ' +
Ya somos el olvido que seremos.
El polvo elemental que nos ignora
y que fue el rojo Adán y que es ahora
todos los hombres, y que no veremos.
+ ', + 'expected_markup' => + ' +
El polvo elemental que nos ignora
y que fue el rojo Adán y que es ahora
todos los hombres, y que no veremos.\', \'test-locale-theme\');?>
+ ', + ), + + 'button' => array( + 'block_markup' => + ' + + ', + 'expected_markup' => + ' +
+ ', + ), + + 'image' => array( + 'block_markup' => + ' +
Windows of a building in Nuremberg, Germany
+ ', + 'expected_markup' => + ' +
<?php echo __(\'Windows of a building in Nuremberg, Germany\', \'test-locale-theme\');?>
+ ', + ), + + 'cover' => array( + 'block_markup' => + ' +
Alternative text for cover image
+

This is a cover caption

+
+ ', + 'expected_markup' => + ' +
<?php echo __(\'Alternative text for cover image\', \'test-locale-theme\');?>
+

+
+ ', + ), + + 'media-text' => array( + 'block_markup' => + ' +
This is alt text
+

Media text content test.

+
+ ', + 'expected_markup' => + ' +
<?php echo __(\'This is alt text\', \'test-locale-theme\');?>
+

+
+ ', + ), + + 'pullquote' => array( + 'block_markup' => + ' +

Yo me equivoqué y pagué, pero la pelota no se mancha.

Diego Armando Maradona
+ ', + 'expected_markup' => + ' +

+ ', + ), + + 'table' => array( + 'block_markup' => + ' +
TeamPoints
Boca74
River2
Score table
+ ', + 'expected_markup' => + ' +
+ ', + ), + + ); + } +} + + + + diff --git a/tests/data/themes/test-theme-locale/style.css b/tests/data/themes/test-theme-locale/style.css new file mode 100644 index 00000000..fbc200a3 --- /dev/null +++ b/tests/data/themes/test-theme-locale/style.css @@ -0,0 +1,15 @@ +/* +Theme Name: Test Locale Theme +Theme URI: https://example.org/themes/test-locale-theme +Author: the WordPress team +Author URI: https://wordpress.org +Description: Test Locale Theme is a theme for testing the text localization/escaping capabilities of the Create Block Theme plugin. +Requires at least: 6.4 +Tested up to: 6.4 +Requires PHP: 7.0 +Version: 1.0 +License: GNU General Public License v2 or later +License URI: http://www.gnu.org/licenses/gpl-2.0.html +Text Domain: test-locale-theme +Tags: test, locale, theme +*/ diff --git a/tests/data/themes/test-theme-locale/theme.json b/tests/data/themes/test-theme-locale/theme.json new file mode 100644 index 00000000..c4c73c82 --- /dev/null +++ b/tests/data/themes/test-theme-locale/theme.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://schemas.wp.org/trunk/theme.json", + "version": 2, + "styles": {}, + "settings": {} +} diff --git a/tests/test-theme-templates.php b/tests/test-theme-templates.php index 6fca386c..faabb623 100644 --- a/tests/test-theme-templates.php +++ b/tests/test-theme-templates.php @@ -1,6 +1,7 @@ assertStringContainsString( 'alt=""', $new_template->content ); - // Check the block attribute - $this->assertStringContainsString( '"alt":""', $new_template->content ); } public function test_localize_quote() {