Security audit fixes: fn() whitelist, escaping, and performance (v0.1.4)
All checks were successful
Create Release Package / PHP Lint (push) Successful in 1m41s
Create Release Package / Build Release (push) Successful in 1m47s

- WooCommerceExtension: ALLOWED_FUNCTIONS whitelist for fn() Twig function
- Notice templates: data attributes use wp_kses_post instead of raw
- Search form: esc_attr on search query value attribute
- Per-request ContextBuilder caching via static variable
- Shared wc_bootstrap_render_in_page_shell() helper (DRY)
- Removed unused WC_BOOTSTRAP_VERSION and WC_BOOTSTRAP_URL constants

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 01:02:43 +01:00
parent e72b4ba3c1
commit 98359d4cfb
9 changed files with 118 additions and 65 deletions

View File

@@ -17,15 +17,8 @@ if ( ! defined( 'ABSPATH' ) ) {
/**
* Define theme constants.
*
* CRITICAL: WordPress reads the version from TWO places:
* 1. style.css header "Version:" — WordPress uses THIS for admin display
* 2. This PHP constant — used internally by the theme
* Both MUST be updated on every release.
*/
define( 'WC_BOOTSTRAP_VERSION', '0.1.0' );
define( 'WC_BOOTSTRAP_PATH', get_stylesheet_directory() . '/' );
define( 'WC_BOOTSTRAP_URL', get_stylesheet_directory_uri() . '/' );
/**
* Load Composer autoloader if present.
@@ -140,6 +133,54 @@ function wc_bootstrap_enqueue_scripts(): void {
}
add_action( 'wp_enqueue_scripts', 'wc_bootstrap_enqueue_scripts' );
/**
* Build the parent theme context for a page render.
*
* Caches the ContextBuilder result per request to avoid redundant database
* queries when multiple WooCommerce rendering functions need the same context.
*
* @since 0.1.1
*
* @return array Theme context array.
*/
function wc_bootstrap_get_theme_context(): array {
static $cached_context = null;
if ( null === $cached_context ) {
$context_builder = new \WPBootstrap\Template\ContextBuilder();
$cached_context = $context_builder->build();
}
return $cached_context;
}
/**
* Render content inside the parent theme's page shell.
*
* Injects the given HTML content into the parent theme's page template,
* replacing the post content. Title and thumbnail are blanked so the
* parent theme does not render its own headings — the content handles that.
*
* @since 0.1.1
*
* @param string $content HTML content to render inside the page shell.
*/
function wc_bootstrap_render_in_page_shell( string $content ): void {
$theme_context = wc_bootstrap_get_theme_context();
$twig = \WPBootstrap\Twig\TwigService::getInstance();
$theme_context['post'] = array_merge(
$theme_context['post'] ?? [],
[
'content' => $content,
'title' => '',
'thumbnail' => '',
]
);
echo $twig->render( 'pages/page.html.twig', $theme_context );
}
/**
* Handle plugin page rendering via plugin render filter.
*
@@ -157,26 +198,10 @@ add_action( 'wp_enqueue_scripts', 'wc_bootstrap_enqueue_scripts' );
function wc_bootstrap_render_page( bool $rendered, string $content, array $context ): bool {
if ( ! class_exists( '\WPBootstrap\Twig\TwigService' )
|| ! class_exists( '\WPBootstrap\Template\ContextBuilder' ) ) {
return false; // Can't render, let plugin use its own fallback
return false;
}
$context_builder = new \WPBootstrap\Template\ContextBuilder();
$theme_context = $context_builder->build();
$twig = \WPBootstrap\Twig\TwigService::getInstance();
// Inject plugin content as the page post content so page.html.twig renders it
// inside the standard content block. Title is empty so the parent theme does not
// render its own <h1> — plugin templates handle their own headings.
$theme_context['post'] = array_merge(
$theme_context['post'] ?? [],
[
'content' => $content,
'title' => '',
'thumbnail' => '',
]
);
echo $twig->render( 'pages/page.html.twig', $theme_context );
wc_bootstrap_render_in_page_shell( $content );
return true;
}
add_filter( 'woocommerce_render_page', 'wc_bootstrap_render_page', 10, 3 );
@@ -359,26 +384,11 @@ function wc_bootstrap_render_product_archive(): void {
return;
}
// Capture WooCommerce archive content via output buffering.
ob_start();
include get_stylesheet_directory() . '/woocommerce/archive-product.php';
$content = ob_get_clean();
// Build parent theme context and inject archive content into page shell.
$context_builder = new \WPBootstrap\Template\ContextBuilder();
$theme_context = $context_builder->build();
$twig = \WPBootstrap\Twig\TwigService::getInstance();
$theme_context['post'] = array_merge(
$theme_context['post'] ?? [],
[
'content' => $content,
'title' => '',
'thumbnail' => '',
]
);
echo $twig->render( 'pages/page.html.twig', $theme_context );
wc_bootstrap_render_in_page_shell( $content );
exit;
}
add_action( 'template_redirect', 'wc_bootstrap_render_product_archive', 11 );
@@ -406,26 +416,11 @@ function wc_bootstrap_render_single_product(): void {
return;
}
// Capture WooCommerce single product content via output buffering.
ob_start();
include get_stylesheet_directory() . '/woocommerce/single-product.php';
$content = ob_get_clean();
// Build parent theme context and inject product content into page shell.
$context_builder = new \WPBootstrap\Template\ContextBuilder();
$theme_context = $context_builder->build();
$twig = \WPBootstrap\Twig\TwigService::getInstance();
$theme_context['post'] = array_merge(
$theme_context['post'] ?? [],
[
'content' => $content,
'title' => '',
'thumbnail' => '',
]
);
echo $twig->render( 'pages/page.html.twig', $theme_context );
wc_bootstrap_render_in_page_shell( $content );
exit;
}
add_action( 'template_redirect', 'wc_bootstrap_render_single_product', 11 );