7 Commits

Author SHA1 Message Date
98359d4cfb 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>
2026-03-01 01:02:43 +01:00
e72b4ba3c1 updated the theme screenshot 2026-03-01 00:42:11 +01:00
bf24b121f5 Add theme screenshot and bump to v0.1.3
All checks were successful
Create Release Package / PHP Lint (push) Successful in 59s
Create Release Package / Build Release (push) Successful in 1m13s
Add dark mode product archive screenshot for WordPress theme preview.
Update version, changelog, and documentation for release.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:35:34 +01:00
cfe089d1fe Fix dark mode rendering for tables, form inputs, and notices
All checks were successful
Create Release Package / PHP Lint (push) Successful in 51s
Create Release Package / Build Release (push) Successful in 1m9s
- Override WooCommerce's .input-text background (specificity 0,3,1) with
  [data-bs-theme="dark"] selector for text inputs and textareas
- Remove table-light from <thead> in cart, review-order, orders, and
  payment-methods templates (forces white in dark mode)
- Add .alert.woocommerce-* compound selectors for notice overrides
  outside .woocommerce wrapper ancestry
- Suppress focus ring on programmatically focused notices
  (woocommerce.js focus_populate_live_region adds tabindex + focus)
- Wrap order-details product table in card matching thank-you page style
- Fix thank-you alert icon/text line wrap with d-flex align-items-center
- Bump version to 0.1.2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:13:31 +01:00
cb2d064441 Prepare v0.1.1 bugfix release
All checks were successful
Create Release Package / PHP Lint (push) Successful in 59s
Create Release Package / Build Release (push) Successful in 1m11s
Update CHANGELOG.md with v0.1.1 entry (dark mode selects, notice
styling, product card overflow). Bump version in style.css. Document
CSS specificity learnings in CLAUDE.md. Update README release example.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 19:32:34 +01:00
e234ba6449 Fix product card images overlapping top border-radius
Add overflow-hidden to the card <article> so the card's own
border-radius clips the product image at the top corners.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 19:25:49 +01:00
f4877833cf Fix dark mode selects, notice borders, and double notice icons
- Override WooCommerce's hardcoded #fff on native <select> and Select2
  widgets for Bootstrap 5 dark mode (background, color, border)
- Suppress WooCommerce icon font ::before on notices — templates
  already render Bootstrap Icons (bi-check-circle, etc.)
- Bump notice selector specificity to .woocommerce .woocommerce-message
  (0,2,0) to beat woocommerce.css border-top: 3px solid override
- Explicitly reset border-top shorthand to prevent WooCommerce's
  colored top-border from bleeding through

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 19:24:55 +01:00
19 changed files with 347 additions and 126 deletions

View File

@@ -2,6 +2,52 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [0.1.4] - 2026-03-01
### Security
- **fn() function whitelist** (`WooCommerceExtension`): The `callFunction()` method (exposed as `fn()` in Twig templates) now restricts callable functions to an explicit `ALLOWED_FUNCTIONS` whitelist. Previously any PHP function could be called, risking arbitrary code execution if template context were compromised. Only the 6 functions actually used in templates are permitted.
- **Notice data attribute escaping**: Changed `{{ notice.data|raw }}` to `{{ notice.data|wp_kses_post }}` in success, error, and notice Twig templates. Defense-in-depth against potential XSS via data attributes.
- **Search query escaping** (`product-searchform.html.twig`): Added `|esc_attr` filter to `get_search_query()` output in the search input value attribute.
### Performance
- **Per-request ContextBuilder caching**: New `wc_bootstrap_get_theme_context()` function with static variable caching eliminates redundant `ContextBuilder::build()` calls (10-20 DB queries each) when multiple WooCommerce render functions fire in the same request.
### Changed
- **Shared page shell helper**: New `wc_bootstrap_render_in_page_shell()` function extracts the duplicated context-injection-and-render pattern from `wc_bootstrap_render_page()`, `wc_bootstrap_render_product_archive()`, and `wc_bootstrap_render_single_product()`.
- **Removed unused constants**: Removed `WC_BOOTSTRAP_VERSION` and `WC_BOOTSTRAP_URL` constants that were defined but never referenced.
## [0.1.3] - 2026-02-28
### Added
- Theme screenshot showing dark mode product archive with Bootstrap 5 card grid
## [0.1.2] - 2026-02-28
### Fixed
- Dark mode: text inputs and textareas showing white background due to WooCommerce's `.woocommerce form .form-row .input-text` (specificity `0,3,1`) overriding theme's checkout form rules with `var(--wc-form-color-background, #fff)` fallback
- Dark mode: `table-light` class on `<thead>` forcing white table headers in cart, checkout review, orders, and payment methods pages
- Dark mode: WooCommerce notice focus ring appearing white when `focus_populate_live_region()` JS programmatically focuses alerts for screen reader accessibility
- WooCommerce notice overrides not matching alerts rendered by Twig templates (added `.alert.woocommerce-*` compound selectors alongside `.woocommerce .woocommerce-*` descendant selectors)
- Order details table on thank-you page not wrapped in a card like other sections
- Thank-you page success message line-wrapping after icon due to block-level `<p>` inside inline alert context
## [0.1.1] - 2026-02-28
### Fixed
- Dark mode: native `<select>` elements showing white background due to WooCommerce's `--wc-form-color-background` falling back to `#fff`
- Dark mode: SelectWoo/Select2 dropdowns (country/state pickers) rendering with hardcoded `#fff` backgrounds, text colors, and borders
- Dark mode: checkout form focus ring color for inputs, textareas, and selects
- WooCommerce notice borders not matching Bootstrap alert styles due to insufficient CSS specificity (bumped to `.woocommerce .woocommerce-*` at `0,2,0`)
- WooCommerce notice `border-top: 3px solid` and `background-color: #f6f5f8` overriding Bootstrap alert colors
- Double icons on WooCommerce notices (WooCommerce icon font `::before` conflicting with Bootstrap Icons)
- Product card images overlapping top rounded corners on catalog page (added `overflow-hidden` to card)
## [0.1.0] - 2026-02-28 ## [0.1.0] - 2026-02-28
### Added ### Added

