You've already forked wp-bootstrap
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17728e81d9 |
16
CHANGELOG.md
16
CHANGELOG.md
@@ -2,6 +2,22 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [1.1.2] - 2026-03-01
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- **WidgetRenderer regex hardening**: Combined two separate `preg_replace` calls for h2→h4 heading downgrade into a single regex that only matches `<h2>` elements with the `wp-block-heading` class. The previous approach replaced all `</h2>` tags unconditionally, risking mismatched tags if a widget contained non-block h2 elements.
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- **O(n) comment tree building** (`ContextBuilder`): Replaced O(n²) recursive scan with a parent-indexed lookup map built in a single pass. Each recursion level now iterates only direct children instead of all comments.
|
||||||
|
- **Consolidated sidebar queries** (`ContextBuilder`): Merged three separate sidebar detection branches (`is_home`, `is_page`+sidebar, `is_singular` post) into a single boolean check with one `getSidebarData()` call, eliminating up to 2 redundant calls per request.
|
||||||
|
- **Transient caching for sidebar data** (`ContextBuilder`): `getSidebarRecentPosts()` and `getSidebarTags()` results cached in WordPress transients (1 hour TTL). Invalidation hooks on `save_post` (recent posts) and `create/edit/delete_post_tag` (tags).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Hex-to-RGB consolidation** (`functions.php`): `wp_bootstrap_hex_to_rgb()` now delegates to `wp_bootstrap_hex_to_rgb_array()` instead of duplicating hex parsing logic. Added `ctype_xdigit()` validation and return type hints to all color utility functions.
|
||||||
|
|
||||||
## [1.1.1] - 2026-02-28
|
## [1.1.1] - 2026-02-28
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
22
CLAUDE.md
22
CLAUDE.md
@@ -34,7 +34,7 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
|
|||||||
|
|
||||||
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
|
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
|
||||||
|
|
||||||
Current version is **v1.1.1**. See `PLAN.md` for details.
|
Current version is **v1.1.2**. See `PLAN.md` for details.
|
||||||
|
|
||||||
## Technical Stack
|
## Technical Stack
|
||||||
|
|
||||||
@@ -234,6 +234,26 @@ Build steps (in order):
|
|||||||
|
|
||||||
## Session History
|
## Session History
|
||||||
|
|
||||||
|
### Session 21 — v1.1.2 Security Audit & Performance Fixes (2026-03-01)
|
||||||
|
|
||||||
|
**Completed:** Cross-theme security audit with 12 findings, all fixed. Covers WidgetRenderer regex hardening, ContextBuilder performance (O(n) comment tree, sidebar query consolidation, transient caching), and hex-to-RGB code consolidation.
|
||||||
|
|
||||||
|
**What was changed:**
|
||||||
|
|
||||||
|
- **WidgetRenderer regex fix** (`inc/Block/WidgetRenderer.php`): Combined two `preg_replace` calls into single regex matching `<h2>` with `wp-block-heading` class, preventing mismatched tags.
|
||||||
|
- **O(n) comment tree** (`inc/Template/ContextBuilder.php`): Parent-indexed lookup map replaces full-scan recursion. Each level now iterates only direct children.
|
||||||
|
- **Sidebar query consolidation** (`inc/Template/ContextBuilder.php`): Three separate sidebar detection branches merged into single boolean + one `getSidebarData()` call.
|
||||||
|
- **Transient caching** (`inc/Template/ContextBuilder.php` + `functions.php`): `getSidebarRecentPosts()` and `getSidebarTags()` cached in 1-hour transients with hook-based invalidation (`save_post`, `create/edit/delete_post_tag`).
|
||||||
|
- **Hex-to-RGB consolidation** (`functions.php`): Eliminated duplicate hex parsing. Added `ctype_xdigit()` validation and type hints to all color utility functions.
|
||||||
|
|
||||||
|
**Files modified:**
|
||||||
|
|
||||||
|
- `inc/Block/WidgetRenderer.php` — single regex for h2→h4
|
||||||
|
- `inc/Template/ContextBuilder.php` — O(n) tree, sidebar consolidation, transient caching
|
||||||
|
- `functions.php` — hex-to-RGB consolidation, type hints, transient invalidation hooks
|
||||||
|
- `style.css` — version bump to 1.1.2
|
||||||
|
- `CHANGELOG.md`, `CLAUDE.md` — documentation
|
||||||
|
|
||||||
### Session 20 — v1.1.1 PHPUnit Test Suite (2026-02-28)
|
### Session 20 — v1.1.1 PHPUnit Test Suite (2026-02-28)
|
||||||
|
|
||||||
**Completed:** PHPUnit test suite with 64 unit tests and 107 assertions, CI integration, and build pipeline gating.
|
**Completed:** PHPUnit test suite with 64 unit tests and 107 assertions, CI integration, and build pipeline gating.
|
||||||
|
|||||||
@@ -320,6 +320,24 @@ add_action( 'save_post_wp_global_styles', function () {
|
|||||||
delete_transient( 'wp_bootstrap_variation_css_' . md5( get_stylesheet() ) );
|
delete_transient( 'wp_bootstrap_variation_css_' . md5( get_stylesheet() ) );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate sidebar transient caches when content changes.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*/
|
||||||
|
add_action( 'save_post', function () {
|
||||||
|
delete_transient( 'wp_bootstrap_sidebar_recent_4' );
|
||||||
|
} );
|
||||||
|
add_action( 'create_post_tag', function () {
|
||||||
|
delete_transient( 'wp_bootstrap_sidebar_tags_15' );
|
||||||
|
} );
|
||||||
|
add_action( 'edit_post_tag', function () {
|
||||||
|
delete_transient( 'wp_bootstrap_sidebar_tags_15' );
|
||||||
|
} );
|
||||||
|
add_action( 'delete_post_tag', function () {
|
||||||
|
delete_transient( 'wp_bootstrap_sidebar_tags_15' );
|
||||||
|
} );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build Bootstrap surface CSS variables for a given background/foreground pair.
|
* Build Bootstrap surface CSS variables for a given background/foreground pair.
|
||||||
*
|
*
|
||||||
@@ -378,17 +396,12 @@ endif;
|
|||||||
* @return string RGB triplet (e.g. "13,110,253") or empty string on failure.
|
* @return string RGB triplet (e.g. "13,110,253") or empty string on failure.
|
||||||
*/
|
*/
|
||||||
if ( ! function_exists( 'wp_bootstrap_hex_to_rgb' ) ) :
|
if ( ! function_exists( 'wp_bootstrap_hex_to_rgb' ) ) :
|
||||||
function wp_bootstrap_hex_to_rgb( $hex ) {
|
function wp_bootstrap_hex_to_rgb( string $hex ): string {
|
||||||
$hex = ltrim( $hex, '#' );
|
$rgb = wp_bootstrap_hex_to_rgb_array( $hex );
|
||||||
if ( strlen( $hex ) === 3 ) {
|
if ( ! $rgb ) {
|
||||||
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
|
|
||||||
}
|
|
||||||
if ( strlen( $hex ) !== 6 ) {
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return hexdec( substr( $hex, 0, 2 ) ) . ','
|
return implode( ',', $rgb );
|
||||||
. hexdec( substr( $hex, 2, 2 ) ) . ','
|
|
||||||
. hexdec( substr( $hex, 4, 2 ) );
|
|
||||||
}
|
}
|
||||||
endif;
|
endif;
|
||||||
|
|
||||||
@@ -403,7 +416,7 @@ endif;
|
|||||||
* @return string Resulting hex color.
|
* @return string Resulting hex color.
|
||||||
*/
|
*/
|
||||||
if ( ! function_exists( 'wp_bootstrap_mix_hex' ) ) :
|
if ( ! function_exists( 'wp_bootstrap_mix_hex' ) ) :
|
||||||
function wp_bootstrap_mix_hex( $color1, $color2, $weight ) {
|
function wp_bootstrap_mix_hex( string $color1, string $color2, float $weight ): string {
|
||||||
$c1 = wp_bootstrap_hex_to_rgb_array( $color1 );
|
$c1 = wp_bootstrap_hex_to_rgb_array( $color1 );
|
||||||
$c2 = wp_bootstrap_hex_to_rgb_array( $color2 );
|
$c2 = wp_bootstrap_hex_to_rgb_array( $color2 );
|
||||||
if ( ! $c1 || ! $c2 ) {
|
if ( ! $c1 || ! $c2 ) {
|
||||||
@@ -425,12 +438,12 @@ endif;
|
|||||||
* @return array|false Array of [r, g, b] or false on failure.
|
* @return array|false Array of [r, g, b] or false on failure.
|
||||||
*/
|
*/
|
||||||
if ( ! function_exists( 'wp_bootstrap_hex_to_rgb_array' ) ) :
|
if ( ! function_exists( 'wp_bootstrap_hex_to_rgb_array' ) ) :
|
||||||
function wp_bootstrap_hex_to_rgb_array( $hex ) {
|
function wp_bootstrap_hex_to_rgb_array( string $hex ): array|false {
|
||||||
$hex = ltrim( $hex, '#' );
|
$hex = ltrim( $hex, '#' );
|
||||||
if ( strlen( $hex ) === 3 ) {
|
if ( strlen( $hex ) === 3 ) {
|
||||||
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
|
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
|
||||||
}
|
}
|
||||||
if ( strlen( $hex ) !== 6 ) {
|
if ( strlen( $hex ) !== 6 || ! ctype_xdigit( $hex ) ) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return array(
|
return array(
|
||||||
@@ -450,7 +463,7 @@ endif;
|
|||||||
* @return float Relative luminance.
|
* @return float Relative luminance.
|
||||||
*/
|
*/
|
||||||
if ( ! function_exists( 'wp_bootstrap_relative_luminance' ) ) :
|
if ( ! function_exists( 'wp_bootstrap_relative_luminance' ) ) :
|
||||||
function wp_bootstrap_relative_luminance( $hex ) {
|
function wp_bootstrap_relative_luminance( string $hex ): float {
|
||||||
$rgb = wp_bootstrap_hex_to_rgb_array( $hex );
|
$rgb = wp_bootstrap_hex_to_rgb_array( $hex );
|
||||||
if ( ! $rgb ) {
|
if ( ! $rgb ) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
|
|||||||
@@ -98,15 +98,12 @@ class WidgetRenderer
|
|||||||
return $content;
|
return $content;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace <h2 with <h4 and </h2> with </h4> for widget headings.
|
// Replace <h2 ... wp-block-heading ...>...</h2> with <h4> pairs.
|
||||||
|
// Single regex ensures only headings with wp-block-heading class are
|
||||||
|
// downgraded, preventing mismatched tags if a widget contains other h2s.
|
||||||
$content = preg_replace(
|
$content = preg_replace(
|
||||||
'/<h2(\s+class="[^"]*wp-block-heading[^"]*")/',
|
'/<h2(\s+class="[^"]*wp-block-heading[^"]*"[^>]*)>(.*?)<\/h2>/s',
|
||||||
'<h4$1',
|
'<h4$1>$2</h4>',
|
||||||
$content
|
|
||||||
);
|
|
||||||
$content = preg_replace(
|
|
||||||
'/<\/h2>/',
|
|
||||||
'</h4>',
|
|
||||||
$content
|
$content
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,9 @@ class ContextBuilder
|
|||||||
$context['search_query'] = get_search_query();
|
$context['search_query'] = get_search_query();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sidebar layout detection.
|
// Sidebar: determine once whether the current page needs sidebar data.
|
||||||
|
$needsSidebar = false;
|
||||||
|
|
||||||
if (is_home()) {
|
if (is_home()) {
|
||||||
$pageId = (int) get_option('page_for_posts');
|
$pageId = (int) get_option('page_for_posts');
|
||||||
if ($pageId) {
|
if ($pageId) {
|
||||||
@@ -67,19 +69,14 @@ class ContextBuilder
|
|||||||
$context['layout'] = 'sidebar';
|
$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 ($needsSidebar) {
|
||||||
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')) {
|
|
||||||
$context['sidebar'] = $this->getSidebarData();
|
$context['sidebar'] = $this->getSidebarData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,21 +306,26 @@ class ContextBuilder
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a nested comment tree from flat comments.
|
* 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 = [];
|
$tree = [];
|
||||||
|
|
||||||
foreach ($comments as $comment) {
|
foreach ($index[$parentId] ?? [] as $comment) {
|
||||||
if ((int) $comment->comment_parent !== $parentId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$tree[] = [
|
$tree[] = [
|
||||||
'id' => (int) $comment->comment_ID,
|
'id' => (int) $comment->comment_ID,
|
||||||
// Escape at source — comment_author is user-supplied, store as safe text.
|
|
||||||
'author' => esc_html($comment->comment_author),
|
'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),
|
'author_url' => esc_url($comment->comment_author_url),
|
||||||
'avatar_url' => get_avatar_url($comment, ['size' => 40]),
|
'avatar_url' => get_avatar_url($comment, ['size' => 40]),
|
||||||
'date' => get_comment_date('', $comment),
|
'date' => get_comment_date('', $comment),
|
||||||
@@ -336,7 +338,7 @@ class ContextBuilder
|
|||||||
'depth' => 1,
|
'depth' => 1,
|
||||||
'max_depth' => get_option('thread_comments_depth', 5),
|
'max_depth' => get_option('thread_comments_depth', 5),
|
||||||
], $comment),
|
], $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.
|
* Get recent posts for sidebar.
|
||||||
|
*
|
||||||
|
* Cached via transient for 1 hour; invalidated on save_post.
|
||||||
*/
|
*/
|
||||||
private function getSidebarRecentPosts(int $count = 4): array
|
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([
|
$query = new \WP_Query([
|
||||||
'posts_per_page' => $count,
|
'posts_per_page' => $count,
|
||||||
'orderby' => 'date',
|
'orderby' => 'date',
|
||||||
@@ -474,14 +484,23 @@ class ContextBuilder
|
|||||||
wp_reset_postdata();
|
wp_reset_postdata();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set_transient($transient_key, $posts, HOUR_IN_SECONDS);
|
||||||
return $posts;
|
return $posts;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get tags for sidebar tag cloud.
|
* Get tags for sidebar tag cloud.
|
||||||
|
*
|
||||||
|
* Cached via transient for 1 hour; invalidated on tag changes.
|
||||||
*/
|
*/
|
||||||
private function getSidebarTags(int $count = 15): array
|
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([
|
$tags = get_tags([
|
||||||
'number' => $count,
|
'number' => $count,
|
||||||
'orderby' => 'count',
|
'orderby' => 'count',
|
||||||
@@ -501,6 +520,7 @@ class ContextBuilder
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set_transient($transient_key, $items, HOUR_IN_SECONDS);
|
||||||
return $items;
|
return $items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Description: A modern WordPress Block Theme built from scratch with Bootstrap 5.
|
|||||||
Requires at least: 6.7
|
Requires at least: 6.7
|
||||||
Tested up to: 6.7
|
Tested up to: 6.7
|
||||||
Requires PHP: 8.3
|
Requires PHP: 8.3
|
||||||
Version: 1.1.1
|
Version: 1.1.2
|
||||||
License: GNU General Public License v2 or later
|
License: GNU General Public License v2 or later
|
||||||
License URI: http://www.gnu.org/licenses/gpl-2.0.html
|
License URI: http://www.gnu.org/licenses/gpl-2.0.html
|
||||||
Text Domain: wp-bootstrap
|
Text Domain: wp-bootstrap
|
||||||
|
|||||||
Reference in New Issue
Block a user