Security audit fixes: regex hardening, performance, and code quality (v1.1.2)
All checks were successful
Create Release Package / PHP Lint (push) Successful in 1m32s
Create Release Package / PHPUnit Tests (push) Successful in 2m35s
Create Release Package / Build Release (push) Successful in 2m36s

- WidgetRenderer: single regex for h2→h4 prevents mismatched tags
- ContextBuilder: O(n) comment tree with parent-indexed lookup map
- ContextBuilder: consolidated sidebar queries into single check
- ContextBuilder: transient caching for sidebar recent posts and tags
- functions.php: hex-to-RGB consolidation, type hints, ctype_xdigit validation
- Transient invalidation hooks for save_post and tag CRUD operations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 01:02:12 +01:00
parent ea2ccef5de
commit 17728e81d9
6 changed files with 110 additions and 44 deletions

View File

@@ -58,7 +58,9 @@ class ContextBuilder
$context['search_query'] = get_search_query();
}
// Sidebar layout detection.
// Sidebar: determine once whether the current page needs sidebar data.
$needsSidebar = false;
if (is_home()) {
$pageId = (int) get_option('page_for_posts');
if ($pageId) {
@@ -67,19 +69,14 @@ class ContextBuilder
$context['layout'] = 'sidebar';
}
}
$context['sidebar'] = $this->getSidebarData();
$needsSidebar = true;
} elseif (is_singular('post')) {
$needsSidebar = true;
} elseif (is_page() && get_page_template_slug() === 'page-sidebar') {
$needsSidebar = true;
}
// Sidebar data for pages using the "Page with Sidebar" template.
if (is_page()) {
$slug = get_page_template_slug();
if ($slug === 'page-sidebar') {
$context['sidebar'] = $this->getSidebarData();
}
}
// Posts always get sidebar data (sidebar is the default layout).
if (is_singular('post')) {
if ($needsSidebar) {
$context['sidebar'] = $this->getSidebarData();
}
@@ -309,21 +306,26 @@ class ContextBuilder
/**
* Build a nested comment tree from flat comments.
*
* Uses a parent-indexed lookup map for O(n) performance instead of
* scanning all comments at each recursion level (O(n^2)).
*/
private function buildCommentTree(array $comments, int $parentId = 0): array
private function buildCommentTree(array $comments, int $parentId = 0, ?array $index = null): array
{
if ($index === null) {
$index = [];
foreach ($comments as $comment) {
$parent = (int) $comment->comment_parent;
$index[$parent][] = $comment;
}
}
$tree = [];
foreach ($comments as $comment) {
if ((int) $comment->comment_parent !== $parentId) {
continue;
}
foreach ($index[$parentId] ?? [] as $comment) {
$tree[] = [
'id' => (int) $comment->comment_ID,
// Escape at source — comment_author is user-supplied, store as safe text.
'author' => esc_html($comment->comment_author),
// esc_url() strips dangerous schemes (javascript:, data:) and encodes for HTML.
'author_url' => esc_url($comment->comment_author_url),
'avatar_url' => get_avatar_url($comment, ['size' => 40]),
'date' => get_comment_date('', $comment),
@@ -336,7 +338,7 @@ class ContextBuilder
'depth' => 1,
'max_depth' => get_option('thread_comments_depth', 5),
], $comment),
'children' => $this->buildCommentTree($comments, (int) $comment->comment_ID),
'children' => $this->buildCommentTree($comments, (int) $comment->comment_ID, $index),
];
}
@@ -452,9 +454,17 @@ class ContextBuilder
/**
* Get recent posts for sidebar.
*
* Cached via transient for 1 hour; invalidated on save_post.
*/
private function getSidebarRecentPosts(int $count = 4): array
{
$transient_key = 'wp_bootstrap_sidebar_recent_' . $count;
$cached = get_transient($transient_key);
if (false !== $cached) {
return $cached;
}
$query = new \WP_Query([
'posts_per_page' => $count,
'orderby' => 'date',
@@ -474,14 +484,23 @@ class ContextBuilder
wp_reset_postdata();
}
set_transient($transient_key, $posts, HOUR_IN_SECONDS);
return $posts;
}
/**
* Get tags for sidebar tag cloud.
*
* Cached via transient for 1 hour; invalidated on tag changes.
*/
private function getSidebarTags(int $count = 15): array
{
$transient_key = 'wp_bootstrap_sidebar_tags_' . $count;
$cached = get_transient($transient_key);
if (false !== $cached) {
return $cached;
}
$tags = get_tags([
'number' => $count,
'orderby' => 'count',
@@ -501,6 +520,7 @@ class ContextBuilder
];
}
set_transient($transient_key, $items, HOUR_IN_SECONDS);
return $items;
}