View File

@@ -228,6 +228,12 @@ Recurring bugs and non-obvious behaviours discovered across sessions. **Read thi
- `overflow: visible !important` on `.wp-block-navigation__container` is essential for dropdowns inside block theme navigation. - `overflow: visible !important` on `.wp-block-navigation__container` is essential for dropdowns inside block theme navigation.
- **WooCommerce `shop_table` borders conflict with Bootstrap `.table`** -- reset with `.woocommerce table.shop_table { border: 0 }` and cell `border-left/right: 0`. - **WooCommerce `shop_table` borders conflict with Bootstrap `.table`** -- reset with `.woocommerce table.shop_table { border: 0 }` and cell `border-left/right: 0`.
- **WooCommerce gallery JS requires modifier classes** -- `--with-images`, `--without-images`, `--columns-N` on `.woocommerce-product-gallery` + `style="opacity: 0"` for fade-in. Without these, zoom and photoswipe won't initialize. - **WooCommerce gallery JS requires modifier classes** -- `--with-images`, `--without-images`, `--columns-N` on `.woocommerce-product-gallery` + `style="opacity: 0"` for fade-in. Without these, zoom and photoswipe won't initialize.
- **WooCommerce dark mode select backgrounds** -- `woocommerce.css` sets `select { background-color: var(--wc-form-color-background, #fff) }`. The custom property is never defined for dark mode, so it falls back to `#fff`. Override with `[data-bs-theme="dark"] .woocommerce select`.
- **Select2/SelectWoo dark mode** -- `select2.css` hardcodes `#fff` on selection containers and dropdowns. Needs overrides for `.select2-selection`, `.select2-dropdown`, `.select2-search__field`, and highlighted options.
- **WooCommerce notice CSS specificity** -- Uses `border-top: 3px solid`, `background-color: #f6f5f8`, and icon font `::before` at specificity `0,1,0`. Must use `.woocommerce .woocommerce-*` AND `.alert.woocommerce-*` (both specificity `0,2,0`) to cover notices both inside `.woocommerce` wrappers and rendered directly by Twig templates.
- **WooCommerce notice focus ring** -- `woocommerce.js:focus_populate_live_region()` adds `tabindex="-1"` and calls `.focus()` on the first `.woocommerce-message[role="alert"]` after 500ms for screen reader announcement. The default browser focus ring appears white in dark mode. Suppress with `outline: 0; box-shadow: none` since the focus is non-interactive.
- **WooCommerce form input specificity** -- `.woocommerce form .form-row .input-text` (specificity `0,3,1`) sets `background-color: var(--wc-form-color-background, #fff)`. This beats theme checkout rules at `0,2,1`. Needs `[data-bs-theme="dark"]` override at `0,4,0`.
- **Bootstrap `table-light` breaks dark mode** -- Forces a light background on `<thead>` regardless of `data-bs-theme`. Remove it and let Bootstrap's default table styling handle theming.
- **WooCommerce float layout fights Bootstrap grid** -- `div.product div.images/summary` have `float:left/right; width:48%` in `woocommerce-layout.css`. Override with `float: none; width: 100%`. - **WooCommerce float layout fights Bootstrap grid** -- `div.product div.images/summary` have `float:left/right; width:48%` in `woocommerce-layout.css`. Override with `float: none; width: 100%`.
- **Bootstrap `g-*` gutters add negative top margin** -- `g-4` sets both `--bs-gutter-x` and `--bs-gutter-y`; the `.row` gets `margin-top: calc(-1 * var(--bs-gutter-y))` pulling it upward. Use `gx-*` for horizontal-only gutters when vertical gap isn't desired. - **Bootstrap `g-*` gutters add negative top margin** -- `g-4` sets both `--bs-gutter-x` and `--bs-gutter-y`; the `.row` gets `margin-top: calc(-1 * var(--bs-gutter-y))` pulling it upward. Use `gx-*` for horizontal-only gutters when vertical gap isn't desired.
@@ -321,10 +327,29 @@ The child theme inherits from `wp-bootstrap` via WordPress `Template: wp-bootstr
## Version History ## Version History
Current version: **v0.1.0** Current version: **v0.1.4**
## Session History ## Session History
### 2026-03-01 — v0.1.4 Security Audit & Performance Fixes
**Scope:** Cross-theme security audit (12 findings), all fixed. Covers fn() whitelist, notice data escaping, search query escaping, per-request context caching, shared render helper, and unused constant removal.
**Files changed (6):**
- `inc/Twig/WooCommerceExtension.php` — Added `ALLOWED_FUNCTIONS` whitelist to `callFunction()`. Only 6 functions (`WC`, `_n`, `get_pagenum_link`, `wc_review_ratings_enabled`, `wc_get_product_category_list`, `wc_get_product_tag_list`) are permitted.
- `templates/notices/success.html.twig``notice.data|raw``notice.data|wp_kses_post`
- `templates/notices/error.html.twig``notice.data|raw``notice.data|wp_kses_post`
- `templates/notices/notice.html.twig``notice.data|raw``notice.data|wp_kses_post`
- `templates/product-searchform.html.twig` — Added `|esc_attr` on `get_search_query()` value
- `functions.php` — Removed unused `WC_BOOTSTRAP_VERSION`/`WC_BOOTSTRAP_URL` constants. Added `wc_bootstrap_get_theme_context()` with static caching and `wc_bootstrap_render_in_page_shell()` helper. Refactored 3 render functions to use shared helpers.
**Key decisions:**
- **fn() whitelist defense-in-depth**: Template files are static PHP, not user-editable, so exploitation requires file write access. Whitelist added anyway as defense-in-depth to prevent `exec()`, `system()`, etc. if template context were ever compromised.
- **`|wp_kses_post` over `|raw` for data attributes**: WooCommerce sanitizes notice data, but belt-and-suspenders approach prevents XSS if upstream behavior changes.
- **Static variable caching over transients**: Per-request `static $cached_context` is sufficient since WooCommerce pages build context once. No transient overhead or invalidation needed.
### 2026-02-28 — My Account Bootstrap 5 Polish ### 2026-02-28 — My Account Bootstrap 5 Polish
**Scope:** Redesigned 8 my-account Twig templates + CSS overrides to feel like a polished Bootstrap 5 application. **Scope:** Redesigned 8 my-account Twig templates + CSS overrides to feel like a polished Bootstrap 5 application.
@@ -387,3 +412,40 @@ Current version: **v0.1.0**
**Files changed (1):** **Files changed (1):**
- `CHANGELOG.md` — Added `[0.1.0]` section documenting all features and fixes between v0.0.1 and v0.1.0 - `CHANGELOG.md` — Added `[0.1.0]` section documenting all features and fixes between v0.0.1 and v0.1.0
### 2026-02-28 — v0.1.1 Dark Mode & Notice CSS Bugfixes
**Scope:** Fixed dark mode select backgrounds, WooCommerce notice styling conflicts, and product card image overflow.
**Files changed (2):**
- `assets/css/wc-bootstrap.css` — Dark mode overrides for native `<select>` and Select2/SelectWoo widgets; bumped notice selectors to `.woocommerce .woocommerce-*` (specificity `0,2,0`) to beat `woocommerce.css`; suppressed WooCommerce icon font `::before` on notices; added checkout form focus color for dark mode
- `templates/content-product.html.twig` — Added `overflow-hidden` to product card `<article>` for border-radius clipping
**Key learnings:**
- WooCommerce CSS sets `select { background-color: var(--wc-form-color-background, #fff) }` — the custom property is never defined for dark mode, so it falls back to white. Override with `[data-bs-theme="dark"] .woocommerce select`.
- Select2/SelectWoo hardcodes `#fff` backgrounds in `select2.css` — needs comprehensive overrides for selection containers, dropdowns, search fields, and highlighted options.
- WooCommerce notice CSS uses `border-top: 3px solid`, `background-color: #f6f5f8`, and WooCommerce icon font `::before` at specificity `0,1,0`. Single-class overrides don't win — must use `.woocommerce .woocommerce-*` (specificity `0,2,0`) and explicitly reset `border-top`.
- Never set `background-image` without `background-repeat: no-repeat` / `background-size` / `background-position` — SVGs will tile.
### 2026-02-28 — v0.1.2 Dark Mode Deep Fixes (Tables, Inputs, Notices)
**Scope:** Fixed dark mode rendering issues across checkout, thank-you, cart, and account pages — white table headers, white form inputs, notice focus rings, and missing card wrappers.
**Files changed (7):**
- `assets/css/wc-bootstrap.css` — Added `[data-bs-theme="dark"]` override for `.input-text` and `textarea` (WooCommerce specificity `0,3,1` beats theme at `0,2,1`); added `.alert.woocommerce-*` compound selectors for notice overrides outside `.woocommerce` wrapper; added focus ring suppression for programmatically focused notices
- `templates/order/order-details.html.twig` — Wrapped product table in `card shadow-sm` with `card-header`; removed `table-light` from `<thead>`
- `templates/checkout/thankyou.html.twig` — Added `d-flex align-items-center` to success alerts to fix icon/text line wrap
- `templates/checkout/review-order.html.twig` — Removed `table-light` from `<thead>`
- `templates/cart/cart.html.twig` — Removed `table-light` from `<thead>`
- `templates/myaccount/orders.html.twig` — Removed `table-light` from `<thead>`
- `templates/myaccount/payment-methods.html.twig` — Removed `table-light` from `<thead>`
**Key findings:**
- WooCommerce's `.woocommerce form .form-row .input-text` at specificity `0,3,1` sets `background-color: var(--wc-form-color-background, #fff)` which beats theme rules. The `textarea` generated by `woocommerce_form_field()` has class `input-text`, so it matches this rule. Needs `[data-bs-theme="dark"]` override at `0,4,0`.
- WooCommerce's `focus_populate_live_region()` in `woocommerce.js` adds `tabindex="-1"` and calls `.focus()` on `.woocommerce-message[role="alert"]` after 500ms for screen reader accessibility. The default browser focus ring appears white in dark mode.
- Notice overrides using `.woocommerce .woocommerce-*` descendant selectors don't match notices rendered by Twig templates where `.alert` and `.woocommerce-message` are on the same element without a `.woocommerce` wrapper ancestor. Both `.woocommerce .woocommerce-*` and `.alert.woocommerce-*` patterns needed.
- Bootstrap's `table-light` class forces a light background on `<thead>` regardless of dark mode. Remove it entirely — let the card-header or default table styling handle visual separation.

View File

@@ -141,8 +141,8 @@ for po in languages/wc-bootstrap-*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
Releases are automated via Gitea Actions. Push a tag matching `vX.X.X` to trigger a release build. Releases are automated via Gitea Actions. Push a tag matching `vX.X.X` to trigger a release build.
```bash ```bash
git tag -a v0.1.0 -m "Version 0.1.0 - Initial release" git tag -a v0.1.3 -m "Version 0.1.3 - Add theme screenshot"
git push origin v0.1.0 git push origin v0.1.3
``` ```
## License ## License

View File

@@ -42,29 +42,43 @@
when notices are rendered outside our Twig templates. when notices are rendered outside our Twig templates.
========================================================================== */ ========================================================================== */
.woocommerce-info, /* Override woocommerce.css which sets border-top: 3px solid, background-color:
.woocommerce-message, #f6f5f8, and a WooCommerce icon font ::before on notice classes.
.woocommerce-error { Two selector patterns at specificity 0,2,0:
- .woocommerce .woocommerce-* — notices inside a .woocommerce wrapper
- .alert.woocommerce-* — notices rendered by our Twig templates */
.woocommerce .woocommerce-info,
.woocommerce .woocommerce-message,
.woocommerce .woocommerce-error,
.alert.woocommerce-info,
.alert.woocommerce-message,
.alert.woocommerce-error {
position: relative; position: relative;
padding: 1rem 3rem 1rem 1rem; padding: 1rem 3rem 1rem 1rem;
margin-bottom: 1rem; margin: 0 0 1rem;
border: 1px solid transparent; border: var(--bs-border-width) solid transparent;
border-top: var(--bs-border-width) solid transparent;
border-radius: var(--bs-border-radius); border-radius: var(--bs-border-radius);
background-color: transparent;
background-image: none;
} }
.woocommerce-info { .woocommerce .woocommerce-info,
.alert.woocommerce-info {
color: var(--bs-info-text-emphasis); color: var(--bs-info-text-emphasis);
background-color: var(--bs-info-bg-subtle); background-color: var(--bs-info-bg-subtle);
border-color: var(--bs-info-border-subtle); border-color: var(--bs-info-border-subtle);
} }
.woocommerce-message { .woocommerce .woocommerce-message,
.alert.woocommerce-message {
color: var(--bs-success-text-emphasis); color: var(--bs-success-text-emphasis);
background-color: var(--bs-success-bg-subtle); background-color: var(--bs-success-bg-subtle);
border-color: var(--bs-success-border-subtle); border-color: var(--bs-success-border-subtle);
} }
.woocommerce-error { .woocommerce .woocommerce-error,
.alert.woocommerce-error {
color: var(--bs-danger-text-emphasis); color: var(--bs-danger-text-emphasis);
background-color: var(--bs-danger-bg-subtle); background-color: var(--bs-danger-bg-subtle);
border-color: var(--bs-danger-border-subtle); border-color: var(--bs-danger-border-subtle);
@@ -72,6 +86,27 @@
padding-left: 1rem; padding-left: 1rem;
} }
/* WooCommerce JS (woocommerce.js:focus_populate_live_region) adds tabindex="-1"
and calls .focus() on notices for screen reader accessibility. The default
browser focus ring appears white in dark mode — suppress it since these are
non-interactive elements (the focus is only for screen reader announcement). */
.alert.woocommerce-info:focus,
.alert.woocommerce-message:focus,
.alert.woocommerce-error:focus {
outline: 0;
box-shadow: none;
}
/* Suppress WooCommerce icon font ::before — our templates use Bootstrap Icons */
.woocommerce .woocommerce-info::before,
.woocommerce .woocommerce-message::before,
.woocommerce .woocommerce-error::before,
.alert.woocommerce-info::before,
.alert.woocommerce-message::before,
.alert.woocommerce-error::before {
display: none;
}
/* ========================================================================== /* ==========================================================================
Quantity Input Quantity Input
Sizing for the Bootstrap input-group quantity widget. Sizing for the Bootstrap input-group quantity widget.
@@ -381,10 +416,68 @@
========================================================================== */ ========================================================================== */
/* Bootstrap 5 dark mode uses data-bs-theme="dark" attribute on <html> */ /* Bootstrap 5 dark mode uses data-bs-theme="dark" attribute on <html> */
[data-bs-theme="dark"] {
/* Checkout form focus color for dark mode */ /* Native <select> elements — woocommerce.css sets:
select { background-color: var(--wc-form-color-background, #fff) }
The custom property is never defined for dark mode, so it falls back to #fff. */
[data-bs-theme="dark"] .woocommerce select,
[data-bs-theme="dark"] .wc-block-checkout select {
background-color: var(--bs-body-bg);
color: var(--bs-body-color);
border-color: var(--bs-border-color);
} }
/* Text inputs & textareas — same issue as <select>: woocommerce.css sets
.form-row .input-text { background-color: var(--wc-form-color-background, #fff) }
with higher specificity than the theme's checkout form rules. */
[data-bs-theme="dark"] .woocommerce .form-row .input-text,
[data-bs-theme="dark"] .woocommerce .form-row textarea {
background-color: var(--bs-body-bg);
color: var(--bs-body-color);
border-color: var(--bs-border-color);
}
/* SelectWoo / Select2 — select2.css hardcodes #fff backgrounds on selection
containers and dropdowns. Override to use Bootstrap's dark mode variables. */
[data-bs-theme="dark"] .select2-container--default .select2-selection--single,
[data-bs-theme="dark"] .select2-container--default .select2-selection--multiple {
background-color: var(--bs-body-bg);
color: var(--bs-body-color);
border-color: var(--bs-border-color);
}
[data-bs-theme="dark"] .select2-container--default .select2-selection--single .select2-selection__rendered {
color: var(--bs-body-color);
}
[data-bs-theme="dark"] .select2-container--default .select2-selection--single .select2-selection__arrow b {
border-color: var(--bs-secondary-color) transparent transparent transparent;
}
[data-bs-theme="dark"] .select2-dropdown {
background-color: var(--bs-body-bg);
color: var(--bs-body-color);
border-color: var(--bs-border-color);
}
[data-bs-theme="dark"] .select2-container--default .select2-search--dropdown .select2-search__field {
background-color: var(--bs-tertiary-bg);
color: var(--bs-body-color);
border-color: var(--bs-border-color);
}
[data-bs-theme="dark"] .select2-container--default .select2-results__option[aria-selected=true],
[data-bs-theme="dark"] .select2-container--default .select2-results__option[data-selected=true] {
background-color: var(--bs-tertiary-bg);
}
[data-bs-theme="dark"] .select2-container--default .select2-results__option--highlighted[aria-selected],
[data-bs-theme="dark"] .select2-container--default .select2-results__option--highlighted[data-selected] {
background-color: var(--bs-primary);
color: #fff;
}
/* Checkout form focus color for dark mode */
[data-bs-theme="dark"] .woocommerce-checkout .form-row input.input-text:focus, [data-bs-theme="dark"] .woocommerce-checkout .form-row input.input-text:focus,
[data-bs-theme="dark"] .woocommerce-checkout .form-row textarea:focus, [data-bs-theme="dark"] .woocommerce-checkout .form-row textarea:focus,
[data-bs-theme="dark"] .woocommerce-checkout .form-row select:focus { [data-bs-theme="dark"] .woocommerce-checkout .form-row select:focus {

View File

@@ -17,15 +17,8 @@ if ( ! defined( 'ABSPATH' ) ) {
/** /**
* Define theme constants. * 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_PATH', get_stylesheet_directory() . '/' );
define( 'WC_BOOTSTRAP_URL', get_stylesheet_directory_uri() . '/' );
/** /**
* Load Composer autoloader if present. * Load Composer autoloader if present.
@@ -140,6 +133,54 @@ function wc_bootstrap_enqueue_scripts(): void {
} }
add_action( 'wp_enqueue_scripts', 'wc_bootstrap_enqueue_scripts' ); 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. * 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 { function wc_bootstrap_render_page( bool $rendered, string $content, array $context ): bool {
if ( ! class_exists( '\WPBootstrap\Twig\TwigService' ) if ( ! class_exists( '\WPBootstrap\Twig\TwigService' )
|| ! class_exists( '\WPBootstrap\Template\ContextBuilder' ) ) { || ! class_exists( '\WPBootstrap\Template\ContextBuilder' ) ) {
return false; // Can't render, let plugin use its own fallback return false;
} }
$context_builder = new \WPBootstrap\Template\ContextBuilder(); wc_bootstrap_render_in_page_shell( $content );
$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 );
return true; return true;
} }
add_filter( 'woocommerce_render_page', 'wc_bootstrap_render_page', 10, 3 ); add_filter( 'woocommerce_render_page', 'wc_bootstrap_render_page', 10, 3 );
@@ -359,26 +384,11 @@ function wc_bootstrap_render_product_archive(): void {
return; return;
} }
// Capture WooCommerce archive content via output buffering.
ob_start(); ob_start();
include get_stylesheet_directory() . '/woocommerce/archive-product.php'; include get_stylesheet_directory() . '/woocommerce/archive-product.php';
$content = ob_get_clean(); $content = ob_get_clean();
// Build parent theme context and inject archive content into page shell. wc_bootstrap_render_in_page_shell( $content );
$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 );
exit; exit;
} }
add_action( 'template_redirect', 'wc_bootstrap_render_product_archive', 11 ); add_action( 'template_redirect', 'wc_bootstrap_render_product_archive', 11 );
@@ -406,26 +416,11 @@ function wc_bootstrap_render_single_product(): void {
return; return;
} }
// Capture WooCommerce single product content via output buffering.
ob_start(); ob_start();
include get_stylesheet_directory() . '/woocommerce/single-product.php'; include get_stylesheet_directory() . '/woocommerce/single-product.php';
$content = ob_get_clean(); $content = ob_get_clean();
// Build parent theme context and inject product content into page shell. wc_bootstrap_render_in_page_shell( $content );
$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 );
exit; exit;
} }
add_action( 'template_redirect', 'wc_bootstrap_render_single_product', 11 ); add_action( 'template_redirect', 'wc_bootstrap_render_single_product', 11 );

View File

@@ -235,18 +235,40 @@ class WooCommerceExtension extends AbstractExtension {
} }
/** /**
* Call a PHP function by name and return its result. * Allowlist of PHP functions that can be called via fn() in Twig templates.
*
* Prevents arbitrary function execution (e.g., exec, system) if template
* context were ever compromised. Only functions actually used in templates
* are permitted.
*/
private const ALLOWED_FUNCTIONS = [
'WC',
'_n',
'get_pagenum_link',
'wc_review_ratings_enabled',
'wc_get_product_category_list',
'wc_get_product_tag_list',
];
/**
* Call a whitelisted PHP function by name and return its result.
* *
* Enables `fn('WC')` in templates to access the WooCommerce singleton * Enables `fn('WC')` in templates to access the WooCommerce singleton
* and chain method calls via Twig's property accessor. * and chain method calls via Twig's property accessor.
* *
* Only functions in the ALLOWED_FUNCTIONS list can be called. This prevents
* arbitrary code execution if template context were ever compromised.
*
* @param string $name Function name. * @param string $name Function name.
* @param mixed ...$args Arguments. * @param mixed ...$args Arguments.
* @return mixed Function return value. * @return mixed Function return value.
* *
* @throws \RuntimeException If function does not exist. * @throws \RuntimeException If function is not allowed or does not exist.
*/ */
public function callFunction( string $name, ...$args ): mixed { public function callFunction( string $name, ...$args ): mixed {
if ( ! in_array( $name, self::ALLOWED_FUNCTIONS, true ) ) {
throw new \RuntimeException( "Function {$name} is not allowed. Add it to ALLOWED_FUNCTIONS." );
}
if ( ! function_exists( $name ) ) { if ( ! function_exists( $name ) ) {
throw new \RuntimeException( "Function {$name} does not exist." ); throw new \RuntimeException( "Function {$name} does not exist." );
} }

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

View File

@@ -7,7 +7,7 @@ Description: A Bootstrap 5 child theme for WP Bootstrap that overrides all WooCo
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: 0.1.0 Version: 0.1.4
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
Template: wp-bootstrap Template: wp-bootstrap

View File

@@ -36,7 +36,7 @@
<div class="table-responsive"> <div class="table-responsive">
<table class="table align-middle shop_table shop_table_responsive cart woocommerce-cart-form__contents"> <table class="table align-middle shop_table shop_table_responsive cart woocommerce-cart-form__contents">
<thead class="table-light"> <thead>
<tr> <tr>
<th class="product-thumbnail" scope="col" style="width: 80px;"> <th class="product-thumbnail" scope="col" style="width: 80px;">
<span class="visually-hidden">{{ __('Thumbnail') }}</span> <span class="visually-hidden">{{ __('Thumbnail') }}</span>

View File

@@ -19,7 +19,7 @@
<div class="card shadow-sm woocommerce-checkout-review-order-table"> <div class="card shadow-sm woocommerce-checkout-review-order-table">
<div class="card-body p-0"> <div class="card-body p-0">
<table class="table table-sm mb-0 shop_table woocommerce-checkout-review-order-table"> <table class="table table-sm mb-0 shop_table woocommerce-checkout-review-order-table">
<thead class="table-light"> <thead>
<tr> <tr>
<th class="product-name" scope="col">{{ __('Product') }}</th> <th class="product-name" scope="col">{{ __('Product') }}</th>
<th class="product-total text-end" scope="col">{{ __('Subtotal') }}</th> <th class="product-total text-end" scope="col">{{ __('Subtotal') }}</th>

View File

@@ -27,7 +27,7 @@
</a> </a>
</p> </p>
{% else %} {% else %}
<div class="alert alert-success mb-4" role="alert"> <div class="alert alert-success d-flex align-items-center mb-4" role="alert">
<i class="bi bi-check-circle me-2" aria-hidden="true"></i> <i class="bi bi-check-circle me-2" aria-hidden="true"></i>
{% include 'checkout/order-received.html.twig' with { order: order } %} {% include 'checkout/order-received.html.twig' with { order: order } %}
</div> </div>
@@ -68,7 +68,7 @@
{{ do_action('woocommerce_thankyou_' ~ order.get_payment_method(), order.get_id()) }} {{ do_action('woocommerce_thankyou_' ~ order.get_payment_method(), order.get_id()) }}
{{ do_action('woocommerce_thankyou', order.get_id()) }} {{ do_action('woocommerce_thankyou', order.get_id()) }}
{% else %} {% else %}
<div class="alert alert-success mb-4" role="alert"> <div class="alert alert-success d-flex align-items-center mb-4" role="alert">
<i class="bi bi-check-circle me-2" aria-hidden="true"></i> <i class="bi bi-check-circle me-2" aria-hidden="true"></i>
{% include 'checkout/order-received.html.twig' %} {% include 'checkout/order-received.html.twig' %}
</div> </div>

View File

@@ -21,7 +21,7 @@
#} #}
<div class="col"> <div class="col">
<article class="card h-100 shadow-sm product"> <article class="card h-100 shadow-sm overflow-hidden product">
{{ do_action('woocommerce_before_shop_loop_item') }} {{ do_action('woocommerce_before_shop_loop_item') }}
{# Product image with sale badge overlay #} {# Product image with sale badge overlay #}

View File

@@ -21,7 +21,7 @@
{% if has_orders %} {% if has_orders %}
<div class="table-responsive"> <div class="table-responsive">
<table class="woocommerce-orders-table table table-hover align-middle mb-4"> <table class="woocommerce-orders-table table table-hover align-middle mb-4">
<thead class="table-light"> <thead>
<tr> <tr>
{% for column_id, column_name in wc_get_account_orders_columns() %} {% for column_id, column_name in wc_get_account_orders_columns() %}
<th scope="col">{{ column_name|esc_html }}</th> <th scope="col">{{ column_name|esc_html }}</th>

View File

@@ -17,7 +17,7 @@
{% if has_methods %} {% if has_methods %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover align-middle mb-4"> <table class="table table-hover align-middle mb-4">
<thead class="table-light"> <thead>
<tr> <tr>
{% for column_id, column_name in wc_get_account_payment_methods_columns() %} {% for column_id, column_name in wc_get_account_payment_methods_columns() %}
<th>{{ column_name|esc_html }}</th> <th>{{ column_name|esc_html }}</th>

View File

@@ -23,7 +23,7 @@
{% else %} {% else %}
<ul class="mb-0 ps-3"> <ul class="mb-0 ps-3">
{% for notice in notices %} {% for notice in notices %}
<li {{ notice.data|default('')|raw }}> <li {{ notice.data|default('')|wp_kses_post }}>
{{ notice.notice|raw }} {{ notice.notice|raw }}
</li> </li>
{% endfor %} {% endfor %}

View File

@@ -17,7 +17,7 @@
{% if notices is defined and notices|length > 0 %} {% if notices is defined and notices|length > 0 %}
{% for notice in notices %} {% for notice in notices %}
<div class="alert alert-info alert-dismissible fade show woocommerce-info" <div class="alert alert-info alert-dismissible fade show woocommerce-info"
{{ notice.data|default('')|raw }} {{ notice.data|default('')|wp_kses_post }}
role="status"> role="status">
<i class="bi bi-info-circle me-2" aria-hidden="true"></i> <i class="bi bi-info-circle me-2" aria-hidden="true"></i>
{{ notice.notice|raw }} {{ notice.notice|raw }}

View File

@@ -17,7 +17,7 @@
{% if notices is defined and notices|length > 0 %} {% if notices is defined and notices|length > 0 %}
{% for notice in notices %} {% for notice in notices %}
<div class="alert alert-success alert-dismissible fade show woocommerce-message" <div class="alert alert-success alert-dismissible fade show woocommerce-message"
{{ notice.data|default('')|raw }} {{ notice.data|default('')|wp_kses_post }}
role="alert"> role="alert">
<i class="bi bi-check-circle me-2" aria-hidden="true"></i> <i class="bi bi-check-circle me-2" aria-hidden="true"></i>
{{ notice.notice|raw }} {{ notice.notice|raw }}

View File

@@ -31,51 +31,54 @@
<section class="woocommerce-order-details"> <section class="woocommerce-order-details">
{{ do_action('woocommerce_order_details_before_order_table', order) }} {{ do_action('woocommerce_order_details_before_order_table', order) }}
<h2 class="h5 mb-3">{{ __('Order details') }}</h2> <div class="card shadow-sm mb-4">
<div class="card-header">
<div class="table-responsive"> <h2 class="h5 mb-0">{{ __('Order details') }}</h2>
<table class="table table-hover align-middle mb-0"> </div>
<thead class="table-light"> <div class="table-responsive">
<tr> <table class="table table-hover align-middle mb-0">
<th>{{ __('Product') }}</th> <thead>
<th class="text-end">{{ __('Total') }}</th>
</tr>
</thead>
<tbody>
{{ do_action('woocommerce_order_details_before_order_table_items', order) }}
{% for item_id, item in order_items %}
{% set product = item.get_product() %}
{% include 'order/order-details-item.html.twig' with {
order: order,
item_id: item_id,
item: item,
show_purchase_note: show_purchase_note,
purchase_note: product ? product.get_purchase_note() : '',
product: product
} %}
{% endfor %}
{{ do_action('woocommerce_order_details_after_order_table_items', order) }}
</tbody>
<tfoot>
{% for key, total in order.get_order_item_totals() %}
<tr> <tr>
<th scope="row">{{ total.label|esc_html }}</th> <th>{{ __('Product') }}</th>
<td class="text-end">{{ total.value|wp_kses_post }}</td> <th class="text-end">{{ __('Total') }}</th>
</tr> </tr>
{% endfor %} </thead>
{% if order.get_customer_note() %} <tbody>
<tr> {{ do_action('woocommerce_order_details_before_order_table_items', order) }}
<th>{{ __('Note:') }}</th>
<td class="text-end">{{ order.get_customer_note()|esc_html|nl2br }}</td> {% for item_id, item in order_items %}
</tr> {% set product = item.get_product() %}
{% endif %} {% include 'order/order-details-item.html.twig' with {
</tfoot> order: order,
</table> item_id: item_id,
item: item,
show_purchase_note: show_purchase_note,
purchase_note: product ? product.get_purchase_note() : '',
product: product
} %}
{% endfor %}
{{ do_action('woocommerce_order_details_after_order_table_items', order) }}
</tbody>
<tfoot>
{% for key, total in order.get_order_item_totals() %}
<tr>
<th scope="row">{{ total.label|esc_html }}</th>
<td class="text-end">{{ total.value|wp_kses_post }}</td>
</tr>
{% endfor %}
{% if order.get_customer_note() %}
<tr>
<th>{{ __('Note:') }}</th>
<td class="text-end">{{ order.get_customer_note()|esc_html|nl2br }}</td>
</tr>
{% endif %}
</tfoot>
</table>
</div>
</div> </div>
{{ do_action('woocommerce_order_details_after_order_table', order) }} {{ do_action('woocommerce_order_details_after_order_table', order) }}

View File

@@ -21,7 +21,7 @@
id="{{ field_id }}" id="{{ field_id }}"
class="form-control" class="form-control"
placeholder="{{ __('Search products…') }}" placeholder="{{ __('Search products…') }}"
value="{{ get_search_query() }}" value="{{ get_search_query()|esc_attr }}"
name="s" /> name="s" />
<button type="submit" class="btn btn-outline-primary" aria-label="{{ __('Search') }}"> <button type="submit" class="btn btn-outline-primary" aria-label="{{ __('Search') }}">
<i class="bi bi-search" aria-hidden="true"></i> <i class="bi bi-search" aria-hidden="true"></i>