Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix template texts localizing/escaping #641

Merged
merged 14 commits into from
May 16, 2024
1 change: 1 addition & 0 deletions admin/class-create-theme.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

require_once __DIR__ . '/resolver_additions.php';
require_once __DIR__ . '/create-theme/theme-locale.php';
require_once __DIR__ . '/create-theme/theme-tags.php';
require_once __DIR__ . '/create-theme/theme-zip.php';
require_once __DIR__ . '/create-theme/theme-media.php';
Expand Down
158 changes: 158 additions & 0 deletions admin/create-theme/theme-locale.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<?php
/*
* Locale related functionality
*/
class CBT_Theme_Locale {

/**
* Escape a string for localization.
*
* @param string $string The string to escape.
* @return string The escaped string.
*/
public static function escape_string( $string ) {
// Avoid escaping if the text is not a string.
if ( ! is_string( $string ) ) {
return $string;
}

// Check if the text is already escaped.
if ( str_starts_with( $string, '<?php echo' ) ) {
return $string;
}

$string = addcslashes( $string, "'" );
return "<?php echo __('" . $string . "', '" . wp_get_theme()->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[^>]*>)(.*?)(<\/p>)/' );
case 'core/heading':
return array( '/(<h[^>]*>)(.*?)(<\/h[^>]*>)/' );
case 'core/list-item':
return array( '/(<li[^>]*>)(.*?)(<\/li>)/' );
case 'core/verse':
return array( '/(<pre[^>]*>)(.*?)(<\/pre>)/' );
case 'core/button':
return array( '/(<a[^>]*>)(.*?)(<\/a>)/' );
case 'core/image':
case 'core/cover':
case 'core/media-text':
return array( '/alt="(.*?)"/' );
case 'core/quote':
case 'core/pullquote':
return array(
'/(<p[^>]*>)(.*?)(<\/p>)/',
'/(<cite[^>]*>)(.*?)(<\/cite>)/',
);
case 'core/table':
return array(
'/(<td[^>]*>)(.*?)(<\/td>)/',
'/(<th[^>]*>)(.*?)(<\/th>)/',
'/(<figcaption[^>]*>)(.*?)(<\/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;
}
}
105 changes: 10 additions & 95 deletions admin/create-theme/theme-templates.php
Original file line number Diff line number Diff line change
Expand Up @@ -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[^>]*>(.*?)<\/p>/', $markup, $matches ) ) {
$text_to_localize[] = $matches[1];
}
// Grab cite tag content
if ( preg_match( '/<cite[^>]*>(.*?)<\/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[^>]*>(.*?)<\/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[^>]*>(.*?)<\/td>/', $markup, $matches ) ) {
$text_to_localize = array_merge( $text_to_localize, $matches[1] );
}
// Grab table header content
if ( preg_match_all( '/<th[^>]*>(.*?)<\/th>/', $markup, $matches ) ) {
$text_to_localize = array_merge( $text_to_localize, $matches[1] );
}
// Grab the caption
if ( preg_match_all( '/<figcaption[^>]*>(.*?)<\/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 "<?php echo __('" . $text . "', '" . wp_get_theme()->get( 'TextDomain' ) . "');?>";
}

private static function eliminate_environment_specific_content_from_block( $block, $options = null ) {

// remove theme attribute from template parts
Expand Down
51 changes: 51 additions & 0 deletions tests/CbtThemeLocale/base.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php
/**
* Base test case for Theme Locale tests.
*
* @package Create_Block_Theme
*/
abstract class CBT_Theme_Locale_UnitTestCase extends WP_UnitTestCase {

/**
* Stores the original active theme slug in order to restore it in tear down.
*
* @var string|null
*/
private $orig_active_theme_slug;

/**
* Stores the custom test theme directory.
*
* @var string|null;
*/
private $test_theme_dir;

/**
* Sets up tests.
*/
public function set_up() {
parent::set_up();

// Store the original active theme.
$this->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 );
}
}
48 changes: 48 additions & 0 deletions tests/CbtThemeLocale/escapeString.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

require_once __DIR__ . '/base.php';

/**
* Tests for the CBT_Theme_Locale::escape_string method.
*
* @package Create_Block_Theme
* @covers CBT_Theme_Locale::escape_string
* @group locale
*/
class CBT_Theme_Locale_EscapeString extends CBT_Theme_Locale_UnitTestCase {
public function test_escape_string() {
$string = 'This is a test text.';
$escaped_string = CBT_Theme_Locale::escape_string( $string );
$this->assertEquals( "<?php echo __('This is a test text.', 'test-locale-theme');?>", $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( "<?php echo __('This is a test text with a single quote \\'', 'test-locale-theme');?>", $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( "<?php echo __('This is a test text with a double quote \"', 'test-locale-theme');?>", $escaped_string );
}

public function test_escape_string_with_html() {
$string = '<p>This is a test text with HTML.</p>';
$escaped_string = CBT_Theme_Locale::escape_string( $string );
$this->assertEquals( "<?php echo __('<p>This is a test text with HTML.</p>', 'test-locale-theme');?>", $escaped_string );
}

public function test_escape_string_with_already_escaped_string() {
$string = "<?php echo __('This is a test text.', 'test-locale-theme');?>";
$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 );
}
}
Loading
Loading