Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions includes/create-theme/theme-patterns.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ public static function strip_php_tags( $content ) {
/**
* Build a pattern .php file from a template stdClass.
*
* The pattern header Title is taken from `$template->title` (the
* human-readable label). When that property is absent or empty the
* template slug is used as a fallback so the header is never blank.
* Any `* /` sequence in either value is escaped to `*/` to prevent
* PHP comment-block injection.
*
* IMPORTANT: this function expects `$template->content` to be already
* sanitised by the caller. The pipeline entry point is
* `CBT_Theme_Templates::prepare_template_for_export`, which strips PHP
Expand All @@ -62,10 +68,12 @@ public static function pattern_from_template( $template, $new_slug = null ) {
$theme_slug = $new_slug ? $new_slug : wp_get_theme()->get( 'TextDomain' );
$template_slug = str_replace( '*/', '*/', $template->slug );
$pattern_slug = $theme_slug . '/' . $template_slug;
$raw_title = ! empty( $template->title ) ? $template->title : $template->slug;
$pattern_title = str_replace( '*/', '*/', $raw_title );
$pattern_content = <<<PHP
<?php
/**
* Title: {$template_slug}
* Title: {$pattern_title}
* Slug: {$pattern_slug}
* Inserter: no
*/
Expand All @@ -86,7 +94,7 @@ public static function pattern_from_wp_block( $pattern_post ) {
$pattern->name = sanitize_title_with_dashes( $pattern_post->post_title );
$pattern->slug = wp_get_theme()->get( 'TextDomain' ) . '/' . $pattern->name;
$pattern_category_list = get_the_terms( $pattern->id, 'wp_pattern_category' );
$pattern->categories = ! empty( $pattern_category_list ) ? join( ', ', wp_list_pluck( $pattern_category_list, 'name' ) ) : '';
$pattern->categories = ! empty( $pattern_category_list ) ? join( ', ', wp_list_pluck( $pattern_category_list, 'slug' ) ) : '';
$pattern_title = str_replace( '*/', '*&#47;', $pattern->title );
$pattern_categories = str_replace( '*/', '*&#47;', $pattern->categories );
$safe_body = self::strip_php_tags( $pattern_post->post_content );
Expand Down
109 changes: 109 additions & 0 deletions tests/test-theme-patterns.php
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,115 @@ public function test_pattern_from_template_keeps_slug_escape_intact() {
$this->assertStringNotContainsString( 'evil */ break', $result['content'] );
}

/**
* When $template->title is set, the pattern header must use that human-readable
* title instead of the machine slug.
*
* @covers CBT_Theme_Patterns::pattern_from_template
*/
public function test_pattern_from_template_uses_title_not_slug() {
$template = new stdClass();
$template->slug = 'test-template';
$template->title = 'Test Template Title';
$template->content = '<p>safe</p>';

$result = CBT_Theme_Patterns::pattern_from_template( $template );

$this->assertStringContainsString( 'Title: Test Template Title', $result['content'] );
$this->assertStringNotContainsString( 'Title: test-template', $result['content'] );
}

/**
* When $template->title is absent or empty, fall back to the slug so the
* header is never blank.
*
* @covers CBT_Theme_Patterns::pattern_from_template
*/
public function test_pattern_from_template_falls_back_to_slug_when_title_empty() {
$template = new stdClass();
$template->slug = 'test-template';
$template->title = '';
$template->content = '<p>safe</p>';

$result = CBT_Theme_Patterns::pattern_from_template( $template );

$this->assertStringContainsString( 'Title: test-template', $result['content'] );
}

/**
* A `* /` sequence inside the title must be escaped to prevent PHP comment
* injection.
*
* @covers CBT_Theme_Patterns::pattern_from_template
*/
public function test_pattern_from_template_escapes_comment_closer_in_title() {
$template = new stdClass();
$template->slug = 'some-template';
$template->title = 'Evil */ Title';
$template->content = '<p>safe</p>';

$result = CBT_Theme_Patterns::pattern_from_template( $template );

$this->assertStringContainsString( '*&#47;', $result['content'] );
$this->assertStringNotContainsString( 'Evil */ Title', $result['content'] );
}

/**
* Categories written into the pattern header must use the taxonomy slug
* (e.g. "category-slug") not the display name (e.g. "Category Name"), so that
* WordPress core can match patterns to the correct inserter category.
*
* @covers CBT_Theme_Patterns::pattern_from_wp_block
*/
public function test_pattern_from_wp_block_uses_category_slugs_not_names() {
// Register a pattern category whose name differs from its slug.
$term = wp_insert_term( 'Category Name', 'wp_pattern_category', array( 'slug' => 'category-slug' ) );
$this->assertFalse( is_wp_error( $term ), 'Term creation should succeed' );

$post = $this->make_wp_block_post( '<p>safe</p>', 'Test Pattern' );

// Assign the category to the pattern post.
wp_set_object_terms( $post->ID, array( (int) $term['term_id'] ), 'wp_pattern_category' );

$pattern = CBT_Theme_Patterns::pattern_from_wp_block( $post );

// The header must contain the slug, not the display name.
$this->assertStringContainsString( 'Categories: category-slug', $pattern->content );
$this->assertStringNotContainsString( 'Categories: Category Name', $pattern->content );

// Cleanup.
wp_delete_term( (int) $term['term_id'], 'wp_pattern_category' );
}

/**
* When a pattern has multiple categories, all slugs should be written
* (comma-separated) in the header.
*
* @covers CBT_Theme_Patterns::pattern_from_wp_block
*/
public function test_pattern_from_wp_block_uses_multiple_category_slugs() {
$term_a = wp_insert_term( 'Category One', 'wp_pattern_category', array( 'slug' => 'category-one-slug' ) );
$term_b = wp_insert_term( 'Category Two', 'wp_pattern_category', array( 'slug' => 'category-two-slug' ) );

$this->assertFalse( is_wp_error( $term_a ), 'Term A creation should succeed' );
$this->assertFalse( is_wp_error( $term_b ), 'Term B creation should succeed' );

$post = $this->make_wp_block_post( '<p>safe</p>', 'Test Pattern' );
wp_set_object_terms( $post->ID, array( (int) $term_a['term_id'], (int) $term_b['term_id'] ), 'wp_pattern_category' );

$pattern = CBT_Theme_Patterns::pattern_from_wp_block( $post );

// Both slugs must appear; display names must not.
$this->assertStringContainsString( 'category-one-slug', $pattern->content );
$this->assertStringContainsString( 'category-two-slug', $pattern->content );
$this->assertStringNotContainsString( 'Category One', $pattern->content );
$this->assertStringNotContainsString( 'Category Two', $pattern->content );

// Cleanup.
wp_delete_term( (int) $term_a['term_id'], 'wp_pattern_category' );
wp_delete_term( (int) $term_b['term_id'], 'wp_pattern_category' );
}

public function test_prepare_template_for_export_preserves_trusted_localize_markers() {
// When localizeText=true is enabled, CBT_Theme_Templates::escape_text_in_template
// injects trusted PHP esc_html_e(...) markers into the template body.
Expand Down
Loading